import { NextRequest, NextResponse } from "next/server" /** * Edge middleware that establishes workspace context from the request Host * when a visitor lands on .reparee.com WITHOUT a prior handoff. * * - Skips if the workspace_uuid cookie is already set (handoff already happened). * - Skips for the shared fallback host `tenant.reparee.com` and any non-tenant * host (apex, localhost, preview URLs). * - For a tenant subdomain, HMAC-signs the subdomain and calls SaaS * `GET /api/saas/workspaces/by-subdomain/` to resolve to * { workspace_uuid, api_base_url }, then sets those as HttpOnly cookies. * * Matcher excludes /api/proxy/* (proxy needs no resolution; cookies already set) * and /activate/* (the handoff itself sets the cookies via the page handler). */ export const config = { matcher: [ "/((?!api/proxy|_next/static|_next/image|favicon\\.ico|robots\\.txt|sitemap\\.xml|activate).*)", ], } const SUBDOMAIN_RE = /^([a-z0-9-]+)\.reparee\.com$/i const SKIP_SUBDOMAINS = new Set(["tenant", "www"]) const COOKIE_MAX_AGE = 60 * 60 * 24 * 7 // 7 days export async function middleware(req: NextRequest) { if (req.cookies.get("workspace_uuid")) { return NextResponse.next() } const host = (req.headers.get("host") || "").toLowerCase() const match = host.match(SUBDOMAIN_RE) if (!match) return NextResponse.next() const sub = match[1].toLowerCase() if (SKIP_SUBDOMAINS.has(sub)) return NextResponse.next() const saasUrl = process.env.NEXT_PUBLIC_SAAS_URL || "https://reparee.com" const secret = process.env.SAAS_SHARED_SECRET || "" if (!secret) { return NextResponse.next() } let workspaceUuid: string | undefined let apiBaseUrl: string | undefined try { const signature = await hmacSha256Hex(sub, secret) const resolveUrl = `${saasUrl.replace(/\/$/, "")}/api/saas/workspaces/by-subdomain/${encodeURIComponent(sub)}` const res = await fetch(resolveUrl, { headers: { "X-SaaS-Signature": signature, Accept: "application/json" }, cache: "no-store", signal: AbortSignal.timeout(2500), }) if (!res.ok) return NextResponse.next() const data = (await res.json()) as { ok?: boolean; workspace_uuid?: string; api_base_url?: string } if (!data.ok || !data.workspace_uuid || !data.api_base_url) { return NextResponse.next() } workspaceUuid = data.workspace_uuid apiBaseUrl = data.api_base_url } catch { return NextResponse.next() } const response = NextResponse.next() response.cookies.set("workspace_uuid", workspaceUuid!, { httpOnly: true, secure: true, sameSite: "strict", path: "/", maxAge: COOKIE_MAX_AGE, }) response.cookies.set("api_base_url", apiBaseUrl!, { httpOnly: true, secure: true, sameSite: "strict", path: "/", maxAge: COOKIE_MAX_AGE, }) return response } async function hmacSha256Hex(message: string, key: string): Promise { const enc = new TextEncoder() const cryptoKey = await crypto.subtle.importKey( "raw", enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ) const sig = await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(message)) return Array.from(new Uint8Array(sig)) .map((b) => b.toString(16).padStart(2, "0")) .join("") }