import { createHmac } from "node:crypto"; import { decodeOrEmpty } from "#lib/uri.ts"; export const AVATAR_DID = /^did:[a-z]+:[a-zA-Z0-9._:%-]{1,256}$/; const PASSED_THROUGH = ["size", "format", "v"]; export const signedAvatarUrl = ( service: string, secret: string, did: string, search: URLSearchParams ): string => { const query = new URLSearchParams(); for (const key of PASSED_THROUGH) { const value = search.get(key); if (value) query.set(key, value); } const signature = createHmac("sha256", secret).update(did).digest("hex"); return `${service}/${signature}/${did}${query.size ? `?${query}` : ""}`; }; const AVATAR_SRC = /src="\/avatar\/([^"?]+)(?:\?([^"]*))?"/g; // hydration keeps a server src. client renders still go through the route export const signAvatarSrcs = (html: string, service: string, secret: string): string => html.replace(AVATAR_SRC, (src, did: string, query = "") => { const decoded = decodeOrEmpty(did); if (!AVATAR_DID.test(decoded)) return src; const search = new URLSearchParams(query.replaceAll("&", "&")); return `src="${signedAvatarUrl(service, secret, decoded, search).replaceAll("&", "&")}"`; });