~james/core

core/web/src/lib/server/avatar.ts -rw-r--r-- 1.1 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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("&", "&")}"`;
	});