// Configuration for the metadata proxy route. Note: the Next.js route segment // config (`dynamic`, `maxDuration`) stays in route.ts — Next only reads those // as literal exports of the route module, not via re-export. import { Logger } from '@/app/shared/lib/logger'; export const USER_AGENT = process.env.NEXT_PUBLIC_METADATA_USER_AGENT ?? 'Solana Explorer'; // Resolve a positive-integer env override, falling back to `fallback` when the // value is unset or malformed. Without this guard a value like "abc" parses to // NaN, every `contentLength > NaN` / `received > NaN` size comparison is false, // and the cap (or timeout) is silently disabled — failing open on the very // guard it configures. The malformed value is reported so a misconfigured // deploy is visible rather than quietly unprotected. Empty string is treated as // unset (no warning). `process.env.X` is read at the call site, not here, so // Next's build-time inlining of `NEXT_PUBLIC_*` still applies. function positiveIntEnv(name: string, raw: string | undefined, fallback: number): number { if (!raw) return fallback; const parsed = Number(raw); if (Number.isInteger(parsed) && parsed > 0) return parsed; Logger.warn(`[api:metadata-proxy] Ignoring invalid ${name}="${raw}"; using default ${fallback}`, { sentry: true, }); return fallback; } // 4 MB default: just under Vercel's ~4.5 MB buffered-response cap (a thin // ~0.5 MB margin), so our cap stays the binding constraint while fitting more // images. Oversize fetches degrade to the ProxiedImage "view original" // fallback. Tune from the success-path size stats logged in fetch-resource.ts. export const MAX_SIZE = positiveIntEnv( 'NEXT_PUBLIC_METADATA_MAX_CONTENT_SIZE', process.env.NEXT_PUBLIC_METADATA_MAX_CONTENT_SIZE, 4_000_000, ); export const TIMEOUT = positiveIntEnv('NEXT_PUBLIC_METADATA_TIMEOUT', process.env.NEXT_PUBLIC_METADATA_TIMEOUT, 10_000); // Prevent proxied content (e.g. SVG with embedded scripts) from executing // anything if the proxy URL is opened directly as a top-level document. export const SECURITY_HEADERS = { 'Content-Security-Policy': "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'", 'X-Content-Type-Options': 'nosniff', }; const BROWSER_MAX_AGE = 86_400; // 1 day // Cache successful (2xx) responses in the viewer's own browser for up to a day // so repeat views skip the round-trip. The proxied content is effectively // immutable (content-addressed Arweave/IPFS, static CDN assets), so a day-long // browser cache is safe; a rare in-place overwrite of a mutable URL is still // picked up once the window lapses (no `immutable`). Browser-only by design: no // `Vercel-CDN-Cache-Control`, so responses are not cached at the Vercel edge — // every cold or cross-client view re-invokes the function. Set on success ONLY // (error responses must not be cached), and the upstream's own Cache-Control is // intentionally not forwarded (it's frequently no-store/no-cache/private). export const CACHE_HEADERS = { 'Cache-Control': `public, max-age=${BROWSER_MAX_AGE}`, }; const ERROR_MAX_AGE = 30; // 30 seconds // Error responses are cached briefly in the viewer's own browser only. This is // what makes ProxiedImage's failure-reason probe free: the `` element can't // read an HTTP status, so on a load failure the component re-`fetch`es the same // URL to learn why — and because the failed `` request populated the cache, // that probe is served from cache instead of re-hitting the proxy (no upstream // re-download for an oversize 413). `private` keeps the verdict out of shared and // edge caches (no `Vercel-CDN-Cache-Control`); the short TTL lets a transient // 5xx clear quickly while a stable 413/404/415 stays cheap to re-read. export const ERROR_CACHE_HEADERS = { 'Cache-Control': `private, max-age=${ERROR_MAX_AGE}`, };