From 58621818d0f206a17c03a44c969152559287d8e7 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 16 Jul 2026 23:46:40 +0800 Subject: [PATCH 001/290] feat(studio): switch TanStack skew protection to ?dpl= query params (#48008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches the TanStack build's Vercel skew protection from the `__vdpl` session cookie to `?dpl=` query params baked into asset URLs at build time. Assets stay pinned to the deployment that built them, while document navigations and API fetches always reach the latest deployment (with the cookie, a session stayed fully pinned — including reloads — until the tab closed). **Removed:** - `pinDeploymentForSession` (the `__vdpl` cookie) from `router.tsx`, plus the cookie clearing in the refresh toast and the `vite:preloadError` backstop - `credentials: 'omit'` on the deployment-commit check — its only purpose was escaping the cookie pin, and API fetches are now inherently unpinned **Added:** - `skewProtectionDpl` plugin + `experimental.renderBuiltUrl` in `vite.config.ts`, active only when `VERCEL_SKEW_PROTECTION_ENABLED=1`. Full coverage needs three mechanisms (Vite has no single hook for this — see [vitejs/vite#13834](https://github.com/vitejs/vite/discussions/13834#discussioncomment-7469745)): 1. `renderBuiltUrl` — CSS `url()`s, images, workers, and `__vite__mapDeps` preload lists 2. a `generateBundle` (`order: 'post'`) rewrite of chunk-to-chunk `import`/`from` specifiers, which Rolldown emits as bare relative paths that `renderBuiltUrl` never sees — with sourcemaps recombined per chunk (`magic-string` + `@jridgewell/remapping` devDeps) so Sentry columns stay exact 3. a post-`buildApp` patch of the prerendered `_shell.html` (script/preload tags + embedded router manifest come from TanStack, not Vite's asset pipeline); without it the entry graph double-downloads because preload and import URLs differ ## To test - Built with fake `VERCEL_SKEW_PROTECTION_ENABLED=1 VERCEL_DEPLOYMENT_ID=dpl_TESTPIN123abc`: every chunk import specifier (static + dynamic), `__vite__mapDeps` entry, CSS font URL, and `_shell.html` asset URL carries `?dpl=`; zero unpinned `/assets/` references remain - Sourcemap accuracy verified by tracing a minified position through the recombined map: resolves to the exact original file/line/column (`use-check-latest-deploy.tsx:62:8`) - Built without the env vars: output contains no `dpl=` anywhere (self-hosted/e2e builds unaffected) - `smoke:tanstack` passes on both builds; `tsc --noEmit` and eslint clean - On the preview: load the dashboard, check Network tab — chunk/CSS requests should carry `?dpl=` matching the deployment; hard reload should hit the latest deployment (no pin on document requests) ## Summary by CodeRabbit * **Improvements** * Improved deployment consistency by pinning generated asset and module URLs to the current deployment (using `?dpl=`). * Simplified refresh and preload-error recovery to reduce reload-loop risk. * Kept API request behavior aligned with the updated deployment routing/pinning approach. * Preserved correct routing across deployment configurations. * **Developer Experience** * Added build-time tooling to rewrite pinned URLs for client assets while maintaining source map integrity. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../data/utils/deployment-commit-query.ts | 16 +- apps/studio/hooks/use-check-latest-deploy.tsx | 7 +- apps/studio/package.json | 2 + apps/studio/router.tsx | 35 +--- apps/studio/vercel.ts | 5 +- apps/studio/vite.config.ts | 185 +++++++++++++++++- pnpm-lock.yaml | 6 + 7 files changed, 210 insertions(+), 46 deletions(-) diff --git a/apps/studio/data/utils/deployment-commit-query.ts b/apps/studio/data/utils/deployment-commit-query.ts index 8729bd57cfd..8fe1f972feb 100644 --- a/apps/studio/data/utils/deployment-commit-query.ts +++ b/apps/studio/data/utils/deployment-commit-query.ts @@ -5,15 +5,13 @@ import { BASE_PATH } from '@/lib/constants' import type { ResponseError, UseCustomQueryOptions } from '@/types' export async function getDeploymentCommit() { - // `credentials: 'omit'` drops cookies for this one request — including the - // `__vdpl` skew-protection pin (TanStack, see router.tsx). With no pin cookie, - // Vercel's edge routes it to the LATEST deployment, so this check can detect a - // newer version even while the rest of the session stays pinned. The endpoint - // is public (no auth needed), and we keep the basePath URL so it still routes - // to studio in production (root `/api/*` there is the marketing site). - const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`, { - credentials: 'omit', - }) + // Deliberately unpinned: API fetches never carry the `?dpl=` skew-protection + // pin — only built asset URLs do (TanStack, see skewProtectionDpl in + // vite.config.ts) — so Vercel's edge routes this to the LATEST deployment + // and the check can detect a newer version while the session's assets stay + // pinned. We keep the basePath URL so it still routes to studio in + // production (root `/api/*` there is the marketing site). + const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`) return (await response.json()) as { commitSha: string; commitTime: string } } diff --git a/apps/studio/hooks/use-check-latest-deploy.tsx b/apps/studio/hooks/use-check-latest-deploy.tsx index 8e1d4a073a7..3240dce6cd8 100644 --- a/apps/studio/hooks/use-check-latest-deploy.tsx +++ b/apps/studio/hooks/use-check-latest-deploy.tsx @@ -6,7 +6,6 @@ import { toast } from 'sonner' import { Button, StatusIcon } from 'ui' import { useDeploymentCommitQuery } from '@/data/utils/deployment-commit-query' -import { BASE_PATH } from '@/lib/constants' const DeployCheckToast = ({ id }: { id: string | number }) => { const router = useRouter() @@ -27,9 +26,9 @@ const DeployCheckToast = ({ id }: { id: string | number }) => {