From 64b809ffbf782fba3cc27db8644aa92dca2504e0 Mon Sep 17 00:00:00 2001 From: Alaister Young <10985857+alaister@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:39:00 +0800 Subject: [PATCH] feat(studio): add self-hosted-only API 404 guard for the TanStack build The Next pages-router build guards platform-only API routes via middleware (proxy.ts, Next 16's renamed middleware convention). TanStack Start has no middleware runtime, so that guard didn't run on the TanStack build at all. Add an equivalent global request middleware on a TanStack start instance (start.ts). On Vercel, /api/* and /_serverFn/* are rewritten to the api/server.js function which runs createStartHandler, so requestMiddleware executes server-side for every API request even though pages are a static SPA shell. Extract the shared HOSTED_SUPPORTED_API_URLS allowlist into lib/hosted-api-allowlist.ts so the Next and TanStack guards can't drift while both frameworks run in parallel. --- apps/studio/lib/hosted-api-allowlist.ts | 37 +++++++++++++++++++++++++ apps/studio/proxy.ts | 37 ++++--------------------- apps/studio/start.ts | 35 +++++++++++++++++++++++ 3 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 apps/studio/lib/hosted-api-allowlist.ts create mode 100644 apps/studio/start.ts diff --git a/apps/studio/lib/hosted-api-allowlist.ts b/apps/studio/lib/hosted-api-allowlist.ts new file mode 100644 index 00000000000..614d863ddc3 --- /dev/null +++ b/apps/studio/lib/hosted-api-allowlist.ts @@ -0,0 +1,37 @@ +// [Joshen] Allowlist of API endpoints supported in hosted (platform) mode. +// Every other /api/* route must 404 in platform mode. Shared by the Next +// middleware (proxy.ts) and the TanStack request middleware (start.ts) so +// the list can't drift between the two frameworks while both run in parallel. +export const HOSTED_SUPPORTED_API_URLS = [ + '/ai/sql/generate-v4', + '/ai/sql/policy', + '/ai/feedback/rate', + '/ai/code/complete', + '/ai/sql/cron-v2', + '/ai/sql/title-v2', + '/ai/sql/filter-v1', + '/ai/onboarding/design', + '/ai/feedback/classify', + '/ai/docs', + '/ai/sql/parse-client-code', + '/get-ip-address', + '/get-utc-time', + '/get-deployment-commit', + '/check-cname', + '/edge-functions/test', + '/edge-functions/body', + '/generate-attachment-url', + '/incident-status', + '/incident-banner', + '/status-override', + '/api/integrations/stripe-sync', + '/content/graphql', + '/parse-query', +] + +// `pathname` must be basePath-relative — Next's `nextUrl.pathname` already is, +// and the TanStack guard strips BASE_PATH before calling. Entries are path +// suffixes, so `endsWith` stays correct regardless. +export function isHostedSupportedApiPath(pathname: string): boolean { + return HOSTED_SUPPORTED_API_URLS.some((url) => pathname.endsWith(url)) +} diff --git a/apps/studio/proxy.ts b/apps/studio/proxy.ts index 2ca590998b7..cfb19146dfe 100644 --- a/apps/studio/proxy.ts +++ b/apps/studio/proxy.ts @@ -1,44 +1,17 @@ import type { NextRequest } from 'next/server' import { IS_PLATFORM } from '@/lib/constants' +import { isHostedSupportedApiPath } from '@/lib/hosted-api-allowlist' export const config = { matcher: '/api/:function*', } -// [Joshen] Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted: -const HOSTED_SUPPORTED_API_URLS = [ - '/ai/sql/generate-v4', - '/ai/sql/policy', - '/ai/feedback/rate', - '/ai/code/complete', - '/ai/sql/cron-v2', - '/ai/sql/title-v2', - '/ai/sql/filter-v1', - '/ai/onboarding/design', - '/ai/feedback/classify', - '/ai/docs', - '/ai/sql/parse-client-code', - '/get-ip-address', - '/get-utc-time', - '/get-deployment-commit', - '/check-cname', - '/edge-functions/test', - '/edge-functions/body', - '/generate-attachment-url', - '/incident-status', - '/incident-banner', - '/status-override', - '/api/integrations/stripe-sync', - '/content/graphql', - '/parse-query', -] - +// Return 404 for all next.js API endpoints EXCEPT the ones we use in hosted. +// The allowlist is shared with the TanStack guard (start.ts) — see +// lib/hosted-api-allowlist.ts. export function proxy(request: NextRequest) { - if ( - IS_PLATFORM && - !HOSTED_SUPPORTED_API_URLS.some((url) => request.nextUrl.pathname.endsWith(url)) - ) { + if (IS_PLATFORM && !isHostedSupportedApiPath(request.nextUrl.pathname)) { return Response.json( { success: false, message: 'Endpoint not supported on hosted' }, { status: 404 } diff --git a/apps/studio/start.ts b/apps/studio/start.ts new file mode 100644 index 00000000000..158248a618a --- /dev/null +++ b/apps/studio/start.ts @@ -0,0 +1,35 @@ +import { createMiddleware, createStart } from '@tanstack/react-start' + +import { BASE_PATH, IS_PLATFORM } from '@/lib/constants' +import { isHostedSupportedApiPath } from '@/lib/hosted-api-allowlist' + +// Self-hosted-only API routes must 404 in platform (hosted) mode. Under the +// Next pages router this lives in middleware (proxy.ts), but TanStack Start +// has no middleware runtime, so the guard is migrated here as a global +// request middleware sharing the same allowlist (lib/hosted-api-allowlist.ts). +// On Vercel our `/api/*` (and `/_serverFn/*`) requests are rewritten to the +// api/server.js function which runs the Start handler, so createStartHandler +// runs this server-side for every API request — even though pages are served +// as a static SPA shell. The guard therefore covers all API routes from a +// single place. + +const platformApiGuard = createMiddleware({ type: 'request' }).server(({ request, next }) => { + const { pathname } = new URL(request.url) + // Path relative to the configured basePath — mirrors Next's basePath- + // relative middleware matcher. + const relativePath = + BASE_PATH && pathname.startsWith(BASE_PATH) ? pathname.slice(BASE_PATH.length) : pathname + + if (IS_PLATFORM && relativePath.startsWith('/api/') && !isHostedSupportedApiPath(relativePath)) { + return Response.json( + { success: false, message: 'Endpoint not supported on hosted' }, + { status: 404 } + ) + } + + return next() +}) + +export const startInstance = createStart(() => ({ + requestMiddleware: [platformApiGuard], +}))