a tiny atproto lexicon validator
0

Configure Feed

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

tinylex / tinylex-install
3.9 kB 114 lines
1#!/bin/sh 2":" //# ; if command -v bun >/dev/null 2>&1; then exec bun "$0" "$@"; elif command -v deno >/dev/null 2>&1; then exec deno run -A "$0" "$@"; else exec node "$0" "$@"; fi 3 4// This Source Code Form is subject to the terms of the Mozilla Public 5// License, v. 2.0. If a copy of the MPL was not distributed with this 6// file, You can obtain one at https://mozilla.org/MPL/2.0/. 7// 8// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/tinylex 9 10import { writeFile, mkdir } from "node:fs/promises"; 11import { dirname, join } from "node:path"; 12 13async function resolveTxt(host) { 14 const r = await fetch( 15 `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(host)}&type=TXT`, 16 { headers: { accept: "application/dns-json" } } 17 ); 18 if (!r.ok) throw new Error(`doh ${r.status}`); 19 const { Answer = [] } = await r.json(); 20 return Answer.filter(a => a.type === 16).map(a => { 21 const chunks = []; 22 const re = /"((?:[^"\\]|\\.)*)"/g; 23 let m; 24 while ((m = re.exec(a.data))) chunks.push(m[1].replace(/\\(.)/g, "$1")); 25 return chunks; 26 }); 27} 28 29const argv = typeof Deno !== "undefined" ? Deno.args : process.argv.slice(2); 30const nsids = []; 31let outDir = "lexicons"; 32let asJs = false; 33 34for (let i = 0; i < argv.length; i++) { 35 const a = argv[i]; 36 if (a === "--out" || a === "-o") outDir = argv[++i]; 37 else if (a.startsWith("--out=")) outDir = a.slice(6); 38 else if (a === "--js") asJs = true; 39 else nsids.push(a); 40} 41if (!nsids.length) { 42 console.error("usage: ./install <nsid>... [--out <dir>] [--js]"); 43 process.exit(1); 44} 45 46function authorityDomain(nsid) { 47 const segs = nsid.split("."); 48 if (segs.length < 3 || segs.some(s => !/^[a-z][a-z0-9-]*$/i.test(s))) { 49 throw new Error(`invalid nsid: ${nsid}`); 50 } 51 return segs.slice(0, -1).reverse().join("."); 52} 53 54async function resolveDid(nsid) { 55 const host = `_lexicon.${authorityDomain(nsid)}`; 56 const records = await resolveTxt(host); 57 for (const chunks of records) { 58 const m = chunks.join("").match(/^did=(.+)$/); 59 if (m) return m[1]; 60 } 61 throw new Error(`no did= TXT at ${host}`); 62} 63 64async function resolvePds(did) { 65 let doc; 66 if (did.startsWith("did:plc:")) { 67 const r = await fetch(`https://plc.directory/${did}`); 68 if (!r.ok) throw new Error(`plc ${r.status}`); 69 doc = await r.json(); 70 } else if (did.startsWith("did:web:")) { 71 const host = did.slice(8).replace(/:/g, "/"); 72 const r = await fetch(`https://${host}/.well-known/did.json`); 73 if (!r.ok) throw new Error(`did:web ${r.status}`); 74 doc = await r.json(); 75 } else { 76 throw new Error(`unsupported did method: ${did}`); 77 } 78 const svc = (doc.service || []).find( 79 s => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer" 80 ); 81 if (!svc) throw new Error("no atproto_pds service in did doc"); 82 return svc.serviceEndpoint.replace(/\/$/, ""); 83} 84 85async function fetchLexicon(nsid) { 86 const did = await resolveDid(nsid); 87 const pds = await resolvePds(did); 88 const url = `${pds}/xrpc/com.atproto.repo.getRecord` 89 + `?repo=${encodeURIComponent(did)}` 90 + `&collection=com.atproto.lexicon.schema` 91 + `&rkey=${encodeURIComponent(nsid)}`; 92 const r = await fetch(url); 93 if (!r.ok) throw new Error(`getRecord ${r.status}`); 94 const { value } = await r.json(); 95 return value; 96} 97 98let failed = 0; 99for (const nsid of nsids) { 100 try { 101 const lex = await fetchLexicon(nsid); 102 const out = join(outDir, `${nsid}.${asJs ? "js" : "json"}`); 103 const body = asJs 104 ? `export default /** @type {const} */ (${JSON.stringify(lex, null, 2)});\n` 105 : JSON.stringify(lex, null, 2) + "\n"; 106 await mkdir(dirname(out), { recursive: true }); 107 await writeFile(out, body); 108 console.log(`ok ${nsid} -> ${out}`); 109 } catch (e) { 110 console.error(`fail ${nsid} (${e.message})`); 111 failed++; 112 } 113} 114process.exit(failed ? 1 : 0);