From 7e85f0fe3ef98a0cf4a63f146fa25fa547be1384 Mon Sep 17 00:00:00 2001 From: commanderxgr Date: Sat, 5 Sep 2026 11:11:08 +0300 Subject: [PATCH] fix(routing): add vercel spa fallback rewrite to index.html --- package.json | 3 +- scripts/validate-vercel-spa-routing.mjs | 76 +++++++++++++++++++++++++ vercel.json | 6 ++ 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 scripts/validate-vercel-spa-routing.mjs diff --git a/package.json b/package.json index 71d17a4..1d80f87 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "security:scan": "npm run security:hardening && npm run security:integrity && npm run security:audit && npm run security:cve", "qa:central-portal-isolation": "node scripts/validate-central-portal-isolation.mjs", "qa:auth-broker-negative": "node scripts/test-auth-broker-negative-matrix.mjs", - "qa:product-terminology": "node scripts/validate-product-terminology.mjs" + "qa:product-terminology": "node scripts/validate-product-terminology.mjs", + "qa:vercel-spa-routing": "node scripts/validate-vercel-spa-routing.mjs" }, "dependencies": { "@dnd-kit/core": "^6.2.0", diff --git a/scripts/validate-vercel-spa-routing.mjs b/scripts/validate-vercel-spa-routing.mjs new file mode 100644 index 0000000..8f3d1e4 --- /dev/null +++ b/scripts/validate-vercel-spa-routing.mjs @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +console.log("--- RUNNING VERCEL SPA ROUTING CONFIG VALIDATION ---"); + +const vercelConfig = JSON.parse(readFileSync(new URL("../vercel.json", import.meta.url), "utf8")); + +// 1. Check rewrites existence +assert.ok(Array.isArray(vercelConfig.rewrites), "vercel.json must have a 'rewrites' array"); +assert.equal(vercelConfig.rewrites.length, 1, "vercel.json should have exactly 1 SPA fallback rewrite"); + +const spaRewrite = vercelConfig.rewrites[0]; +assert.equal(spaRewrite.destination, "/index.html", "Rewrite destination must be /index.html"); + +// 2. Validate source regex behavior +// Pattern: /((?!assets/|api/|.*\\.[\\w]+$).*) +const sourceRegex = new RegExp(`^${spaRewrite.source.replace(":path*", ".*")}$`); + +console.log("Testing pattern:", spaRewrite.source); + +// SPA routes that MUST rewrite to index.html +const routesShouldRewrite = [ + "/login", + "/register", + "/forgot-password", + "/reset-password", + "/select-tenant", + "/stores", + "/app", + "/request-token", + "/admin", + "/admin-console", + "/any/nested/client/route" +]; + +for (const route of routesShouldRewrite) { + assert.match(route, sourceRegex, `Route '${route}' must match SPA rewrite pattern`); +} + +// Static assets and API routes that MUST NOT rewrite to index.html +const routesShouldNotRewrite = [ + "/assets/index-B_93c1.js", + "/assets/index-D_81b2.css", + "/assets/vendor-chunk.js", + "/api/auth-broker", + "/api/v1/status", + "/favicon.ico", + "/manifest.json", + "/robots.txt", + "/logo.png" +]; + +for (const route of routesShouldNotRewrite) { + assert.doesNotMatch(route, sourceRegex, `Static/API path '${route}' must NOT match SPA rewrite pattern`); +} + +// 3. Ensure security headers are intact +assert.ok(Array.isArray(vercelConfig.headers), "vercel.json must have a 'headers' array"); +const globalHeaderEntry = vercelConfig.headers.find(h => h.source === "/:path*"); +assert.ok(globalHeaderEntry, "Global headers for /:path* must exist"); + +const headerKeys = globalHeaderEntry.headers.map(h => h.key); +const requiredHeaders = [ + "Strict-Transport-Security", + "X-Content-Type-Options", + "X-Frame-Options", + "Referrer-Policy", + "Permissions-Policy", + "Content-Security-Policy" +]; + +for (const header of requiredHeaders) { + assert.ok(headerKeys.includes(header), `Header '${header}' must be present in global headers`); +} + +console.log("✓ All Vercel SPA routing and security header tests passed."); diff --git a/vercel.json b/vercel.json index ea08777..2a13e5f 100644 --- a/vercel.json +++ b/vercel.json @@ -68,5 +68,11 @@ } ] } + ], + "rewrites": [ + { + "source": "/((?!assets/|api/|.*\\.[\\w]+$).*)", + "destination": "/index.html" + } ] }