grain.social is a photo sharing platform built on atproto.

beginnings of moderation support, show labels in ui, update profile page to support labelers"

+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
··· 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
··· 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`
+2
__generated__/types/social/grain/actor/defs.ts
··· 9 9 is$typed as _is$typed, 10 10 type OmitKey, 11 11 } from '../../../../util.ts' 12 + import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.ts' 12 13 13 14 const is$typed = _is$typed, 14 15 validate = _validate ··· 20 21 handle: string 21 22 displayName?: string 22 23 description?: string 24 + labels?: ComAtprotoLabelDefs.Label[] 23 25 avatar?: string 24 26 createdAt?: string 25 27 }
+2
__generated__/types/social/grain/gallery.ts
··· 5 5 import { CID } from "npm:multiformats/cid" 6 6 import { validate as _validate } from '../../../lexicons.ts' 7 7 import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util.ts' 8 + import type * as ComAtprotoLabelDefs from '../../com/atproto/label/defs.ts' 8 9 9 10 const is$typed = _is$typed, 10 11 validate = _validate ··· 14 15 $type: 'social.grain.gallery' 15 16 title: string 16 17 description?: string 18 + labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string } 17 19 createdAt: string 18 20 [k: string]: unknown 19 21 }
+2
__generated__/types/social/grain/gallery/defs.ts
··· 11 11 } from '../../../../util.ts' 12 12 import type * as SocialGrainActorDefs from '../actor/defs.ts' 13 13 import type * as SocialGrainPhotoDefs from '../photo/defs.ts' 14 + import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.ts' 14 15 15 16 const is$typed = _is$typed, 16 17 validate = _validate ··· 23 24 creator: SocialGrainActorDefs.ProfileView 24 25 record: { [_ in string]: unknown } 25 26 items?: ($Typed<SocialGrainPhotoDefs.PhotoView> | { $type: string })[] 27 + labels?: ComAtprotoLabelDefs.Label[] 26 28 indexedAt: string 27 29 } 28 30
+101
__generated__/types/social/grain/labeler/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 + import type * as SocialGrainActorDefs from '../actor/defs.ts' 13 + import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.ts' 14 + import type * as AppBskyActorDefs from '../../../app/bsky/actor/defs.ts' 15 + import type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.ts' 16 + 17 + const is$typed = _is$typed, 18 + validate = _validate 19 + const id = 'social.grain.labeler.defs' 20 + 21 + export interface LabelerView { 22 + $type?: 'social.grain.labeler.defs#labelerView' 23 + uri: string 24 + cid: string 25 + creator: SocialGrainActorDefs.ProfileView 26 + favoriteCount?: number 27 + viewer?: LabelerViewerState 28 + indexedAt: string 29 + labels?: ComAtprotoLabelDefs.Label[] 30 + } 31 + 32 + const hashLabelerView = 'labelerView' 33 + 34 + export function isLabelerView<V>(v: V) { 35 + return is$typed(v, id, hashLabelerView) 36 + } 37 + 38 + export function validateLabelerView<V>(v: V) { 39 + return validate<LabelerView & V>(v, id, hashLabelerView) 40 + } 41 + 42 + export interface LabelerViewDetailed { 43 + $type?: 'social.grain.labeler.defs#labelerViewDetailed' 44 + uri: string 45 + cid: string 46 + creator: AppBskyActorDefs.ProfileView 47 + policies: SocialGrainActorDefs.LabelerPolicies 48 + favoriteCount?: number 49 + viewer?: LabelerViewerState 50 + indexedAt: string 51 + labels?: ComAtprotoLabelDefs.Label[] 52 + /** 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. */ 53 + reasonTypes?: ComAtprotoModerationDefs.ReasonType[] 54 + /** The set of subject types (account, record, etc) this service accepts reports on. */ 55 + subjectTypes?: ComAtprotoModerationDefs.SubjectType[] 56 + /** 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. */ 57 + subjectCollections?: string[] 58 + } 59 + 60 + const hashLabelerViewDetailed = 'labelerViewDetailed' 61 + 62 + export function isLabelerViewDetailed<V>(v: V) { 63 + return is$typed(v, id, hashLabelerViewDetailed) 64 + } 65 + 66 + export function validateLabelerViewDetailed<V>(v: V) { 67 + return validate<LabelerViewDetailed & V>(v, id, hashLabelerViewDetailed) 68 + } 69 + 70 + export interface LabelerViewerState { 71 + $type?: 'social.grain.labeler.defs#labelerViewerState' 72 + like?: string 73 + } 74 + 75 + const hashLabelerViewerState = 'labelerViewerState' 76 + 77 + export function isLabelerViewerState<V>(v: V) { 78 + return is$typed(v, id, hashLabelerViewerState) 79 + } 80 + 81 + export function validateLabelerViewerState<V>(v: V) { 82 + return validate<LabelerViewerState & V>(v, id, hashLabelerViewerState) 83 + } 84 + 85 + export interface LabelerPolicies { 86 + $type?: 'social.grain.labeler.defs#labelerPolicies' 87 + /** The label values which this labeler publishes. May include global or custom labels. */ 88 + labelValues: ComAtprotoLabelDefs.LabelValue[] 89 + /** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. */ 90 + labelValueDefinitions?: ComAtprotoLabelDefs.LabelValueDefinition[] 91 + } 92 + 93 + const hashLabelerPolicies = 'labelerPolicies' 94 + 95 + export function isLabelerPolicies<V>(v: V) { 96 + return is$typed(v, id, hashLabelerPolicies) 97 + } 98 + 99 + export function validateLabelerPolicies<V>(v: V) { 100 + return validate<LabelerPolicies & V>(v, id, hashLabelerPolicies) 101 + }
+42
__generated__/types/social/grain/labeler/service.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 type * as AppBskyLabelerDefs from '../../../app/bsky/labeler/defs.ts' 13 + import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs.ts' 14 + import type * as ComAtprotoModerationDefs from '../../../com/atproto/moderation/defs.ts' 15 + 16 + const is$typed = _is$typed, 17 + validate = _validate 18 + const id = 'social.grain.labeler.service' 19 + 20 + export interface Record { 21 + $type: 'social.grain.labeler.service' 22 + policies: AppBskyLabelerDefs.LabelerPolicies 23 + labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string } 24 + createdAt: string 25 + /** 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. */ 26 + reasonTypes?: ComAtprotoModerationDefs.ReasonType[] 27 + /** The set of subject types (account, record, etc) this service accepts reports on. */ 28 + subjectTypes?: ComAtprotoModerationDefs.SubjectType[] 29 + /** 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. */ 30 + subjectCollections?: string[] 31 + [k: string]: unknown 32 + } 33 + 34 + const hashRecord = 'main' 35 + 36 + export function isRecord<V>(v: V) { 37 + return is$typed(v, id, hashRecord) 38 + } 39 + 40 + export function validateRecord<V>(v: V) { 41 + return validate<Record & V>(v, id, hashRecord, true) 42 + }
+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
··· 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
··· 4 4 "app.bsky.feed.post", 5 5 "app.bsky.actor.profile", 6 6 "app.bsky.actor.defs", 7 - "app.bsky.graph.follow" 7 + "app.bsky.graph.follow", 8 + "com.atproto.label.defs", 9 + "com.atproto.moderation.defs" 8 10 ] 9 11 }
+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 + }
+7
lexicons/social/grain/actor/defs.json
··· 18 18 "maxLength": 2560, 19 19 "maxGraphemes": 256 20 20 }, 21 + "labels": { 22 + "type": "array", 23 + "items": { 24 + "ref": "com.atproto.label.defs#label", 25 + "type": "ref" 26 + } 27 + }, 21 28 "avatar": { "type": "string", "format": "uri" }, 22 29 "createdAt": { "type": "string", "format": "datetime" } 23 30 }
+4
lexicons/social/grain/gallery/defs.json
··· 22 22 ] 23 23 } 24 24 }, 25 + "labels": { 26 + "type": "array", 27 + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 28 + }, 25 29 "indexedAt": { "type": "string", "format": "datetime" } 26 30 } 27 31 }
+5
lexicons/social/grain/gallery/gallery.json
··· 11 11 "properties": { 12 12 "title": { "type": "string", "maxLength": 100 }, 13 13 "description": { "type": "string", "maxLength": 1000 }, 14 + "labels": { 15 + "type": "union", 16 + "description": "Self-label values for this post. Effectively content warnings.", 17 + "refs": ["com.atproto.label.defs#selfLabels"] 18 + }, 14 19 "createdAt": { "type": "string", "format": "datetime" } 15 20 } 16 21 }
+94
lexicons/social/grain/labelers/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.labeler.defs", 4 + "defs": { 5 + "labelerView": { 6 + "type": "object", 7 + "required": ["uri", "cid", "creator", "indexedAt"], 8 + "properties": { 9 + "uri": { "type": "string", "format": "at-uri" }, 10 + "cid": { "type": "string", "format": "cid" }, 11 + "creator": { 12 + "type": "ref", 13 + "ref": "social.grain.actor.defs#profileView" 14 + }, 15 + "favoriteCount": { "type": "integer", "minimum": 0 }, 16 + "viewer": { "type": "ref", "ref": "#labelerViewerState" }, 17 + "indexedAt": { "type": "string", "format": "datetime" }, 18 + "labels": { 19 + "type": "array", 20 + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 21 + } 22 + } 23 + }, 24 + "labelerViewDetailed": { 25 + "type": "object", 26 + "required": ["uri", "cid", "creator", "policies", "indexedAt"], 27 + "properties": { 28 + "uri": { "type": "string", "format": "at-uri" }, 29 + "cid": { "type": "string", "format": "cid" }, 30 + "creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" }, 31 + "policies": { 32 + "type": "ref", 33 + "ref": "social.grain.actor.defs#labelerPolicies" 34 + }, 35 + "favoriteCount": { "type": "integer", "minimum": 0 }, 36 + "viewer": { "type": "ref", "ref": "#labelerViewerState" }, 37 + "indexedAt": { "type": "string", "format": "datetime" }, 38 + "labels": { 39 + "type": "array", 40 + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 41 + }, 42 + "reasonTypes": { 43 + "description": "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.", 44 + "type": "array", 45 + "items": { 46 + "type": "ref", 47 + "ref": "com.atproto.moderation.defs#reasonType" 48 + } 49 + }, 50 + "subjectTypes": { 51 + "description": "The set of subject types (account, record, etc) this service accepts reports on.", 52 + "type": "array", 53 + "items": { 54 + "type": "ref", 55 + "ref": "com.atproto.moderation.defs#subjectType" 56 + } 57 + }, 58 + "subjectCollections": { 59 + "type": "array", 60 + "description": "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.", 61 + "items": { "type": "string", "format": "nsid" } 62 + } 63 + } 64 + }, 65 + "labelerViewerState": { 66 + "type": "object", 67 + "properties": { 68 + "like": { "type": "string", "format": "at-uri" } 69 + } 70 + }, 71 + "labelerPolicies": { 72 + "type": "object", 73 + "required": ["labelValues"], 74 + "properties": { 75 + "labelValues": { 76 + "type": "array", 77 + "description": "The label values which this labeler publishes. May include global or custom labels.", 78 + "items": { 79 + "type": "ref", 80 + "ref": "com.atproto.label.defs#labelValue" 81 + } 82 + }, 83 + "labelValueDefinitions": { 84 + "type": "array", 85 + "description": "Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.", 86 + "items": { 87 + "type": "ref", 88 + "ref": "com.atproto.label.defs#labelValueDefinition" 89 + } 90 + } 91 + } 92 + } 93 + } 94 + }
+47
lexicons/social/grain/labelers/service.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "social.grain.labeler.service", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A declaration of the existence of labeler service.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "required": ["policies", "createdAt"], 12 + "properties": { 13 + "policies": { 14 + "type": "ref", 15 + "ref": "app.bsky.labeler.defs#labelerPolicies" 16 + }, 17 + "labels": { 18 + "type": "union", 19 + "refs": ["com.atproto.label.defs#selfLabels"] 20 + }, 21 + "createdAt": { "type": "string", "format": "datetime" }, 22 + "reasonTypes": { 23 + "description": "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.", 24 + "type": "array", 25 + "items": { 26 + "type": "ref", 27 + "ref": "com.atproto.moderation.defs#reasonType" 28 + } 29 + }, 30 + "subjectTypes": { 31 + "description": "The set of subject types (account, record, etc) this service accepts reports on.", 32 + "type": "array", 33 + "items": { 34 + "type": "ref", 35 + "ref": "com.atproto.moderation.defs#subjectType" 36 + } 37 + }, 38 + "subjectCollections": { 39 + "type": "array", 40 + "description": "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.", 41 + "items": { "type": "string", "format": "nsid" } 42 + } 43 + } 44 + } 45 + } 46 + } 47 + }
+4
services/mod/.dockerignore
··· 1 + **/node_modules 2 + **/*.db 3 + *.toml 4 + .env
+3
services/mod/.env.example
··· 1 + MOD_SERVICE_PORT=8080 2 + MOD_SERVICE_DATABASE_URL=sqlite.db 3 + MOD_SERVICE_SIGNING_KEY=your-signing-key
+11
services/mod/Dockerfile
··· 1 + FROM denoland/deno:2.3.3 2 + 3 + WORKDIR /app 4 + 5 + COPY . . 6 + 7 + RUN deno cache main.ts 8 + 9 + EXPOSE 8080 10 + 11 + CMD ["run", "-A", "main.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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + { 2 + "lexicons": [ 3 + "com.atproto.label.defs", 4 + "com.atproto.label.queryLabels", 5 + "com.atproto.label.subscribeLabels" 6 + ] 7 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 16 16 gallery.creator.handle, 17 17 new AtUri(gallery.uri).rkey, 18 18 )} 19 - class={cn("flex w-full max-w-md aspect-[3/2] overflow-hidden", gap)} 19 + class={cn("flex w-full aspect-[3/2] overflow-hidden", gap)} 20 20 > 21 21 <div class="w-2/3 h-full"> 22 22 <img
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 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
··· 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
··· 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 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
··· 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
··· 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
··· 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
··· 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 {