import { NextResponse } from "next/server"; import { headers } from "next/headers"; const ALLOWED_ORIGINS = (process.env.NEXT_PUBLIC_APP_URL ?? "") .split(",") .map((o) => o.trim()) .filter(Boolean); /** * Wrap an API route handler to catch errors and return a safe * generic message instead of leaking backend details. * Internal errors are logged server-side. */ export function safeRoute( handler: (...args: T) => Promise, ) { return async (...args: T) => { try { return await handler(...args); } catch (e) { console.error("[api]", (e as Error).message); return NextResponse.json( { error: "An unexpected error occurred" }, { status: 500 }, ); } }; } /** * Return a 400 response with a user-facing message. */ export function badRequest(message: string) { return NextResponse.json({ error: message }, { status: 400 }); } /** * Check the Origin header on mutating requests to prevent CSRF. * Returns a 403 response if the origin is invalid, or null if OK. */ export async function checkCsrf(): Promise { const hdrs = await headers(); const origin = hdrs.get("origin"); // If no Origin header (e.g. same-origin non-CORS), allow the request. // If Origin is present, it must match our app URL. if (origin && ALLOWED_ORIGINS.length > 0 && !ALLOWED_ORIGINS.includes(origin)) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } return null; } /** * Validate that a value is a non-empty string. */ export function isNonEmptyString(v: unknown): v is string { return typeof v === "string" && v.trim().length > 0; } /** * Validate that a value is a positive integer. */ export function isPositiveInt(v: unknown): v is number { return typeof v === "number" && Number.isInteger(v) && v > 0; } /** * Validate a basic email format. */ export function isValidEmail(v: unknown): v is string { return typeof v === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); } /** * Validate that an ID matches Medusa's expected format (prefix_alphanumeric). * Prevents path traversal via ID parameters. */ export function isValidMedusaId(v: unknown): v is string { return typeof v === "string" && /^[a-z]+_[A-Za-z0-9]+$/.test(v); } /** * Pick only allowed address fields from an unknown object. * Prevents mass assignment of unexpected fields. */ export function pickAddressFields(obj: unknown): Record | null { if (!obj || typeof obj !== "object") return null; const src = obj as Record; const ALLOWED_KEYS = [ "first_name", "last_name", "address_1", "address_2", "city", "province", "postal_code", "country_code", "phone", ] as const; const result: Record = {}; for (const key of ALLOWED_KEYS) { const val = src[key]; if (typeof val === "string") { result[key] = val; } } return result; } /** * Safely parse JSON from a request body. * Returns null if the body is invalid. */ export async function parseBody>( request: Request, ): Promise { try { return await request.json(); } catch { return null; } } /** * Escape a JSON string for safe embedding in a injection in JSON-LD blocks. */ export function safeJsonLd(data: unknown): string { return JSON.stringify(data).replace(/