diff --git a/src/runtime/app/internal/vue-safe-auth-proxy.ts b/src/runtime/app/internal/vue-safe-auth-proxy.ts index 720ee7d2..bcd777dc 100644 --- a/src/runtime/app/internal/vue-safe-auth-proxy.ts +++ b/src/runtime/app/internal/vue-safe-auth-proxy.ts @@ -44,5 +44,33 @@ export function createVueSafeAuthProxy(target: T): T { return proxy as V } - return wrap(target) + const createRootFacade = (value: V): V => { + if (!isObjectLike(value)) + return value + + const propertyCache = new Map() + const facadeTarget = {} + Object.defineProperty(facadeTarget, '__v_skip', { + value: true, + configurable: true, + }) + + const proxy = new Proxy(facadeTarget, { + get(_target, prop, receiver) { + if (prop === '__v_skip') + return true + if (isAuthProxyProbeKey(prop)) + return undefined + if (propertyCache.has(prop)) + return propertyCache.get(prop) + const wrapped = wrap(Reflect.get(value, prop, receiver)) + propertyCache.set(prop, wrapped) + return wrapped + }, + }) + cache.set(value, proxy) + return proxy as V + } + + return createRootFacade(target) } diff --git a/test/cases/pinia-setup-store/app/pages/index.vue b/test/cases/pinia-setup-store/app/pages/index.vue index 30938fab..c167632d 100644 --- a/test/cases/pinia-setup-store/app/pages/index.vue +++ b/test/cases/pinia-setup-store/app/pages/index.vue @@ -18,5 +18,11 @@ const sessionState = useSessionStateStore()

Full actions: {{ typeof fullAuth.signOut }}

+

+ Client type: {{ typeof fullAuth.authClient }} +

+

+ Verification type: {{ typeof fullAuth.authClient?.sendVerificationEmail }} +

diff --git a/test/cases/pinia-setup-store/app/stores/full-auth.ts b/test/cases/pinia-setup-store/app/stores/full-auth.ts index d1432489..2f6f7b17 100644 --- a/test/cases/pinia-setup-store/app/stores/full-auth.ts +++ b/test/cases/pinia-setup-store/app/stores/full-auth.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia' export const useFullAuthStore = defineStore('full-auth', () => { - const auth = useUserSession() + const { client: authClient, ...auth } = useUserSession() - return { ...auth } + return { authClient, ...auth } }) diff --git a/test/use-user-session.test.ts b/test/use-user-session.test.ts index bb3465e8..8578c87a 100644 --- a/test/use-user-session.test.ts +++ b/test/use-user-session.test.ts @@ -1,3 +1,4 @@ +import { createPinia, defineStore, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { isReactive, isReadonly, isRef, reactive, ref, watch } from 'vue' @@ -502,14 +503,17 @@ describe('useUserSession hydration bootstrap', () => { const secondAuth = useUserSession() expect(auth.client).not.toBe(rawClient) + expect(typeof auth.client).toBe('object') expect(secondAuth.client).toBe(auth.client) expect(secondAuth.client!.signIn).toBe(auth.client!.signIn) expect(secondAuth.client!.signIn.email).toBe(auth.client!.signIn.email) + expect(typeof (auth.client as Record).sendVerificationEmail).toBe('function') expectVueInspectionSafe(auth.client) expectVueInspectionSafe(auth.client!.signIn) expectVueInspectionSafe(auth.client!.signIn.email) expectVueInspectionSafe(auth.client!.signUp) expectVueInspectionSafe(auth.client!.signUp.email) + expectVueInspectionSafe((auth.client as Record).sendVerificationEmail) expectVueInspectionSafe(auth.client!.admin.impersonateUser) }) @@ -537,12 +541,41 @@ describe('useUserSession hydration bootstrap', () => { expect(isStateLike(store.authClient)).toBe(false) expect(isStateLike(store.signIn)).toBe(false) expect(isStateLike(store.signUp)).toBe(false) + expect(typeof store.authClient).toBe('object') + expect(typeof (store.authClient as Record).sendVerificationEmail).toBe('function') expect(isStateLike((store.authClient as Record).signIn)).toBe(false) expect(isStateLike((store.authClient as Record).admin)).toBe(false) + expect(isStateLike((store.authClient as Record).sendVerificationEmail)).toBe(false) expect(isStateLike((store.signIn as Record).email)).toBe(false) expect(isStateLike((store.signUp as Record).email)).toBe(false) }) + it('keeps forwarded authClient methods callable in an actual Pinia setup store', async () => { + const rawClient = createDynamicAuthProxy({ + useSession: mockClient.useSession, + getSession: mockClient.getSession, + signOut: mockClient.signOut, + signIn: createDynamicAuthProxy(), + signUp: createDynamicAuthProxy(), + $store: mockClient.$store, + }) + activeClient = rawClient + setActivePinia(createPinia()) + + const useUserSession = await loadUseUserSession() + const useAuthStore = defineStore('auth-client-forwarding', () => { + const { client: authClient, ...auth } = useUserSession() + return { authClient, ...auth } + }) + const store = useAuthStore() + + expect(typeof store.authClient).toBe('object') + expect(typeof (store.authClient as Record).sendVerificationEmail).toBe('function') + expect(isRef(store.authClient as unknown)).toBe(false) + expect(isReactive(store.authClient as unknown)).toBe(false) + expect(isReactive((store.authClient as Record).sendVerificationEmail)).toBe(false) + }) + it('allows server-side auth method reads but still rejects invocation', async () => { setRuntimeFlags({ client: false, server: true }) diff --git a/test/vue-safe-auth-proxy.test.ts b/test/vue-safe-auth-proxy.test.ts index c6851a5a..ed6da151 100644 --- a/test/vue-safe-auth-proxy.test.ts +++ b/test/vue-safe-auth-proxy.test.ts @@ -24,11 +24,13 @@ describe('createVueSafeAuthProxy', () => { }) const safeClient = createVueSafeAuthProxy(client) + expect(typeof safeClient).toBe('object') expectVueInspectionSafe(safeClient) expectVueInspectionSafe(safeClient.signIn) expectVueInspectionSafe(safeClient.signIn.email) expectVueInspectionSafe(safeClient.signUp) expectVueInspectionSafe(safeClient.signUp.email) + expectVueInspectionSafe((safeClient as Record).sendVerificationEmail) expectVueInspectionSafe(safeClient.admin.impersonateUser) expect(fetch).not.toHaveBeenCalled() }) @@ -49,6 +51,25 @@ describe('createVueSafeAuthProxy', () => { expect(fetch).toHaveBeenCalledOnce() }) + it('preserves root dynamic client methods without making the root callable', async () => { + const fetch = vi.fn(async () => new Response('{}', { + headers: { 'content-type': 'application/json' }, + })) + const client = createAuthClient({ + baseURL: 'http://localhost:3000/api/auth', + fetchOptions: { customFetchImpl: fetch }, + }) + const safeClient = createVueSafeAuthProxy(client) + const sendVerificationEmail = (safeClient as Record).sendVerificationEmail + + expect(typeof safeClient).toBe('object') + expect(typeof sendVerificationEmail).toBe('function') + + await sendVerificationEmail({ email: 'user@example.com' }) + + expect(fetch).toHaveBeenCalledOnce() + }) + it('does not probe then/catch/finally on raw dynamic proxy values while wrapping', () => { const probed: PropertyKey[] = [] const rawDynamicProxy = new Proxy(() => Promise.resolve({}), {