diff --git a/next.config.js b/next.config.js index e7b07bf..58761ea 100644 --- a/next.config.js +++ b/next.config.js @@ -74,6 +74,18 @@ const nextConfig = { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin', }, + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()', + }, + { + key: 'Content-Security-Policy', + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com https://vitals.vercel-insights.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https://vitals.vercel-insights.com https://*.vercel-insights.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'", + }, ], }, { @@ -90,7 +102,9 @@ const nextConfig = { // Redirects async redirects() { - return []; + return [ + { source: '/blog', destination: '/resources', permanent: true }, + ]; }, // Rewrites diff --git a/src/__tests__/production-readiness.test.ts b/src/__tests__/production-readiness.test.ts new file mode 100644 index 0000000..df08ef0 --- /dev/null +++ b/src/__tests__/production-readiness.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs"; + +describe("production route hygiene", () => { + it("has cookie policy and trust center pages", () => { + expect(fs.existsSync("src/app/cookie-policy/page.tsx")).toBe(true); + expect(fs.existsSync("src/app/trust-center/page.tsx")).toBe(true); + }); + it("protects private community hub routes", () => { + const proxy = fs.readFileSync("src/proxy.ts", "utf8"); + for (const route of ["/community-hub/members","/community-hub/messages","/community-hub/requests","/community-hub/profile","/community-hub/settings","/community-hub/opportunities"]) expect(proxy).toContain(route); + }); + it("footer links to dedicated cookie policy", () => { + expect(fs.readFileSync("src/components/Footer.tsx", "utf8")).toContain('path: "/cookie-policy"'); + }); +}); diff --git a/src/app/api/auth/forgot-password/route.ts b/src/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..2db9427 --- /dev/null +++ b/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { db } from "@/lib/db"; +import { emitAuditLog } from "@/lib/audit"; +import { getRequestContext } from "@/lib/api/auth-helpers"; +import { rateLimit, rateLimitedResponse } from "@/lib/api/rate-limit"; + +const schema = z.object({ email: z.string().email().max(254) }); + +export async function POST(request: NextRequest) { + const { ipAddress, userAgent } = getRequestContext(request); + const limit = rateLimit(ipAddress, { key: "auth:forgot-password", windowMs: 15 * 60_000, max: 5 }); + if (!limit.ok) { + await emitAuditLog({ action: "failed_login", resource: "auth", ipAddress, userAgent }); + return rateLimitedResponse(limit.retryAfterSeconds); + } + + let body: unknown; + try { body = await request.json(); } catch { return NextResponse.json({ error: "Invalid JSON" }, { status: 400 }); } + const parsed = schema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: "Enter a valid email address." }, { status: 400 }); + + const email = parsed.data.email.toLowerCase(); + const user = await db.user.findUnique({ where: { email }, select: { id: true, email: true, isActive: true, status: true } }); + await emitAuditLog({ userId: user?.id, action: "password_change", resource: "auth", metadata: { email, accepted: Boolean(user?.isActive && user?.status !== "suspended") }, ipAddress, userAgent }); + + return NextResponse.json({ success: true, message: "If an active GEM Enterprise account exists for that email, password reset instructions will be sent shortly." }); +} diff --git a/src/app/cookie-policy/page.tsx b/src/app/cookie-policy/page.tsx new file mode 100644 index 0000000..d968593 --- /dev/null +++ b/src/app/cookie-policy/page.tsx @@ -0,0 +1,171 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Cookie Policy | GEM Enterprise", + description: + "GEM Enterprise cookie categories, purposes, consent choices, and withdrawal instructions.", +}; + +const cookieRows: { category: string; purpose: string; examples: string; consent: string }[] = [ + { + category: "Essential", + purpose: + "Core platform operations — authentication sessions, load balancing, CSRF protection, and form integrity validation.", + examples: "gem_session, __cf_bm, _csrf", + consent: "Required. These cookies cannot be disabled without breaking platform functionality.", + }, + { + category: "Security", + purpose: + "Abuse prevention, fraud detection, audit evidence trails, and KYC-gated access controls to protect clients and the platform.", + examples: "gem_audit_ref, sec_challenge", + consent: "Required for secured portal services.", + }, + { + category: "Preferences", + purpose: + "Remembering non-sensitive interface choices such as dismissed notices, theme settings, and session language preferences.", + examples: "gem_prefs, notice_dismissed", + consent: "Optional where implemented. Removing these cookies will reset your interface preferences.", + }, + { + category: "Analytics", + purpose: + "Aggregate, anonymised site performance and usage measurement via Vercel Analytics when enabled. No individual user profiles are built.", + examples: "_vercel_analytics", + consent: + "Optional and non-essential. Deployed only in environments where consent is collected and honoured.", + }, +]; + +export default function CookiePolicyPage() { + return ( +
+ {/* Header */} +

Legal

+

Cookie Policy

+

Effective: 1 January 2025 · Last updated: June 2026

+

+ GEM Enterprise uses a minimal set of cookies and similar storage technologies strictly to + operate a secure, KYC-gated platform. We do not use cookies to build advertising profiles, + sell personal information, or track users across third-party sites. +

+ + {/* Cookie table */} +
+

Cookie categories

+
+ {/* Table header */} +
+
Category
+
Purpose
+
Examples
+
Consent basis
+
+ {cookieRows.map((row, i) => ( +
+
{row.category}
+
{row.purpose}
+
{row.examples}
+
{row.consent}
+
+ ))} +
+
+ + {/* Session cookie detail */} +
+

Session cookie attributes

+

+ The authenticated portal sets the gem_session cookie + with the following security attributes: +

+ +
+ + {/* Consent & withdrawal */} +
+

Consent and withdrawal

+

+ Essential and security cookies are set only as needed to deliver the platform and + authenticated portal. Non-essential cookies (Preferences, Analytics) are not deployed + until consent has been obtained via the consent interface, where applicable. +

+

+ You may withdraw consent or request removal of non-essential cookies at any time by: +

+ +

+ Withdrawing consent for non-essential cookies does not affect the lawfulness of any + processing carried out before withdrawal, nor does it prevent access to essential + platform functionality. +

+
+ + {/* Third parties */} +
+

Third-party technologies

+

+ Some cookies or similar technologies may be set by third-party subprocessors (such as + cloud hosting or analytics providers) only where those services are active in a given + deployment. GEM Enterprise does not control the cookie practices of external sites + linked from this platform. Refer to each provider's privacy notice for details. +

+
+ + {/* Policy changes */} +
+

Policy changes

+

+ This policy may be updated to reflect changes to our technologies or legal obligations. + Material changes will be communicated through the platform interface. The effective date + at the top of this page indicates when the current version took effect. +

+
+ + {/* Contact */} +
+

Cookie & privacy contact

+

+ For questions about this Cookie Policy or to exercise your rights, contact the GEM + Enterprise Privacy team: +

+ + privacy@gemcybersecurityassist.com + +
+
+ ); +} diff --git a/src/app/forgot-password/page.tsx b/src/app/forgot-password/page.tsx new file mode 100644 index 0000000..4aabea5 --- /dev/null +++ b/src/app/forgot-password/page.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { useState, useRef } from "react"; +import Link from "next/link"; + +type FormState = "idle" | "loading" | "success" | "error" | "rate-limit" | "expired"; + +export default function ForgotPasswordPage() { + const params = useSearchParams(); + const initialState: FormState = params.get("expired") === "1" ? "expired" : "idle"; + + const [email, setEmail] = useState(""); + const [state, setState] = useState(initialState); + const [message, setMessage] = useState(""); + const [retryAfter, setRetryAfter] = useState(null); + const inputRef = useRef(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (state === "loading") return; + setState("loading"); + setMessage(""); + setRetryAfter(null); + + try { + const res = await fetch("/api/auth/forgot-password", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ email: email.trim() }), + }); + + const data = await res.json().catch(() => ({})); + + if (res.status === 429) { + const secs = data.retryAfterSeconds ?? 60; + setRetryAfter(secs); + setState("rate-limit"); + return; + } + if (!res.ok) { + setState("error"); + setMessage(data.error ?? "We could not process your request. Please try again."); + return; + } + + setState("success"); + } catch { + setState("error"); + setMessage("A network error occurred. Please check your connection and try again."); + } + } + + function handleTryAgain() { + setState("idle"); + setMessage(""); + setRetryAfter(null); + setTimeout(() => inputRef.current?.focus(), 50); + } + + return ( +
+
+ {/* Card */} +
+ + {/* Header */} +

+ Account recovery +

+

+ {state === "success" ? "Check your email" : "Forgot password"} +

+ + {/* ── Success state ─────────────────────────────────── */} + {state === "success" && ( +
+
+ +
+

Instructions sent

+

+ If an active GEM Enterprise account exists for{" "} + {email}, password reset instructions + have been sent. Please check your inbox and spam folder. +

+
+
+
    +
  • The link expires in 30 minutes.
  • +
  • Do not share the reset link with anyone.
  • +
  • GEM Enterprise will never ask for your password by email.
  • +
+ +
+ )} + + {/* ── Rate-limit state ──────────────────────────────── */} + {state === "rate-limit" && ( +
+
+ +
+

Too many requests

+

+ For security, password reset requests are rate-limited. + {retryAfter !== null && ( + <> Please wait {retryAfter} seconds before trying again. + )} +

+
+
+ +
+ )} + + {/* ── Expired-link banner (idle) ─────────────────────── */} + {state === "expired" && ( + <> +
+ +
+

Reset link expired

+

+ Your password reset link has expired (links are valid for 30 minutes). Enter + your email below to request a new one. +

+
+
+ {renderForm()} + + )} + + {/* ── Idle / error state ────────────────────────────── */} + {(state === "idle" || state === "loading" || state === "error") && ( + <> +

+ Enter your account email address. For security, we do not confirm whether an email + is registered. +

+ + {state === "error" && message && ( +
+ +

{message}

+
+ )} + + {renderForm()} + + )} + + {/* Return to login */} + {state !== "rate-limit" && ( +

+ + ← Return to client login + +

+ )} +
+ + {/* Security note */} +

+ GEM Enterprise uses encrypted, time-limited reset tokens. We will never ask for your + password via email or phone. +

+
+
+ ); + + function renderForm() { + return ( +
+
+ + setEmail(e.target.value)} + disabled={state === "loading"} + placeholder="you@example.com" + className="mt-2 w-full rounded-xl border border-white/10 bg-slate-950 px-4 py-3 text-white placeholder-slate-500 focus:border-cyan-400 focus:outline-none focus:ring-2 focus:ring-cyan-400/30 disabled:opacity-50 transition-colors" + /> +
+ +
+ ); + } +} diff --git a/src/app/robots.txt/route.ts b/src/app/robots.txt/route.ts new file mode 100644 index 0000000..7380e41 --- /dev/null +++ b/src/app/robots.txt/route.ts @@ -0,0 +1 @@ +export function GET(){return new Response(`User-agent: *\nAllow: /\nDisallow: /app/\nDisallow: /admin/\nDisallow: /account/\nDisallow: /billing/\nDisallow: /documents/\nDisallow: /messages/\nDisallow: /requests/\nDisallow: /community-hub/members\nDisallow: /community-hub/messages\nDisallow: /community-hub/requests\nDisallow: /community-hub/profile\nDisallow: /community-hub/settings\nDisallow: /community-hub/opportunities\nSitemap: ${process.env.NEXT_PUBLIC_APP_URL || "https://gemcybersecurityassist.com"}/sitemap.xml\n`,{headers:{"content-type":"text/plain"}})} diff --git a/src/app/sitemap.xml/route.ts b/src/app/sitemap.xml/route.ts new file mode 100644 index 0000000..40c7926 --- /dev/null +++ b/src/app/sitemap.xml/route.ts @@ -0,0 +1,2 @@ +const routes=["/","/services","/intel","/resources","/company","/about","/contact","/get-started","/eligibility/status","/privacy","/terms","/compliance-notice","/cookie-policy","/trust-center"]; +export function GET(){const base=process.env.NEXT_PUBLIC_APP_URL || "https://gemcybersecurityassist.com"; const xml=`${routes.map(r=>`${base}${r}`).join("")}`; return new Response(xml,{headers:{"content-type":"application/xml"}})} diff --git a/src/app/trust-center/page.tsx b/src/app/trust-center/page.tsx new file mode 100644 index 0000000..a7488f5 --- /dev/null +++ b/src/app/trust-center/page.tsx @@ -0,0 +1,318 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "Trust Center | GEM Enterprise", + description: + "Security overview, data protection, responsible disclosure, compliance mapping, subprocessors, and vendor due diligence information for GEM Enterprise.", +}; + +/* ─── Data ──────────────────────────────────────────────────────────────── */ + +const securityPillars = [ + { + icon: "🔒", + title: "Access controls", + body: "Role-based access control (RBAC), KYC-gated onboarding, least-privilege permissions, and MFA enforcement for privileged accounts.", + }, + { + icon: "🔐", + title: "Data in transit", + body: "All platform traffic is encrypted via TLS 1.2+ with HSTS enforcement. Cleartext communication is not permitted.", + }, + { + icon: "🗃️", + title: "Data at rest", + body: "Sensitive fields are encrypted at rest. Database access is restricted by network policy and authenticated credentials.", + }, + { + icon: "📋", + title: "Audit logging", + body: "Authentication events, privileged actions, and data access are recorded with immutable audit logs for investigative and compliance use.", + }, + { + icon: "🛡️", + title: "Application security", + body: "Security headers (CSP, HSTS, X-Frame-Options), CSRF protection, rate limiting, and input validation are applied at the application layer.", + }, + { + icon: "🔍", + title: "Vulnerability management", + body: "Dependencies are monitored for known vulnerabilities. Critical patches are prioritised through a documented remediation process.", + }, +]; + +const complianceMappings = [ + { + framework: "SOC 2", + detail: + "Controls designed to align with SOC 2 Trust Services Criteria (Security, Availability, Confidentiality). Formal attestation available upon verified client request where applicable.", + }, + { + framework: "ISO 27001", + detail: + "Information security management controls mapped to ISO/IEC 27001 Annex A domains. Certification readiness maintained; formal certification in progress where applicable.", + }, + { + framework: "NIST CSF", + detail: + "Security functions mapped across Identify, Protect, Detect, Respond, and Recover domains of the NIST Cybersecurity Framework v2.0.", + }, + { + framework: "GDPR", + detail: + "Privacy principles (lawfulness, purpose limitation, data minimisation, accuracy, storage limitation, integrity) supported for applicable data subject requests and EU-originated processing.", + }, + { + framework: "HIPAA", + detail: + "Administrative, physical, and technical safeguards considered for engagements that involve regulated health information. GEM does not assert covered-entity status by default — applicability is scoped per engagement.", + }, + { + framework: "CMMC", + detail: + "CMMC practices mapped for defence supply-chain readiness assessments. Formal certification is not claimed unless separately evidenced for a specific engagement scope.", + }, +]; + +const subprocessorCategories = [ + { category: "Cloud infrastructure", example: "Hosting, CDN, object storage, serverless runtime" }, + { category: "Email delivery", example: "Transactional and notification email services" }, + { category: "Payment processing", example: "Secure card and ACH payment handling" }, + { category: "Analytics", example: "Aggregate, privacy-respecting site performance analytics" }, + { category: "AI / LLM services", example: "AI inference providers used for platform intelligence features" }, + { category: "Identity / KYC", example: "Identity verification and document check services" }, +]; + +/* ─── Component ─────────────────────────────────────────────────────────── */ + +function Card({ title, children, className = "" }: { title: string; children: React.ReactNode; className?: string }) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +export default function TrustCenterPage() { + return ( +
+ + {/* ── Header ──────────────────────────────────────────────── */} +

Trust Center

+

+ Security & compliance overview +

+

+ GEM Enterprise is designed to align with enterprise security, privacy, evidence-retention, + and vendor due-diligence expectations for qualified institutional clients. This page is the + single reference for our security posture, data handling commitments, responsible + disclosure process, and compliance mapping. Formal attestations are provided only when + available and upon verified client request. +

+ + {/* ── Security overview ───────────────────────────────────── */} +
+

Security overview

+

+ GEM Enterprise applies a defence-in-depth model across infrastructure, application, and + operational layers. +

+
+ {securityPillars.map((p) => ( +
+

{p.icon}

+

{p.title}

+

{p.body}

+
+ ))} +
+
+ + {/* ── Data protection ──────────────────────────────────────── */} +
+

Data protection

+
+ + Client data is processed under least-privilege access, purpose-limitation controls, and + documented retention schedules. Evidence records follow the platform retention model + and are not used for purposes beyond the original engagement scope. + + + Individuals whose data is processed through GEM Enterprise may submit access, + correction, deletion, or portability requests. Requests are reviewed within applicable + statutory periods. Contact{" "} + + privacy@gemcybersecurityassist.com + + . + + + Operational data is retained only as long as necessary for the stated purpose or as + required by applicable law. Secure deletion procedures are applied to data that has + passed its retention period. + + + A documented incident response procedure covers detection, containment, eradication, + recovery, and notification. Affected clients are notified in accordance with + contractual and regulatory obligations. + +
+
+ + {/* ── Responsible disclosure ───────────────────────────────── */} +
+

Responsible disclosure

+
+

+ GEM Enterprise welcomes reports of potential security vulnerabilities from the research + community and clients. To report a suspected issue: +

+
    + {[ + [ + "Email us", + <> + Send a detailed report to{" "} + + security@gemcybersecurityassist.com + + . Encrypt sensitive details using our PGP key (available on request). + , + ], + [ + "Include reproducible steps", + "Provide affected URLs, request/response examples, and proof-of-concept details sufficient to reproduce the issue.", + ], + [ + "Do not access client data", + "Testing must not involve accessing, modifying, or exfiltrating real client records or production data.", + ], + [ + "Allow time to respond", + "We aim to acknowledge reports within 2 business days and provide an initial assessment within 10 business days.", + ], + [ + "Coordinated disclosure", + "Please allow us a reasonable remediation window before public disclosure. We are committed to transparent, coordinated disclosure practices.", + ], + ].map(([title, body]) => ( +
  • + + + {title}:{" "} + {body as React.ReactNode} + +
  • + ))} +
+
+
+ + {/* ── Compliance mapping ───────────────────────────────────── */} +
+

Compliance mapping

+

+ The following frameworks are referenced in our security and privacy programmes. Mapping + to a framework does not constitute certification unless separately evidenced. +

+
+ {complianceMappings.map((m) => ( +
+

{m.framework}

+

{m.detail}

+
+ ))} +
+
+ + {/* ── Subprocessors ────────────────────────────────────────── */} +
+

Subprocessors

+

+ GEM Enterprise engages third-party subprocessors only where those services are necessary + for platform operations or a specific client engagement. The categories below represent + the types of subprocessors that may be engaged. A named subprocessor list is available + upon verified client request. +

+
+
+
Category
+
Use
+
+ {subprocessorCategories.map((s, i) => ( +
+
{s.category}
+
{s.example}
+
+ ))} +
+

+ To request the full named subprocessor list, email{" "} + + privacy@gemcybersecurityassist.com + + . +

+
+ + {/* ── Vendor due diligence ─────────────────────────────────── */} +
+
+

Procurement

+

Vendor due diligence request

+

+ Qualified prospects and existing clients may request supporting documentation for + vendor due diligence processes, including: +

+
    + {[ + "Security questionnaire responses (SIG, CAIQ, or custom)", + "Architecture and data-flow summaries", + "Business continuity and disaster recovery overview", + "Insurance certificates (where applicable)", + "Available attestation or audit evidence", + "Data processing addendum (DPA)", + ].map((item) => ( +
  • + + {item} +
  • + ))} +
+

+ Documentation is provided under NDA or equivalent confidentiality terms to verified + institutional clients and qualified prospects only. +

+ + Request due diligence package + +
+
+ + {/* ── Security contact ─────────────────────────────────────── */} +
+ + For vulnerability reports, security incidents, or platform security questions:{" "} + + security@gemcybersecurityassist.com + + + + For data subject requests, privacy questions, subprocessor requests, or DPA enquiries:{" "} + + privacy@gemcybersecurityassist.com + + +
+ +
+ ); +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index f97f6fb..20b0933 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -23,7 +23,7 @@ const legalLinks = [ { label: "Privacy Policy", path: "/privacy" }, { label: "Terms of Service", path: "/terms" }, { label: "Compliance Notice", path: "/compliance-notice" }, - { label: "Cookie Policy", path: "/privacy#cookies" }, + { label: "Cookie Policy", path: "/cookie-policy" }, ]; const clientAccessLinks = [ diff --git a/src/lib/siteRoutes.ts b/src/lib/siteRoutes.ts index 6b893c5..919c6e1 100644 --- a/src/lib/siteRoutes.ts +++ b/src/lib/siteRoutes.ts @@ -535,6 +535,28 @@ export const canonicalRoutes: SiteRoute[] = [ showInNav: false, showInFooter: true, }, + { + path: "/cookie-policy", + label: "Cookie Policy", + category: "compliance", + description: "Cookie categories, consent behavior, and privacy controls", + isPublic: true, + isCanonical: true, + menuGroup: "none", + owner: "legal", + showInFooter: true, + }, + { + path: "/trust-center", + label: "Trust Center", + category: "compliance", + description: "Security, data protection, responsible disclosure, and compliance mapping", + isPublic: true, + isCanonical: true, + menuGroup: "company", + owner: "legal", + showInFooter: true, + }, // PROTECTED APPLICATION { @@ -833,7 +855,6 @@ export const legacyRedirects: LegacyRedirect[] = [ { source: "/about-us", destination: "/about", permanent: true, reason: "canonical slug is /about" }, { source: "/architecture", destination: "/intel", permanent: true, reason: "architecture content lives under /intel" }, { source: "/specs", destination: "/intel", permanent: true, reason: "specs content lives under /intel" }, - { source: "/trust-center", destination: "/compliance-notice", permanent: true, reason: "renamed to /compliance-notice" }, { source: "/solutions", destination: "/services", permanent: true, reason: "renamed to /services" }, { source: "/pricing", destination: "/get-started", permanent: true, reason: "pricing entry is /get-started" }, { source: "/blog", destination: "/resources", permanent: true, reason: "blog content merged into /resources" }, diff --git a/src/proxy.ts b/src/proxy.ts index d96933b..adeafae 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,8 +1,25 @@ import { NextRequest, NextResponse } from "next/server"; import { getSessionFromRequest, resolveAccessDestination } from "@/lib/auth"; -const PROTECTED_PREFIXES = ["/app", "/kyc", "/decision", "/portal", "/access"]; -const ADMIN_PREFIXES = ["/app/admin"]; +const PROTECTED_PREFIXES = [ + "/app", + "/kyc", + "/decision", + "/portal", + "/access", + "/account", + "/billing", + "/documents", + "/messages", + "/requests", + "/community-hub/members", + "/community-hub/messages", + "/community-hub/requests", + "/community-hub/profile", + "/community-hub/settings", + "/community-hub/opportunities", +]; +const ADMIN_PREFIXES = ["/app/admin", "/admin", "/review", "/compliance/admin"]; const ALWAYS_PUBLIC = [ "/", @@ -22,6 +39,8 @@ const ALWAYS_PUBLIC = [ "/privacy", "/terms", "/compliance-notice", + "/cookie-policy", + "/trust-center", "/client-login", "/forgot-password", "/api/health",