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
2 changes: 1 addition & 1 deletion docs/content/2.core-concepts/2.sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ When rendering UI that depends on both auth and domain data, fetch that domain d
```ts [pages/app.vue]
const { data: customerState, pending, error } = await useAuthAsyncData(
'customer-state',
requestFetch => requestFetch<{ activeSubscriptions?: unknown[] }>('/api/auth/customer/state'),
requestFetch => requestFetch('/api/auth/customer/state'),
)
```

Expand Down
4 changes: 3 additions & 1 deletion docs/content/5.api/1.composables.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,12 @@ SSR-safe helper for auth-bound data loading.
```ts [pages/app.vue]
const { data: customerState, pending, error } = await useAuthAsyncData(
'customer-state',
requestFetch => requestFetch<{ activeSubscriptions?: unknown[] }>('/api/auth/customer/state'),
requestFetch => requestFetch('/api/auth/customer/state'),
)
```

When route typing is enabled, payload types for `/api/auth/*` endpoints are inferred automatically.

By default:
- `requireAuth` is `true` (unauthenticated users resolve to `null` without calling the endpoint).
- `data` defaults to `null`.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
}
},
"dependencies": {
"@better-auth/cli": "^1.5.0-beta.3",
"@better-auth/cli": "1.5.0-beta.13",
"@nuxt/kit": "^4.2.2",
"@nuxt/ui": "^4.2.1",
"defu": "^6.1.4",
Expand All @@ -90,7 +90,7 @@
"@nuxthub/core": "^0.10.5",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "latest",
"better-auth": "^1.5.0-beta.3",
"better-auth": "1.5.0-beta.18",
"better-sqlite3": "^11.9.1",
"bumpp": "^10.3.2",
"changelogen": "^0.6.2",
Expand Down Expand Up @@ -119,6 +119,7 @@
"@peculiar/x509@1.14.2": "patches/@peculiar__x509@1.14.2.patch"
},
"overrides": {
"better-call": "1.3.2",
"reka-ui": "^2.6.1"
}
}
Expand Down
2 changes: 2 additions & 0 deletions playground/app/auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { navigateTo } from '#imports'
import { sentinelClient } from '@better-auth/infra/client'
import { passkeyClient } from '@better-auth/passkey/client'
import { adminClient, lastLoginMethodClient, multiSessionClient, twoFactorClient } from 'better-auth/client/plugins'
import { defineClientAuth } from '../../src/runtime/config'

