import "server-only"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import type { CommerceFailure, CommerceResult } from "./repository"; const MAX_COMMERCE_BODY_BYTES = 8_192; export function validateJsonMutationRequest( request: NextRequest, ): { readonly ok: true } | { readonly response: NextResponse; readonly ok: false } { const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim(); if (contentType !== "application/json") { return { ok: false, response: errorResponse("INVALID_REQUEST", "Expected JSON input.", 415) }; } const contentLength = Number(request.headers.get("content-length") ?? "0"); if ( !Number.isFinite(contentLength) || contentLength < 0 || contentLength > MAX_COMMERCE_BODY_BYTES ) { return { ok: false, response: errorResponse("INVALID_REQUEST", "Request body is too large.", 413), }; } const origin = request.headers.get("origin"); const fetchSite = request.headers.get("sec-fetch-site"); const sameOriginRequest = fetchSite === "same-origin" || (origin === null ? fetchSite === null : sameOrigin(origin, request.nextUrl.origin)); if (!sameOriginRequest || fetchSite === "cross-site") { return { ok: false, response: errorResponse("INVALID_REQUEST", "Cross-site mutation denied.", 403), }; } return { ok: true }; } export function resultResponse(result: CommerceResult): NextResponse { return result.ok ? noStoreJson({ data: result.data, ok: true }, 200) : failureResponse(result.error); } export function failureResponse(error: CommerceFailure): NextResponse { return noStoreJson( { error: { code: error.code, message: error.message }, ok: false }, error.status, ); } export function errorResponse(code: string, message: string, status: number): NextResponse { return noStoreJson({ error: { code, message }, ok: false }, status); } export function noStoreJson(body: unknown, status: number): NextResponse { const response = NextResponse.json(body, { status }); response.headers.set("cache-control", "no-store"); response.headers.set("x-content-type-options", "nosniff"); return response; } function sameOrigin(value: string, expected: string): boolean { try { return new URL(value).origin === expected; } catch { return false; } }