a tiny atproto lexicon validator
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/tinylex
6
7// ---------------------------------------------------------------------------
8// String format names (see FORMATS validators below)
9// ---------------------------------------------------------------------------
10
11/**
12 * @typedef {"at-identifier" | "at-uri" | "cid" | "datetime" | "did" | "handle"
13 * | "nsid" | "tid" | "record-key" | "uri" | "language"} FormatName
14 */
15
16// ---------------------------------------------------------------------------
17// Structural atproto types
18// ---------------------------------------------------------------------------
19
20/** @typedef {{ readonly $bytes: string }} Bytes */
21/** @typedef {{ readonly $link: string }} CidLink */
22/**
23 * @typedef {{
24 * readonly $type: "blob";
25 * readonly mimeType: string;
26 * readonly size: number;
27 * readonly ref: CidLink;
28 * }} Blob
29 */
30
31// ---------------------------------------------------------------------------
32// Standard Schema v1 result types
33// ---------------------------------------------------------------------------
34
35/**
36 * @typedef {{
37 * readonly message: string;
38 * readonly path?: ReadonlyArray<{ readonly key: string | number }>;
39 * }} Issue
40 */
41
42/**
43 * @template T
44 * @typedef {{ readonly value: T; readonly issues?: undefined } | { readonly issues: ReadonlyArray<Issue> }} Result
45 */
46
47/**
48 * @template T
49 * @typedef {{
50 * readonly "~standard": {
51 * readonly version: 1;
52 * readonly vendor: "tinylex";
53 * readonly validate: (value: unknown) => Result<T>;
54 * readonly types?: { readonly input: unknown; readonly output: T };
55 * };
56 * }} StandardSchema
57 */
58
59// ---------------------------------------------------------------------------
60// Flat catalog map: "nsid#defName" | "nsid" (alias for "nsid#main") -> type
61// ---------------------------------------------------------------------------
62
63/** @typedef {Record<string, unknown>} CatalogMap */
64
65// ---------------------------------------------------------------------------
66// Ref resolution — direct key lookup into flat map
67// ---------------------------------------------------------------------------
68
69/**
70 * @template {string} Ref
71 * @template {CatalogMap} Catalog
72 * @template {string} CurrentNsid
73 * @typedef {Ref extends `#${infer Name}`
74 * ? `${CurrentNsid}#${Name}` extends keyof Catalog ? Catalog[`${CurrentNsid}#${Name}`] : unknown
75 * : Ref extends `${string}#${string}`
76 * ? Ref extends keyof Catalog ? Catalog[Ref] : unknown
77 * : `${Ref}#main` extends keyof Catalog
78 * ? Catalog[`${Ref}#main`]
79 * : Ref extends keyof Catalog ? Catalog[Ref] : unknown
80 * } ResolveRef
81 */
82
83// ---------------------------------------------------------------------------
84// Core type inference
85// ---------------------------------------------------------------------------
86
87/**
88 * @template Def
89 * @typedef {Def extends { const: infer C extends boolean } ? C : boolean} InferBoolean
90 */
91
92/**
93 * @template Def
94 * @typedef {Def extends { const: infer C extends number } ? C
95 * : Def extends { enum: ReadonlyArray<infer E extends number> } ? E
96 * : number
97 * } InferInteger
98 */
99
100/**
101 * @template Def
102 * @typedef {Def extends { const: infer C extends string } ? C
103 * : Def extends { enum: ReadonlyArray<infer E extends string> } ? E
104 * : string
105 * } InferString
106 */
107
108/**
109 * @template Def
110 * @template {CatalogMap} Catalog
111 * @template {string} CurrentNsid
112 * @typedef {Def extends { type: "null" } ? null
113 * : Def extends { type: "boolean" } ? InferBoolean<Def>
114 * : Def extends { type: "integer" } ? InferInteger<Def>
115 * : Def extends { type: "string" } ? InferString<Def>
116 * : Def extends { type: "bytes" } ? Bytes
117 * : Def extends { type: "cid-link" } ? CidLink
118 * : Def extends { type: "blob" } ? Blob
119 * : Def extends { type: "array"; items: infer Items } ? Infer<Items, Catalog, CurrentNsid>[]
120 * : Def extends { type: "object" | "params" } ? InferObject<Def, Catalog, CurrentNsid>
121 * : Def extends { type: "record"; record: infer R } ? InferObject<R, Catalog, CurrentNsid> & { readonly $type: CurrentNsid }
122 * : Def extends { type: "ref"; ref: infer R extends string } ? ResolveRef<R, Catalog, CurrentNsid>
123 * : Def extends { type: "union"; refs: ReadonlyArray<infer R extends string> } ? ResolveRef<R, Catalog, CurrentNsid>
124 * : Def extends { type: "unknown" } ? Record<string, unknown>
125 * : unknown
126 * } Infer
127 */
128
129// ---------------------------------------------------------------------------
130// Object inference
131// ---------------------------------------------------------------------------
132
133/**
134 * @template Props
135 * @template Req
136 * @typedef {Req extends ReadonlyArray<string> ? Extract<Req[number], keyof Props> : never} RequiredKeys
137 */
138
139/**
140 * @template Props
141 * @template Req
142 * @typedef {Req extends ReadonlyArray<string> ? Exclude<keyof Props, Req[number]> : keyof Props} OptionalKeys
143 */
144
145/**
146 * @template Nul
147 * @typedef {Nul extends ReadonlyArray<string> ? Nul[number] : never} NullableKeys
148 */
149
150/**
151 * @template K
152 * @template Props
153 * @template Nul
154 * @template {CatalogMap} Catalog
155 * @template {string} CurrentNsid
156 * @typedef {K extends keyof Props
157 * ? K extends NullableKeys<Nul>
158 * ? Infer<Props[K], Catalog, CurrentNsid> | null
159 * : Infer<Props[K], Catalog, CurrentNsid>
160 * : never
161 * } InferField
162 */
163
164/**
165 * @template Def
166 * @template {CatalogMap} Catalog
167 * @template {string} CurrentNsid
168 * @typedef {Def extends { properties: infer Props; required?: infer Req; nullable?: infer Nul }
169 * ? { readonly [K in RequiredKeys<Props, Req>]: InferField<K, Props, Nul, Catalog, CurrentNsid> }
170 * & { readonly [K in OptionalKeys<Props, Req>]?: InferField<K, Props, Nul, Catalog, CurrentNsid> }
171 * : Record<string, unknown>
172 * } InferObject
173 */
174
175// ---------------------------------------------------------------------------
176// Flatten a lexicon file's defs into the catalog's flat map format:
177// "nsid#defName" -> InferredType
178// "nsid" -> InferredType (alias for #main, if present)
179// ---------------------------------------------------------------------------
180
181/**
182 * @template {string} Nsid
183 * @template Defs
184 * @template {CatalogMap} Catalog
185 * @typedef {{ readonly [K in keyof Defs as `${Nsid}#${string & K}`]: Infer<Defs[K], Catalog, Nsid> }
186 * & ("main" extends keyof Defs ? { readonly [N in Nsid]: Infer<Defs["main"], Catalog, Nsid> } : {})
187 * } FlattenDefs
188 */
189
190// ---------------------------------------------------------------------------
191// Internal runtime types
192// ---------------------------------------------------------------------------
193
194/** @typedef {any} SchemaDef */
195
196/**
197 * @typedef {Object} ValidateOptions
198 * @property {(ref: string, currentNsid?: string) => { nsid: string, def: SchemaDef }} [resolveRef]
199 * @property {string} [nsid]
200 */
201
202// ---------------------------------------------------------------------------
203// String format validators
204// ---------------------------------------------------------------------------
205
206/** Module-level helpers — shared by FORMATS entries and other validators. */
207const isDid = (/** @type {string} */ v) => /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(v);
208
209const isHandle = (/** @type {string} */ v) =>
210 v.length <= 253 &&
211 /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$/.test(v);
212
213const isCid = (/** @type {string} */ v) => /^[a-zA-Z][a-zA-Z0-9+/=_-]+$/.test(v);
214
215/** @type {Record<FormatName, (v: string) => boolean>} */
216const FORMATS = {
217 did: isDid,
218 handle: isHandle,
219 "at-identifier": (v) => isDid(v) || isHandle(v),
220 cid: isCid,
221 nsid: (v) =>
222 v.length <= 317 && /^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z0-9-]+){1,}\.[a-zA-Z][a-zA-Z0-9]*$/.test(v),
223 "at-uri": (v) =>
224 /^at:\/\/[a-zA-Z0-9._:%-]+(\/[a-zA-Z0-9._~:@!$&'()*+,;=%-]*)*(\/[a-zA-Z0-9._~:@!$&'()*+,;=%-]*)?$/.test(
225 v,
226 ),
227 datetime: (v) =>
228 /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(
229 v,
230 ) && !v.endsWith("-00:00"),
231 tid: (v) => /^[2-7a-z]{13}$/.test(v),
232 "record-key": (v) => v.length > 0 && /^[a-zA-Z0-9_~.:-]{1,512}$/.test(v),
233 uri: (v) => v.length <= 8192 && /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(v),
234 language: (v) => /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.test(v),
235};
236
237// ---------------------------------------------------------------------------
238// Internal helpers
239// ---------------------------------------------------------------------------
240
241const TEXT_ENCODER = new TextEncoder();
242const SEGMENTER = typeof Intl !== "undefined" && Intl.Segmenter ? new Intl.Segmenter() : null;
243
244/** @typedef {ReadonlyArray<{ key: string | number }>} Path */
245
246/**
247 * @param {Path} path
248 * @param {string | number} key
249 * @returns {Path}
250 */
251const push = (path, key) => [...path, { key }];
252
253/**
254 * @param {string} message
255 * @param {Path} path
256 * @returns {{ issues: ReadonlyArray<Issue> }}
257 */
258const fail = (message, path) => ({ issues: [{ message, ...(path.length ? { path } : {}) }] });
259
260/** @param {unknown} x */
261const isPlainObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
262
263/**
264 * Bound check helper — returns a failure when `value` falls outside [min, max],
265 * or null when in range (or both bounds are undefined). The min/max labels are
266 * passed explicitly so callers can match the lexicon spec's exact key names
267 * (e.g. "minimum"/"maximum" vs "minLength"/"maxLength").
268 *
269 * @param {number} value
270 * @param {number | undefined} min
271 * @param {number | undefined} max
272 * @param {string} minLabel Name of the min bound (e.g. "minLength").
273 * @param {string} maxLabel Name of the max bound (e.g. "maxLength").
274 * @param {string} prefix Prefix for the error message (e.g. "Byte length", "Value").
275 * @param {Path} path
276 * @returns {{ issues: ReadonlyArray<Issue> } | null}
277 */
278function checkRange(value, min, max, minLabel, maxLabel, prefix, path) {
279 if (min !== undefined && value < min)
280 return fail(`${prefix} ${value} < ${minLabel} ${min}`, path);
281 if (max !== undefined && value > max)
282 return fail(`${prefix} ${value} > ${maxLabel} ${max}`, path);
283 return null;
284}
285
286// ---------------------------------------------------------------------------
287// Lexicon catalog — immutable builder
288// ---------------------------------------------------------------------------
289
290/**
291 * @template {CatalogMap} [Catalog={}]
292 */
293export default class TinyLex {
294 #files = new Map();
295
296 /**
297 * @template {string} const Nsid
298 * @template {Record<string, unknown>} const Defs
299 * @param {{ readonly id: Nsid; readonly defs: Defs; readonly [k: string]: unknown }} file
300 * @returns {TinyLex<Catalog & FlattenDefs<Nsid, Defs, Catalog & FlattenDefs<Nsid, Defs, Catalog>>>}
301 */
302 add(file) {
303 this.#files.set(file.id, file);
304 return /** @type {any} */ (this);
305 }
306
307 /**
308 * @param {string} ref
309 * @param {string} [currentNsid]
310 * @returns {{ nsid: string, def: SchemaDef }}
311 */
312 #resolve(ref, currentNsid) {
313 let nsid, name;
314 if (ref.startsWith("#")) {
315 if (!currentNsid) throw new Error(`Cannot resolve local ref "${ref}" without a current NSID`);
316 nsid = currentNsid;
317 name = ref.slice(1);
318 } else {
319 const hash = ref.indexOf("#");
320 nsid = hash === -1 ? ref : ref.slice(0, hash);
321 name = hash === -1 ? "main" : ref.slice(hash + 1);
322 }
323 const file = this.#files.get(nsid);
324 if (!file) throw new Error(`Lexicon not found in catalog: "${nsid}"`);
325 const def = file.defs[name];
326 if (!def) throw new Error(`Definition "${name}" not found in "${nsid}"`);
327 return { nsid, def };
328 }
329
330 /**
331 * Create a Standard Schema v1 schema for a ref in this catalog.
332 * https://standardschema.dev
333 *
334 * @template {string} Ref
335 * @param {Ref} ref
336 * @returns {StandardSchema<ResolveRef<Ref, Catalog, never>>}
337 */
338 schema(ref) {
339 const { def, nsid } = this.#resolve(ref);
340 const resolveRef = (/** @type {string} */ r, /** @type {string | undefined} */ n) =>
341 this.#resolve(r, n);
342 return {
343 "~standard": {
344 version: 1,
345 vendor: "tinylex",
346 validate: (value) =>
347 /** @type {Result<ResolveRef<Ref, Catalog, never>>} */ (
348 validateType(value, def, { resolveRef, nsid }, [])
349 ),
350 },
351 };
352 }
353
354 /** @returns {string[]} */
355 nsids() {
356 return [...this.#files.keys()];
357 }
358}
359
360// ---------------------------------------------------------------------------
361// Internal type dispatch — not exported
362// ---------------------------------------------------------------------------
363
364/** @typedef {(data: any, def: any, opts: ValidateOptions, path: Path) => Result<unknown>} Validator */
365
366/**
367 * @param {unknown} data
368 * @param {SchemaDef} def
369 * @param {ValidateOptions} opts
370 * @param {Path} path
371 * @returns {Result<unknown>}
372 */
373function validateType(data, def, opts, path) {
374 const fn = VALIDATORS[def.type];
375 return fn ? fn(data, def, opts, path) : fail(`Unknown schema type: "${def.type}"`, path);
376}
377
378/**
379 * Shared const/enum check. Returns failure on mismatch, null on success.
380 * @param {any} data
381 * @param {any} def
382 * @param {Path} path
383 * @returns {{ issues: ReadonlyArray<Issue> } | null}
384 */
385function checkConstEnum(data, def, path) {
386 if (def.const !== undefined && data !== def.const)
387 return fail(`Expected const ${JSON.stringify(def.const)}`, path);
388 if (def.enum !== undefined && !def.enum.includes(data))
389 return fail(`${JSON.stringify(data)} not in enum ${JSON.stringify(def.enum)}`, path);
390 return null;
391}
392
393/**
394 * Canonicalize a union ref to its absolute "nsid#name" form.
395 * @param {string} ref
396 * @param {string | undefined} currentNsid
397 * @returns {string}
398 */
399function canonicalizeRef(ref, currentNsid) {
400 if (ref.startsWith("#")) return `${currentNsid ?? ""}${ref}`;
401 if (ref.includes("#")) return ref;
402 return `${ref}#main`;
403}
404
405/** @type {Validator} */
406const validateNull = (data, _def, _opts, path) =>
407 data === null ? { value: data } : fail("Expected null", path);
408
409/** @type {Validator} */
410function validateBoolean(data, def, _opts, path) {
411 if (typeof data !== "boolean") return fail(`Expected boolean, got ${typeof data}`, path);
412 return checkConstEnum(data, def, path) ?? { value: data };
413}
414
415/** @type {Validator} */
416function validateInteger(data, def, _opts, path) {
417 if (typeof data !== "number" || !Number.isInteger(data))
418 return fail(`Expected integer, got ${typeof data}`, path);
419 return (
420 checkConstEnum(data, def, path) ??
421 checkRange(data, def.minimum, def.maximum, "minimum", "maximum", "Value", path) ?? {
422 value: data,
423 }
424 );
425}
426
427/** @type {Validator} */
428function validateString(data, def, _opts, path) {
429 if (typeof data !== "string") return fail(`Expected string, got ${typeof data}`, path);
430 const ce = checkConstEnum(data, def, path);
431 if (ce) return ce;
432 const byteLen = TEXT_ENCODER.encode(data).length;
433 const lenFail = checkRange(
434 byteLen,
435 def.minLength,
436 def.maxLength,
437 "minLength",
438 "maxLength",
439 "Byte length",
440 path,
441 );
442 if (lenFail) return lenFail;
443 if (def.minGraphemes !== undefined || def.maxGraphemes !== undefined) {
444 const count = SEGMENTER ? [...SEGMENTER.segment(data)].length : [...data].length;
445 const gFail = checkRange(
446 count,
447 def.minGraphemes,
448 def.maxGraphemes,
449 "minGraphemes",
450 "maxGraphemes",
451 "Grapheme count",
452 path,
453 );
454 if (gFail) return gFail;
455 }
456 if (def.format !== undefined) {
457 const fn = FORMATS[/** @type {FormatName} */ (def.format)];
458 if (!fn) return fail(`Unknown string format: "${def.format}"`, path);
459 if (!fn(data)) return fail(`Does not match format "${def.format}": "${data}"`, path);
460 }
461 return { value: data };
462}
463
464/** @type {Validator} */
465function validateBytes(data, _def, _opts, path) {
466 if (!isPlainObject(data) || typeof data.$bytes !== "string")
467 return fail(`Expected {$bytes: "<base64>"}, got ${JSON.stringify(data)}`, path);
468 try {
469 atob(data.$bytes);
470 } catch {
471 return fail("Invalid base64 in $bytes", path);
472 }
473 return { value: data };
474}
475
476/** @type {Validator} */
477function validateCidLink(data, _def, _opts, path) {
478 if (!isPlainObject(data) || typeof data.$link !== "string")
479 return fail(`Expected {$link: "<cid>"}, got ${JSON.stringify(data)}`, path);
480 if (!isCid(data.$link)) return fail(`Invalid CID: "${data.$link}"`, path);
481 return { value: data };
482}
483
484/** @type {Validator} */
485function validateBlob(data, def, _opts, path) {
486 if (!data || typeof data !== "object") return fail(`Expected blob object`, path);
487 if (data.$type !== "blob") return fail(`Blob must have $type "blob", got "${data.$type}"`, path);
488 if (typeof data.mimeType !== "string") return fail(`Blob must have string "mimeType"`, path);
489 if (def.accept?.length) {
490 const ok = def.accept.some((/** @type {string} */ p) => {
491 if (p === "*/*") return true;
492 if (p.endsWith("/*")) return data.mimeType.startsWith(p.slice(0, -1));
493 return p === data.mimeType;
494 });
495 if (!ok) return fail(`mimeType "${data.mimeType}" not in [${def.accept.join(", ")}]`, path);
496 }
497 if (def.maxSize !== undefined) {
498 if (typeof data.size !== "number") return fail(`Blob must have numeric "size"`, path);
499 if (data.size > def.maxSize)
500 return fail(`Blob size ${data.size} > maxSize ${def.maxSize}`, path);
501 }
502 return { value: data };
503}
504
505/** @type {Validator} */
506function validateArray(data, def, opts, path) {
507 if (!Array.isArray(data)) return fail(`Expected array, got ${typeof data}`, path);
508 const lenFail = checkRange(
509 data.length,
510 def.minLength,
511 def.maxLength,
512 "minLength",
513 "maxLength",
514 "Length",
515 path,
516 );
517 if (lenFail) return lenFail;
518 if (!def.items) return fail(`Array schema missing "items"`, path);
519 const issues = data.flatMap(
520 (item, i) => validateType(item, def.items, opts, push(path, i)).issues ?? [],
521 );
522 return issues.length ? { issues } : { value: data };
523}
524
525/**
526 * Handles both `object` and `params` — params skips `nullable` handling.
527 * @type {Validator}
528 */
529function validateObject(data, def, opts, path) {
530 const isParams = def.type === "params";
531 if (!isPlainObject(data)) {
532 return fail(
533 isParams
534 ? `Expected params object`
535 : `Expected object, got ${Array.isArray(data) ? "array" : typeof data}`,
536 path,
537 );
538 }
539 const required = new Set(def.required ?? []);
540 const nullable = isParams ? null : new Set(def.nullable ?? []);
541 const noun = isParams ? "param" : "field";
542 const issues = [];
543 for (const field of required)
544 if (!(field in data)) issues.push({ message: `Missing required ${noun} "${field}"`, path });
545 for (const [field, fieldDef] of Object.entries(def.properties ?? {})) {
546 if (!(field in data)) continue;
547 if (nullable && data[field] === null) {
548 if (!nullable.has(field))
549 issues.push({ message: `Field "${field}" is not nullable`, path: push(path, field) });
550 continue;
551 }
552 const result = validateType(data[field], fieldDef, opts, push(path, field));
553 if (result.issues) issues.push(...result.issues);
554 }
555 return issues.length ? { issues } : { value: data };
556}
557
558/** @type {Validator} */
559function validateRecord(data, def, opts, path) {
560 if (!isPlainObject(data))
561 return fail(`Expected object, got ${Array.isArray(data) ? "array" : typeof data}`, path);
562 if (opts.nsid) {
563 if (typeof data.$type !== "string") return fail(`Record must have "$type"`, path);
564 if (data.$type !== opts.nsid && data.$type !== `${opts.nsid}#main`)
565 return fail(`$type "${data.$type}" does not match "${opts.nsid}"`, path);
566 }
567 return validateObject(data, def.record, opts, path);
568}
569
570/** @type {Validator} */
571function validateRef(data, def, opts, path) {
572 if (!def.ref) return fail(`ref schema missing "ref"`, path);
573 if (!opts.resolveRef) return fail(`Cannot resolve ref "${def.ref}" without a catalog`, path);
574 try {
575 const { def: resolved, nsid } = opts.resolveRef(def.ref, opts.nsid);
576 return validateType(data, resolved, { ...opts, nsid }, path);
577 } catch (e) {
578 return fail(e instanceof Error ? e.message : String(e), path);
579 }
580}
581
582/** @type {Validator} */
583function validateUnion(data, def, opts, path) {
584 if (!isPlainObject(data)) return fail(`Union value must be an object`, path);
585 const refs = def.refs ?? [];
586 const closed = def.closed ?? false;
587 if (closed && refs.length === 0) return fail(`Closed union with empty refs is invalid`, path);
588 if (typeof data.$type !== "string") return fail(`Union value must include "$type"`, path);
589 for (const ref of refs) {
590 const full = canonicalizeRef(ref, opts.nsid);
591 if (full !== data.$type && full !== `${data.$type}#main`) continue;
592 if (opts.resolveRef) {
593 try {
594 const { def: resolved, nsid } = opts.resolveRef(ref, opts.nsid);
595 return validateType(data, resolved, { ...opts, nsid }, path);
596 } catch {
597 /* catalog miss on open union */
598 }
599 }
600 return { value: data };
601 }
602 return closed
603 ? fail(`$type "${data.$type}" not in closed union: [${refs.join(", ")}]`, path)
604 : { value: data };
605}
606
607/** @type {Validator} */
608function validateUnknown(data, _def, _opts, path) {
609 if (!isPlainObject(data)) return fail(`unknown type must be an object`, path);
610 if ("$link" in data) return fail(`unknown type cannot have $link at top level`, path);
611 if ("$bytes" in data) return fail(`unknown type cannot have $bytes at top level`, path);
612 return { value: data };
613}
614
615/** @type {Validator} */
616const validateToken = (_data, _def, _opts, path) =>
617 fail("token type has no data representation", path);
618
619/** @type {Record<string, Validator>} */
620const VALIDATORS = {
621 null: validateNull,
622 boolean: validateBoolean,
623 integer: validateInteger,
624 string: validateString,
625 bytes: validateBytes,
626 "cid-link": validateCidLink,
627 blob: validateBlob,
628 array: validateArray,
629 object: validateObject,
630 params: validateObject,
631 ref: validateRef,
632 union: validateUnion,
633 unknown: validateUnknown,
634 record: validateRecord,
635 token: validateToken,
636};