diff --git a/app/client/api-client/client.gen.ts b/app/client/api-client/client.gen.ts index 457f3c3d..a5761642 100644 --- a/app/client/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -1,19 +1,27 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts - import { type ClientOptions, type Config, createClient, createConfig } from "./client"; import type { ClientOptions as ClientOptions2 } from "./types.gen"; +import { getRequestClient } from "../../lib/request-client"; -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ export type CreateClientConfig = ( override?: Config, ) => Config & T>; -export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })); +const fallbackClient = createClient(createConfig({ baseUrl: "http://localhost:4096" })); + +/** + * Proxy client that automatically uses the per-request client from request-scoped server storage. + * Falls back to a default client if no request context is available. + */ +export const client = new Proxy(fallbackClient, { + get(target, prop, receiver) { + try { + const requestClient = getRequestClient(); + return Reflect.get(requestClient, prop, receiver); + } catch { + // No request context available, use fallback + return Reflect.get(target, prop, receiver); + } + }, +}); diff --git a/app/lib/request-client.ts b/app/lib/request-client.ts new file mode 100644 index 00000000..7f30904b --- /dev/null +++ b/app/lib/request-client.ts @@ -0,0 +1,57 @@ +import type { Config } from "~/client/api-client/client"; +import { createClient, createConfig } from "~/client/api-client/client"; + +export type RequestClient = ReturnType; + +type RequestClientStore = { + getStore: () => RequestClient | undefined; + run: (client: RequestClient, fn: () => T) => T; +}; + +type AsyncLocalStorageConstructor = new () => { + getStore: () => T | undefined; + run: (store: T, callback: () => R) => R; +}; + +let requestClientStore: RequestClientStore | undefined; + +const ASYNC_HOOKS_MODULE = "node:async_hooks"; + +const loadRequestClientStore = async (): Promise => { + if (typeof window !== "undefined") { + return undefined; + } + + if (!requestClientStore) { + const asyncHooksModule = (await import(/* @vite-ignore */ ASYNC_HOOKS_MODULE)) as { + AsyncLocalStorage: AsyncLocalStorageConstructor; + }; + requestClientStore = new asyncHooksModule.AsyncLocalStorage(); + } + + return requestClientStore; +}; + +export function getRequestClient(): RequestClient { + const client = requestClientStore?.getStore(); + + if (!client) { + throw new Error("No request client available"); + } + + return client; +} + +export async function runWithRequestClient(client: RequestClient, fn: () => T): Promise { + const store = await loadRequestClientStore(); + + if (!store) { + return fn(); + } + + return store.run(client, fn); +} + +export function createRequestClient(config: Config): RequestClient { + return createClient(createConfig(config)); +} diff --git a/app/middleware/api-client.ts b/app/middleware/api-client.ts index 49dba36c..95bebbde 100644 --- a/app/middleware/api-client.ts +++ b/app/middleware/api-client.ts @@ -1,14 +1,17 @@ import { createMiddleware } from "@tanstack/react-start"; -import { client } from "~/client/api-client/client.gen"; import { getRequestHeaders } from "@tanstack/react-start/server"; +import { + createRequestClient, + runWithRequestClient, +} from "~/lib/request-client"; export const apiClientMiddleware = createMiddleware().server(async ({ next, request }) => { - client.setConfig({ - baseUrl: `${new URL(request.url).origin}`, + const client = createRequestClient({ + baseUrl: new URL(request.url).origin, headers: { cookie: getRequestHeaders().get("cookie") ?? "", }, }); - return next(); + return runWithRequestClient(client, () => next()); }); diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts index 70625f2a..08bf80b9 100644 --- a/openapi-ts.config.ts +++ b/openapi-ts.config.ts @@ -9,7 +9,10 @@ export default defineConfig({ "// @ts-nocheck", "// This file is auto-generated by @hey-api/openapi-ts", ], - postProcess: ["oxfmt"], + postProcess: [ + "oxfmt", + { command: "bun", args: ["scripts/patch-api-client.ts"] }, + ], }, plugins: [...defaultPlugins, "@tanstack/react-query", "@hey-api/client-fetch"], }); diff --git a/react-router.config.ts b/react-router.config.ts deleted file mode 100644 index c8a71f61..00000000 --- a/react-router.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Config } from "@react-router/dev/config"; - -export default { - buildDirectory: "dist", - ssr: true, - future: { - v8_middleware: true, - }, -} satisfies Config; diff --git a/scripts/patch-api-client.ts b/scripts/patch-api-client.ts new file mode 100755 index 00000000..40e46197 --- /dev/null +++ b/scripts/patch-api-client.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/** + * Post-process script for openapi-ts generation. + * Patches client.gen.ts to use request-scoped clients from server storage, + * preventing cross-request cookie/origin leakage during SSR. + */ + +/* eslint-disable no-console */ + +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const CLIENT_GEN_PATH = resolve(process.cwd(), "app/client/api-client/client.gen.ts"); + +const PATCHED_CONTENT = `// @ts-nocheck +// This file is auto-generated by @hey-api/openapi-ts +import { type ClientOptions, type Config, createClient, createConfig } from "./client"; +import type { ClientOptions as ClientOptions2 } from "./types.gen"; +import { getRequestClient } from "../../lib/request-client"; + +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +const fallbackClient = createClient(createConfig({ baseUrl: "http://localhost:4096" })); + +/** + * Proxy client that automatically uses the per-request client from request-scoped server storage. + * Falls back to a default client if no request context is available. + */ +export const client = new Proxy(fallbackClient, { + get(target, prop, receiver) { + try { + const requestClient = getRequestClient(); + return Reflect.get(requestClient, prop, receiver); + } catch { + // No request context available, use fallback + return Reflect.get(target, prop, receiver); + } + }, +}); +`; + +try { + writeFileSync(CLIENT_GEN_PATH, PATCHED_CONTENT); + console.log("✓ Patched client.gen.ts with request-scoped client support"); +} catch (error) { + console.error("✗ Failed to patch client.gen.ts:", error instanceof Error ? error.message : String(error)); + process.exit(1); +}