From e16b40aeadde4203c883f121e7e5f7a2ec04d6b2 Mon Sep 17 00:00:00 2001 From: Taesu <166604494+bytaesu@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:34:53 +0000 Subject: [PATCH] refactor(vue): make Nuxt session fetching type-safe (#11085) --- .changeset/type-safe-nuxt-session-fetch.md | 5 + .../nuxt/test/nuxt/session-fetch.test-d.ts | 12 ++ .../better-auth/src/client/client-ssr.test.ts | 57 ++++++-- packages/better-auth/src/client/config.ts | 1 + packages/better-auth/src/client/vue/index.ts | 128 ++++++++++-------- 5 files changed, 139 insertions(+), 64 deletions(-) create mode 100644 .changeset/type-safe-nuxt-session-fetch.md create mode 100644 e2e/integration/nuxt/test/nuxt/session-fetch.test-d.ts diff --git a/.changeset/type-safe-nuxt-session-fetch.md b/.changeset/type-safe-nuxt-session-fetch.md new file mode 100644 index 0000000000..f2500d223d --- /dev/null +++ b/.changeset/type-safe-nuxt-session-fetch.md @@ -0,0 +1,5 @@ +--- +"better-auth": patch +--- + +Provide type-safe Nuxt `useFetch` integration for the Vue client's `useSession` hook. diff --git a/e2e/integration/nuxt/test/nuxt/session-fetch.test-d.ts b/e2e/integration/nuxt/test/nuxt/session-fetch.test-d.ts new file mode 100644 index 0000000000..42f8005a56 --- /dev/null +++ b/e2e/integration/nuxt/test/nuxt/session-fetch.test-d.ts @@ -0,0 +1,12 @@ +import type { UseFetchOptions } from "nuxt/app"; +import type { authClient } from "../../lib/auth-client"; + +type SessionFetch = Parameters[0]; +type SessionFetchOptions = Parameters[1]; + +type NuxtCompatibleOptions> = Options; +type NuxtCompatibleFetch = Fetch; + +export type SessionFetchOptionsContract = + NuxtCompatibleOptions; +export type NuxtUseFetchContract = NuxtCompatibleFetch; diff --git a/packages/better-auth/src/client/client-ssr.test.ts b/packages/better-auth/src/client/client-ssr.test.ts index f60c8ceab6..7a46e51952 100644 --- a/packages/better-auth/src/client/client-ssr.test.ts +++ b/packages/better-auth/src/client/client-ssr.test.ts @@ -26,23 +26,64 @@ it("should call '/api/auth' for vue client", async () => { /** * @see https://github.com/better-auth/better-auth/issues/5358 */ -it("uses stable Nuxt options for a session fetch", async () => { - const headers = { cookie: "better-auth.session_token=session" }; - const useFetch = vi.fn(async () => ({ - data: { value: null }, - error: { value: null }, - })); +it("uses stable Nuxt options for a session fetch", () => { + const headers = { + cookie: "better-auth.session_token=session", + "x-optional": undefined, + }; + const pendingFetch = new Promise(() => undefined); + const useFetch = vi.fn(() => pendingFetch); const client = createVueClient({ baseURL: "http://localhost:3000", fetchOptions: { headers }, }); - await client.useSession(useFetch); + void client.useSession(useFetch); expect(useFetch).toHaveBeenCalledWith( "http://localhost:3000/api/auth/get-session", { - headers, + headers: { cookie: "better-auth.session_token=session" }, + key: "better-auth:session:http://localhost:3000:/api/auth", + watch: [expect.anything()], + }, + ); +}); + +/** + * @see https://github.com/better-auth/better-auth/issues/5358 + */ +it.each([ + [ + "Headers", + new Headers({ cookie: "better-auth.session_token=session" }), + new Headers({ cookie: "better-auth.session_token=session" }), + ], + [ + "tuple array", + [["cookie", "better-auth.session_token=session"]] satisfies [ + string, + string, + ][], + [["cookie", "better-auth.session_token=session"]] satisfies [ + string, + string, + ][], + ], +])("preserves %s for a Nuxt session fetch", (_name, headers, expectedHeaders) => { + const pendingFetch = new Promise(() => undefined); + const useFetch = vi.fn(() => pendingFetch); + const client = createVueClient({ + baseURL: "http://localhost:3000", + fetchOptions: { headers }, + }); + + void client.useSession(useFetch); + + expect(useFetch).toHaveBeenCalledWith( + "http://localhost:3000/api/auth/get-session", + { + headers: expectedHeaders, key: "better-auth:session:http://localhost:3000:/api/auth", watch: [expect.anything()], }, diff --git a/packages/better-auth/src/client/config.ts b/packages/better-auth/src/client/config.ts index 3909361ceb..9be8df7ce0 100644 --- a/packages/better-auth/src/client/config.ts +++ b/packages/better-auth/src/client/config.ts @@ -191,6 +191,7 @@ export const getClientConfig = ( pluginPathMethods, atomListeners, hydrateSession, + $sessionSignal, $fetch, $store, }; diff --git a/packages/better-auth/src/client/vue/index.ts b/packages/better-auth/src/client/vue/index.ts index 1b1a02e106..3a373db4d3 100644 --- a/packages/better-auth/src/client/vue/index.ts +++ b/packages/better-auth/src/client/vue/index.ts @@ -1,11 +1,14 @@ -import type { BetterAuthClientOptions } from "@better-auth/core"; +import type { + BetterAuthClientOptions, + ClientFetchOption, +} from "@better-auth/core"; import type { BASE_ERROR_CODES } from "@better-auth/core/error"; import { capitalizeFirstLetter } from "@better-auth/core/utils/string"; import type { BetterFetchError, BetterFetchResponse, } from "@better-fetch/fetch"; -import type { DeepReadonly, Ref } from "vue"; +import type { DeepReadonly, Ref, WatchSource } from "vue"; import type { PrettifyDeep, UnionToIntersection } from "../../types/helper"; import { getClientConfig } from "../config"; import { createDynamicPathProxy } from "../proxy"; @@ -22,6 +25,20 @@ function getAtomKey(str: string) { return `use${capitalizeFirstLetter(str)}`; } +/** Preserves standard `HeadersInit` values and removes undefined record entries. */ +function toHeadersInit( + headers: ClientFetchOption["headers"], +): HeadersInit | undefined { + if (!headers) return undefined; + if (headers instanceof Headers || Array.isArray(headers)) return headers; + + const normalizedHeaders: Record = {}; + for (const [name, value] of Object.entries(headers)) { + if (value !== undefined) normalizedHeaders[name] = value; + } + return normalizedHeaders; +} + type InferResolvedHooks = O extends { plugins: Array; } @@ -56,28 +73,41 @@ type ClientSession