Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/runtime/app/internal/vue-safe-auth-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,33 @@ export function createVueSafeAuthProxy<T>(target: T): T {
return proxy as V
}

return wrap(target)
const createRootFacade = <V>(value: V): V => {
if (!isObjectLike(value))
return value

const propertyCache = new Map<PropertyKey, unknown>()
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)
}
6 changes: 6 additions & 0 deletions test/cases/pinia-setup-store/app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,11 @@ const sessionState = useSessionStateStore()
<p data-testid="full-actions">
Full actions: {{ typeof fullAuth.signOut }}
</p>
<p data-testid="client-type">
Client type: {{ typeof fullAuth.authClient }}
</p>
<p data-testid="verification-type">
Verification type: {{ typeof fullAuth.authClient?.sendVerificationEmail }}
</p>
</main>
</template>
4 changes: 2 additions & 2 deletions test/cases/pinia-setup-store/app/stores/full-auth.ts
Original file line number Diff line number Diff line change
@@ -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 }
})
33 changes: 33 additions & 0 deletions test/use-user-session.test.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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<string, unknown>).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<string, any>).sendVerificationEmail)
expectVueInspectionSafe(auth.client!.admin.impersonateUser)
})

Expand Down Expand Up @@ -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<string, unknown>).sendVerificationEmail).toBe('function')
expect(isStateLike((store.authClient as Record<string, unknown>).signIn)).toBe(false)
expect(isStateLike((store.authClient as Record<string, unknown>).admin)).toBe(false)
expect(isStateLike((store.authClient as Record<string, unknown>).sendVerificationEmail)).toBe(false)
expect(isStateLike((store.signIn as Record<string, unknown>).email)).toBe(false)
expect(isStateLike((store.signUp as Record<string, unknown>).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<string, unknown>).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<string, unknown>).sendVerificationEmail)).toBe(false)
})

it('allows server-side auth method reads but still rejects invocation', async () => {
setRuntimeFlags({ client: false, server: true })

Expand Down
21 changes: 21 additions & 0 deletions test/vue-safe-auth-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>).sendVerificationEmail)
expectVueInspectionSafe(safeClient.admin.impersonateUser)
expect(fetch).not.toHaveBeenCalled()
})
Expand All @@ -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<string, any>).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({}), {
Expand Down
Loading