@onmax/nuxt-better-auth 0.0.2-alpha.2 → 0.0.2-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.mjs CHANGED
@@ -1,12 +1,18 @@
1
- import { existsSync } from 'node:fs';
1
+ import { randomBytes } from 'node:crypto';
2
+ import { existsSync, writeFileSync, readFileSync } from 'node:fs';
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
- import { defineNuxtModule, createResolver, hasNuxtModule, addTemplate, addTypeTemplate, updateTemplates, addServerImportsDir, addServerScanDir, addServerHandler, addImportsDir, addPlugin, addComponentsDir, extendPages } from '@nuxt/kit';
4
+ import { defineNuxtModule, createResolver, hasNuxtModule, addTemplate, addTypeTemplate, updateTemplates, addServerImportsDir, addServerImports, addServerScanDir, addServerHandler, addImportsDir, addPlugin, addComponentsDir, installModule, extendPages } from '@nuxt/kit';
4
5
  import { consola as consola$1 } from 'consola';
5
6
  import { defu } from 'defu';
6
- import { join } from 'pathe';
7
+ import { join, dirname } from 'pathe';
7
8
  import { toRouteMatcher, createRouter } from 'radix3';
9
+ import { isCI, isTest } from 'std-env';
10
+ import { generateDrizzleSchema as generateDrizzleSchema$1 } from '@better-auth/cli/api';
11
+ import { getAuthTables } from 'better-auth/db';
8
12
  export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
9
13
 
