A plain JavaScript validator for AT Protocol lexicon schemas
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};