Skip to content
Draft
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
22 changes: 22 additions & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
'src/nitro',
'src/nitro/config',
{
builder: 'mkdist',
input: 'src/nitro/runtime/',
outDir: 'dist/nitro/runtime',
addRelativeDeclarationExtensions: true,
ext: 'js',
pattern: [
'**',
'!**/*.stories.{js,cts,mts,ts,jsx,tsx}',
'!**/*.{spec,test}.{js,cts,mts,ts,jsx,tsx}',
],
esbuild: {
jsxImportSource: 'vue',
jsx: 'automatic',
jsxFactory: 'h',
},
},
],
externals: ['consola', '@better-auth/cli', '@better-auth/cli/api', 'drizzle-orm/utils'],
failOnWarn: false,
})
21 changes: 20 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
"./config": {
"types": "./dist/runtime/config.d.ts",
"import": "./dist/runtime/config.js"
},
"./nitro": {
"types": "./dist/nitro.d.mts",
"import": "./dist/nitro.mjs"
},
"./nitro/config": {
"types": "./dist/nitro/config.d.mts",
"import": "./dist/nitro/config.mjs"
}
},
"main": "./dist/module.mjs",
Expand All @@ -36,6 +44,12 @@
],
"config": [
"./dist/runtime/config.d.ts"
],
"nitro": [
"./dist/nitro.d.mts"
],
"nitro/config": [
"./dist/nitro/config.d.mts"
]
}
},
Expand All @@ -62,11 +76,15 @@
},
"peerDependencies": {
"@nuxthub/core": ">=0.10.5",
"better-auth": ">=1.0.0"
"better-auth": ">=1.0.0",
"nitro": "^3.0.0-0"
},
"peerDependenciesMeta": {
"@nuxthub/core": {
"optional": true
},
"nitro": {
"optional": true
}
},
"dependencies": {
Expand Down Expand Up @@ -105,6 +123,7 @@
"eslint": "^10.0.3",
"nitropack": "2.13.1",
"npm-agentskills": "https://pkg.pr.new/onmax/npm-agentskills@394499e",
"nitro": "3.0.260311-beta",

Check failure on line 126 in package.json

View workflow job for this annotation

GitHub Actions / ci

Expected object keys to be in ascending order. 'nitro' should be before 'nitropack'
"nuxt": "^4.3.1",
"pinia": "^3.0.4",
"postgres": "^3.4.9",
Expand Down
8,281 changes: 5,266 additions & 3,015 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/nitro.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type {} from './nitro/augment'

export { default } from './nitro/module'
export { serverAuth } from './nitro/runtime/server/utils/auth'
export { getRequestSession, getUserSession, requireUserSession } from './nitro/runtime/server/utils/session'
export type { BetterAuthNitroOptions } from './nitro/module-types'

Check failure on line 6 in src/nitro.ts

View workflow job for this annotation

GitHub Actions / ci

Expected "./nitro/module-types" to come before "./nitro/runtime/server/utils/session"
export type { AppSession, AuthMeta, AuthMode, AuthRouteRules, AuthSession, AuthUser, RequireSessionOptions, ServerAuthContext, UserMatch } from './nitro/runtime/types'
18 changes: 18 additions & 0 deletions src/nitro/augment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { BetterAuthNitroOptions } from './module-types'
import type { AuthMeta } from './runtime/types'

declare module 'nitro/types' {
interface NitroConfig {
betterAuth?: BetterAuthNitroOptions
}

interface NitroRouteRules {
auth?: AuthMeta
}

interface NitroRouteConfig {
auth?: AuthMeta
}
}

export {}
45 changes: 45 additions & 0 deletions src/nitro/config-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { existsSync } from 'node:fs'
import { isAbsolute, relative, resolve } from 'pathe'

const CONFIG_EXTENSIONS = ['.ts', '.mts', '.js', '.mjs', '.cts', '.cjs'] as const

function hasKnownConfigExtension(path: string): boolean {
return CONFIG_EXTENSIONS.some(extension => path.endsWith(extension))
}

export interface ResolvedNitroAuthConfigPath {
requestedPath: string
resolvedPath: string | null
}

export function resolveNitroAuthConfigPath(rootDir: string, configPath: string): ResolvedNitroAuthConfigPath {
const requestedPath = isAbsolute(configPath) ? configPath : resolve(rootDir, configPath)

if (hasKnownConfigExtension(requestedPath)) {
return {
requestedPath,
resolvedPath: existsSync(requestedPath) ? requestedPath : null,
}
}

for (const extension of CONFIG_EXTENSIONS) {
const candidate = `${requestedPath}${extension}`
if (existsSync(candidate)) {
return {
requestedPath,
resolvedPath: candidate,
}
}
}

return {
requestedPath,
resolvedPath: null,
}
}

export function formatMissingNitroAuthConfigError(rootDir: string, configPath: string): string {
const expectedPath = hasKnownConfigExtension(configPath) ? configPath : `${configPath}.ts`
const displayPath = isAbsolute(expectedPath) ? relative(rootDir, expectedPath) : expectedPath
return `[nuxt-better-auth] Missing ${displayPath} - export default defineServerAuth(...) from @onmax/nuxt-better-auth/nitro/config`
}
16 changes: 16 additions & 0 deletions src/nitro/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {} from './augment'
import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'

export interface ServerAuthContext {
runtimeConfig: Record<string, unknown>
}

export type ServerAuthConfig = Omit<BetterAuthOptions, 'secret' | 'baseURL'> & {
plugins?: readonly BetterAuthPlugin[]
}

export function defineServerAuth<const R>(config: (ctx: ServerAuthContext) => R & ServerAuthConfig): (ctx: ServerAuthContext) => R
export function defineServerAuth<const R>(config: R & ServerAuthConfig): (ctx: ServerAuthContext) => R
export function defineServerAuth(config: ServerAuthConfig | ((ctx: ServerAuthContext) => ServerAuthConfig)): (ctx: ServerAuthContext) => ServerAuthConfig {
return typeof config === 'function' ? config : () => config
}
17 changes: 17 additions & 0 deletions src/nitro/module-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface BetterAuthNitroOptions {
/**
* Path to the Better Auth server config. Relative paths resolve from the Nitro root.
* Defaults to `server/auth.config`.
*/
config?: string
}

export interface ResolvedBetterAuthNitroOptions {
config: string
}

export function normalizeBetterAuthNitroOptions(options?: BetterAuthNitroOptions): ResolvedBetterAuthNitroOptions {
return {
config: options?.config || 'server/auth.config',
}
}
111 changes: 111 additions & 0 deletions src/nitro/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type {} from './augment'
import type { Nitro } from 'nitro/types'
import type { BetterAuthNitroOptions } from './module-types'
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { formatMissingNitroAuthConfigError, resolveNitroAuthConfigPath } from './config-path'
import { normalizeBetterAuthNitroOptions } from './module-types'

interface NitroHandlerLike {
route: string
handler: string
method?: string
middleware?: boolean
}

interface NitroPluginTarget {
plugins?: string[]
}

function resolveRuntimePath(path: string): string {
const candidates = ['.js', '.mjs', '.ts', '.mts']
const prefixes = ['./nitro/runtime', './runtime']

for (const prefix of prefixes) {
for (const extension of candidates) {
const resolved = fileURLToPath(new URL(`${prefix}${path}${extension}`, import.meta.url))
if (existsSync(resolved))
return resolved
}
}

throw new Error(`[nuxt-better-auth] Missing runtime file for ${path}`)
}

function addNitroHandler(target: { handlers?: NitroHandlerLike[] }, handler: NitroHandlerLike): void {
target.handlers ||= []

const alreadyRegistered = target.handlers.some(existing =>
existing.route === handler.route
&& existing.handler === handler.handler
&& existing.method === handler.method
&& Boolean(existing.middleware) === Boolean(handler.middleware),
)

if (!alreadyRegistered)
target.handlers.push(handler)
}

function addNitroPlugin(target: NitroPluginTarget, plugin: string): void {
target.plugins ||= []

if (!target.plugins.includes(plugin))
target.plugins.push(plugin)
}

function setupRuntimeConfig(nitro: Nitro): void {
const options = nitro.options as typeof nitro.options & {
runtimeConfig?: Record<string, any>

Check failure on line 58 in src/nitro/module.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
}

options.runtimeConfig ||= {}
options.runtimeConfig.public ||= {}

if (!options.runtimeConfig.public.siteUrl && process.env.NUXT_PUBLIC_SITE_URL)
options.runtimeConfig.public.siteUrl = process.env.NUXT_PUBLIC_SITE_URL

const currentSecret = typeof options.runtimeConfig.betterAuthSecret === 'string'
? options.runtimeConfig.betterAuthSecret
: undefined

options.runtimeConfig.betterAuthSecret = currentSecret || process.env.NUXT_BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET || ''

const betterAuthSecret = options.runtimeConfig.betterAuthSecret as string
if (!options.dev && !betterAuthSecret) {
throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET is required in production. Set NUXT_BETTER_AUTH_SECRET or BETTER_AUTH_SECRET environment variable.')
}
if (betterAuthSecret && betterAuthSecret.length < 32) {
throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET must be at least 32 characters for security')
}
}

export function installBetterAuthNitroModule(nitro: Nitro, rawOptions?: BetterAuthNitroOptions): void {
const options = nitro.options as typeof nitro.options & {
alias?: Record<string, string>
betterAuth?: BetterAuthNitroOptions
handlers?: NitroHandlerLike[]
}

const config = normalizeBetterAuthNitroOptions(rawOptions ?? options.betterAuth)
const { resolvedPath } = resolveNitroAuthConfigPath(nitro.options.rootDir, config.config)
if (!resolvedPath)
throw new Error(formatMissingNitroAuthConfigError(nitro.options.rootDir, config.config))

options.alias ||= {}
options.alias['#better-auth-nitro/server'] = resolvedPath

setupRuntimeConfig(nitro)
addNitroPlugin(options, resolveRuntimePath('/plugin'))

addNitroHandler(options, {
route: '/api/auth/**',
handler: resolveRuntimePath('/server/api/auth/[...all]'),
})
}

export default {
name: '@onmax/nuxt-better-auth/nitro',
setup(nitro: Nitro) {
installBetterAuthNitroModule(nitro)
},
}
8 changes: 8 additions & 0 deletions src/nitro/runtime/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { definePlugin } from 'nitro'
import { enforceRouteAccess } from './server/internal/route-access'

export default definePlugin((nitroApp) => {
nitroApp.hooks.hook('request', async (event) => {
await enforceRouteAccess(event)
})
})
8 changes: 8 additions & 0 deletions src/nitro/runtime/server/api/auth/[...all].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { H3Event } from 'nitro/h3'
import { defineEventHandler } from 'nitro/h3'
import { serverAuth } from '../../utils/auth'

export default defineEventHandler(async (event: H3Event) => {
const auth = serverAuth(event)
return auth.handler(event.req)
})
Loading
Loading