apps/web/proxy.ts
Rama main · réplica de lectura del repositorio original.
import { statSync } from "node:fs";
import { resolve, sep } from "node:path";
import { db } from "@cap/database";
import {
DUB_CLICK_ID_COOKIE,
dubCookieDomain,
parseDubClickId,
} from "@cap/database/referrals";
import { organizations } from "@cap/database/schema";
import { buildEnv, serverEnv } from "@cap/env";
import { eq } from "drizzle-orm";
import { notFound } from "next/navigation";
import { type NextRequest, NextResponse, userAgent } from "next/server";
import { agentMarkdownResponse } from "@/lib/agent-markdown";
import { getShareIframeRedirectUrl } from "@/lib/share-iframe-navigation";
const addHttps = (s?: string) => {
if (!s) return s;
return `https://${s}`;
};
const publicDir = resolve(process.cwd(), "public");
const isPublicAsset = (path: string) => {
try {
const file = resolve(publicDir, `.${decodeURIComponent(path)}`);
if (!file.startsWith(`${publicDir}${sep}`)) return false;
return statSync(file, { throwIfNoEntry: false })?.isFile() ?? false;
} catch {
return false;
}
};
const mainOrigins = [
"https://cap.so",
"https://cap.link",
"http://localhost",
serverEnv().WEB_URL,
addHttps(serverEnv().VERCEL_URL_HOST),
addHttps(serverEnv().VERCEL_BRANCH_URL_HOST),
addHttps(serverEnv().VERCEL_PROJECT_PRODUCTION_URL_HOST),
].filter(Boolean) as string[];
const FRAME_PROTECTED_PATHS = [
"/dashboard",
"/onboarding",
"/invite",
"/cli/authorize",
"/mcp/authorize",
"/admin",
"/signup",
"/verify-otp",
"/checkout",
];
const isFrameProtectedPath = (path: string) =>
FRAME_PROTECTED_PATHS.some(
(protectedPath) =>
path === protectedPath || path.startsWith(`${protectedPath}/`),
);
const setFrameHeaders = (response: NextResponse) => {
response.headers.set("X-Frame-Options", "SAMEORIGIN");
response.headers.set(
"Content-Security-Policy",
"frame-ancestors https://cap.so",
);
return response;
};
const REFERRAL_CLICK_MAX_AGE_SECONDS = 90 * 24 * 60 * 60;
// Partner links (go.cap.so) redirect here with ?dub_id=<click>. The Dub script
// stores it too, but ad blockers and no-JS visits skip the script, and without
// the cookie the sign-up and purchase are never credited to the partner.
const rememberReferralClick = (
request: NextRequest,
response: NextResponse,
) => {
if (buildEnv.NEXT_PUBLIC_IS_CAP !== "true" || request.method !== "GET") {
return response;
}
const url = request.nextUrl;
const clickId = parseDubClickId(url.searchParams.get(DUB_CLICK_ID_COOKIE));
if (!clickId) return response;
response.cookies.set(DUB_CLICK_ID_COOKIE, clickId, {
domain: dubCookieDomain(url.hostname),
path: "/",
sameSite: "lax",
secure: url.protocol === "https:",
maxAge: REFERRAL_CLICK_MAX_AGE_SECONDS,
});
return response;
};
const DUB_PROXY_PREFIX = "/_proxy/dub/";
const dubProxyTarget = (path: string) => {
if (path === `${DUB_PROXY_PREFIX}script.js`) {
return "https://www.dubcdn.com/analytics/script.js";
}
const track = /^\/_proxy\/dub\/track\/([a-z-]{1,32})$/.exec(path);
return track ? `https://api.dub.co/track/${track[1]}` : null;
};
// Serves the Dub script and its click tracking from cap.so so ad blockers
// don't drop them. Visitor cookies (including the Cap session) are stripped:
// Dub needs none of them.
const proxyDub = (request: NextRequest) => {
const target =
buildEnv.NEXT_PUBLIC_IS_CAP === "true"
? dubProxyTarget(request.nextUrl.pathname)
: null;
if (!target) return new NextResponse(null, { status: 404 });
const headers = new Headers(request.headers);
headers.delete("cookie");
headers.delete("authorization");
return NextResponse.rewrite(new URL(target), { request: { headers } });
};
export async function proxy(request: NextRequest) {
const path = new URL(request.url).pathname;
if (path.startsWith(DUB_PROXY_PREFIX)) return proxyDub(request);
if (path.startsWith("/login")) {
return rememberReferralClick(request, setFrameHeaders(NextResponse.next()));
}
const response = await route(request);
return rememberReferralClick(
request,
isFrameProtectedPath(path) ? setFrameHeaders(response) : response,
);
}
async function route(request: NextRequest) {
const url = new URL(request.url);
const path = url.pathname;
if (path === "/" && request.cookies.has("next-auth.session-token")) {
return NextResponse.redirect(new URL("/dashboard/caps", url.origin));
}
const shareIframeRedirectUrl = getShareIframeRedirectUrl({
method: request.method,
requestUrl: request.url,
fetchDestination: request.headers.get("sec-fetch-dest"),
});
if (shareIframeRedirectUrl) {
const response = NextResponse.redirect(shareIframeRedirectUrl);
response.headers.set("Cache-Control", "private, no-store");
response.headers.set("Vary", "Sec-Fetch-Dest");
return response;
}
const hostname = url.hostname;
if (buildEnv.NEXT_PUBLIC_IS_CAP !== "true") {
// Files under public/ have no route of their own, so without this every
// <img src="/logos/..."> on a self-hosted instance redirects to /login.
if (
!(
isPublicAsset(path) ||
path.startsWith("/s/") ||
path.startsWith("/c/") ||
path.startsWith("/cli/") ||
path.startsWith("/mcp/") ||
path.startsWith("/middleware") ||
path.startsWith("/dashboard") ||
path.startsWith("/onboarding") ||
path.startsWith("/api") ||
path.startsWith("/login") ||
path.startsWith("/signup") ||
path.startsWith("/invite") ||
path.startsWith("/self-hosting") ||
path.startsWith("/download") ||
path.startsWith("/terms") ||
path.startsWith("/verify-otp") ||
path.startsWith("/embed/") ||
path.startsWith("/.well-known/workflow/") ||
path.startsWith("/.well-known/oauth-") ||
path.startsWith("/.well-known/mcp") ||
path === "/.well-known/api-catalog" ||
path === "/openapi.json"
) &&
process.env.NODE_ENV !== "development"
)
return NextResponse.redirect(new URL("/login", url.origin));
else return NextResponse.next();
}
if (mainOrigins.some((d) => url.origin.startsWith(d))) {
const markdown = await agentMarkdownResponse(request, { isPublicAsset });
if (!markdown) return NextResponse.next();
return new NextResponse(markdown.body, {
status: markdown.status,
headers: markdown.headers,
});
}
const webUrl = new URL(serverEnv().WEB_URL).hostname;
try {
if (!(path.startsWith("/s/") || path.startsWith("/c/"))) {
const url = new URL(request.url);
url.hostname = webUrl;
return NextResponse.redirect(url);
}
const verifiedDomain = request.cookies.get("verified_domain");
if (verifiedDomain?.value === hostname) return NextResponse.next();
const [organization] = await db()
.select()
.from(organizations)
.where(eq(organizations.customDomain, hostname));
if (!organization || !organization.domainVerified) {
const url = new URL(request.url);
url.hostname = webUrl;
return NextResponse.redirect(url);
}
const response = NextResponse.next();
response.cookies.set("verified_domain", hostname, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 3600,
});
const { pathname } = request.nextUrl;
const referrer = request.headers.get("referer") || "";
const ua = userAgent(request);
response.headers.set("x-pathname", pathname);
response.headers.set("x-referrer", referrer);
response.headers.set("x-user-agent", JSON.stringify(ua));
return response;
} catch (error) {
console.error("Error in proxy:", error);
return notFound();
}
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
],
};