security hardening

This commit is contained in:
Gerhard Scheikl
2026-05-31 09:35:31 +02:00
parent d7d437a871
commit 01b4734477
31 changed files with 1234 additions and 238 deletions
@@ -7,9 +7,14 @@
* hammering the network when regenerating an invoice multiple times.
*/
import { safeFetch, SafeFetchError, SHOPIFY_CDN_HOSTS } from "./safeFetch.server";
import pLimit from "p-limit";
const MAX_BYTES = 2 * 1024 * 1024; // 2 MB cap per image
const CACHE_MAX_ENTRIES = 200;
/** Max images fetched/embedded per invoice (DoS bound for large carts). */
const MAX_IMAGES_PER_INVOICE = 100;
/** Max concurrent image fetches per invoice. */
const IMAGE_FETCH_CONCURRENCY = 6;
const cache = new Map<string, string>(); // url -> data URL
@@ -76,17 +81,28 @@ export async function fetchProductImageDataUrl(url: string): Promise<string | un
}
/**
* Resolves images for every line in parallel, mutating `imageDataUrl` in place.
* Failures are swallowed (the row simply renders without an icon).
* Resolves images for every line, mutating `imageDataUrl` in place. Fetches
* run with bounded concurrency and a hard cap on the number of images
* embedded per invoice, so a large cart (Shopify allows hundreds of line
* items) can't trigger an unbounded fan-out of network requests. Failures are
* swallowed (the row simply renders without an icon).
*/
export async function attachLineItemImages(
lines: { imageUrl?: string; imageDataUrl?: string }[],
): Promise<void> {
await Promise.all(
lines.map(async (line) => {
if (!line.imageUrl) return;
const dataUrl = await fetchProductImageDataUrl(line.imageUrl);
if (dataUrl) line.imageDataUrl = dataUrl;
}),
);
const limit = pLimit(IMAGE_FETCH_CONCURRENCY);
let budget = MAX_IMAGES_PER_INVOICE;
const tasks: Promise<void>[] = [];
for (const line of lines) {
if (!line.imageUrl) continue;
if (budget <= 0) break; // cap reached — remaining rows render iconless
budget -= 1;
tasks.push(
limit(async () => {
const dataUrl = await fetchProductImageDataUrl(line.imageUrl!);
if (dataUrl) line.imageDataUrl = dataUrl;
}),
);
}
await Promise.all(tasks);
}