Files
calendar/apps/web/proxy.ts
Keith WilliamsGitHubcubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
8a3944f987 chore: Remove /api/ routes from proxy (#27883)
* chore: Remove /api/ routes from proxy

* Update apps/web/proxy.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* Add tests: apps/web/proxy.test.ts

Generated by Paragon from proposal for PR #27883

* Update proxy.test.ts

* fix: update proxy tests to account for /api/auth/signup in matcher

Co-Authored-By: unknown <>

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-12 07:33:47 -03:00

170 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import process from "node:process";
import { getCspHeader, getCspNonce } from "@lib/csp";
import { get } from "@vercel/edge-config";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
try {
return get<T>(key);
} catch (error) {
// Don't crash if EDGE_CONFIG env var is missing
}
};
// Vercel/Edge rejects nonASCII header values (see: https://github.com/vercel/next.js/issues/85631)
const isAscii = (s: string) => {
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) return false;
return true;
};
const stripNonAscii = (s: string) => {
let out = "";
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) <= 0x7f) out += s[i];
return out;
};
const sanitizeRequestHeaders = (headers: Iterable<[string, string]>): Headers => {
const out = new Headers();
for (const [name, raw] of Array.from(headers)) {
if (!isAscii(name)) continue;
let value = raw;
if (!isAscii(value)) {
// Heuristic: if the string contains common mojibake markers (Ã: 0xC3, Â: 0xC2),
// prefer a simple strip (avoids introducing spurious ASCII letters like 'A').
let hasMojibakeMarker = false;
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code === 0xc3 || code === 0xc2) {
hasMojibakeMarker = true;
break;
}
}
if (hasMojibakeMarker) {
value = stripNonAscii(value);
} else {
try {
value = stripNonAscii(value.normalize("NFKD"));
} catch {
value = stripNonAscii(value);
}
}
}
if (value) out.set(name, value);
}
return out;
};
const isPagePathRequest = (url: URL) => {
const isNonPagePathPrefix = /^\/(?:_next|api)\//;
const isFile = /\..*$/;
const { pathname } = url;
return !isNonPagePathPrefix.test(pathname) && !isFile.test(pathname);
};
const shouldEnforceCsp = (url: URL) => {
return url.pathname.startsWith("/auth/login") || url.pathname.startsWith("/login");
};
const proxy = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const url = req.nextUrl;
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
if (url.pathname.startsWith("/api/auth/signup")) {
const isSignupDisabled = await safeGet<boolean>("isSignupDisabled");
// If is in maintenance mode, point the url pathname to the maintenance page
if (isSignupDisabled) {
// TODO: Consider using responseWithHeaders here
return NextResponse.json({ error: "Signup is disabled" }, { status: 503 });
}
}
if (url.pathname.startsWith("/apps/installed")) {
const returnTo = reqWithEnrichedHeaders.cookies.get("return-to");
if (returnTo?.value) {
const response = NextResponse.redirect(new URL(returnTo.value, reqWithEnrichedHeaders.url), {
headers: requestHeaders,
});
response.cookies.delete("return-to");
return response;
}
}
const res = NextResponse.next({
request: {
headers: sanitizeRequestHeaders(requestHeaders),
},
});
if (url.pathname.startsWith("/auth/logout")) {
res.cookies.delete("next-auth.session-token");
}
return responseWithHeaders({ url, res, req: reqWithEnrichedHeaders });
};
const embeds = {
addResponseHeaders: ({ url, res }: { url: URL; res: NextResponse }) => {
if (!url.pathname.endsWith("/embed")) {
return res;
}
const isCOEPEnabled = url.searchParams.get("flag.coep") === "true";
if (isCOEPEnabled) {
res.headers.set("Cross-Origin-Embedder-Policy", "require-corp");
}
const embedColorScheme = url.searchParams.get("ui.color-scheme");
if (embedColorScheme) {
res.headers.set("x-embedColorScheme", embedColorScheme);
}
res.headers.set("x-isEmbed", "true");
return res;
},
};
const contentSecurityPolicy = {
addResponseHeaders: ({ res, req }: { res: NextResponse; req: NextRequest }) => {
const nonce = req.headers.get("x-csp-nonce");
if (!nonce) {
res.headers.set("x-csp-status", "not-opted-in");
return res;
}
const cspHeader = getCspHeader({ shouldEnforceCsp: shouldEnforceCsp(req.nextUrl), nonce });
if (cspHeader) {
res.headers.set(cspHeader.name, cspHeader.value);
}
return res;
},
addRequestHeaders: ({ req }: { req: NextRequest }) => {
if (!process.env.CSP_POLICY) {
return req;
}
const isCspApplicable = isPagePathRequest(req.nextUrl);
if (!isCspApplicable) {
return req;
}
const nonce = getCspNonce();
req.headers.set("x-csp-nonce", nonce);
return req;
},
};
function responseWithHeaders({ url, res, req }: { url: URL; res: NextResponse; req: NextRequest }) {
const resWithCSP = contentSecurityPolicy.addResponseHeaders({ res, req });
const resWithEmbeds = embeds.addResponseHeaders({ url, res: resWithCSP });
return resWithEmbeds;
}
function enrichRequestWithHeaders({ req }: { req: NextRequest }) {
const reqWithCSP = contentSecurityPolicy.addRequestHeaders({ req });
return reqWithCSP;
}
export const config = {
matcher: ["/auth/login", "/login", "/apps/installed", "/auth/logout", "/:path*/embed", "/availability", "/api/auth/signup"],
};
export default proxy;