serve a static website from your pds

Improve proxy

Changed files
+104 -43
+104 -43
proxy.js
··· 1 1 import { createServer } from "node:http"; 2 + import { parseArgs } from "node:util"; 3 + 4 + const { values } = parseArgs({ 5 + args: process.argv.slice(2), 6 + options: { 7 + port: { type: "string", default: "3000" }, 8 + did: { type: "string" }, 9 + rkey: { type: "string" }, 10 + }, 11 + }); 2 12 3 - import { Client, simpleFetchHandler } from "@atcute/client"; 13 + const did = values.did, 14 + collection = "com.jakelazaroff.test", 15 + rkey = values.rkey; 16 + 17 + if (!did) { 18 + console.error("--did is required."); 19 + process.exit(1); 20 + } 4 21 5 - const did = "did:plc:vrrdgcidwpvn4omvn7uuufoo"; 6 - const pds = "https://bsky.social"; 22 + if (!rkey) { 23 + console.error("--rkey is required."); 24 + process.exit(1); 25 + } 7 26 8 - const handler = simpleFetchHandler({ service: pds }); 9 - const rpc = new Client({ handler }); 27 + const doc = await resolveDid(did); 28 + const pds = doc.service[0].serviceEndpoint; 10 29 11 30 const server = createServer(async (req, res) => { 31 + process.stdout.write(`${req.method} ${req.url} `); 32 + if (req.method !== "GET") { 33 + res.statusCode = 405; 34 + res.end("Method not supported"); 35 + return; 36 + } 37 + 12 38 // TODO: keep leading slash 13 39 const path = req.url.slice(1); 14 40 15 - const { data } = await rpc.get("com.atproto.repo.getRecord", { 16 - params: { 17 - repo: did, 18 - collection: "com.jakelazaroff.test", 19 - rkey: "jakelazaroff.com", 20 - }, 21 - }); 41 + const record = await getRecord(pds, did, collection, rkey); 22 42 23 - for (const asset of data.value.assets) { 43 + for (const asset of record.value.assets) { 24 44 if (asset.path !== path) continue; 25 45 26 - const url = `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${asset.file.ref.$link}`; 27 - const response = await fetch(url); 46 + try { 47 + const blob = await getBlob(pds, did, asset.file.ref.$link); 28 48 29 - if (!response.ok) { 30 - res.statusCode = response.status; 31 - res.end(`Failed to fetch blob: ${response.statusText}`); 32 - return; 33 - } 49 + if (!blob.ok) { 50 + console.error(`Upstream error ${blob.status}: ${blob.statusText}`); 51 + throw new Error("Response not ok"); 52 + } 34 53 35 - const contentType = response.headers.get("content-type"); 36 - if (contentType) res.setHeader("content-type", contentType); 54 + if (!blob.body) { 55 + console.error(`Blob body missing`); 56 + throw new Error("Blob body missing"); 57 + } 37 58 38 - if (response.body) { 39 - const reader = response.body.getReader(); 59 + const contentType = blob.headers.get("content-type") || asset.file.mimeType; 60 + res.setHeader("content-type", contentType); 40 61 41 - try { 42 - while (true) { 43 - const { done, value } = await reader.read(); 44 - if (done) break; 45 - res.write(value); 46 - } 47 - res.end(); 48 - return; 49 - } catch (error) { 50 - res.statusCode = 500; 51 - res.end("Error streaming blob"); 52 - return; 62 + const reader = blob.body.getReader(); 63 + while (true) { 64 + const { done, value } = await reader.read(); 65 + if (done) break; 66 + res.write(value); 53 67 } 54 - } else { 55 - res.statusCode = 500; 56 - res.end("No response body"); 68 + 69 + res.end(); 70 + process.stdout.write(`${res.statusCode} \n`); 71 + return; 72 + } catch (error) { 73 + res.statusCode = 502; 74 + res.end("Error streaming blob"); 75 + process.stdout.write(`${res.statusCode} \n`); 57 76 return; 58 77 } 59 78 } 60 79 61 80 res.statusCode = 404; 62 81 res.end("Not Found"); 82 + process.stdout.write(`${res.statusCode} \n`); 63 83 }); 64 84 65 - const PORT = 3000; 85 + const port = Number.parseInt(values.port) || 3000; 86 + server.listen(port, () => { 87 + console.log(`Server running at http://localhost:${port}`); 88 + console.log(`Proxying to at://${did}/${collection}/${rkey}`); 89 + console.log(""); 90 + }); 91 + 92 + /** @param {string} did */ 93 + async function resolveDid(did) { 94 + let url; 95 + if (did.startsWith("did:web:")) url = `https://${did.slice(8)}/.well-known/did.json`; 96 + else if (did.startsWith("did:plc:")) url = `https://plc.directory/${did}`; 97 + else throw new Error(`Unsupported did: ${did}`); 98 + 99 + const res = await fetch(url); 100 + const doc = await res.json(); 101 + return doc; 102 + } 66 103 67 - server.listen(PORT, () => { 68 - console.log(`Server running at http://localhost:${PORT}/`); 69 - }); 104 + /** 105 + * @param {string} pds 106 + * @param {string} did 107 + * @param {string} collection 108 + * @param {string} rkey 109 + */ 110 + async function getRecord(pds, did, collection, rkey) { 111 + const url = new URL(`${pds}/xrpc/com.atproto.repo.getRecord`); 112 + url.searchParams.set("repo", did); 113 + url.searchParams.set("collection", collection); 114 + url.searchParams.set("rkey", rkey); 115 + const response = await fetch(url); 116 + return await response.json(); 117 + } 118 + 119 + /** 120 + * @param {string} pds 121 + * @param {string} did 122 + * @param {string} collection 123 + * @param {string} rkey 124 + */ 125 + async function getBlob(pds, did, cid) { 126 + const url = new URL(`${pds}/xrpc/com.atproto.sync.getBlob`); 127 + url.searchParams.set("did", did); 128 + url.searchParams.set("cid", cid); 129 + return await fetch(url); 130 + }