Something went wrong. Try again.
serve a static website from your pds
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223import { createServer } from "node:http";import { Readable } from "node:stream";import { parseArgs } from "node:util";
const { values } = parseArgs({ args: process.argv.slice(2), options: { port: { type: "string", default: "3000" }, did: { type: "string" }, rkey: { type: "string" }, },});
const did = values.did, collection = "com.jakelazaroff.athost", rkey = values.rkey;
if (!did) { console.error("--did is required."); process.exit(1);}
if (!rkey) { console.error("--rkey is required."); process.exit(1);}
const doc = await resolveDid(did);const pds = doc.service[0].serviceEndpoint;
/** @type {Error | undefined} */let error;
let record = await getRecord(pds, did, collection, rkey);let files = untar(await getBlob(pds, did, record.value.assets.ref.$link));let updating = false;
async function updateRecord() { if (updating) return; try { updating = true; record = await getRecord(pds, did, collection, rkey); files = untar(await getBlob(pds, did, record.value.assets.ref.$link)); error = undefined; } catch (e) { error = e; } finally { updating = false; }}
/** * * @param {number} status * @param {string} message */function fail(res, status, message) { res.statusCode = status; res.end(message);}
const server = createServer(async (req, res) => { process.stdout.write(`${req.method} ${req.url} `); const start = performance.now();
let err; try { if (req.method !== "GET") return fail(res, 405, "Method not supported"); queueMicrotask(updateRecord);
if (error) return fail(res, 502, "Bad gateway");
let path = req.url.slice(1); let asset = files[req.url.slice(1)]; let status = 200;
// if there's no matching file, try to treat it as a folder and use an index.html inside it if (!asset) { path = req.url.slice(1).split("/").filter(Boolean).concat("index.html").join("/"); asset = files[path]; }
// if there's *still* no matching file, return a generic 404 if (!asset) return fail(res, 404, "Not found");
res.statusCode = status; res.setHeader("content-type", getMimeType(path)); res.setHeader("content-length", asset.size);
const stream = Readable.fromWeb(asset.stream()); await new Promise((resolve, reject) => { stream.pipe(res); stream.on("error", reject); stream.on("end", resolve); res.on("error", reject); }); } finally { const ms = performance.now() - start; process.stdout.write(`${res.statusCode} - ${Math.round(ms)}ms\n`); if (err) console.error(err); }});
const port = Number.parseInt(values.port) || 3000;server.listen(port, () => { console.log(`Server running at http://localhost:${port}`); console.log(`Proxying to at://${did}/${collection}/${rkey}`); console.log("");});
/** @param {string} did */async function resolveDid(did) { let url; if (did.startsWith("did:web:")) url = `https://${did.slice(8)}/.well-known/did.json`; else if (did.startsWith("did:plc:")) url = `https://plc.directory/${did}`; else throw new Error(`Unsupported did: ${did}`);
const res = await fetch(url); const doc = await res.json(); return doc;}
/** * @param {string} pds * @param {string} did * @param {string} collection * @param {string} rkey */async function getRecord(pds, did, collection, rkey) { const url = new URL(`${pds}/xrpc/com.atproto.repo.getRecord`); url.searchParams.set("repo", did); url.searchParams.set("collection", collection); url.searchParams.set("rkey", rkey); const response = await fetch(url); return await response.json();}
/** * @param {string} pds * @param {string} did * @param {string} collection * @param {string} rkey */async function getBlob(pds, did, cid) { const url = new URL(`${pds}/xrpc/com.atproto.sync.getBlob`); url.searchParams.set("did", did); url.searchParams.set("cid", cid);
const res = await fetch(url); return new Uint8Array(await res.arrayBuffer());}
/** @param {Uint8Array} data */function untar(data) { /** @type {Record<string, File>} */ const files = {}; let offset = 0;
while (offset < data.length) { // check if we've hit the end (two empty 512-byte blocks) if (data[offset] === 0) break;
// read header (512 bytes) const header = data.slice(offset, offset + 512);
// type flag (156) const typeflag = String.fromCharCode(header[156]);
// file size (124-135, octal string) const sizeBytes = header.slice(124, 136); const sizeStr = new TextDecoder().decode(sizeBytes).trim().replace(/\0/g, ""); const size = Number.parseInt(sizeStr, 8) || 0;
offset += 512; const paddedSize = Math.ceil(size / 512) * 512;
// kkip directories and other non-file entries if (typeflag === "5" || typeflag === "x" || typeflag === "g") { offset += paddedSize; continue; }
// file name (first 100 bytes, null-terminated) const nameBytes = header.slice(0, 100); const nameEnd = nameBytes.indexOf(0); const name = new TextDecoder().decode(nameBytes.slice(0, nameEnd > 0 ? nameEnd : 100));
if (!name) { offset += paddedSize; continue; }
// read content const content = data.slice(offset, offset + size); offset += paddedSize;
files[name] = new File([content], name.split("/").pop() || name); }
return files;}
/** @param {string} filename */function getMimeType(filename) { const ext = filename.split(".").pop()?.toLowerCase(); const mimeTypes = { txt: "text/plain", html: "text/html", css: "text/css", js: "text/javascript", json: "application/json", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", pdf: "application/pdf", zip: "application/zip", xml: "application/xml", }; return mimeTypes[ext] || "application/octet-stream";}