diff --git a/.gitignore b/.gitignore index 0076a3a3e..e5d3327c6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ __pycache__ /build node_modules dist +dist-types +*.tsbuildinfo .output .nitro doc diff --git a/AGENTS.md b/AGENTS.md index a733bb19a..c74b3246e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -229,6 +229,20 @@ invariant in full, the labels (minimal) and auth (carve-out) shapes, the pure-policy-vs-framework-shell principle, and when to introduce `service.ts`. +### Client-reachable files import server modules via `@server/*` + +`apps/web` type-checks as two projects (`pnpm typecheck` runs both): +`tsconfig.server.json` (Node types) for `src/server`, and +`tsconfig.app.json` (DOM lib, no Node types) for browser code, which +resolves `@server/*` to the server project's emitted declarations. + +Any file reachable from the browser program — including framework +entries pulled in by `routeTree.gen.ts`, like `start.ts` — must import +server modules through the `@server/*` alias, never a relative +`./server/*` path. A relative import bypasses the declaration remap and +drags the server source graph (and `@types/node` globals) back into the +browser program. + ### Authentication is enforced by global middleware Every TanStack Start server function is authenticated by default. diff --git a/apps/web/package.json b/apps/web/package.json index e9503ff91..d92648f68 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,7 +9,7 @@ "develop": "vite", "start": "node scripts/check-api.mjs && node .output/server/index.mjs", "test": "TZ=UTC vitest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc -p tsconfig.server.json && tsc -p tsconfig.app.json" }, "dependencies": { "@hookform/resolvers": "^5.4.0", diff --git a/apps/web/src/start.ts b/apps/web/src/start.ts index f8d980bc9..3f79e4703 100644 --- a/apps/web/src/start.ts +++ b/apps/web/src/start.ts @@ -1,22 +1,20 @@ -import { randomBytes } from "node:crypto"; import { sentryGlobalFunctionMiddleware, sentryGlobalRequestMiddleware, } from "@sentry/tanstackstart-react"; -import { - createCsrfMiddleware, - createMiddleware, - createStart, -} from "@tanstack/react-start"; - import { createFirstUserFn, loginFn, logoutFn, resetPasswordFn, -} from "./server/auth/functions"; -import { createAuthenticationMiddleware } from "./server/auth/middleware"; -import { errorLoggingMiddleware } from "./server/error-logging"; +} from "@server/auth/functions"; +import { createAuthenticationMiddleware } from "@server/auth/middleware"; +import { errorLoggingMiddleware } from "@server/error-logging"; +import { + createCsrfMiddleware, + createMiddleware, + createStart, +} from "@tanstack/react-start"; // logoutFn must be exempt so stale or missing cookies can still be cleared. // createFirstUserFn runs before any user or session exists. @@ -73,6 +71,16 @@ const documentHeaders: DocumentHeader[] = [ buildCacheControl, ]; +// Per-request CSP nonce. Deliberately uses the Web Crypto and `btoa` globals +// rather than node:crypto/Buffer: this file is reachable from the browser +// program (routeTree.gen.ts imports it) and must type-check without Node types. +// Both globals exist in our Node runtime, so the server middleware is safe. +function generateNonce(): string { + return btoa( + String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16))), + ); +} + const documentHeadersMiddleware = createMiddleware().server( async ({ next }) => { const result = await next(); @@ -83,7 +91,7 @@ const documentHeadersMiddleware = createMiddleware().server( } const html = await response.text(); - const nonce = randomBytes(16).toString("base64"); + const nonce = generateNonce(); const body = html.replace(/])/g, `