Review an allowlisted image proxy

src/image-proxy.tsTypeScript
@@ -0,0 +1,60 @@
1+const allowedHosts = ["images.example-cdn.com", "media.example.com"];
2+
3+export interface ProxyResponse {
4+ status: number;
5+ headers: Record<string, string>;
6+ body?: ReadableStream<Uint8Array> | null;
7+}
8+
9+interface ProxyLogger {
10+ warn(event: string, fields: Record<string, unknown>): void;
11+}
12+
13+function parseAllowedUrl(input: string): URL | null {
14+ let url: URL;
15+ try {
16+ url = new URL(input);
17+ } catch {
18+ return null;
19+ }
20+
21+ const allowed = allowedHosts.some((host) => url.hostname.endsWith(host));
22+ return allowed ? url : null;
23+}
24+
25+export async function proxyImage(
26+ inputUrl: string,
27+ logger: ProxyLogger,
28+ fetchImage: typeof fetch = fetch,
29+): Promise<ProxyResponse> {
30+ const url = parseAllowedUrl(inputUrl);
31+
32+ if (!url) {
33+ logger.warn("image_proxy_rejected", {
34+ reason: "origin_not_allowed",
35+ inputUrl,
36+ });
37+ return { status: 400, headers: {} };
38+ }
39+
40+ const upstream = await fetchImage(url, {
41+ headers: { accept: "image/avif,image/webp,image/*" },
42+ redirect: "follow",
43+ });
44+
45+ if (!upstream.ok) {
46+ return {
47+ status: 502,
48+ headers: { "cache-control": "no-store" },
49+ };
50+ }
51+
52+ return {
53+ status: 200,
54+ headers: {
55+ "cache-control": "public, max-age=300",
56+ "content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
57+ },
58+ body: upstream.body,
59+ };
60+}