a tiny atproto lexicon validator
0

Configure Feed

Select the types of activity you want to include in your feed.

tinylex / tinylex.js
22 kB 636 lines
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};