security hardening
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user