A plain JavaScript validator for AT Protocol lexicon schemas
at main 294 lines 11 kB view raw
1/** 2 * Validates a datetime string against RFC 3339/ISO 8601 format. 3 * Timezone is required; whole seconds minimum with optional fractional precision. 4 * @see https://atproto.com/specs/lexicon - datetime format 5 * @param {string} value - The datetime string to validate 6 * @returns {boolean} True if valid datetime format 7 */ 8export function isValidDatetime(value: string): boolean; 9/** 10 * Validates a handle identifier. 11 * Handles are DNS hostnames with specific constraints: 12 * - Max 253 characters total 13 * - At least 2 labels separated by periods 14 * - Labels: 1-63 chars, alphanumeric and hyphens, can't start/end with hyphen 15 * - TLD can't start with digit; certain TLDs are disallowed 16 * @see https://atproto.com/specs/handle 17 * @param {string} value - The handle to validate 18 * @returns {boolean} True if valid handle format 19 */ 20export function isValidHandle(value: string): boolean; 21/** 22 * Validates a Decentralized Identifier (DID). 23 * DIDs follow the pattern: did:[method]:[identifier] 24 * - Starts with lowercase "did:" 25 * - Method: 1+ lowercase letters, followed by ":" 26 * - Identifier: alphanumeric, period, underscore, colon, hyphen, percent-encoded 27 * - Max 2048 characters (AT Protocol limit) 28 * @see https://atproto.com/specs/did 29 * @param {string} value - The DID to validate 30 * @returns {boolean} True if valid DID format 31 */ 32export function isValidDid(value: string): boolean; 33/** 34 * Validates a generic URI per RFC 3986. 35 * Scheme is case-insensitive; uppercase accepted for robustness. 36 * Max 8192 characters. 37 * @see https://atproto.com/specs/lexicon - uri format 38 * @param {string} value - The URI to validate 39 * @returns {boolean} True if valid URI format 40 */ 41export function isValidUri(value: string): boolean; 42/** 43 * Validates an AT Protocol URI (at://). 44 * Structure: at://AUTHORITY[/COLLECTION[/RKEY]] 45 * - Authority: valid handle or DID (required) 46 * - Collection: valid NSID (optional) 47 * - Record key: valid record-key (optional) 48 * Max 8KB. 49 * @see https://atproto.com/specs/at-uri-scheme 50 * @param {string} value - The AT-URI to validate 51 * @returns {boolean} True if valid AT-URI format 52 */ 53export function isValidAtUri(value: string): boolean; 54/** 55 * Validates a Timestamp Identifier (TID). 56 * TIDs are 64-bit integers encoded as 13-character base32-sortable strings. 57 * - Exactly 13 characters 58 * - First char: [234567abcdefghij] (top bit always 0) 59 * - All chars: base32-sortable alphabet (234567abcdefghijklmnopqrstuvwxyz) 60 * @see https://atproto.com/specs/tid 61 * @param {string} value - The TID to validate 62 * @returns {boolean} True if valid TID format 63 */ 64export function isValidTid(value: string): boolean; 65/** 66 * Validates a record key. 67 * Record keys can contain: a-z, A-Z, 0-9, period, hyphen, underscore, colon, tilde. 68 * Max 512 characters. Cannot be "." or "..". 69 * @see https://atproto.com/specs/lexicon - record-key format 70 * @param {string} value - The record key to validate 71 * @returns {boolean} True if valid record key format 72 */ 73export function isValidRecordKey(value: string): boolean; 74/** 75 * Validates a Content Identifier (CID). 76 * AT Protocol only blesses CIDv1 format (rejects CIDv0 "Qm" prefix). 77 * - 8-256 characters 78 * - Alphanumeric only 79 * - No "Qmb" prefix (CIDv0) 80 * @see https://atproto.com/specs/data-model - CID/Link section 81 * @param {string} value - The CID to validate 82 * @returns {boolean} True if valid CID format 83 */ 84export function isValidCid(value: string): boolean; 85/** 86 * Validates a BCP-47 language tag. 87 * Simplified validation: 2-3 letter primary subtag with optional subtags. 88 * Supports grandfathered "i-" prefixed tags. 89 * @see https://atproto.com/specs/lexicon - language format 90 * @param {string} value - The language tag to validate 91 * @returns {boolean} True if valid language tag format 92 */ 93export function isValidLanguageTag(value: string): boolean; 94/** 95 * Validates a Namespaced Identifier (NSID). 96 * Structure: reversed-domain.name (e.g., "app.bsky.feed.post") 97 * - Max 317 characters, minimum 3 segments 98 * - Authority (domain): max 253 chars, at least 2 segments, handle-like rules 99 * - Name segment: 1-63 chars, letters and digits only, must start with lowercase letter 100 * @see https://atproto.com/specs/nsid 101 * @param {string} value - The NSID to validate 102 * @returns {boolean} True if valid NSID format 103 */ 104export function isValidNsid(value: string): boolean; 105/** 106 * Validates an AT Protocol identifier (handle or DID). 107 * @see https://atproto.com/specs/lexicon - at-identifier format 108 * @param {string} value - The identifier to validate 109 * @returns {boolean} True if valid handle or DID 110 */ 111export function isValidAtIdentifier(value: string): boolean; 112/** 113 * Validate lexicon schemas are well-formed. 114 * Validates each definition in each lexicon against the Lexicon specification. 115 * @see https://atproto.com/specs/lexicon 116 * @param {Lexicon[]} lexicons - Array of lexicon JSON objects 117 * @returns {null | Object<string, string[]>} null if valid, errors by NSID if invalid 118 * @example 119 * const errors = validateLexicons([myLexicon]); 120 * if (errors) { 121 * console.error('Invalid lexicons:', errors); 122 * } 123 */ 124export function validateLexicons(lexicons: Lexicon[]): null | { 125 [x: string]: string[]; 126}; 127/** 128 * Validate a record against a lexicon schema. 129 * The collection NSID must match a lexicon with a "record" type main definition. 130 * @see https://atproto.com/specs/lexicon - Record type 131 * @param {Lexicon[]} lexicons - Array of lexicon JSON objects 132 * @param {string} collection - NSID of the collection (e.g., "app.bsky.feed.post") 133 * @param {object} record - The record data to validate 134 * @returns {null | {path: string, message: string}} null if valid, error if invalid 135 * @example 136 * const error = validateRecord(lexicons, 'app.bsky.feed.post', postData); 137 * if (error) { 138 * console.error(`Validation error at ${error.path}: ${error.message}`); 139 * } 140 */ 141export function validateRecord(lexicons: Lexicon[], collection: string, record: object): null | { 142 path: string; 143 message: string; 144}; 145export function defineLexicon<T extends Lexicon>(lex: T): T; 146export type LexiconFormat = "datetime" | "uri" | "at-uri" | "did" | "handle" | "at-identifier" | "nsid" | "cid" | "language" | "tid" | "record-key"; 147export type LexiconStringSchema = { 148 type: "string"; 149 description?: string | undefined; 150 minLength?: number | undefined; 151 maxLength?: number | undefined; 152 minGraphemes?: number | undefined; 153 maxGraphemes?: number | undefined; 154 format?: LexiconFormat | undefined; 155 enum?: string[] | undefined; 156 const?: string | undefined; 157 knownValues?: string[] | undefined; 158 default?: string | undefined; 159}; 160export type LexiconIntegerSchema = { 161 type: "integer"; 162 description?: string | undefined; 163 minimum?: number | undefined; 164 maximum?: number | undefined; 165 enum?: number[] | undefined; 166 const?: number | undefined; 167 default?: number | undefined; 168}; 169export type LexiconBooleanSchema = { 170 type: "boolean"; 171 description?: string | undefined; 172 const?: boolean | undefined; 173 default?: boolean | undefined; 174}; 175export type LexiconObjectSchema = { 176 type: "object"; 177 description?: string | undefined; 178 properties?: { 179 [x: string]: LexiconSchema; 180 } | undefined; 181 required?: string[] | undefined; 182 nullable?: string[] | undefined; 183}; 184export type LexiconArraySchema = { 185 type: "array"; 186 description?: string | undefined; 187 items: LexiconSchema; 188 minLength?: number | undefined; 189 maxLength?: number | undefined; 190}; 191export type LexiconRefSchema = { 192 type: "ref"; 193 description?: string | undefined; 194 ref: string; 195}; 196export type LexiconUnionSchema = { 197 type: "union"; 198 description?: string | undefined; 199 refs: string[]; 200 closed?: boolean | undefined; 201}; 202export type LexiconBlobSchema = { 203 type: "blob"; 204 description?: string | undefined; 205 accept?: string[] | undefined; 206 maxSize?: number | undefined; 207}; 208export type LexiconBytesSchema = { 209 type: "bytes"; 210 description?: string | undefined; 211 minLength?: number | undefined; 212 maxLength?: number | undefined; 213}; 214export type LexiconTokenSchema = { 215 type: "token"; 216 description?: string | undefined; 217}; 218export type LexiconUnknownSchema = { 219 type: "unknown"; 220 description?: string | undefined; 221}; 222export type LexiconCidLinkSchema = { 223 type: "cid-link"; 224 description?: string | undefined; 225}; 226export type LexiconNullSchema = { 227 type: "null"; 228 description?: string | undefined; 229}; 230export type LexiconSchema = LexiconStringSchema | LexiconIntegerSchema | LexiconBooleanSchema | LexiconObjectSchema | LexiconArraySchema | LexiconRefSchema | LexiconUnionSchema | LexiconBlobSchema | LexiconBytesSchema | LexiconTokenSchema | LexiconUnknownSchema | LexiconCidLinkSchema | LexiconNullSchema; 231export type LexiconBody = { 232 description?: string | undefined; 233 encoding: string; 234 schema?: LexiconSchema | undefined; 235}; 236export type LexiconMessage = { 237 description?: string | undefined; 238 schema?: LexiconSchema | undefined; 239}; 240export type LexiconRecordDef = { 241 type: "record"; 242 description?: string | undefined; 243 key?: string | undefined; 244 record: LexiconObjectSchema; 245}; 246export type LexiconParamsDef = { 247 type: "params"; 248 description?: string | undefined; 249 properties?: { 250 [x: string]: LexiconSchema; 251 } | undefined; 252 required?: string[] | undefined; 253}; 254export type LexiconQueryDef = { 255 type: "query"; 256 description?: string | undefined; 257 parameters?: LexiconObjectSchema | undefined; 258 output?: LexiconBody | undefined; 259 errors?: object[] | undefined; 260}; 261export type LexiconProcedureDef = { 262 type: "procedure"; 263 description?: string | undefined; 264 parameters?: LexiconObjectSchema | undefined; 265 input?: LexiconBody | undefined; 266 output?: LexiconBody | undefined; 267 errors?: object[] | undefined; 268}; 269export type LexiconSubscriptionDef = { 270 type: "subscription"; 271 description?: string | undefined; 272 parameters?: LexiconObjectSchema | undefined; 273 message?: LexiconMessage | undefined; 274 errors?: object[] | undefined; 275}; 276export type LexiconDef = LexiconRecordDef | LexiconQueryDef | LexiconProcedureDef | LexiconSubscriptionDef | LexiconParamsDef | LexiconSchema; 277export type Lexicon = { 278 lexicon: 1; 279 id: string; 280 revision?: number | undefined; 281 description?: string | undefined; 282 defs: { 283 [x: string]: LexiconDef; 284 }; 285}; 286export type ValidationContext = { 287 lexicons: Lexicon[]; 288 currentLexicon: string; 289 path?: string | undefined; 290}; 291export type ValidationError = { 292 path: string; 293 message: string; 294};