14
+ const version = "0.0.2-alpha.21";
15
+
10
16
  function setupDevTools(nuxt) {
11
17
  nuxt.hook("devtools:customTabs", (tabs) => {
12
18
  tabs.push({
@@ -22,178 +28,363 @@ function setupDevTools(nuxt) {
22
28
  });
23
29
  }
24
30
 
25
- function generateDrizzleSchema(tables, dialect) {
26
- const imports = getImports(dialect);
27
- const tableDefinitions = Object.entries(tables).map(([tableName, table]) => generateTable(tableName, table, dialect, tables)).join("\n\n");
28
- return `${imports}
29
-
30
- ${tableDefinitions}
31
- `;
32
- }
33
- function getImports(dialect) {
34
- switch (dialect) {
35
- case "sqlite":
36
- return `import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'`;
37
- case "postgresql":
38
- return `import { boolean, pgTable, text, timestamp, integer } from 'drizzle-orm/pg-core'`;
39
- case "mysql":
40
- return `import { boolean, int, mysqlTable, text, timestamp, varchar } from 'drizzle-orm/mysql-core'`;
41
- }
42
- }
43
- function generateTable(tableName, table, dialect, allTables) {
44
- const tableFunc = dialect === "sqlite" ? "sqliteTable" : dialect === "postgresql" ? "pgTable" : "mysqlTable";
45
- const dbTableName = table.modelName || tableName;
46
- const fields = Object.entries(table.fields).map(([fieldName, field]) => generateField(fieldName, field, dialect, allTables)).join(",\n ");
47
- const idField = generateIdField(dialect);
48
- return `export const ${tableName} = ${tableFunc}('${dbTableName}', {
49
- ${idField},
50
- ${fields}
51
- })`;
52
- }
53
- function generateIdField(dialect) {
54
- switch (dialect) {
55
- case "sqlite":
56
- case "postgresql":
57
- return `id: text('id').primaryKey()`;
58
- case "mysql":
59
- return `id: varchar('id', { length: 36 }).primaryKey()`;
60
- }
31
+ function dialectToProvider(dialect) {
32
+ return dialect === "postgresql" ? "pg" : dialect;
61
33
  }
62
- function generateField(fieldName, field, dialect, allTables) {
63
- const dbFieldName = fieldName;
64
- let fieldDef = getFieldType(field.type, dialect, dbFieldName);
65
- if (field.required && field.defaultValue === void 0)
66
- fieldDef += ".notNull()";
67
- if (field.unique)
68
- fieldDef += ".unique()";
69
- if (field.defaultValue !== void 0) {
70
- if (typeof field.defaultValue === "boolean")
71
- fieldDef += `.default(${field.defaultValue})`;
72
- else if (typeof field.defaultValue === "string")
73
- fieldDef += `.default('${field.defaultValue}')`;
74
- else
75
- fieldDef += `.default(${field.defaultValue})`;
76
- if (field.required)
77
- fieldDef += ".notNull()";
78
- }
79
- if (field.references) {
80
- const refTable = field.references.model;
81
- if (allTables[refTable])
82
- fieldDef += `.references(() => ${refTable}.${field.references.field})`;
83
- }
84
- return `${fieldName}: ${fieldDef}`;
85
- }
86
- function getFieldType(type, dialect, fieldName) {
87
- const normalizedType = Array.isArray(type) ? "string" : type;
88
- switch (dialect) {
89
- case "sqlite":
90
- return getSqliteType(normalizedType, fieldName);
91
- case "postgresql":
92
- return getPostgresType(normalizedType, fieldName);
93
- case "mysql":
94
- return getMysqlType(normalizedType, fieldName);
34
+ async function generateDrizzleSchema(authOptions, dialect, schemaOptions) {
35
+ const provider = dialectToProvider(dialect);
36
+ const options = {
37
+ ...authOptions,
38
+ advanced: {
39
+ ...authOptions.advanced,
40
+ database: {
41
+ ...authOptions.advanced?.database,
42
+ ...schemaOptions?.useUuid && { generateId: "uuid" }
43
+ }
44
+ }
45
+ };
46
+ const adapter = {
47
+ id: "drizzle",
48
+ options: {
49
+ provider,
50
+ camelCase: schemaOptions?.casing !== "snake_case",
51
+ adapterConfig: { usePlural: schemaOptions?.usePlural ?? false }
52
+ }
53
+ };
54
+ const result = await generateDrizzleSchema$1({ adapter, options });
55
+ if (!result.code) {
56
+ throw new Error(`Schema generation returned empty result for ${dialect}`);
95
57
  }
58
+ return result.code;
96
59
  }
97
- function getSqliteType(type, fieldName) {
98
- switch (type) {
99
- case "string":
100
- return `text('${fieldName}')`;
101
- case "boolean":
102
- return `integer('${fieldName}', { mode: 'boolean' })`;
103
- case "date":
104
- return `integer('${fieldName}', { mode: 'timestamp' })`;
105
- case "number":
106
- return `integer('${fieldName}')`;
107
- default:
108
- return `text('${fieldName}')`;
109
- }
60
+ const convexIndexFields = {
61
+ account: ["accountId", ["accountId", "providerId"], ["providerId", "userId"]],
62
+ rateLimit: ["key"],
63
+ session: ["expiresAt", ["expiresAt", "userId"]],
64
+ verification: ["expiresAt", "identifier"],
65
+ user: [["email", "name"], "name", "userId"],
66
+ passkey: ["credentialID"],
67
+ oauthConsent: [["clientId", "userId"]]
68
+ };
69
+ function getConvexSpecialFields(tables) {
70
+ return Object.fromEntries(
71
+ Object.entries(tables).map(([key, table]) => {
72
+ const fields = Object.fromEntries(
73
+ Object.entries(table.fields).map(([fieldKey, field]) => [
74
+ field.fieldName ?? fieldKey,
75
+ {
76
+ ...field.sortable ? { sortable: true } : {},
77
+ ...field.unique ? { unique: true } : {},
78
+ ...field.references ? { references: field.references } : {}
79
+ }
80
+ ]).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true)
81
+ );
82
+ return [key, fields];
83
+ }).filter(([_key, value]) => typeof value === "object" ? Object.keys(value).length > 0 : true)
84
+ );
110
85
  }
111
- function getPostgresType(type, fieldName) {
112
- switch (type) {
113
- case "string":
114
- return `text('${fieldName}')`;
115
- case "boolean":
116
- return `boolean('${fieldName}')`;
117
- case "date":
118
- return `timestamp('${fieldName}')`;
119
- case "number":
120
- return `integer('${fieldName}')`;
121
- default:
122
- return `text('${fieldName}')`;
123
- }
86
+ function getMergedConvexIndexFields(tables) {
87
+ return Object.fromEntries(
88
+ Object.entries(tables).map(([key, table]) => {
89
+ const manualIndexes = convexIndexFields[key]?.map((index) => {
90
+ return typeof index === "string" ? table.fields[index]?.fieldName ?? index : index.map((i) => table.fields[i]?.fieldName ?? i);
91
+ }) || [];
92
+ const specialFieldsObj = getConvexSpecialFields(tables);
93
+ const specialFieldIndexes = Object.keys(specialFieldsObj[key] || {}).filter(
94
+ (index) => !manualIndexes.some((m) => Array.isArray(m) ? m[0] === index : m === index)
95
+ );
96
+ return [key, manualIndexes.concat(specialFieldIndexes)];
97
+ })
98
+ );
124
99
  }
125
- function getMysqlType(type, fieldName) {
126
- switch (type) {
127
- case "string":
128
- return `text('${fieldName}')`;
129
- case "boolean":
130
- return `boolean('${fieldName}')`;
131
- case "date":
132
- return `timestamp('${fieldName}')`;
133
- case "number":
134
- return `int('${fieldName}')`;
135
- default:
136
- return `text('${fieldName}')`;
100
+ async function generateConvexSchema(authOptions) {
101
+ const tables = getAuthTables(authOptions);
102
+ let code = `/**
103
+ * Auto-generated Better Auth tables for Convex.
104
+ * Import these tables in your convex/schema.ts:
105
+ *
106
+ * import { defineSchema } from 'convex/server'
107
+ * import { authTables } from './_generated/auth-tables'
108
+ *
109
+ * export default defineSchema({ ...authTables, ...yourTables })
110
+ */
111
+
112
+ import { defineTable } from 'convex/server'
113
+ import { v } from 'convex/values'
114
+
115
+ export const authTables = {
116
+ `;
117
+ const getType = (_name, field) => {
118
+ const type = field.type;
119
+ const typeMap = {
120
+ "string": "v.string()",
121
+ "boolean": "v.boolean()",
122
+ "number": "v.number()",
123
+ "date": "v.number()",
124
+ "json": "v.string()",
125
+ "number[]": "v.array(v.number())",
126
+ "string[]": "v.array(v.string())"
127
+ };
128
+ return typeMap[type];
129
+ };
130
+ for (const tableKey in tables) {
131
+ const table = tables[tableKey];
132
+ const modelName = table.modelName;
133
+ const fields = Object.fromEntries(Object.entries(table.fields).filter(([key]) => key !== "id"));
134
+ const indexes = getMergedConvexIndexFields(tables)[tableKey]?.map((index) => {
135
+ const indexArray = Array.isArray(index) ? index.sort() : [index];
136
+ const indexName = indexArray.join("_");
137
+ return `.index('${indexName}', ${JSON.stringify(indexArray)})`;
138
+ }) || [];
139
+ const schema = `${modelName}: defineTable({
140
+ ${Object.keys(fields).map((field) => {
141
+ const attr = fields[field];
142
+ const type = getType(field, attr);
143
+ const optional = (fieldSchema) => attr.required ? fieldSchema : `v.optional(v.union(v.null(), ${fieldSchema}))`;
144
+ return ` ${attr.fieldName ?? field}: ${optional(type)},`;
145
+ }).join("\n")}
146
+ })${indexes.length > 0 ? `
147
+ ${indexes.join("\n ")}` : ""},
148
+ `;
149
+ code += ` ${schema}`;
137
150
  }
151
+ code += `}
152
+ `;
153
+ return code;
138
154
  }
139
- async function loadUserAuthConfig(configPath) {
155
+ async function loadUserAuthConfig(configPath, throwOnError = false) {
140
156
  const { createJiti } = await import('jiti');
141
- const jiti = createJiti(import.meta.url, { interopDefault: true });
157
+ const { defineServerAuth } = await import('../dist/runtime/config.js');
158
+ const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
159
+ if (!globalThis.defineServerAuth) {
160
+ defineServerAuth._count = 0;
161
+ globalThis.defineServerAuth = defineServerAuth;
162
+ }
163
+ globalThis.defineServerAuth._count++;
142
164
  try {
143
165
  const mod = await jiti.import(configPath);
144
- const configFn = mod.default || mod;
166
+ const configFn = mod.default;
145
167
  if (typeof configFn === "function") {
146
168
  return configFn({ runtimeConfig: {}, db: null });
147
169
  }
170
+ consola$1.warn("[@onmax/nuxt-better-auth] auth.config.ts does not export default. Expected: export default defineServerAuth(...)");
171
+ if (throwOnError) {
172
+ throw new Error("auth.config.ts must export default defineServerAuth(...)");
173
+ }
148
174
  return {};
149
175
  } catch (error) {
176
+ if (throwOnError) {
177
+ throw new Error(`Failed to load auth config: ${error instanceof Error ? error.message : error}`);
178
+ }
150
179
  consola$1.error("[@onmax/nuxt-better-auth] Failed to load auth config for schema generation. Schema may be incomplete:", error);
151
180
  return {};
181
+ } finally {
182
+ globalThis.defineServerAuth._count--;
183
+ if (!globalThis.defineServerAuth._count) {
184
+ globalThis.defineServerAuth = void 0;
185
+ }
152
186
  }
153
187
  }
154
188
 
189
+ function getHubDialect(hub) {
190
+ if (!hub?.db)
191
+ return void 0;
192
+ if (typeof hub.db === "string")
193
+ return hub.db;
194
+ if (typeof hub.db === "object" && hub.db !== null)
195
+ return hub.db.dialect;
196
+ return void 0;
197
+ }
198
+ function getHubCasing(hub) {
199
+ if (!hub?.db || typeof hub.db !== "object" || hub.db === null)
200
+ return void 0;
201
+ return hub.db.casing;
202
+ }
155
203
  const consola = consola$1.withTag("nuxt-better-auth");
204
+ function resolveConvexUrl(nuxt) {
205
+ const convexConfig = nuxt.options.convex;
206
+ if (convexConfig?.url)
207
+ return convexConfig.url;
208
+ const runtimeUrl = nuxt.options.runtimeConfig.public?.convex?.url;
209
+ if (runtimeUrl)
210
+ return runtimeUrl;
211
+ if (process.env.CONVEX_URL)
212
+ return process.env.CONVEX_URL;
213
+ if (process.env.NUXT_PUBLIC_CONVEX_URL)
214
+ return process.env.NUXT_PUBLIC_CONVEX_URL;
215
+ return "";
216
+ }
217
+ const generateSecret = () => randomBytes(32).toString("hex");
218
+ function readEnvFile(rootDir) {
219
+ const envPath = join(rootDir, ".env");
220
+ return existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
221
+ }
222
+ function hasEnvSecret(rootDir) {
223
+ const match = readEnvFile(rootDir).match(/^BETTER_AUTH_SECRET=(.+)$/m);
224
+ return !!match && !!match[1] && match[1].trim().length > 0;
225
+ }
226
+ function appendSecretToEnv(rootDir, secret) {
227
+ const envPath = join(rootDir, ".env");
228
+ let content = readEnvFile(rootDir);
229
+ if (content.length > 0 && !content.endsWith("\n"))
230
+ content += "\n";
231
+ content += `BETTER_AUTH_SECRET=${secret}
232
+ `;
233
+ writeFileSync(envPath, content, "utf-8");
234
+ }
235
+ async function promptForSecret(rootDir) {
236
+ if (process.env.BETTER_AUTH_SECRET || hasEnvSecret(rootDir))
237
+ return void 0;
238
+ if (isCI || isTest) {
239
+ const secret2 = generateSecret();
240
+ appendSecretToEnv(rootDir, secret2);
241
+ consola.info("Generated BETTER_AUTH_SECRET and added to .env (CI mode)");
242
+ return secret2;
243
+ }
244
+ consola.box("BETTER_AUTH_SECRET is required for authentication.\nThis will be appended to your .env file.");
245
+ const choice = await consola.prompt("How do you want to set it?", {
246
+ type: "select",
247
+ options: [
248
+ { label: "Generate for me", value: "generate", hint: "uses crypto.randomBytes(32)" },
249
+ { label: "Enter manually", value: "paste" },
250
+ { label: "Skip", value: "skip", hint: "will fail in production" }
251
+ ],
252
+ cancel: "null"
253
+ });
254
+ if (typeof choice === "symbol" || choice === "skip") {
255
+ consola.warn("Skipping BETTER_AUTH_SECRET. Auth will fail without it in production.");
256
+ return void 0;
257
+ }
258
+ let secret;
259
+ if (choice === "generate") {
260
+ secret = generateSecret();
261
+ } else {
262
+ const input = await consola.prompt("Paste your secret (min 32 chars):", { type: "text", cancel: "null" });
263
+ if (typeof input === "symbol" || !input || input.length < 32) {
264
+ consola.warn("Invalid secret. Skipping.");
265
+ return void 0;
266
+ }
267
+ secret = input;
268
+ }
269
+ const preview = `${secret.slice(0, 8)}...${secret.slice(-4)}`;
270
+ const confirm = await consola.prompt(`Add to .env:
271
+ BETTER_AUTH_SECRET=${preview}
272
+ Proceed?`, { type: "confirm", initial: true, cancel: "null" });
273
+ if (typeof confirm === "symbol" || !confirm) {
274
+ consola.info("Cancelled. Secret not written.");
275
+ return void 0;
276
+ }
277
+ appendSecretToEnv(rootDir, secret);
278
+ consola.success("Added BETTER_AUTH_SECRET to .env");
279
+ return secret;
280
+ }
156
281
  const module$1 = defineNuxtModule({
157
- meta: { name: "@onmax/nuxt-better-auth", configKey: "auth", compatibility: { nuxt: ">=3.0.0" } },
282
+ meta: { name: "@onmax/nuxt-better-auth", version, configKey: "auth", compatibility: { nuxt: ">=3.0.0" } },
158
283
  defaults: {
284
+ clientOnly: false,
159
285
  serverConfig: "server/auth.config",
160
286
  clientConfig: "app/auth.config",
161
287
  redirects: { login: "/login", guest: "/" },
162
288
  secondaryStorage: false
163
289
  },
290
+ async onInstall(nuxt) {
291
+ const generatedSecret = await promptForSecret(nuxt.options.rootDir);
292
+ if (generatedSecret)
293
+ process.env.BETTER_AUTH_SECRET = generatedSecret;
294
+ const serverPath = join(nuxt.options.rootDir, "server/auth.config.ts");
295
+ const clientPath = join(nuxt.options.srcDir, "auth.config.ts");
296
+ const serverTemplate = `import { defineServerAuth } from '@onmax/nuxt-better-auth/config'
297
+
298
+ export default defineServerAuth({
299
+ emailAndPassword: { enabled: true },
300
+ })
301
+ `;
302
+ const clientTemplate = `import { defineClientAuth } from '@onmax/nuxt-better-auth/config'
303
+
304
+ export default defineClientAuth({})
305
+ `;
306
+ if (!existsSync(serverPath)) {
307
+ await mkdir(dirname(serverPath), { recursive: true });
308
+ await writeFile(serverPath, serverTemplate);
309
+ consola.success("Created server/auth.config.ts");
310
+ }
311
+ if (!existsSync(clientPath)) {
312
+ await mkdir(dirname(clientPath), { recursive: true });
313
+ await writeFile(clientPath, clientTemplate);
314
+ const relativePath = clientPath.replace(`${nuxt.options.rootDir}/`, "");
315
+ consola.success(`Created ${relativePath}`);
316
+ }
317
+ },
164
318
  async setup(options, nuxt) {
165
319
  const resolver = createResolver(import.meta.url);
320
+ if (options.clientConfig === "app/auth.config") {
321
+ const srcDirRelative = nuxt.options.srcDir.replace(`${nuxt.options.rootDir}/`, "");
322
+ options.clientConfig = srcDirRelative === nuxt.options.srcDir ? "auth.config" : `${srcDirRelative}/auth.config`;
323
+ }
324
+ const clientOnly = options.clientOnly;
166
325
  const serverConfigFile = options.serverConfig;
167
326
  const clientConfigFile = options.clientConfig;
168
327
  const serverConfigPath = resolver.resolve(nuxt.options.rootDir, serverConfigFile);
169
328
  const clientConfigPath = resolver.resolve(nuxt.options.rootDir, clientConfigFile);
170
329
  const serverConfigExists = existsSync(`${serverConfigPath}.ts`) || existsSync(`${serverConfigPath}.js`);
171
330
  const clientConfigExists = existsSync(`${clientConfigPath}.ts`) || existsSync(`${clientConfigPath}.js`);
172
- if (!serverConfigExists)
173
- throw new Error(`[nuxt-better-auth] Missing ${serverConfigFile}.ts - create with defineServerAuth()`);
331
+ if (!clientOnly && !serverConfigExists)
332
+ throw new Error(`[nuxt-better-auth] Missing ${serverConfigFile}.ts - export default defineServerAuth(...)`);
174
333
  if (!clientConfigExists)
175
- throw new Error(`[nuxt-better-auth] Missing ${clientConfigFile}.ts - export createAppAuthClient()`);
334
+ throw new Error(`[nuxt-better-auth] Missing ${clientConfigFile}.ts - export default defineClientAuth(...)`);
176
335
  const hasNuxtHub = hasNuxtModule("@nuxthub/core", nuxt);
177
336
  const hub = hasNuxtHub ? nuxt.options.hub : void 0;
178
- const hasHubDb = hasNuxtHub && !!hub?.db;
337
+ const hasHubDb = !clientOnly && hasNuxtHub && !!hub?.db;
338
+ const hasConvex = hasNuxtModule("nuxt-convex", nuxt);
339
+ const convexUrl = resolveConvexUrl(nuxt);
340
+ const hasConvexDb = !clientOnly && hasConvex && !!convexUrl && !hasHubDb;
341
+ if (hasConvexDb) {
342
+ consola.info("Detected Convex - using Convex HTTP adapter for Better Auth database");
343
+ }
179
344
  let secondaryStorageEnabled = options.secondaryStorage ?? false;
180
- if (secondaryStorageEnabled && (!hasNuxtHub || !hub?.kv)) {
345
+ if (secondaryStorageEnabled && clientOnly) {
346
+ consola.warn("secondaryStorage is not available in clientOnly mode. Disabling.");
347
+ secondaryStorageEnabled = false;
348
+ } else if (secondaryStorageEnabled && (!hasNuxtHub || !hub?.kv)) {
181
349
  consola.warn("secondaryStorage requires @nuxthub/core with hub.kv: true. Disabling.");
182
350
  secondaryStorageEnabled = false;
183
351
  }
184
352
  nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
185
353
  nuxt.options.runtimeConfig.public.auth = defu(nuxt.options.runtimeConfig.public.auth, {
186
354
  redirects: { login: options.redirects?.login ?? "/login", guest: options.redirects?.guest ?? "/" },
187
- useDatabase: hasHubDb
188
- });
189
- nuxt.options.runtimeConfig.betterAuthSecret ||= process.env.BETTER_AUTH_SECRET || process.env.NUXT_BETTER_AUTH_SECRET || "";
190
- nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth, {
191
- secondaryStorage: secondaryStorageEnabled
355
+ useDatabase: hasHubDb || hasConvexDb,
356
+ clientOnly
192
357
  });
358
+ if (clientOnly) {
359
+ const siteUrl = process.env.NUXT_PUBLIC_SITE_URL || nuxt.options.runtimeConfig.public.siteUrl;
360
+ if (!siteUrl) {
361
+ consola.warn("clientOnly mode: NUXT_PUBLIC_SITE_URL should be set to your frontend URL");
362
+ }
363
+ consola.info("clientOnly mode enabled - server utilities (serverAuth, getUserSession, requireUserSession) are not available");
364
+ }
365
+ if (!clientOnly) {
366
+ const currentSecret = nuxt.options.runtimeConfig.betterAuthSecret;
367
+ nuxt.options.runtimeConfig.betterAuthSecret = currentSecret || process.env.BETTER_AUTH_SECRET || "";
368
+ const betterAuthSecret = nuxt.options.runtimeConfig.betterAuthSecret;
369
+ if (!nuxt.options.dev && !nuxt.options._prepare && !betterAuthSecret) {
370
+ throw new Error("[nuxt-better-auth] BETTER_AUTH_SECRET is required in production. Set BETTER_AUTH_SECRET or NUXT_BETTER_AUTH_SECRET environment variable.");
371
+ }
372
+ if (betterAuthSecret && betterAuthSecret.length < 32) {
373
+ throw new Error("[nuxt-better-auth] BETTER_AUTH_SECRET must be at least 32 characters for security");
374
+ }
375
+ nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth, {
376
+ secondaryStorage: secondaryStorageEnabled
377
+ });
378
+ }
193
379
  nuxt.options.alias["#nuxt-better-auth"] = resolver.resolve("./runtime/types/augment");
194
- nuxt.options.alias["#auth/server"] = serverConfigPath;
380
+ if (!clientOnly)
381
+ nuxt.options.alias["#auth/server"] = serverConfigPath;
195
382
  nuxt.options.alias["#auth/client"] = clientConfigPath;
196
- const secondaryStorageCode = secondaryStorageEnabled ? `import { kv } from 'hub:kv'
383
+ if (!clientOnly) {
384
+ if (secondaryStorageEnabled && !nuxt.options.alias["hub:kv"]) {
385
+ throw new Error("[nuxt-better-auth] hub:kv not found. Ensure @nuxthub/core is loaded before this module and hub.kv is enabled.");
386
+ }
387
+ const secondaryStorageCode = secondaryStorageEnabled ? `import { kv } from '@nuxthub/kv'
197
388
  export function createSecondaryStorage() {
198
389
  return {
199
390
  get: async (key) => kv.get(\`_auth:\${key}\`),
@@ -201,21 +392,47 @@ export function createSecondaryStorage() {
201
392
  delete: async (key) => kv.del(\`_auth:\${key}\`),
202
393
  }
203
394
  }` : `export function createSecondaryStorage() { return undefined }`;
204
- const secondaryStorageTemplate = addTemplate({ filename: "better-auth/secondary-storage.mjs", getContents: () => secondaryStorageCode, write: true });
205
- nuxt.options.alias["#auth/secondary-storage"] = secondaryStorageTemplate.dst;
206
- const hubDbPath = nuxt.options.alias["hub:db"];
207
- const databaseCode = hasHubDb && hubDbPath ? `import { db, schema } from '${hubDbPath}'
395
+ const secondaryStorageTemplate = addTemplate({ filename: "better-auth/secondary-storage.mjs", getContents: () => secondaryStorageCode, write: true });
396
+ nuxt.options.alias["#auth/secondary-storage"] = secondaryStorageTemplate.dst;
397
+ if (hasHubDb && !nuxt.options.alias["hub:db"]) {
398
+ throw new Error("[nuxt-better-auth] hub:db not found. Ensure @nuxthub/core is loaded before this module and hub.db is configured.");
399
+ }
400
+ const hubDialect = getHubDialect(hub) ?? "sqlite";
401
+ const usePlural = options.schema?.usePlural ?? false;
402
+ const camelCase = (options.schema?.casing ?? getHubCasing(hub)) !== "snake_case";
403
+ let databaseCode;
404
+ if (hasHubDb) {
405
+ databaseCode = `import { db, schema } from '@nuxthub/db'
208
406
  import { drizzleAdapter } from 'better-auth/adapters/drizzle'
209
- const rawDialect = '${hub?.db?.dialect ?? "sqlite"}'
407
+ const rawDialect = '${hubDialect}'
210
408
  const dialect = rawDialect === 'postgresql' ? 'pg' : rawDialect
211
- export function createDatabase() { return drizzleAdapter(db, { provider: dialect, schema }) }
212
- export { db }` : `export function createDatabase() { return undefined }
409
+ export function createDatabase() { return drizzleAdapter(db, { provider: dialect, schema, usePlural: ${usePlural}, camelCase: ${camelCase} }) }
410
+ export { db }`;
411
+ } else if (hasConvexDb) {
412
+ nuxt.options.runtimeConfig.betterAuth = defu(
413
+ nuxt.options.runtimeConfig.betterAuth || {},
414
+ { convexUrl }
415
+ );
416
+ databaseCode = `import { useRuntimeConfig } from '#imports'
417
+ import { createConvexHttpAdapter } from '@onmax/nuxt-better-auth/adapters/convex'
418
+ import { api } from '#convex/api'
419
+
420
+ export function createDatabase() {
421
+ const config = useRuntimeConfig()
422
+ const convexUrl = config.betterAuth?.convexUrl || config.public?.convex?.url
423
+ if (!convexUrl) throw new Error('[nuxt-better-auth] CONVEX_URL not configured')
424
+ return createConvexHttpAdapter({ url: convexUrl, api: api.auth })
425
+ }
213
426
  export const db = undefined`;
214
- const databaseTemplate = addTemplate({ filename: "better-auth/database.mjs", getContents: () => databaseCode, write: true });
215
- nuxt.options.alias["#auth/database"] = databaseTemplate.dst;
216
- addTypeTemplate({
217
- filename: "types/auth-secondary-storage.d.ts",
218
- getContents: () => `
427
+ } else {
428
+ databaseCode = `export function createDatabase() { return undefined }
429
+ export const db = undefined`;
430
+ }
431
+ const databaseTemplate = addTemplate({ filename: "better-auth/database.mjs", getContents: () => databaseCode, write: true });
432
+ nuxt.options.alias["#auth/database"] = databaseTemplate.dst;
433
+ addTypeTemplate({
434
+ filename: "types/auth-secondary-storage.d.ts",
435
+ getContents: () => `
219
436
  declare module '#auth/secondary-storage' {
220
437
  interface SecondaryStorage {
221
438
  get: (key: string) => Promise<string | null>
@@ -225,59 +442,85 @@ declare module '#auth/secondary-storage' {
225
442
  export function createSecondaryStorage(): SecondaryStorage | undefined
226
443
  }
227
444
  `
228
- });
229
- addTypeTemplate({
230
- filename: "types/auth-database.d.ts",
231
- getContents: () => `
445
+ }, { nitro: true, node: true });
446
+ addTypeTemplate({
447
+ filename: "types/auth-database.d.ts",
448
+ getContents: () => `
232
449
  declare module '#auth/database' {
233
450
  import type { drizzleAdapter } from 'better-auth/adapters/drizzle'
234
451
  export function createDatabase(): ReturnType<typeof drizzleAdapter> | undefined
235
452
  export const db: unknown
236
453
  }
237
454
  `
238
- });
239
- addTypeTemplate({
240
- filename: "types/nuxt-better-auth.d.ts",
241
- getContents: () => `
242
- export * from '${resolver.resolve("./runtime/types/augment")}'
243
- export type { AuthMeta, AuthMode, AuthRouteRules, UserMatch, RequireSessionOptions, Auth, InferUser, InferSession } from '${resolver.resolve("./runtime/types")}'
244
- `
245
- });
246
- addTypeTemplate({
247
- filename: "types/nuxt-better-auth-infer.d.ts",
248
- getContents: () => `
249
- import type { InferUser, InferSession } from 'better-auth'
455
+ }, { nitro: true, node: true });
456
+ addTypeTemplate({
457
+ filename: "types/nuxt-better-auth-infer.d.ts",
458
+ getContents: () => `
459
+ import type { InferUser, InferSession, InferPluginTypes } from 'better-auth'
250
460
  import type { RuntimeConfig } from 'nuxt/schema'
251
- import type configFn from '${serverConfigPath}'
461
+ import type createServerAuth from '${serverConfigPath}'
252
462
 
253
- type _Config = ReturnType<typeof configFn>
463
+ type _Config = ReturnType<typeof createServerAuth>
254
464
 
255
465
  declare module '#nuxt-better-auth' {
256
466
  interface AuthUser extends InferUser<_Config> {}
257
- interface AuthSession { session: InferSession<_Config>['session'], user: InferUser<_Config> }
467
+ interface AuthSession extends InferSession<_Config> {}
258
468
  interface ServerAuthContext {
259
469
  runtimeConfig: RuntimeConfig
260
- ${hasHubDb ? `db: typeof import('hub:db')['db']` : ""}
470
+ ${hasHubDb ? `db: typeof import('@nuxthub/db')['db']` : ""}
261
471
  }
472
+ type PluginTypes = InferPluginTypes<_Config>
473
+ }
474
+
475
+ // Augment the config module to use the extended ServerAuthContext
476
+ interface _AugmentedServerAuthContext {
477
+ runtimeConfig: RuntimeConfig
478
+ ${hasHubDb ? `db: typeof import('@nuxthub/db')['db']` : "db: unknown"}
479
+ }
480
+
481
+ declare module '@onmax/nuxt-better-auth/config' {
482
+ import type { BetterAuthOptions } from 'better-auth'
483
+ type ServerAuthConfig = Omit<BetterAuthOptions, 'database' | 'secret' | 'baseURL'>
484
+ export function defineServerAuth<T extends ServerAuthConfig>(config: T | ((ctx: _AugmentedServerAuthContext) => T)): (ctx: _AugmentedServerAuthContext) => T
262
485
  }
263
486
  `
264
- });
487
+ }, { nuxt: true, nitro: true, node: true });
488
+ addTypeTemplate({
489
+ filename: "types/nuxt-better-auth-nitro.d.ts",
490
+ getContents: () => `
491
+ declare module 'nitropack' {
492
+ interface NitroRouteRules {
493
+ auth?: import('${resolver.resolve("./runtime/types")}').AuthMeta
494
+ }
495
+ interface NitroRouteConfig {
496
+ auth?: import('${resolver.resolve("./runtime/types")}').AuthMeta
497
+ }
498
+ }
499
+ declare module 'nitropack/types' {
500
+ interface NitroRouteRules {
501
+ auth?: import('${resolver.resolve("./runtime/types")}').AuthMeta
502
+ }
503
+ interface NitroRouteConfig {
504
+ auth?: import('${resolver.resolve("./runtime/types")}').AuthMeta
505
+ }
506
+ }
507
+ export {}
508
+ `
509
+ }, { nuxt: true, nitro: true, node: true });
510
+ }
265
511
  addTypeTemplate({
266
- filename: "types/nuxt-better-auth-client.d.ts",
512
+ filename: "types/nuxt-better-auth.d.ts",
267
513
  getContents: () => `
268
- import type { createAppAuthClient } from '${clientConfigPath}'
269
- declare module '#nuxt-better-auth' {
270
- export type AppAuthClient = ReturnType<typeof createAppAuthClient>
271
- }
514
+ export * from '${resolver.resolve("./runtime/types/augment")}'
515
+ export type { AuthMeta, AuthMode, AuthRouteRules, UserMatch, RequireSessionOptions, Auth, InferUser, InferSession } from '${resolver.resolve("./runtime/types")}'
272
516
  `
273
517
  });
274
518
  addTypeTemplate({
275
- filename: "types/nuxt-better-auth-nitro.d.ts",
519
+ filename: "types/nuxt-better-auth-client.d.ts",
276
520
  getContents: () => `
277
- declare module 'nitropack/types' {
278
- interface NitroRouteRules {
279
- auth?: import('${resolver.resolve("./runtime/types")}').AuthMeta
280
- }
521
+ import type createAppAuthClient from '${clientConfigPath}'
522
+ declare module '#nuxt-better-auth' {
523
+ export type AppAuthClient = ReturnType<typeof createAppAuthClient>
281
524
  }
282
525
  `
283
526
  });
@@ -286,21 +529,31 @@ declare module 'nitropack/types' {
286
529
  await updateTemplates({ filter: (t) => t.filename.includes("nuxt-better-auth") });
287
530
  }
288
531
  });
289
- addServerImportsDir(resolver.resolve("./runtime/server/utils"));
290
- addServerScanDir(resolver.resolve("./runtime/server/middleware"));
291
- addServerHandler({ route: "/api/auth/**", handler: resolver.resolve("./runtime/server/api/auth/[...all]") });
532
+ if (!clientOnly) {
533
+ addServerImportsDir(resolver.resolve("./runtime/server/utils"));
534
+ addServerImports([{ name: "defineServerAuth", from: resolver.resolve("./runtime/config") }]);
535
+ addServerScanDir(resolver.resolve("./runtime/server/middleware"));
536
+ addServerHandler({ route: "/api/auth/**", handler: resolver.resolve("./runtime/server/api/auth/[...all]") });
537
+ }
292
538
  addImportsDir(resolver.resolve("./runtime/app/composables"));
293
539
  addImportsDir(resolver.resolve("./runtime/utils"));
294
- addPlugin({ src: resolver.resolve("./runtime/app/plugins/session.server"), mode: "server" });
540
+ if (!clientOnly)
541
+ addPlugin({ src: resolver.resolve("./runtime/app/plugins/session.server"), mode: "server" });
295
542
  addPlugin({ src: resolver.resolve("./runtime/app/plugins/session.client"), mode: "client" });
296
543
  addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
297
544
  nuxt.hook("app:resolve", (app) => {
298
545
  app.middleware.push({ name: "auth", path: resolver.resolve("./runtime/app/middleware/auth.global"), global: true });
299
546
  });
300
547
  if (hasHubDb) {
301
- await setupBetterAuthSchema(nuxt, serverConfigPath);
548
+ await setupBetterAuthSchema(nuxt, serverConfigPath, options);
302
549
  }
303
- if (nuxt.options.dev && process.env.NODE_ENV !== "production") {
550
+ if (hasConvexDb) {
551
+ await setupConvexAuthSchema(nuxt, serverConfigPath);
552
+ }
553
+ const isProduction = process.env.NODE_ENV === "production" || !nuxt.options.dev;
554
+ if (!isProduction && !clientOnly) {
555
+ if (!hasNuxtModule("@nuxt/ui"))
556
+ await installModule("@nuxt/ui");
304
557
  setupDevTools(nuxt);
305
558
  addServerHandler({ route: "/api/_better-auth/config", method: "get", handler: resolver.resolve("./runtime/server/api/_better-auth/config.get") });
306
559
  if (hasHubDb) {
@@ -333,37 +586,70 @@ declare module 'nitropack/types' {
333
586
  });
334
587
  }
335
588
  });
336
- async function setupBetterAuthSchema(nuxt, serverConfigPath) {
589
+ async function setupBetterAuthSchema(nuxt, serverConfigPath, options) {
337
590
  const hub = nuxt.options.hub;
338
- const dialect = typeof hub.db === "string" ? hub.db : hub.db?.dialect;
591
+ const dialect = getHubDialect(hub);
339
592
  if (!dialect || !["sqlite", "postgresql", "mysql"].includes(dialect)) {
340
593
  consola.warn(`Unsupported database dialect: ${dialect}`);
341
594
  return;
342
595
  }
596
+ const isProduction = !nuxt.options.dev;
343
597
  try {
344
598
  const configFile = `${serverConfigPath}.ts`;
345
- const userConfig = await loadUserAuthConfig(configFile);
599
+ const userConfig = await loadUserAuthConfig(configFile, isProduction);
346
600
  const extendedConfig = {};
347
601
  await nuxt.callHook("better-auth:config:extend", extendedConfig);
348
602
  const plugins = [...userConfig.plugins || [], ...extendedConfig.plugins || []];
349
- const { getAuthTables } = await import('better-auth/db');
350
- const tables = getAuthTables({ plugins });
351
- const schemaCode = generateDrizzleSchema(tables, dialect);
603
+ const authOptions = {
604
+ ...userConfig,
605
+ plugins,
606
+ secondaryStorage: options.secondaryStorage ? { get: async (_key) => null, set: async (_key, _value, _ttl) => {
607
+ }, delete: async (_key) => {
608
+ } } : void 0
609
+ };
610
+ const hubCasing = getHubCasing(hub);
611
+ const schemaOptions = { ...options.schema, useUuid: userConfig.advanced?.database?.generateId === "uuid", casing: options.schema?.casing ?? hubCasing };
612
+ const schemaCode = await generateDrizzleSchema(authOptions, dialect, schemaOptions);
352
613
  const schemaDir = join(nuxt.options.buildDir, "better-auth");
353
614
  const schemaPath = join(schemaDir, `schema.${dialect}.ts`);
354
615
  await mkdir(schemaDir, { recursive: true });
355
616
  await writeFile(schemaPath, schemaCode);
356
617
  addTemplate({ filename: `better-auth/schema.${dialect}.ts`, getContents: () => schemaCode, write: true });
357
- consola.info(`Generated ${dialect} schema with ${Object.keys(tables).length} tables`);
618
+ consola.info(`Generated ${dialect} schema`);
358
619
  } catch (error) {
620
+ if (isProduction) {
621
+ throw error;
622
+ }
359
623
  consola.error("Failed to generate schema:", error);
360
624
  }
361
- nuxt.hook("hub:db:schema:extend", ({ paths, dialect: hookDialect }) => {
625
+ const nuxtWithHubHooks = nuxt;
626
+ nuxtWithHubHooks.hook("hub:db:schema:extend", ({ paths, dialect: hookDialect }) => {
362
627
  const schemaPath = join(nuxt.options.buildDir, "better-auth", `schema.${hookDialect}.ts`);
363
628
  if (existsSync(schemaPath)) {
364
- paths.push(schemaPath);
629
+ paths.unshift(schemaPath);
365
630
  }
366
631
  });
367
632
  }
633
+ async function setupConvexAuthSchema(nuxt, serverConfigPath) {
634
+ const isProduction = !nuxt.options.dev;
635
+ try {
636
+ const configFile = `${serverConfigPath}.ts`;
637
+ const userConfig = await loadUserAuthConfig(configFile, isProduction);
638
+ const authOptions = { ...userConfig };
639
+ const schemaCode = await generateConvexSchema(authOptions);
640
+ const schemaDir = join(nuxt.options.buildDir, "better-auth");
641
+ const schemaPath = join(schemaDir, "auth-tables.convex.ts");
642
+ await mkdir(schemaDir, { recursive: true });
643
+ await writeFile(schemaPath, schemaCode);
644
+ addTemplate({ filename: "better-auth/auth-tables.convex.ts", getContents: () => schemaCode, write: true });
645
+ nuxt.options.alias["#auth/convex-schema"] = schemaPath;
646
+ consola.info("Generated Convex auth schema at .nuxt/better-auth/auth-tables.convex.ts");
647
+ } catch (error) {
648
+ if (isProduction) {
649
+ throw error;
650
+ }
651
+ consola.error("Failed to generate Convex schema:", error);
652
+ }
653
+ }
368
654
 
369
655
  export { module$1 as default };