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>
This commit is contained in:
Keith Williams
2026-02-12 07:33:47 -03:00
committed by GitHub
co-authored by cubic-dev-ai[bot] unknown Devin AI
parent be0b707296
commit 8a3944f987
2 changed files with 19 additions and 174 deletions
+18 -61
View File
@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
import type { Mock } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// We'll test the wrapped proxy as it would be used in production
import proxy, { checkPostMethod, POST_METHODS_ALLOWED_API_ROUTES } from "./proxy";
import proxy from "./proxy";
import { config } from "./proxy";
// Mock dependencies at module level
@@ -146,34 +146,6 @@ const callProxy = async (req: NextRequest): Promise<Response> => {
return (await (proxy as any)(req)) as Response;
};
describe("Middleware - POST requests restriction", () => {
const createRequest = (path: string, method: string) => {
return new NextRequest(
new Request(`${WEBAPP_URL}${path}`, {
method,
})
);
};
it("should allow POST requests to /api routes", async () => {
const req1 = createRequest("/api/auth/signup", "POST");
const res1 = checkPostMethod(req1);
expect(res1).toBeNull();
});
it("should allow GET requests to app routes", async () => {
const req = createRequest("/team/xyz", "GET");
const res = checkPostMethod(req);
expect(res).toBeNull();
});
it("should allow GET requests to /api routes", async () => {
const req = createRequest("/api/auth/signup", "GET");
const res = checkPostMethod(req);
expect(res).toBeNull();
});
});
describe("Middleware Integration Tests", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -486,9 +458,11 @@ describe("Middleware Matcher Configuration", () => {
it("should include all core middleware routes", () => {
expect(matcher).toContain("/auth/login");
expect(matcher).toContain("/login");
expect(matcher).toContain("/apps/installed");
expect(matcher).toContain("/auth/logout");
expect(matcher).toContain("/api/auth/signup");
expect(matcher).toContain("/apps/installed");
expect(matcher).toContain("/availability");
expect(matcher).toContain("/login");
expect(matcher).toContain("/:path*/embed");
});
@@ -497,37 +471,20 @@ describe("Middleware Matcher Configuration", () => {
expect(uniqueEntries.size).toBe(matcher.length);
});
it("should cover every POST_METHODS_ALLOWED_API_ROUTES route via exact match or wildcard", () => {
const wildcardPrefixes = matcher
.filter((entry) => entry.endsWith(":path*"))
.map((entry) => entry.replace(":path*", ""));
for (const route of POST_METHODS_ALLOWED_API_ROUTES) {
const matcherEntry = route.endsWith("/") ? `${route}:path*` : route;
const coveredByWildcard = wildcardPrefixes.some((prefix) => route.startsWith(prefix));
const coveredByExact = matcher.includes(matcherEntry);
expect(
coveredByWildcard || coveredByExact,
`POST route "${route}" is not covered by any matcher entry`
).toBe(true);
}
it("should not contain any /api/ routes except /api/auth/signup", () => {
const apiRoutes = matcher.filter((entry) => entry.startsWith("/api/") && entry !== "/api/auth/signup");
expect(apiRoutes).toEqual([]);
});
it("should not contain API matcher entries without corresponding POST_METHODS_ALLOWED_API_ROUTES routes", () => {
const NON_API_ROUTES = ["/auth/login", "/login", "/apps/installed", "/auth/logout", "/:path*/embed"];
const apiMatcherEntries = matcher.filter((entry) => !NON_API_ROUTES.includes(entry));
for (const entry of apiMatcherEntries) {
if (entry.endsWith(":path*")) {
const prefix = entry.replace(":path*", "");
const hasCoveredRoutes = POST_METHODS_ALLOWED_API_ROUTES.some((route) => route.startsWith(prefix));
expect(hasCoveredRoutes, `Matcher wildcard "${entry}" doesn't cover any POST route`).toBe(true);
} else {
expect(
POST_METHODS_ALLOWED_API_ROUTES,
`Matcher has "${entry}" but it's missing from POST_METHODS_ALLOWED_API_ROUTES`
).toContain(entry);
}
}
it("should only contain the expected reduced route set", () => {
expect(matcher).toEqual([
"/auth/login",
"/login",
"/apps/installed",
"/auth/logout",
"/:path*/embed",
"/availability",
"/api/auth/signup",
]);
});
});
+1 -113
View File
@@ -12,71 +12,6 @@ const safeGet = async <T = any>(key: string): Promise<T | undefined> => {
}
};
export const POST_METHODS_ALLOWED_API_ROUTES = [
"/api/auth/forgot-password",
"/api/auth/oauth/me",
"/api/auth/oauth/refreshToken",
"/api/auth/oauth/token",
"/api/auth/reset-password",
"/api/auth/saml/callback",
"/api/auth/saml/token",
"/api/auth/setup",
"/api/auth/signup",
"/api/auth/two-factor/totp/disable",
"/api/auth/two-factor/totp/enable",
"/api/auth/two-factor/totp/setup",
"/api/auth/session",
"/api/availability/calendar",
"/api/cancel",
"/api/cron/bookingReminder",
"/api/cron/calendar-cache-cleanup",
"/api/cron/changeTimeZone",
"/api/cron/checkSmsPrices",
"/api/cron/downgradeUsers",
"/api/cron/monthlyDigestEmail",
"/api/cron/syncAppMeta",
"/api/cron/webhookTriggers",
"/api/cron/workflows/scheduleEmailReminders",
"/api/cron/workflows/scheduleSMSReminders",
"/api/cron/workflows/scheduleWhatsappReminders",
"/api/get-inbound-dynamic-variables",
"/api/integrations/", // for /api/integrations/[...args] and webhooks
"/api/recorded-daily-video",
"/api/router",
"/api/routing-forms/queued-response",
"/api/scim/v2.0/", // /api/scim/v2.0/[...directory]
"/api/support/conversation",
"/api/sync/helpscout",
"/api/twilio/webhook",
"/api/username",
"/api/verify-booking-token",
"/api/video/guest-session",
"/api/webhook/app-credential",
"/api/webhooks/calendar-subscription/", // /api/webhooks/calendar-subscription/[provider]
"/api/webhooks/retell-ai",
"/api/workflows/sms/user-response",
"/api/trpc/", // for tRPC
"/api/auth/callback/", // for NextAuth
"/api/book/event",
"/api/book/instant-event",
"/api/book/recurring-event",
"/availability",
];
export function checkPostMethod(req: NextRequest) {
const pathname = req.nextUrl.pathname;
if (!POST_METHODS_ALLOWED_API_ROUTES.some((route) => pathname.startsWith(route)) && req.method === "POST") {
return new NextResponse(null, {
status: 405,
statusText: "Method Not Allowed",
headers: {
Allow: "GET",
},
});
}
return null;
}
// Vercel/Edge rejects non‑ASCII 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;
@@ -131,9 +66,6 @@ const shouldEnforceCsp = (url: URL) => {
};
const proxy = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const postCheckResult = checkPostMethod(req);
if (postCheckResult) return postCheckResult;
const url = req.nextUrl;
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
@@ -231,51 +163,7 @@ function enrichRequestWithHeaders({ req }: { req: NextRequest }) {
}
export const config = {
matcher: [
"/auth/login",
"/login",
"/apps/installed",
"/auth/logout",
"/:path*/embed",
"/api/auth/forgot-password",
"/api/auth/oauth/me",
"/api/auth/oauth/refreshToken",
"/api/auth/oauth/token",
"/api/auth/reset-password",
"/api/auth/saml/callback",
"/api/auth/saml/token",
"/api/auth/setup",
"/api/auth/signup",
"/api/auth/two-factor/totp/disable",
"/api/auth/two-factor/totp/enable",
"/api/auth/two-factor/totp/setup",
"/api/auth/session",
"/api/availability/calendar",
"/api/cancel",
"/api/cron/:path*",
"/api/get-inbound-dynamic-variables",
"/api/integrations/:path*",
"/api/recorded-daily-video",
"/api/router",
"/api/routing-forms/queued-response",
"/api/scim/v2.0/:path*",
"/api/support/conversation",
"/api/sync/helpscout",
"/api/twilio/webhook",
"/api/username",
"/api/verify-booking-token",
"/api/video/guest-session",
"/api/webhook/app-credential",
"/api/webhooks/calendar-subscription/:path*",
"/api/webhooks/retell-ai",
"/api/workflows/sms/user-response",
"/api/trpc/:path*",
"/api/auth/callback/:path*",
"/api/book/event",
"/api/book/instant-event",
"/api/book/recurring-event",
"/availability",
],
matcher: ["/auth/login", "/login", "/apps/installed", "/auth/logout", "/:path*/embed", "/availability", "/api/auth/signup"],
};
export default proxy;