/** * BlueAT Network CDN — Cloudflare Worker Set Up * Version: 2.4.0 * * A 1:1 of cdn.bsky.app with D1-backed DID caching * * ───────────────────────────────────────────────────────────────────────────── * SETUP * ───────────────────────────────────────────────────────────────────────────── * * 1. D1 DATABASE * ────────────── * Create a D1 database in the Cloudflare dashboard (or via Wrangler): * wrangler d1 create blueat-cdn-db * * Then run the following SQL to initialise the schema: * * CREATE TABLE IF NOT EXISTS did_cache ( * did TEXT PRIMARY KEY, * pds_url TEXT NOT NULL, * cached_at INTEGER NOT NULL * ); * * CREATE TABLE IF NOT EXISTS meta ( * key TEXT PRIMARY KEY, * value TEXT NOT NULL * ); * * Apply it with: * wrangler d1 execute blueat-cdn-db --file=schema.sql * * ───────────────────────────────────────────────────────────────────────────── * * 2. WRANGLER CONFIG (wrangler.toml) * ─────────────────────────────────── * Add the D1 binding and cron trigger to your wrangler.toml: * * name = "blueat-cdn" * main = "worker.js" * compatibility_date = "2024-09-23" * * [[d1_databases]] * binding = "DB" * database_name = "blueat-cdn-db" * database_id = "" * * [triggers] * crons = ["0 * * * *"] # every hour — prunes stale DID cache entries * * ───────────────────────────────────────────────────────────────────────────── * * 3. ENVIRONMENT BINDINGS (set in Cloudflare Dashboard) * ────────────────────────────────────────────────────── * No secret environment variables are required for core operation. * The DB binding above is the only required binding. * * ───────────────────────────────────────────────────────────────────────────── * * 4. ROUTES * ───────── * The worker handles the following request paths: * * GET /xrpc/_health * Returns JSON with service status and D1 cache statistics. * Not cached — always live. * * GET /clear * ⚠️ Clears all DID cache entries from D1. * Consider restricting this route in production (e.g. via a Cloudflare * WAF rule or by adding token-based auth) as it is currently open. * * GET /img/:type/plain/:did/:cid * Main blob proxy endpoint. Resolves the DID to its PDS, fetches the * blob via com.atproto.sync.getBlob, and caches the response in the * Cloudflare Cache API for 7 days (immutable). * * :type — e.g. "avatar", "banner", "feed_thumbnail" * :did — a did:plc or did:web identifier * :cid — the blob CID * * For avatar and banner types, if the blob CID has rotated (e.g. after * a profile update), the worker falls back to re-resolving the current * CID from com.atproto.repo.getRecord on the PDS. * * ───────────────────────────────────────────────────────────────────────────── * * 5. CACHING BEHAVIOUR * ───────────────────── * - DID → PDS resolution is cached in D1 for 24 hours (DID_TTL_MS). * - Blob responses are cached in the Cloudflare Cache API for 7 days. * - Stale cache entries use stale-while-revalidate: the stale response is * returned immediately and a background revalidation is queued via * ctx.waitUntil(). * - The hourly cron only prunes D1 rows older than 24 hours — it does NOT * bulk-delete the entire table. * * ───────────────────────────────────────────────────────────────────────────── * * 6. DID RESOLUTION * ────────────────── * - did:plc — resolved via https://plc.directory/ * - did:web — resolved via https:///.well-known/did.json * (or //did.json for path-based did:web identifiers) * - PLC/did:web responses are edge-cached by Cloudflare for 1 hour * (cf: { cacheTtl: 3600, cacheEverything: true }). * * ───────────────────────────────────────────────────────────────────────────── */ const VERSION = "2.4.0"; const ADMIN_DID = "did:plc:l37td5yhxl2irrzrgvei4qay"; const CONTACT_EMAIL = "danielmorrisey@pm.me"; const SERVICE_NAME = "BlueAT Network CDN"; const PLC_DIRECTORY = "https://plc.directory"; const PATH_RE = /^\/img\/([^/]+)\/plain\/(did:[^/]+)\/([^@/]+)/; const DID_TTL_MS = 86_400_000; // 24 hours export default { async fetch(request, env, ctx) { if (!["GET", "HEAD"].includes(request.method)) { return new Response("Method Not Allowed", { status: 405 }); } const url = new URL(request.url); // ── Health ─────────────────────────────────────────────────────────────── if (url.pathname === "/xrpc/_health") { const [stats, lastClear] = await Promise.all([ env.DB.prepare(` SELECT COUNT(*) AS total, SUM(cached_at < ?) AS stale, MIN(cached_at) AS oldest, MAX(cached_at) AS newest FROM did_cache `).bind(Date.now() - DID_TTL_MS).first(), env.DB.prepare( "SELECT value FROM meta WHERE key = 'last_cache_clear'" ).first(), ]); return Response.json( { status: "ok", version: VERSION, admin_did: ADMIN_DID, admin_contact_email: CONTACT_EMAIL, service_name: SERVICE_NAME, db: { cached_dids: stats?.total ?? 0, stale_dids: stats?.stale ?? 0, oldest_entry: stats?.oldest ? new Date(stats.oldest).toISOString() : null, newest_entry: stats?.newest ? new Date(stats.newest).toISOString() : null, last_cache_clear: lastClear?.value ?? null, }, }, { headers: { "Cache-Control": "no-store" } } ); } // ── Clear DID cache ────────────────────────────────────────────────────── if (url.pathname === "/clear") { const { meta } = await env.DB.prepare("DELETE FROM did_cache").run(); return Response.json( { cleared: meta.changes }, { headers: { "Cache-Control": "no-store" } } ); } const match = url.pathname.match(PATH_RE); if (!match) return new Response("Not Found", { status: 404 }); const cache = caches.default; const cachedResponse = await cache.match(request); if (cachedResponse) { const age = Date.now() - new Date(cachedResponse.headers.get("X-Cached-At") || 0).getTime(); if (age > 86_400_000) ctx.waitUntil(revalidate(request, match, cache, env)); return cachedResponse; } const type = match[1]; const did = match[2]; const cid = match[3]; try { const pdsUrl = await resolvePds(did, env); if (!pdsUrl) return new Response("PDS Not Found", { status: 404 }); let blobRes = await fetch(`${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`); if (blobRes.status === 404 && (type === "avatar" || type === "banner")) { blobRes = await resolveProfileFallback(pdsUrl, did, type, cid); } if (!blobRes?.ok) return new Response("Asset Not Found", { status: 404 }); const finalRes = buildResponse(blobRes, "pds-direct"); ctx.waitUntil(cache.put(request, finalRes.clone())); return finalRes; } catch { return new Response("Bad Gateway", { status: 502 }); } }, // ── Cron: runs every 60 min — only PURGES stale entries, never re-resolves ─ async scheduled(event, env, ctx) { const staleThreshold = Date.now() - DID_TTL_MS; const { meta } = await env.DB.prepare( "DELETE FROM did_cache WHERE cached_at < ?" ).bind(staleThreshold).run(); await env.DB.prepare( `INSERT INTO meta (key, value) VALUES ('last_cache_clear', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value` ).bind(new Date().toISOString()).run(); console.log(`[cron] pruned ${meta.changes} stale DID entries`); }, }; // ── Helpers ─────────────────────────────────────────────────────────────────── function buildResponse(source, proxySource) { const res = new Response(source.body, source); res.headers.set("Cache-Control", "public, max-age=604800, immutable"); res.headers.set("X-Proxy-Source", proxySource); res.headers.set("X-Cached-At", new Date().toUTCString()); res.headers.set("Content-Disposition", "inline"); return res; } async function resolveProfileFallback(pdsUrl, did, type, cid) { const profileRes = await fetch( `${pdsUrl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=app.bsky.actor.profile&rkey=self` ); if (!profileRes.ok) return null; const profileData = await profileRes.json(); const originalCid = profileData.value?.[type]?.ref?.$link; if (!originalCid || originalCid === cid) return null; return fetch(`${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${originalCid}`); } async function revalidate(request, match, cache, env) { const [, , did, cid] = match; try { const pdsUrl = await resolvePds(did, env); if (!pdsUrl) return; const blobRes = await fetch(`${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}`); if (blobRes.ok) { await cache.put(request, buildResponse(blobRes, "pds-direct-revalidate")); } } catch {} } async function resolvePds(did, env) { // 1. Check D1 first const row = await env.DB.prepare( "SELECT pds_url, cached_at FROM did_cache WHERE did = ?" ).bind(did).first(); if (row && Date.now() - row.cached_at < DID_TTL_MS) { return row.pds_url; } // 2. Fetch from PLC / did:web let reqUrl; if (did.startsWith("did:web:")) { const parts = did.slice(8).split(":"); const host = decodeURIComponent(parts[0]); const path = parts.length === 1 ? "/.well-known/did.json" : `/${parts.slice(1).map(decodeURIComponent).join("/")}/did.json`; reqUrl = `https://${host}${path}`; } else { reqUrl = `${PLC_DIRECTORY}/${did}`; } const res = await fetch(reqUrl, { cf: { cacheTtl: 3600, cacheEverything: true } }); if (!res.ok) return null; const doc = await res.json(); if (!doc?.service) return null; let pdsUrl = null; for (const s of doc.service) { if (s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer") { pdsUrl = s.serviceEndpoint; break; } } // 3. Upsert into D1 if (pdsUrl) { await env.DB.prepare( `INSERT INTO did_cache (did, pds_url, cached_at) VALUES (?, ?, ?) ON CONFLICT(did) DO UPDATE SET pds_url = excluded.pds_url, cached_at = excluded.cached_at` ).bind(did, pdsUrl, Date.now()).run(); } return pdsUrl; }