import { gradientSvg } from "./gradient.js"; import { widthFor } from "./size.js"; const SANDBOX_CSP = "default-src 'none'; style-src 'unsafe-inline'; sandbox"; const contentTypes = { webp: "image/webp", jpeg: "image/jpeg", png: "image/png", }; const rasterAvatarType = /^image\/(?:png|jpeg|webp|avif|gif)$/; class ServedAvatarType { #header; static fromUpstream(raw) { const header = (raw ?? "").split(";")[0].trim().toLowerCase(); return rasterAvatarType.test(header) ? new ServedAvatarType(header) : null; } static resized(format) { const header = contentTypes[format]; return header ? new ServedAvatarType(header) : null; } constructor(header) { this.#header = header; } get header() { return this.#header; } } class SandboxedResponse extends Response { constructor(body, init = {}) { super(body, init); this.headers.set("X-Content-Type-Options", "nosniff"); this.headers.set("Content-Security-Policy", SANDBOX_CSP); } } export default { async fetch(request, env) { // Helper function to fetch Tangled profile from PDS const getTangledAvatarFromPDS = async (actor) => { try { const identityResponse = await fetch( `${env.BOBBIN_URL.replace(/\/$/, "")}/xrpc/blue.microcosm.identity.resolveMiniDoc?identifier=${encodeURIComponent(actor)}`, ); const identity = identityResponse.ok ? await identityResponse.json() : null; if (!identity?.pds) { console.log({ level: "debug", message: "failed to resolve identity", actor: actor, }); return null; } const did = identity.did; const pdsEndpoint = identity.pds.replace(/\/$/, ""); // Remove trailing slash if (!pdsEndpoint) { console.log({ level: "debug", message: "no PDS endpoint found", actor: actor, did: did, }); return null; } const profileUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=org.tangled.actor.profile&rkey=self`; // Fetch the Tangled profile record from PDS const profileResponse = await fetch(profileUrl); if (!profileResponse.ok) { console.log({ level: "debug", message: "no Tangled profile found on PDS", actor: actor, status: profileResponse.status, }); return null; } const profileData = await profileResponse.json(); const avatarBlob = profileData?.value?.avatar; if (!avatarBlob) { console.log({ level: "debug", message: "Tangled profile has no avatar", actor: actor, }); return null; } // Extract CID from blob reference object // The ref might be an object with $link property or a string let avatarCID; if (typeof avatarBlob.ref === "string") { avatarCID = avatarBlob.ref; } else if (avatarBlob.ref?.$link) { avatarCID = avatarBlob.ref.$link; } else if (typeof avatarBlob === "string") { avatarCID = avatarBlob; } if (!avatarCID || typeof avatarCID !== "string") { console.log({ level: "warn", message: "could not extract valid CID from avatar blob", actor: actor, avatarBlob: avatarBlob, avatarBlobRef: avatarBlob.ref, }); return null; } // Construct blob URL (pdsEndpoint already has trailing slash removed) const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`; return blobUrl; } catch (e) { console.log({ level: "warn", message: "error fetching Tangled avatar from PDS", actor: actor, error: e.message, }); return null; } }; // the old site showed the bluesky avatar when the tangled profile had none const getBlueskyAvatar = async (actor) => { try { const profileResponse = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actor)}`, ); if (!profileResponse.ok) return null; return (await profileResponse.json()).avatar ?? null; } catch { return null; } }; const url = new URL(request.url); const { pathname, searchParams } = url; if (!pathname || pathname === "/") { return new Response( `This is Tangled's avatar service. It fetches your pretty avatar from your PDS, or generates a placeholder. You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`, ); } const width = widthFor(searchParams.get("size")); const format = searchParams.get("format") || "webp"; const validFormats = ["webp", "jpeg", "png"]; const outputFormat = validFormats.includes(format) ? format : "webp"; const cache = caches.default; let cacheKey = request.url; let response = await cache.match(cacheKey); if (response) return new SandboxedResponse(response.body, response); const pathParts = pathname.slice(1).split("/"); if (pathParts.length < 2) { return new Response("Bad URL", { status: 400 }); } const [signatureHex, actor] = pathParts; const actorBytes = new TextEncoder().encode(actor); const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(env.AVATAR_SHARED_SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"], ); const computedSigBuffer = await crypto.subtle.sign("HMAC", key, actorBytes); const computedSig = Array.from(new Uint8Array(computedSigBuffer)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); console.log({ level: "debug", message: "avatar request for: " + actor, computedSignature: computedSig, providedSignature: signatureHex, }); const sigBytes = Uint8Array.from( signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)), ); const valid = await crypto.subtle.verify("HMAC", key, sigBytes, actorBytes); if (!valid) { return new Response("Invalid signature", { status: 403 }); } try { const cfOptions = outputFormat !== "webp" || width ? { cf: { image: { format: outputFormat, ...(width ? { width, height: width, fit: "cover" } : {}), }, }, }: {}; const fetchAvatar = (avatarUrl) => fetch(avatarUrl, cfOptions).catch(() => null); const tangledUrl = await getTangledAvatarFromPDS(actor); let avatarResponse = tangledUrl ? await fetchAvatar(tangledUrl) : null; const status = avatarResponse?.status ?? 502; // a missing blob won't come back soon. an outage or a rate limit might const gone = !tangledUrl || (status >= 400 && status < 500 && status !== 408 && status !== 429); // liar profiles can name a blob that no pds ever stored if (!avatarResponse?.ok && gone) { const blueskyUrl = await getBlueskyAvatar(actor); if (blueskyUrl) avatarResponse = await fetchAvatar(blueskyUrl); } if (!avatarResponse?.ok && !tangledUrl) { console.log({ level: "debug", message: "no avatar found, generating placeholder", actor: actor, }); const size = width ?? 128; const svg = gradientSvg(actor, size); const svgData = new TextEncoder().encode(svg); response = new SandboxedResponse(svgData, { headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=43200", }, }); await cache.put(cacheKey, response.clone()); return response; } const served = cfOptions.cf ? ServedAvatarType.resized(outputFormat) : avatarResponse?.ok ? ServedAvatarType.fromUpstream(avatarResponse.headers.get("Content-Type")) : null; if (!avatarResponse?.ok || !served) { response = new Response(`failed to fetch avatar for ${actor}.`, { status: avatarResponse?.ok ? 415 : status, headers: { "Cache-Control": `public, max-age=${gone ? 3600 : 120}`, }, }); await cache.put(cacheKey, response.clone()); return response; } const avatarData = await avatarResponse.arrayBuffer(); response = new SandboxedResponse(avatarData, { headers: { "Content-Type": served.header, "Cache-Control": "public, max-age=43200", }, }); await cache.put(cacheKey, response.clone()); return response; } catch (error) { return new Response(`error fetching avatar: ${error.message}`, { status: 500, }); } }, };