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,
});
}
},
};