// Next middleware (Next 16: proxy.ts). Two jobs: route the marketing // landing for logged-out visitors at "/", and (in clerk mode) run Clerk's // middleware so resolveBearer() has an auth context. import { NextResponse, type NextRequest } from "next/server"; import type { NextFetchEvent } from "next/server"; const SESSION_COOKIE = "cm_session"; // Rewrite "/" to the public marketing landing. Returns the rewrite Response, or // null to let the request fall through to the workspace app. function marketingRewrite(request: NextRequest): NextResponse | null { if (request.nextUrl.pathname !== "/") return null; const url = request.nextUrl.clone(); url.pathname = "/marketing"; return NextResponse.rewrite(url); } export default async function proxy( request: NextRequest, event: NextFetchEvent, ) { // Clerk mode: run Clerk's middleware (it also establishes the auth context // resolveBearer() needs) and decide the "/" rewrite from the REAL auth state. // We must NOT sniff the __session cookie ourselves — it's a short-lived (~60s) // token that is frequently absent for a signed-in user, which would bounce // authed users to the marketing page. Use Clerk's auth() instead. if (process.env.AUTH_MODE === "clerk") { const { clerkMiddleware } = await import("@clerk/nextjs/server"); return clerkMiddleware(async (auth, req) => { if (req.nextUrl.pathname === "/") { const { userId } = await auth(); if (!userId) return marketingRewrite(req) ?? undefined; } return undefined; })(request, event); } // Local mode: key on our own session cookie. if (!request.cookies.has(SESSION_COOKIE)) { // LOCAL-ONLY: a single-user local stack goes straight to the dashboard. // The route itself re-checks both env vars and 404s without them, so this // redirect is inert in any deployment that has not opted in. Excluded // paths would otherwise loop: /auth/autologin sets the cookie, and /login // must stay reachable to show a failed-autologin message. const path = request.nextUrl.pathname; if ( process.env.LOCAL_AUTOLOGIN_EMAIL && process.env.LOCAL_AUTOLOGIN_PASSWORD && path !== "/auth/autologin" && path !== "/login" && // The podcast feed and its audio authenticate by `?token=`, and are // fetched by a podcast app that has no cookie and follows redirects // blindly. Sent to autologin it would store an HTML page as the episode. // The backend still validates the token, so this exempts the redirect, // not the auth. !path.startsWith("/api/podcast/") ) { const url = request.nextUrl.clone(); url.pathname = "/auth/autologin"; url.search = `?next=${encodeURIComponent(path + request.nextUrl.search)}`; return NextResponse.redirect(url); } const rewrite = marketingRewrite(request); if (rewrite) return rewrite; } return NextResponse.next(); } export const config = { // Everything except static assets and prebuilt files. matcher: ["/((?!_next|icons|fonts|.*\\.(?:png|svg|ico|woff2?|js|css|map)$).*)"], };