export default defineClientAuth({
plugins: [
adminClient(),
sentinelClient(),
passkeyClient(),
multiSessionClient(),
lastLoginMethodClient(),
Expand Down
2 changes: 1 addition & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default defineNuxtConfig({

runtimeConfig: {
public: {
siteUrl: 'https://demo-nuxt-better-auth.onmax.me',
siteUrl: '',
},
},

Expand Down
7 changes: 4 additions & 3 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@
"db:migrate": "nuxt db migrate"
},
"dependencies": {
"@better-auth/passkey": "1.5.0-beta.4",
"@better-auth/infra": "^0.1.7",
"@better-auth/passkey": "1.5.0-beta.19",
"@nuxt/fonts": "^0.12.1",
"@nuxt/ui": "^4.2.1",
"@nuxthub/core": "^0.10.5",
"@nuxtjs/i18n": "^9.4.0",
"better-auth": "1.5.0-beta.4",
"better-auth": "1.5.0-beta.19",
"nuxt": "^4.2.2",
"nuxt-qrcode": "^0.4.8",
"resend": "^6.6.0"
},
"devDependencies": {
"@better-auth/cli": "1.5.0-beta.4",
"@better-auth/cli": "1.5.0-beta.13",
"@libsql/client": "^0.15.15",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
Expand Down
3 changes: 3 additions & 0 deletions playground/server/auth.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { dash } from '@better-auth/infra'
import { passkey } from '@better-auth/passkey'
import { admin, lastLoginMethod, multiSession, twoFactor } from 'better-auth/plugins'
import { consola } from 'consola'
Expand All @@ -11,6 +12,7 @@ const isEmailEnabled = () => import.meta.dev

export default defineServerAuth(() => ({
appName: 'Nuxt Better Auth Playground',
trustedOrigins: ['https://nuxt-better-auth-demo.maximogarciamtnez.workers.dev'],
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID || process.env.NUXT_GITHUB_CLIENT_ID || '',
Expand All @@ -19,6 +21,7 @@ export default defineServerAuth(() => ({
},
plugins: [
admin(),
dash(),
passkey(),
multiSession(),
lastLoginMethod(),
Expand Down
1,692 changes: 1,380 additions & 312 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions src/module/type-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,76 @@ declare module '#nuxt-better-auth' {
addTypeTemplate({
filename: 'types/nuxt-better-auth-nitro.d.ts',
getContents: () => `
import type createServerAuth from '${serverConfigPath}'
import type { BetterAuthOptions } from 'better-auth'
import type { getEndpoints } from 'better-auth/api'
import type { Serialize, Simplify } from 'nitropack/types'

type _RawConfig = ReturnType<typeof createServerAuth>
type _RawPlugins = _RawConfig extends { plugins: infer P } ? P : _RawConfig extends { plugins?: infer P } ? P : []
type _Config = Omit<BetterAuthOptions, 'plugins'> & Omit<_RawConfig, 'plugins'> & {
plugins?: _RawPlugins
}

type _AuthApi = ReturnType<typeof getEndpoints<_Config>>['api']
type _NormalizeMethod<M extends string> = M extends '*' ? 'default' : Lowercase<M>
type _RouteMethodFromOption<M> = M extends readonly (infer T)[]
? _NormalizeMethod<Extract<T, string>>
: M extends string
? _NormalizeMethod<M>
: 'default'
type _RouteMethodFromEndpoint<E> = E extends { options: { method: infer M } } ? _RouteMethodFromOption<M> : 'default'
type _RoutePathFromEndpoint<E> = E extends { path: infer P extends string }
? string extends P
? never
: \`/api/auth\${P}\`
: never
type _RouteResponseFromEndpoint<E> = E extends (...args: any[]) => Promise<infer R> ? Simplify<Serialize<Awaited<R>>> : never
type _UnionToIntersection<U> = (U extends unknown ? (value: U) => void : never) extends (value: infer I) => void ? I : never

type _CoreAuthInternalApi = {
[K in keyof _AuthApi as _RoutePathFromEndpoint<_AuthApi[K]>]: {
[M in _RouteMethodFromEndpoint<_AuthApi[K]> | 'default']: _RouteResponseFromEndpoint<_AuthApi[K]>
}
}
type _PluginEndpointMaps<Plugins> = Plugins extends readonly (infer Plugin)[]
? Plugin extends { endpoints: infer Endpoints extends Record<string, unknown> }
? {
[K in keyof Endpoints as _RoutePathFromEndpoint<Endpoints[K]>]: {
[M in _RouteMethodFromEndpoint<Endpoints[K]> | 'default']: _RouteResponseFromEndpoint<Endpoints[K]>
}
}
: {}
: Plugins extends (infer Plugin)[]
? Plugin extends { endpoints: infer Endpoints extends Record<string, unknown> }
? {
[K in keyof Endpoints as _RoutePathFromEndpoint<Endpoints[K]>]: {
[M in _RouteMethodFromEndpoint<Endpoints[K]> | 'default']: _RouteResponseFromEndpoint<Endpoints[K]>
}
}
: {}
: {}
type _PluginAuthInternalApi = _UnionToIntersection<_PluginEndpointMaps<_RawPlugins>>
type _GeneratedAuthInternalApi = _CoreAuthInternalApi & _PluginAuthInternalApi

declare module '#nuxt-better-auth' {
export type AuthApiInternalRoutes = _GeneratedAuthInternalApi
export type AuthApiEndpointPath = Extract<keyof AuthApiInternalRoutes, string>
export type AuthApiEndpointMethod<Path extends AuthApiEndpointPath> = Extract<keyof AuthApiInternalRoutes[Path], string>
export type AuthApiEndpointResponse<
Path extends AuthApiEndpointPath,
Method extends AuthApiEndpointMethod<Path> = AuthApiEndpointMethod<Path>,
> = AuthApiInternalRoutes[Path][Method]
}

declare module 'nitropack' {
interface NitroRouteRules {
auth?: import('${runtimeTypesPath}').AuthMeta
}
interface NitroRouteConfig {
auth?: import('${runtimeTypesPath}').AuthMeta
}
interface InternalApi extends _GeneratedAuthInternalApi {}
}
declare module 'nitropack/types' {
interface NitroRouteRules {
Expand All @@ -146,6 +209,7 @@ declare module 'nitropack/types' {
interface NitroRouteConfig {
auth?: import('${runtimeTypesPath}').AuthMeta
}
interface InternalApi extends _GeneratedAuthInternalApi {}
}
export {}
`,
Expand Down
7 changes: 6 additions & 1 deletion src/schema-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ export async function generateDrizzleSchema(authOptions: BetterAuthOptions, dial
},
}

const result = await _generateDrizzleSchema({ adapter: adapter as unknown as DBAdapter, options })
// @better-auth/cli may resolve a different @better-auth/core type instance in monorepos/workspaces.
// Cast to the callee's parameter type to avoid nominal-type incompatibilities across identical versions.
const result = await _generateDrizzleSchema({
adapter: adapter as unknown as DBAdapter,
options,
} as unknown as Parameters<typeof _generateDrizzleSchema>[0])
if (!result.code) {
throw new Error(`Schema generation returned empty result for ${dialect}`)
}
Expand Down
1 change: 1 addition & 0 deletions test/cases/nitro-endpoints-type-inference/.nuxtrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
setups.@onmax/nuxt-better-auth="0.0.2-alpha.29"
13 changes: 13 additions & 0 deletions test/cases/nitro-endpoints-type-inference/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { fileURLToPath } from 'node:url'

const serverConfig = fileURLToPath(new URL('./server/auth.config', import.meta.url))

export default defineNuxtConfig({
extends: ['../_base-module'],
runtimeConfig: {
public: { siteUrl: 'http://localhost:3000' },
},
auth: {
serverConfig,
},
})
26 changes: 26 additions & 0 deletions test/cases/nitro-endpoints-type-inference/server/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createAuthEndpoint } from 'better-auth/api'
import { defineServerAuth } from '../../../../src/runtime/config'

function customerPlugin() {
return {
id: 'customer-plugin',
endpoints: {
customerState: createAuthEndpoint('/customer/state', { method: 'GET' }, async () => {
return {
activeSubscriptions: ['starter', 'pro'],
hasBillingIssue: false,
}
}),
customerSession: createAuthEndpoint('/customer/session', { method: ['GET', 'POST'] as const }, async () => {
return {
ok: true,
}
}),
},
} as const
}

export default defineServerAuth({
emailAndPassword: { enabled: true },
plugins: [customerPlugin()] as const,
})
3 changes: 3 additions & 0 deletions test/cases/nitro-endpoints-type-inference/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}
26 changes: 26 additions & 0 deletions test/cases/nitro-endpoints-type-inference/tsconfig.type-check.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": [
"ESNext",
"DOM"
],
"baseUrl": ".",
"module": "preserve",
"moduleResolution": "bundler",
"paths": {
"#auth/client": ["../_base-module/app/auth.config"],
"#auth/server": ["./server/auth.config"],
"#nuxt-better-auth": ["../../../src/runtime/types/augment"]
},
"types": [],
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"files": [
"./.nuxt/types/nuxt-better-auth-infer.d.ts",
"./.nuxt/types/nuxt-better-auth-nitro.d.ts",
"./typecheck-target.ts"
]
}
38 changes: 38 additions & 0 deletions test/cases/nitro-endpoints-type-inference/typecheck-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { AuthApiEndpointPath, AuthApiEndpointResponse } from '#nuxt-better-auth'
import type { Base$Fetch } from 'nitropack/types'

declare const requestFetch: Base$Fetch

type CustomerStatePath = Extract<AuthApiEndpointPath, '/api/auth/customer/state'>
const customerStatePath: CustomerStatePath = '/api/auth/customer/state'

async function assertEndpointInference() {
const customerState = await requestFetch('/api/auth/customer/state')
customerState.activeSubscriptions[0]?.toUpperCase()
customerState.hasBillingIssue.valueOf()
// @ts-expect-error no unknown key
void customerState.missingField

const customerViaHelper: AuthApiEndpointResponse<'/api/auth/customer/state'> = customerState
void customerViaHelper

const customerSessionGet = await requestFetch('/api/auth/customer/session')
const customerSessionPost = await requestFetch('/api/auth/customer/session', { method: 'POST' })
customerSessionGet.ok.valueOf()
customerSessionPost.ok.valueOf()

const session = await requestFetch('/api/auth/get-session')
if (session) {
void session.user.id
void session.session.expiresAt
}
// @ts-expect-error get-session can be null
const shouldFailNullability: { user: { id: string } } = session
void shouldFailNullability

const sessionViaHelper: AuthApiEndpointResponse<'/api/auth/get-session', 'get'> = session
void sessionViaHelper
}

void customerStatePath
void assertEndpointInference
27 changes: 27 additions & 0 deletions test/infer-nitro-endpoints-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'

const fixtureDir = fileURLToPath(new URL('./cases/nitro-endpoints-type-inference', import.meta.url))
const env = {
...process.env,
BETTER_AUTH_SECRET: 'test-secret-for-testing-only-32chars',
}

describe('typed nitro route inference #194', () => {
it('typechecks generated InternalApi endpoint inference for /api/auth routes', () => {
const prepare = spawnSync('npx', ['nuxi', 'prepare'], {
cwd: fixtureDir,
env,
encoding: 'utf8',
})
expect(prepare.status, `nuxi prepare failed:\n${prepare.stdout}\n${prepare.stderr}`).toBe(0)

const typecheck = spawnSync('npx', ['tsc', '--noEmit', '--pretty', 'false', '-p', 'tsconfig.type-check.json'], {
cwd: fixtureDir,
env,
encoding: 'utf8',
})
expect(typecheck.status, `tsc failed:\n${typecheck.stdout}\n${typecheck.stderr}`).toBe(0)
}, 60_000)
})
Loading