atproto user agency toolkit for individuals and groups
at main 98 lines 3.0 kB view raw
1import { parse, ValidationError, type BaseSchema } from "@atcute/lexicons/validations"; 2import { validateP2pdsRecord, loadP2pdsLexicons, getLexicon } from "./lexicons.js"; 3 4import { 5 AppBskyActorProfile, 6 AppBskyFeedGenerator, 7 AppBskyFeedLike, 8 AppBskyFeedPost, 9 AppBskyFeedPostgate, 10 AppBskyFeedRepost, 11 AppBskyFeedThreadgate, 12 AppBskyGraphBlock, 13 AppBskyGraphFollow, 14 AppBskyGraphList, 15 AppBskyGraphListblock, 16 AppBskyGraphListitem, 17 AppBskyGraphStarterpack, 18 AppBskyGraphVerification, 19 AppBskyLabelerService, 20} from "@atcute/bluesky"; 21 22/** 23 * Map of collection NSID to validation schema. 24 */ 25const recordSchemas: Record<string, BaseSchema> = { 26 "app.bsky.actor.profile": AppBskyActorProfile.mainSchema, 27 "app.bsky.feed.generator": AppBskyFeedGenerator.mainSchema, 28 "app.bsky.feed.like": AppBskyFeedLike.mainSchema, 29 "app.bsky.feed.post": AppBskyFeedPost.mainSchema, 30 "app.bsky.feed.postgate": AppBskyFeedPostgate.mainSchema, 31 "app.bsky.feed.repost": AppBskyFeedRepost.mainSchema, 32 "app.bsky.feed.threadgate": AppBskyFeedThreadgate.mainSchema, 33 "app.bsky.graph.block": AppBskyGraphBlock.mainSchema, 34 "app.bsky.graph.follow": AppBskyGraphFollow.mainSchema, 35 "app.bsky.graph.list": AppBskyGraphList.mainSchema, 36 "app.bsky.graph.listblock": AppBskyGraphListblock.mainSchema, 37 "app.bsky.graph.listitem": AppBskyGraphListitem.mainSchema, 38 "app.bsky.graph.starterpack": AppBskyGraphStarterpack.mainSchema, 39 "app.bsky.graph.verification": AppBskyGraphVerification.mainSchema, 40 "app.bsky.labeler.service": AppBskyLabelerService.mainSchema, 41}; 42 43// Load p2pds lexicons at module init so they're available for validation. 44loadP2pdsLexicons(); 45 46/** 47 * Record validator for AT Protocol records. 48 * Uses optimistic validation: known schemas are validated, unknown are allowed. 49 * Delegates org.p2pds.* collections to the custom lexicon validator. 50 */ 51export class RecordValidator { 52 private strictMode: boolean; 53 54 constructor(options: { strict?: boolean } = {}) { 55 this.strictMode = options.strict ?? false; 56 } 57 58 validateRecord(collection: string, record: unknown): void { 59 // Delegate p2pds collections to the custom lexicon validator 60 if (collection.startsWith("org.p2pds.")) { 61 validateP2pdsRecord(collection, record); 62 return; 63 } 64 65 const schema = recordSchemas[collection]; 66 67 if (!schema) { 68 if (this.strictMode) { 69 throw new Error( 70 `No lexicon schema loaded for collection: ${collection}`, 71 ); 72 } 73 return; 74 } 75 76 try { 77 parse(schema, record); 78 } catch (error) { 79 if (error instanceof ValidationError) { 80 throw new Error( 81 `Lexicon validation failed for ${collection}: ${error.message}`, 82 ); 83 } 84 throw error; 85 } 86 } 87 88 hasSchema(collection: string): boolean { 89 return collection in recordSchemas || getLexicon(collection) !== undefined; 90 } 91 92 getLoadedSchemas(): string[] { 93 const p2pdsSchemas = Array.from(loadP2pdsLexicons().keys()); 94 return [...Object.keys(recordSchemas), ...p2pdsSchemas]; 95 } 96} 97 98export const validator = new RecordValidator({ strict: false });