the code used for the cdn.blueat.net cloudflare worker
worker.js
259 lines 7.3 kB view raw
1const PLC_DIRECTORY = "https://plc.directory"; 2 3const PATH_RE = /^\/img\/([^/]+)\/plain\/(did:[^/]+)\/([^@/]+)(?:@([^/]+))?$/; 4 5export default { 6 async fetch(request, env, ctx) { 7 const url = new URL(request.url); 8 9 if (url.pathname === "/" || url.pathname === "") { 10 return new Response(LANDING_HTML, { 11 headers: { "Content-Type": "text/html;charset=UTF-8" }, 12 }); 13 } 14 15 const match = url.pathname.match(PATH_RE); 16 if (!match) return new Response("400: Invalid Path", { status: 400 }); 17 const [_, type, did, cid] = match; 18 19 const cache = caches.default; 20 21 const cachedResponse = await cache.match(request); 22 if (cachedResponse) return cachedResponse; 23 24 const pdsUrl = await resolvePds(did); 25 if (!pdsUrl) { 26 return new Response("404: PDS not found for this DID", { status: 404 }); 27 } 28 29 try { 30 let blobRes = await fetchBlob(pdsUrl, did, cid); 31 32 if (blobRes.status === 404 && (type === "avatar" || type === "banner")) { 33 const originalCid = await findOriginalCidFromProfile(pdsUrl, did, type); 34 if (originalCid && originalCid !== cid) { 35 blobRes = await fetchBlob(pdsUrl, did, originalCid); 36 } 37 } 38 39 if (!blobRes.ok) { 40 return new Response("404: Asset not found on PDS.", { status: 404 }); 41 } 42 43 const finalRes = new Response(blobRes.body, blobRes); 44 finalRes.headers.set("Cache-Control", "public, s-maxage=604800"); 45 finalRes.headers.set("X-Proxy-Source", "PDS-Direct"); 46 finalRes.headers.set("Content-Disposition", "inline"); 47 ctx.waitUntil(cache.put(request, finalRes.clone())); 48 return finalRes; 49 50 } catch (err) { 51 return new Response(`502: PDS Error: ${err.message}`, { status: 502 }); 52 } 53 }, 54}; 55 56async function fetchBlob(pdsUrl, did, cid) { 57 return fetch( 58 `${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}` 59 ); 60} 61 62async function resolvePds(did) { 63 const doc = did.startsWith("did:web:") 64 ? await resolveDidWeb(did) 65 : await resolveDidPlc(did); 66 if (!doc) return null; 67 const pds = doc.service?.find( 68 (s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer" 69 ); 70 return pds?.serviceEndpoint ?? null; 71} 72 73async function resolveDidPlc(did) { 74 const res = await fetch(`${PLC_DIRECTORY}/${did}`); 75 if (!res.ok) return null; 76 return res.json(); 77} 78 79async function resolveDidWeb(did) { 80 const identifier = did.slice("did:web:".length); 81 const parts = identifier.split(":"); 82 const host = decodeURIComponent(parts[0]); 83 const didUrl = parts.length === 1 84 ? `https://${host}/.well-known/did.json` 85 : `https://${host}/${parts.slice(1).map(decodeURIComponent).join("/")}/did.json`; 86 const res = await fetch(didUrl); 87 if (!res.ok) return null; 88 return res.json(); 89} 90 91async function findOriginalCidFromProfile(pdsUrl, did, type) { 92 const res = await fetch( 93 `${pdsUrl}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.actor.profile&rkey=self` 94 ); 95 if (!res.ok) return null; 96 const data = await res.json(); 97 return data.value?.[type]?.ref?.$link ?? null; 98} 99 100const LANDING_HTML = `<!DOCTYPE html> 101<html lang="en"> 102<head> 103 <meta charset="UTF-8" /> 104 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 105 <title>cdn.blueat.net</title> 106 <style> 107 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 108 109 body { 110 background: #0d1117; 111 color: #e6edf3; 112 font-family: system-ui, sans-serif; 113 min-height: 100vh; 114 display: flex; 115 align-items: center; 116 justify-content: center; 117 padding: 1.5rem; 118 } 119 120 .wrap { 121 width: 100%; 122 max-width: 480px; 123 } 124 125 h1 { 126 font-size: 1.1rem; 127 font-weight: 600; 128 margin-bottom: .25rem; 129 color: #e6edf3; 130 } 131 132 p { 133 font-size: .85rem; 134 color: #7d8590; 135 margin-bottom: 1.5rem; 136 line-height: 1.5; 137 } 138 139 .row { 140 display: flex; 141 gap: .5rem; 142 margin-bottom: .75rem; 143 } 144 145 input { 146 flex: 1; 147 background: #161b22; 148 border: 1px solid #30363d; 149 border-radius: .4rem; 150 padding: .55rem .75rem; 151 color: #e6edf3; 152 font-size: .85rem; 153 outline: none; 154 } 155 input:focus { border-color: #388bfd; } 156 input::placeholder { color: #484f58; } 157 158 button { 159 background: #238636; 160 border: 1px solid #2ea043; 161 border-radius: .4rem; 162 padding: .55rem 1rem; 163 color: #fff; 164 font-size: .85rem; 165 font-weight: 500; 166 cursor: pointer; 167 white-space: nowrap; 168 } 169 button:hover { background: #2ea043; } 170 171 .output { 172 background: #161b22; 173 border: 1px solid #30363d; 174 border-radius: .4rem; 175 padding: .55rem .75rem; 176 font-size: .8rem; 177 font-family: monospace; 178 color: #7d8590; 179 word-break: break-all; 180 min-height: 2.4rem; 181 display: flex; 182 align-items: center; 183 gap: .5rem; 184 } 185 .output.ok { border-color: #238636; color: #3fb950; } 186 .output.err { border-color: #da3633; color: #f85149; } 187 .output span { flex: 1; } 188 189 .copy { 190 background: transparent; 191 border: 1px solid #30363d; 192 border-radius: .3rem; 193 padding: .2rem .5rem; 194 font-size: .75rem; 195 color: #7d8590; 196 flex-shrink: 0; 197 } 198 .copy:hover { border-color: #388bfd; color: #388bfd; background: transparent; } 199 </style> 200</head> 201<body> 202 <div class="wrap"> 203 <h1>cdn.blueat.net</h1> 204 <p>Paste a <code>cdn.bsky.app</code> image URL to get the cdn.blueat.net equivalent, served directly from the user's PDS and powered by Cloudflare Workers.<br><br> 205 <i>This is intended to be used when cdn.bsky.app is down.</i></p> 206 207 <div class="row"> 208 <input id="inp" type="text" placeholder="https://cdn.bsky.app/img/avatar/plain/did:plc:…/cid@jpeg" spellcheck="false" /> 209 <button onclick="go()">Convert</button> 210 </div> 211 212 <div id="out" class="output"><span>—</span></div> 213 </div> 214 215 <script> 216 const inp = document.getElementById("inp"); 217 const out = document.getElementById("out"); 218 inp.addEventListener("keydown", e => e.key === "Enter" && go()); 219 220 function go() { 221 const raw = inp.value.trim(); 222 out.className = "output"; 223 out.innerHTML = "<span>—</span>"; 224 if (!raw) return; 225 226 let u; 227 try { u = new URL(raw); } catch { return err("Not a valid URL."); } 228 229 if (u.hostname !== "cdn.bsky.app" || !/^\\/img\\/[^\\/]+\\/plain\\/did:[^\\/]+\\//.test(u.pathname)) { 230 return err("Not a recognised cdn.bsky.app image URL."); 231 } 232 233 const result = "https://cdn.blueat.net" + u.pathname; 234 out.classList.add("ok"); 235 236 const s = document.createElement("span"); 237 s.textContent = result; 238 239 const btn = document.createElement("button"); 240 btn.className = "copy"; 241 btn.textContent = "Copy"; 242 btn.onclick = () => { 243 navigator.clipboard.writeText(result); 244 btn.textContent = "✓"; 245 setTimeout(() => btn.textContent = "Copy", 1500); 246 }; 247 248 out.innerHTML = ""; 249 out.appendChild(s); 250 out.appendChild(btn); 251 } 252 253 function err(msg) { 254 out.classList.add("err"); 255 out.innerHTML = "<span>" + msg + "</span>"; 256 } 257 </script> 258</body> 259</html>`;