@nuxtjs/better-auth 0.2.2 → 0.2.4

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.d.mts CHANGED
@@ -2,11 +2,15 @@ import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { Nuxt } from '@nuxt/schema';
3
3
  import { BetterAuthModuleOptions } from '../dist/runtime/config.js';
4
4
  export { BetterAuthModuleOptions, defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
5
- import { BetterAuthOptions } from 'better-auth';
5
+ import { BetterAuthPlugin } from 'better-auth';
6
6
  export { AppSession, Auth, AuthActionError, AuthMeta, AuthMode, AuthRouteRules, AuthSession, AuthSocialProviderId, AuthUser, InferSession, InferUser, RequireSessionOptions, ServerAuthContext, UserMatch } from '../dist/runtime/types.js';
7
7
 
8
8
  type DbDialect = 'sqlite' | 'postgresql' | 'mysql';
9
9
 
10
+ interface BetterAuthPluginSources {
11
+ server?: string[];
12
+ client?: string[];
13
+ }
10
14
  interface BetterAuthDatabaseProviderBuildContext {
11
15
  hubDialect: DbDialect;
12
16
  usePlural: boolean;
@@ -28,12 +32,17 @@ interface BetterAuthDatabaseProviderDefinition {
28
32
  }
29
33
  declare module '@nuxt/schema' {
30
34
  interface NuxtHooks {
35
+ /** Register absolute plugin module paths during module setup. Sources are additive and are not deduplicated. */
36
+ 'better-auth:plugins:extend': (sources: BetterAuthPluginSources) => void | Promise<void>;
31
37
  /**
32
- * Extend better-auth config with additional plugins or options.
33
- * Called after user's auth.config.ts is loaded.
34
- * @param config - Partial config to merge into the auth options
38
+ * Add plugin schemas to generated Better Auth database tables.
39
+ * This build-time hook does not install plugins in the runtime auth instance.
40
+ * @param config - Plugins to include during schema generation
41
+ * @param config.plugins - Better Auth plugins whose schemas should be generated
35
42
  */
36
- 'better-auth:config:extend': (config: Partial<BetterAuthOptions>) => void | Promise<void>;
43
+ 'better-auth:config:extend': (config: {
44
+ plugins?: BetterAuthPlugin[];
45
+ }) => void | Promise<void>;
37
46
  /**
38
47
  * Register or override Better Auth database providers.
39
48
  * Providers are auto-selected via `isEnabled` + `priority`.
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/better-auth",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "configKey": "auth",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.0.0"
package/dist/module.mjs CHANGED
@@ -2,7 +2,7 @@ import { existsSync, statSync, writeFileSync, readFileSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { getLayerDirectories, updateTemplates, addServerImportsDir, addServerImports, addServerScanDir, addServerHandler, addImportsDir, addPlugin, addComponentsDir, extendPages, addTemplate, hasNuxtModule, addTypeTemplate, defineNuxtModule, createResolver } from '@nuxt/kit';
4
4
  import { consola as consola$1 } from 'consola';
5
- import { isAbsolute, join, relative, dirname } from 'pathe';
5
+ import { isAbsolute, join, resolve, relative, dirname } from 'pathe';
6
6
  import { defu } from 'defu';
7
7
  import { toRouteMatcher, createRouter } from 'radix3';
8
8
  import { generateDrizzleSchema as generateDrizzleSchema$1 } from 'auth/api';
@@ -10,10 +10,10 @@ import { randomBytes } from 'node:crypto';
10
10
  import { isCI, isTest } from 'std-env';
11
11
  export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
12
12
 
13
- const version = "0.2.2";
13
+ const version = "0.2.4";
14
14
 
15
15
  const CONFIG_EXTENSIONS = [".ts", ".js"];
16
- const CONFIG_EXTENSION_RE = /\.(?:ts|js)$/;
16
+ const CONFIG_EXTENSION_RE$1 = /\.(?:ts|js)$/;
17
17
  const DEFAULT_CONFIG_FILES = {
18
18
  server: "server/auth.config",
19
19
  client: "app/auth.config"
@@ -22,8 +22,12 @@ const OPTION_KEY_BY_KIND = {
22
22
  server: "serverConfig",
23
23
  client: "clientConfig"
24
24
  };
25
+ const PLUGIN_OPTION_KEY_BY_KIND = {
26
+ server: "serverPluginSources",
27
+ client: "clientPluginSources"
28
+ };
25
29
  function stripConfigExtension(path) {
26
- return path.replace(CONFIG_EXTENSION_RE, "");
30
+ return path.replace(CONFIG_EXTENSION_RE$1, "");
27
31
  }
28
32
  function defaultConfigExists(path) {
29
33
  return CONFIG_EXTENSIONS.some((ext) => existsSync(`${path}${ext}`));
@@ -125,6 +129,18 @@ function resolveAuthConfigDescriptors(nuxt, files = {}, dependencies = {}) {
125
129
  client: resolveAuthConfigDescriptor(nuxt, "client", files.client, dependencies)
126
130
  };
127
131
  }
132
+ function resolveAuthPluginSources(nuxt) {
133
+ const resolved = { server: [], client: [] };
134
+ for (const { directory, layer } of getLayerDirectoriesWithConfigs(nuxt)) {
135
+ for (const kind of ["server", "client"]) {
136
+ const sources = layer?.config?.auth?.[PLUGIN_OPTION_KEY_BY_KIND[kind]];
137
+ if (!Array.isArray(sources))
138
+ continue;
139
+ resolved[kind].push(...sources.map((source) => isAbsolute(source) ? source : resolve(directory.root, source)));
140
+ }
141
+ }
142
+ return resolved;
143
+ }
128
144
 
129
145
  function resolveNitroCompatibilityImports(nuxtVersion) {
130
146
  const major = Number.parseInt(nuxtVersion, 10);
@@ -311,6 +327,8 @@ function loadLocalEnv(rootDir) {
311
327
  return;
312
328
  process.loadEnvFile(envPath);
313
329
  }
330
+ const NO_DEFAULT_EXPORT_MESSAGE = "[@nuxtjs/better-auth] auth.config.ts does not export default. Expected: export default defineServerAuth(...)";
331
+ const SCHEMA_NOT_REGENERATED_MESSAGE = "The schema was not regenerated and any existing generated schema file was left unchanged.";
314
332
  async function loadUserAuthConfig(configPath, throwOnError = false, alias, runtimeConfig = {}, rootDir) {
315
333
  const { createJiti } = await import('jiti');
316
334
  const { defineServerAuth: runtimeDefineServerAuth } = await import('../dist/runtime/config.js');
@@ -331,17 +349,18 @@ async function loadUserAuthConfig(configPath, throwOnError = false, alias, runti
331
349
  if (typeof configFn === "function") {
332
350
  return configFn({ runtimeConfig, db: null });
333
351
  }
334
- consola$1.warn("[@nuxtjs/better-auth] auth.config.ts does not export default. Expected: export default defineServerAuth(...)");
335
352
  if (throwOnError) {
353
+ consola$1.warn(NO_DEFAULT_EXPORT_MESSAGE);
336
354
  throw new Error("auth.config.ts must export default defineServerAuth(...)");
337
355
  }
338
- return {};
356
+ consola$1.error(`${NO_DEFAULT_EXPORT_MESSAGE}. ${SCHEMA_NOT_REGENERATED_MESSAGE}`);
357
+ return null;
339
358
  } catch (error) {
340
359
  if (throwOnError) {
341
360
  throw new Error(`Failed to load auth config: ${error instanceof Error ? error.message : error}`);
342
361
  }
343
- consola$1.error("[@nuxtjs/better-auth] Failed to load auth config for schema generation. Schema may be incomplete:", error);
344
- return {};
362
+ consola$1.error(`[@nuxtjs/better-auth] Failed to load auth config for schema generation. ${SCHEMA_NOT_REGENERATED_MESSAGE}`, error);
363
+ return null;
345
364
  } finally {
346
365
  const sharedDefineServerAuth = schemaGlobals.__nuxtBetterAuthDefineServerAuth;
347
366
  if (sharedDefineServerAuth) {
@@ -372,6 +391,7 @@ function getHubCasing(hub) {
372
391
  }
373
392
 
374
393
  const NODE_MODULES_SEGMENT_RE = /[\\/]/;
394
+ const CONFIG_EXTENSION_RE = /\.[cm]?[jt]s$/;
375
395
  function resolveSchemaSecondaryStorageInjection(hubSecondaryStorage, userHasSecondaryStorage, isProduction) {
376
396
  if (hubSecondaryStorage === true)
377
397
  return { inject: false };
@@ -399,13 +419,26 @@ function resolveHubSchemaPath(buildDir, rootDir, dialect, exists = existsSync) {
399
419
  return mjsPath;
400
420
  return null;
401
421
  }
422
+ function registerNuxtHubSchemaHook(nuxt, finishSetup) {
423
+ const nuxtWithHubHooks = nuxt;
424
+ nuxtWithHubHooks.hook("hub:db:schema:extend", async ({ paths, dialect }) => {
425
+ const hasHubSchema = await finishSetup();
426
+ if (!hasHubSchema)
427
+ return;
428
+ const schemaPath = resolveHubSchemaPath(nuxt.options.buildDir, nuxt.options.rootDir, dialect);
429
+ if (schemaPath && !paths.includes(schemaPath))
430
+ paths.unshift(schemaPath);
431
+ });
432
+ }
402
433
  async function loadAuthOptions(context) {
403
434
  const isProduction = !context.nuxt.options.dev;
404
- const configFile = `${context.serverConfigPath}.ts`;
435
+ const configFile = CONFIG_EXTENSION_RE.test(context.serverConfigPath) ? context.serverConfigPath : `${context.serverConfigPath}.ts`;
405
436
  const alias = Object.fromEntries(
406
437
  Object.entries(context.nuxt.options.alias).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value])
407
438
  );
408
439
  const userConfig = await loadUserAuthConfig(configFile, isProduction, alias, context.nuxt.options.runtimeConfig, context.nuxt.options.rootDir);
440
+ if (!userConfig)
441
+ return null;
409
442
  const extendedConfig = {};
410
443
  await context.nuxt.callHook("better-auth:config:extend", extendedConfig);
411
444
  const plugins = [...userConfig.plugins || [], ...extendedConfig.plugins || []];
@@ -420,7 +453,10 @@ async function setupBetterAuthSchema(nuxt, serverConfigPath, options, consola, h
420
453
  }
421
454
  const context = { nuxt, serverConfigPath };
422
455
  try {
423
- const { userConfig, plugins } = await loadAuthOptions(context);
456
+ const authConfig = await loadAuthOptions(context);
457
+ if (!authConfig)
458
+ return;
459
+ const { userConfig, plugins } = authConfig;
424
460
  const userHasSecondaryStorage = userConfig.secondaryStorage != null;
425
461
  const secondaryStorageResolution = resolveSchemaSecondaryStorageInjection(hubSecondaryStorage, userHasSecondaryStorage, !nuxt.options.dev);
426
462
  if (secondaryStorageResolution.error)
@@ -458,12 +494,6 @@ async function setupBetterAuthSchema(nuxt, serverConfigPath, options, consola, h
458
494
  addTemplate({ filename: `better-auth/schema.${dialect}.ts`, getContents: () => schemaCode, write: true });
459
495
  addTemplate({ filename: `better-auth/schema.${dialect}.mjs`, getContents: () => schemaCode, write: true });
460
496
  consola.info(`Generated ${dialect} schema (.ts + .mjs)`);
461
- const nuxtWithHubHooks = nuxt;
462
- nuxtWithHubHooks.hook("hub:db:schema:extend", ({ paths, dialect: hookDialect }) => {
463
- const schemaPath = resolveHubSchemaPath(nuxt.options.buildDir, nuxt.options.rootDir, hookDialect);
464
- if (schemaPath)
465
- paths.unshift(schemaPath);
466
- });
467
497
  } catch (error) {
468
498
  const isProduction = !nuxt.options.dev;
469
499
  if (isProduction)
@@ -563,21 +593,24 @@ function resolveDatabaseProvider(input) {
563
593
  }
564
594
 
565
595
  function resolveSecondaryStorage(input) {
566
- const { options, clientOnly, hasNuxtHub, hub } = input;
596
+ const { options, clientOnly, consola } = input;
567
597
  const opt = options.hubSecondaryStorage ?? false;
568
- const useHubKV = opt === true;
569
- const secondaryStorageEnabled = opt === true || opt === "custom";
598
+ if (opt === true) {
599
+ if (clientOnly)
600
+ consola.warn("[nuxt-better-auth] hubSecondaryStorage: true is ignored in clientOnly mode because this module does not run the auth server.");
601
+ else
602
+ consola.warn('[nuxt-better-auth] hubSecondaryStorage: true cannot use NuxtHub KV with Better Auth 1.7 because NuxtHub KV lacks atomic getAndDelete and increment operations. The module will ignore this option and continue without injecting secondary storage. Without a custom secondary store, sessions use the configured database when one exists and rate limiting defaults to process-local memory. Configure rateLimit.storage as "database" or provide rateLimit.customStorage for shared limits. Set auth.hubSecondaryStorage to false to silence this warning, or use "custom" with an atomic secondaryStorage in defineServerAuth(). Track NuxtHub support: https://github.com/nuxt-hub/core/pull/927');
603
+ return { hubSecondaryStorage: false, secondaryStorageEnabled: false };
604
+ }
605
+ const secondaryStorageEnabled = opt === "custom";
570
606
  if (secondaryStorageEnabled && clientOnly) {
571
607
  throw new Error("[nuxt-better-auth] hubSecondaryStorage is not available in clientOnly mode. Either disable clientOnly or remove auth.hubSecondaryStorage.");
572
608
  }
573
- if (useHubKV && (!hasNuxtHub || !hub?.kv)) {
574
- throw new Error("[nuxt-better-auth] hubSecondaryStorage: true requires @nuxthub/core with hub.kv: true. Either add hub.kv: true to your nuxt.config or remove auth.hubSecondaryStorage.");
575
- }
576
- return { useHubKV, secondaryStorageEnabled };
609
+ return { hubSecondaryStorage: opt, secondaryStorageEnabled };
577
610
  }
578
611
  function setupRuntimeConfig(input) {
579
612
  const { nuxt, options, clientOnly, databaseProvider, consola } = input;
580
- const { useHubKV, secondaryStorageEnabled } = resolveSecondaryStorage(input);
613
+ const { hubSecondaryStorage, secondaryStorageEnabled } = resolveSecondaryStorage(input);
581
614
  nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
582
615
  const configuredSiteUrl = nuxt.options.runtimeConfig.public.siteUrl;
583
616
  nuxt.options.runtimeConfig.public.siteUrl = configuredSiteUrl || process.env.NUXT_PUBLIC_SITE_URL || "";
@@ -599,27 +632,44 @@ function setupRuntimeConfig(input) {
599
632
  if (!siteUrl)
600
633
  consola.warn("clientOnly mode: set runtimeConfig.public.siteUrl (or NUXT_PUBLIC_SITE_URL) to your frontend URL");
601
634
  consola.info("clientOnly mode enabled - server utilities (serverAuth, getRequestSession, getUserSession, requireUserSession) are not available");
602
- return { useHubKV, secondaryStorageEnabled };
635
+ return { secondaryStorageEnabled };
603
636
  }
604
637
  const currentSecret = nuxt.options.runtimeConfig.betterAuthSecret;
605
638
  nuxt.options.runtimeConfig.betterAuthSecret = currentSecret || process.env.NUXT_BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET || "";
606
- nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth, {
607
- hubSecondaryStorage: options.hubSecondaryStorage ?? false
639
+ const authRuntimeConfig = defu(nuxt.options.runtimeConfig.auth, {
640
+ hubSecondaryStorage
608
641
  });
609
- return { useHubKV, secondaryStorageEnabled };
642
+ authRuntimeConfig.hubSecondaryStorage = hubSecondaryStorage;
643
+ nuxt.options.runtimeConfig.auth = authRuntimeConfig;
644
+ return { secondaryStorageEnabled };
610
645
  }
611
646
 
612
- function buildSecondaryStorageCode(useHubKV) {
613
- if (!useHubKV)
614
- return "export function createSecondaryStorage() { return undefined }";
615
- return `import { kv } from '@nuxthub/kv'
616
- export function createSecondaryStorage() {
647
+ function buildImports(sources) {
617
648
  return {
618
- get: async (key) => kv.get(\`_auth:\${key}\`),
619
- set: async (key, value, ttl) => kv.set(\`_auth:\${key}\`, value, { ttl }),
620
- delete: async (key) => kv.del(\`_auth:\${key}\`),
621
- }
622
- }`;
649
+ imports: sources.map((source, index) => `import plugin${index} from ${JSON.stringify(source)}`).join("\n"),
650
+ names: sources.map((_, index) => `plugin${index}`).join(", ")
651
+ };
652
+ }
653
+ function buildExtendedServerAuthCode(configPath, sources) {
654
+ const plugins = buildImports(sources);
655
+ return `import createAppAuth from ${JSON.stringify(configPath)}
656
+ import { extendServerAuth } from '@nuxtjs/better-auth/config'
657
+ ${plugins.imports}
658
+
659
+ export default extendServerAuth(createAppAuth, [${plugins.names}])
660
+ `;
661
+ }
662
+ function buildExtendedClientAuthCode(configPath, sources) {
663
+ const plugins = buildImports(sources);
664
+ return `import createAppAuthClient from ${JSON.stringify(configPath)}
665
+ import { extendClientAuth } from '@nuxtjs/better-auth/config'
666
+ ${plugins.imports}
667
+
668
+ export default extendClientAuth(createAppAuthClient, [${plugins.names}])
669
+ `;
670
+ }
671
+ function buildSecondaryStorageCode() {
672
+ return "export function createSecondaryStorage() { return undefined }";
623
673
  }
624
674
  function buildDatabaseCode(input) {
625
675
  if (input.provider === "nuxthub") {
@@ -813,6 +863,9 @@ async function resolveAuthModuleSetup(input, dependencies = {}) {
813
863
  configExists: dependencies.configExists
814
864
  });
815
865
  assertConfigPresence(configs, clientOnly);
866
+ const pluginSources = resolveAuthPluginSources(nuxt);
867
+ pluginSources.server.push(...input.registeredPluginSources?.server || []);
868
+ pluginSources.client.push(...input.registeredPluginSources?.client || []);
816
869
  const aliases = {
817
870
  "#auth/server": clientOnly ? void 0 : configs.server.path,
818
871
  "#auth/client": configs.client.path
@@ -850,13 +903,8 @@ async function resolveAuthModuleSetup(input, dependencies = {}) {
850
903
  options,
851
904
  clientOnly,
852
905
  databaseProvider: providerId,
853
- hasNuxtHub,
854
- hub,
855
906
  consola
856
907
  });
857
- if (runtime.useHubKV && !nuxt.options.alias["hub:kv"]) {
858
- throw new Error("[nuxt-better-auth] hub:kv not found. Ensure @nuxthub/core is loaded before this module and hub.kv is enabled.");
859
- }
860
908
  const hasHubDb = providerId === "nuxthub";
861
909
  if (hasHubDb && !nuxt.options.alias["hub:db"]) {
862
910
  throw new Error("[nuxt-better-auth] hub:db not found. Ensure @nuxthub/core is loaded before this module and hub.db is configured.");
@@ -865,6 +913,7 @@ async function resolveAuthModuleSetup(input, dependencies = {}) {
865
913
  clientOnly,
866
914
  configs,
867
915
  aliases,
916
+ pluginSources,
868
917
  hub: {
869
918
  hasNuxtHub,
870
919
  options: hub,
@@ -912,12 +961,7 @@ function registerServerTypeTemplates(input) {
912
961
  filename: "types/auth-secondary-storage.d.ts",
913
962
  getContents: () => `
914
963
  declare module '#auth/secondary-storage' {
915
- interface SecondaryStorage {
916
- get: (key: string) => Promise<string | null>
917
- set: (key: string, value: unknown, ttl?: number) => Promise<void>
918
- delete: (key: string) => Promise<void>
919
- }
920
- export function createSecondaryStorage(): SecondaryStorage | undefined
964
+ export function createSecondaryStorage(): undefined
921
965
  }
922
966
  `
923
967
  }, { nitro: true });
@@ -1371,94 +1415,132 @@ const module$1 = defineNuxtModule({
1371
1415
  process.env.NUXT_BETTER_AUTH_SECRET = generatedSecret;
1372
1416
  await createDefaultAuthConfigFiles(nuxt);
1373
1417
  },
1374
- async setup(options, nuxt) {
1375
- const resolver = createResolver(import.meta.url);
1376
- const nitroImports = resolveNitroCompatibilityImports(nuxt._version);
1377
- nuxt.options.alias["#better-auth/nitro-compat"] = resolver.resolve(`./runtime/server/internal/${nitroImports.runtime}`);
1378
- const setup = await resolveAuthModuleSetup({
1379
- nuxt,
1380
- options,
1381
- runtimeTypesAugmentPath: resolver.resolve("./runtime/types/augment"),
1382
- consola
1383
- });
1384
- if (setup.aliases["#auth/server"])
1385
- nuxt.options.alias["#auth/server"] = setup.aliases["#auth/server"];
1386
- nuxt.options.alias["#auth/client"] = setup.aliases["#auth/client"];
1387
- if (!setup.clientOnly) {
1388
- const secondaryStorageTemplate = addTemplate({
1389
- filename: "better-auth/secondary-storage.mjs",
1390
- getContents: () => buildSecondaryStorageCode(setup.runtime.useHubKV),
1391
- write: true
1392
- });
1393
- nuxt.options.alias["#auth/secondary-storage"] = secondaryStorageTemplate.dst;
1394
- const databaseTemplate = addTemplate({
1395
- filename: "better-auth/database.mjs",
1396
- getContents: () => setup.database.providerDefinition.buildDatabaseCode(setup.database.buildContext),
1397
- write: true
1418
+ setup(options, nuxt) {
1419
+ const finishSetup = async () => {
1420
+ const resolver = createResolver(import.meta.url);
1421
+ const nitroImports = resolveNitroCompatibilityImports(nuxt._version);
1422
+ nuxt.options.alias["#better-auth/nitro-compat"] = resolver.resolve(`./runtime/server/internal/${nitroImports.runtime}`);
1423
+ const registeredPluginSources = {};
1424
+ await nuxt.callHook("better-auth:plugins:extend", registeredPluginSources);
1425
+ for (const source of [...registeredPluginSources.server || [], ...registeredPluginSources.client || []]) {
1426
+ if (!isAbsolute(source))
1427
+ throw new Error(`[nuxt-better-auth] Modules must register absolute plugin source paths. Received: ${source}`);
1428
+ }
1429
+ const setup = await resolveAuthModuleSetup({
1430
+ nuxt,
1431
+ options,
1432
+ runtimeTypesAugmentPath: resolver.resolve("./runtime/types/augment"),
1433
+ consola,
1434
+ registeredPluginSources
1398
1435
  });
1399
- nuxt.options.alias["#auth/database"] = databaseTemplate.dst;
1400
- const schemaTemplate = addTemplate({
1401
- filename: "better-auth/schema.mjs",
1402
- getContents: () => buildSchemaExportCode(setup.database.hasHubDb, setup.database.buildContext?.hubDialect ?? "sqlite"),
1436
+ let serverConfigPath = setup.aliases["#auth/server"];
1437
+ let clientConfigPath = setup.aliases["#auth/client"];
1438
+ if (serverConfigPath && setup.pluginSources.server.length) {
1439
+ const serverConfigCode = buildExtendedServerAuthCode(setup.configs.server.path, setup.pluginSources.server);
1440
+ serverConfigPath = addTemplate({
1441
+ filename: "better-auth/server.config.ts",
1442
+ getContents: () => serverConfigCode,
1443
+ write: true
1444
+ }).dst;
1445
+ await mkdir(dirname(serverConfigPath), { recursive: true });
1446
+ await writeFile(serverConfigPath, serverConfigCode);
1447
+ }
1448
+ if (setup.pluginSources.client.length) {
1449
+ clientConfigPath = addTemplate({
1450
+ filename: "better-auth/client.config.ts",
1451
+ getContents: () => buildExtendedClientAuthCode(setup.configs.client.path, setup.pluginSources.client),
1452
+ write: true
1453
+ }).dst;
1454
+ }
1455
+ if (serverConfigPath)
1456
+ nuxt.options.alias["#auth/server"] = serverConfigPath;
1457
+ nuxt.options.alias["#auth/client"] = clientConfigPath;
1458
+ if (!setup.clientOnly) {
1459
+ const secondaryStorageTemplate = addTemplate({
1460
+ filename: "better-auth/secondary-storage.mjs",
1461
+ getContents: () => buildSecondaryStorageCode(),
1462
+ write: true
1463
+ });
1464
+ nuxt.options.alias["#auth/secondary-storage"] = secondaryStorageTemplate.dst;
1465
+ const databaseTemplate = addTemplate({
1466
+ filename: "better-auth/database.mjs",
1467
+ getContents: () => setup.database.providerDefinition.buildDatabaseCode(setup.database.buildContext),
1468
+ write: true
1469
+ });
1470
+ nuxt.options.alias["#auth/database"] = databaseTemplate.dst;
1471
+ const schemaTemplate = addTemplate({
1472
+ filename: "better-auth/schema.mjs",
1473
+ getContents: () => buildSchemaExportCode(setup.database.hasHubDb, setup.database.buildContext?.hubDialect ?? "sqlite"),
1474
+ write: true
1475
+ });
1476
+ nuxt.options.alias["#auth/schema"] = schemaTemplate.dst;
1477
+ if (setup.schemaGeneration)
1478
+ await ensureSchemaBootstrap(schemaTemplate.dst, setup.database.buildContext?.hubDialect ?? "sqlite");
1479
+ }
1480
+ if (setup.prepareTypes) {
1481
+ registerPrepareTypesHook({
1482
+ nuxt,
1483
+ serverDir: setup.prepareTypes.serverDir,
1484
+ hasHubDb: setup.prepareTypes.hasHubDb
1485
+ });
1486
+ }
1487
+ if (setup.database.providerDefinition) {
1488
+ const setupCtx = {
1489
+ nuxt,
1490
+ options,
1491
+ clientOnly: setup.clientOnly
1492
+ };
1493
+ await setup.database.providerDefinition.setup?.(setupCtx);
1494
+ }
1495
+ const authRouteRulesTemplate = addTemplate({
1496
+ filename: "better-auth/route-rules.mjs",
1497
+ getContents: () => buildAuthRouteRulesCode(collectAuthRouteRules(nuxt)),
1403
1498
  write: true
1404
1499
  });
1405
- nuxt.options.alias["#auth/schema"] = schemaTemplate.dst;
1406
- if (setup.schemaGeneration)
1407
- await ensureSchemaBootstrap(schemaTemplate.dst, setup.database.buildContext?.hubDialect ?? "sqlite");
1408
- }
1409
- if (setup.prepareTypes) {
1410
- registerPrepareTypesHook({
1411
- nuxt,
1412
- serverDir: setup.prepareTypes.serverDir,
1413
- hasHubDb: setup.prepareTypes.hasHubDb
1414
- });
1415
- }
1416
- if (setup.database.providerDefinition) {
1417
- const setupCtx = {
1418
- nuxt,
1419
- options,
1420
- clientOnly: setup.clientOnly
1421
- };
1422
- await setup.database.providerDefinition.setup?.(setupCtx);
1423
- }
1424
- const authRouteRulesTemplate = addTemplate({
1425
- filename: "better-auth/route-rules.mjs",
1426
- getContents: () => buildAuthRouteRulesCode(collectAuthRouteRules(nuxt)),
1427
- write: true
1428
- });
1429
- nuxt.options.alias["#auth/route-rules"] = authRouteRulesTemplate.dst;
1430
- if (setup.serverTypes) {
1431
- registerServerTypeTemplates({
1432
- serverConfigPath: setup.serverTypes.serverConfigPath,
1433
- hasHubDb: setup.serverTypes.hasHubDb,
1500
+ nuxt.options.alias["#auth/route-rules"] = authRouteRulesTemplate.dst;
1501
+ if (setup.serverTypes) {
1502
+ registerServerTypeTemplates({
1503
+ serverConfigPath,
1504
+ hasHubDb: setup.serverTypes.hasHubDb,
1505
+ runtimeTypesPath: resolver.resolve("./runtime/types"),
1506
+ sharedServerConfigSafe: [setup.configs.server.path, ...setup.pluginSources.server].every(isServerConfigSharedTypeSafe),
1507
+ h3TypesPath: nitroImports.h3,
1508
+ nitroTypesPath: nitroImports.types
1509
+ });
1510
+ }
1511
+ if (setup.schemaGeneration) {
1512
+ if (setup.schemaGeneration.externalizeNuxtHubDatabase)
1513
+ registerNuxtHubDatabaseExternalHook(nuxt);
1514
+ await setupBetterAuthSchema(
1515
+ nuxt,
1516
+ serverConfigPath,
1517
+ options,
1518
+ consola,
1519
+ setup.schemaGeneration.hubSecondaryStorage
1520
+ );
1521
+ }
1522
+ registerSharedTypeTemplates({
1523
+ runtimeTypesAugmentPath: setup.sharedTypes.runtimeTypesAugmentPath,
1434
1524
  runtimeTypesPath: resolver.resolve("./runtime/types"),
1435
- sharedServerConfigSafe: isServerConfigSharedTypeSafe(setup.serverTypes.serverConfigPath),
1436
- h3TypesPath: nitroImports.h3,
1437
- nitroTypesPath: nitroImports.types
1525
+ clientConfigPath,
1526
+ h3TypesPath: nitroImports.h3
1438
1527
  });
1439
- }
1440
- if (setup.schemaGeneration) {
1441
- if (setup.schemaGeneration.externalizeNuxtHubDatabase)
1442
- registerNuxtHubDatabaseExternalHook(nuxt);
1443
- await setupBetterAuthSchema(
1444
- nuxt,
1445
- setup.schemaGeneration.serverConfigPath,
1446
- options,
1447
- consola,
1448
- setup.schemaGeneration.hubSecondaryStorage
1449
- );
1450
- }
1451
- registerSharedTypeTemplates({
1452
- runtimeTypesAugmentPath: setup.sharedTypes.runtimeTypesAugmentPath,
1453
- runtimeTypesPath: resolver.resolve("./runtime/types"),
1454
- clientConfigPath: setup.sharedTypes.clientConfigPath,
1455
- h3TypesPath: nitroImports.h3
1528
+ registerTemplateHmrHook(nuxt);
1529
+ registerServerRuntime({ clientOnly: setup.clientOnly, resolve: resolver.resolve });
1530
+ registerAuthMiddlewareHook(nuxt, resolver.resolve);
1531
+ await registerDevtools({ nuxt, clientOnly: setup.clientOnly, hasHubDb: setup.database.hasHubDb, resolve: resolver.resolve });
1532
+ registerRouteRulesMetaHook(nuxt);
1533
+ return Boolean(setup.schemaGeneration);
1534
+ };
1535
+ let setupPromise;
1536
+ const finishSetupOnce = () => {
1537
+ setupPromise ||= finishSetup();
1538
+ return setupPromise;
1539
+ };
1540
+ registerNuxtHubSchemaHook(nuxt, finishSetupOnce);
1541
+ nuxt.hook("modules:done", async () => {
1542
+ await finishSetupOnce();
1456
1543
  });
1457
- registerTemplateHmrHook(nuxt);
1458
- registerServerRuntime({ clientOnly: setup.clientOnly, resolve: resolver.resolve });
1459
- registerAuthMiddlewareHook(nuxt, resolver.resolve);
1460
- await registerDevtools({ nuxt, clientOnly: setup.clientOnly, hasHubDb: setup.database.hasHubDb, resolve: resolver.resolve });
1461
- registerRouteRulesMetaHook(nuxt);
1462
1544
  }
1463
1545
  });
1464
1546
 
@@ -1,5 +1,5 @@
1
1
  import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth';
2
- import type { BetterAuthClientOptions } from 'better-auth/client';
2
+ import type { BetterAuthClientOptions, BetterAuthClientPlugin } from 'better-auth/client';
3
3
  import type { ServerAuthContext as BaseServerAuthContext } from './types/augment.js';
4
4
  import { createAuthClient } from 'better-auth/vue';
5
5
  export interface ServerAuthContextExtension {
@@ -17,6 +17,23 @@ export type ClientAuthConfig = Omit<BetterAuthClientOptions, 'baseURL'> & {
17
17
  };
18
18
  export type ServerAuthConfigFn = (ctx: ServerAuthContext) => ServerAuthConfig;
19
19
  export type ClientAuthConfigFn = (ctx: ClientAuthContext) => ClientAuthConfig;
20
+ type ServerPlugins<T> = T extends {
21
+ plugins: infer P extends readonly BetterAuthPlugin[];
22
+ } ? P : [];
23
+ type ClientPlugins<T> = T extends {
24
+ plugins: infer P extends BetterAuthClientPlugin[];
25
+ } ? P : [];
26
+ type ExtendedServerAuthConfig<T, P extends readonly BetterAuthPlugin[]> = Omit<T, 'plugins'> & {
27
+ plugins: [...ServerPlugins<T>, ...P];
28
+ };
29
+ type ExtendedClientAuthConfig<T, P extends BetterAuthClientPlugin[]> = Omit<T, 'plugins'> & {
30
+ plugins: [...ClientPlugins<T>, ...P];
31
+ };
32
+ export type ClientAuthFactory<T extends ClientAuthConfig> = ((baseURL: string) => ReturnType<typeof createAuthClient<T>>) & {
33
+ resolveOptions: (baseURL: string) => T & {
34
+ baseURL: string;
35
+ };
36
+ };
20
37
  export type ModuleDatabaseProviderId = 'none' | 'nuxthub' | (string & {});
21
38
  export type EffectiveDatabaseProviderId = 'user' | ModuleDatabaseProviderId;
22
39
  export interface BetterAuthModuleOptions {
@@ -26,6 +43,10 @@ export interface BetterAuthModuleOptions {
26
43
  serverConfig?: string;
27
44
  /** Client config path. Relative paths resolve from the layer that declares them. Default: 'app/auth.config' */
28
45
  clientConfig?: string;
46
+ /** Server plugin modules. Relative paths resolve from the declaring layer. Sources are additive and are not deduplicated. */
47
+ serverPluginSources?: string[];
48
+ /** Client plugin modules. Relative paths resolve from the declaring layer. Sources are additive and are not deduplicated. */
49
+ clientPluginSources?: string[];
29
50
  redirects?: {
30
51
  /** Where to redirect unauthenticated users. Default: '/login' */
31
52
  login?: string;
@@ -50,9 +71,9 @@ export interface BetterAuthModuleOptions {
50
71
  */
51
72
  redirectQueryKey?: string;
52
73
  /**
53
- * Enable secondary storage for sessions.
54
- * - `true`: Use NuxtHub KV (requires hub.kv: true)
55
- * - `'custom'`: User provides own secondaryStorage in defineServerAuth()
74
+ * Configure secondary storage for sessions.
75
+ * - `true`: Temporarily falls back to database-backed sessions with a setup warning
76
+ * - `'custom'`: User provides an atomic secondaryStorage in defineServerAuth()
56
77
  * - `false` (default): No secondary storage from module
57
78
  */
58
79
  hubSecondaryStorage?: boolean | 'custom';
@@ -84,4 +105,7 @@ export interface AuthPrivateRuntimeConfig {
84
105
  }
85
106
  export declare function defineServerAuth<const R>(config: (ctx: ServerAuthContext) => R & ServerAuthConfig): (ctx: ServerAuthContext) => R;
86
107
  export declare function defineServerAuth<const R>(config: R & ServerAuthConfig): (ctx: ServerAuthContext) => R;
87
- export declare function defineClientAuth<T extends ClientAuthConfig>(config: T | ((ctx: ClientAuthContext) => T)): (baseURL: string) => ReturnType<typeof createAuthClient<T>>;
108
+ export declare function extendServerAuth<const R extends ServerAuthConfig, const P extends readonly BetterAuthPlugin[]>(createConfig: (ctx: ServerAuthContext) => R, plugins: P): (ctx: ServerAuthContext) => ExtendedServerAuthConfig<R, P>;
109
+ export declare function defineClientAuth<T extends ClientAuthConfig>(config: T | ((ctx: ClientAuthContext) => T)): ClientAuthFactory<T>;
110
+ export declare function extendClientAuth<T extends ClientAuthConfig, const P extends BetterAuthClientPlugin[]>(createClient: ClientAuthFactory<T>, plugins: P): ClientAuthFactory<ExtendedClientAuthConfig<T, P>>;
111
+ export {};
@@ -2,12 +2,30 @@ import { createAuthClient } from "better-auth/vue";
2
2
  export function defineServerAuth(config) {
3
3
  return typeof config === "function" ? config : () => config;
4
4
  }
5
+ export function extendServerAuth(createConfig, plugins) {
6
+ return (ctx) => {
7
+ const config = createConfig(ctx);
8
+ const basePlugins = config.plugins || [];
9
+ return { ...config, plugins: [...basePlugins, ...plugins] };
10
+ };
11
+ }
12
+ function createClientAuthFactory(resolveOptions) {
13
+ const factory = ((baseURL) => createAuthClient(resolveOptions(baseURL)));
14
+ factory.resolveOptions = resolveOptions;
15
+ return factory;
16
+ }
5
17
  export function defineClientAuth(config) {
6
- return (baseURL) => {
18
+ return createClientAuthFactory((baseURL) => {
7
19
  const ctx = { siteUrl: baseURL };
8
20
  const resolved = typeof config === "function" ? config(ctx) : config;
9
21
  const { baseURL: configuredBaseURL, ...resolvedOptions } = resolved;
10
- const clientOptions = { ...resolvedOptions, baseURL: configuredBaseURL ?? baseURL };
11
- return createAuthClient(clientOptions);
12
- };
22
+ return { ...resolvedOptions, baseURL: configuredBaseURL ?? baseURL };
23
+ });
24
+ }
25
+ export function extendClientAuth(createClient, plugins) {
26
+ return createClientAuthFactory((baseURL) => {
27
+ const config = createClient.resolveOptions(baseURL);
28
+ const basePlugins = config.plugins || [];
29
+ return { ...config, plugins: [...basePlugins, ...plugins] };
30
+ });
13
31
  }
@@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
25
25
  },
26
26
  preserveRedirect: publicAuth?.preserveRedirect ?? true,
27
27
  redirectQueryKey: publicAuth?.redirectQueryKey ?? "redirect",
28
- hubSecondaryStorage: privateAuth?.hubSecondaryStorage ?? false,
28
+ hubSecondaryStorage: privateAuth?.hubSecondaryStorage === true ? false : privateAuth?.hubSecondaryStorage ?? false,
29
29
  useDatabase: publicAuth?.useDatabase ?? false,
30
30
  databaseProvider: publicAuth?.databaseProvider ?? "none"
31
31
  },
@@ -4,6 +4,7 @@ import { betterAuth } from 'better-auth';
4
4
  import createServerAuth from '#auth/server';
5
5
  type AuthOptions = ReturnType<typeof createServerAuth>;
6
6
  type UserAuthConfig = AuthOptions & {
7
+ rateLimit?: BetterAuthOptions['rateLimit'];
7
8
  secrets?: BetterAuthOptions['secrets'];
8
9
  trustedOrigins?: BetterAuthOptions['trustedOrigins'];
9
10
  secondaryStorage?: BetterAuthOptions['secondaryStorage'];
@@ -1,7 +1,6 @@
1
1
  import { betterAuth, env } from "better-auth";
2
2
  import { withoutProtocol } from "ufo";
3
3
  import { createDatabase, db } from "#auth/database";
4
- import { createSecondaryStorage } from "#auth/secondary-storage";
5
4
  import createServerAuth from "#auth/server";
6
5
  import { getRequestHost, getRequestProtocol, useRuntimeConfig } from "../internal/nitro-compat.js";
7
6
  import { resolveCustomSecondaryStorageRequirement } from "./custom-secondary-storage.js";
@@ -9,6 +8,8 @@ const _authCache = /* @__PURE__ */ new Map();
9
8
  const requestAuthKey = Symbol.for("nuxt-better-auth.requestAuth");
10
9
  let _baseURLInferenceLogged = false;
11
10
  let _customSecondaryStorageMisconfigWarned = false;
11
+ let _unsupportedHubSecondaryStorageWarned = false;
12
+ let _secondaryStorageRateLimitFallbackWarned = false;
12
13
  const fallbackRequestAuthContext = /* @__PURE__ */ new WeakMap();
13
14
  function getRequestAuthContext(event) {
14
15
  const eventWithContext = event;
@@ -214,6 +215,10 @@ export function serverAuth(event) {
214
215
  const database = createDatabase(event);
215
216
  const trustedOrigins = withDevTrustedOrigins(userConfig.trustedOrigins);
216
217
  const hubSecondaryStorage = runtimeConfig.auth?.hubSecondaryStorage;
218
+ if (hubSecondaryStorage === true && !_unsupportedHubSecondaryStorageWarned) {
219
+ _unsupportedHubSecondaryStorageWarned = true;
220
+ console.warn("[nuxt-better-auth] Runtime hubSecondaryStorage: true is unsupported with Better Auth 1.7 and will be ignored. The module will preserve secondaryStorage from defineServerAuth(). Remove NUXT_AUTH_HUB_SECONDARY_STORAGE or set it to false.");
221
+ }
217
222
  const customSecondaryStorage = resolveCustomSecondaryStorageRequirement(hubSecondaryStorage, userConfig.secondaryStorage != null, Boolean(import.meta.dev));
218
223
  if (customSecondaryStorage?.shouldThrow)
219
224
  throw new Error(customSecondaryStorage.message);
@@ -221,6 +226,12 @@ export function serverAuth(event) {
221
226
  _customSecondaryStorageMisconfigWarned = true;
222
227
  console.warn(customSecondaryStorage.message);
223
228
  }
229
+ const useMemoryRateLimit = userConfig.rateLimit?.storage === "secondary-storage" && !userConfig.rateLimit.customStorage && !userConfig.secondaryStorage;
230
+ if (useMemoryRateLimit && !_secondaryStorageRateLimitFallbackWarned) {
231
+ _secondaryStorageRateLimitFallbackWarned = true;
232
+ console.warn('[nuxt-better-auth] rateLimit.storage: "secondary-storage" requires secondaryStorage. Falling back to process-local memory. Set rateLimit.storage to "database" or provide rateLimit.customStorage for shared limits.');
233
+ }
234
+ const rateLimit = useMemoryRateLimit ? { ...userConfig.rateLimit, storage: "memory" } : userConfig.rateLimit;
224
235
  if (!database) {
225
236
  const cached = _authCache.get(cacheKey);
226
237
  if (cached) {
@@ -231,8 +242,8 @@ export function serverAuth(event) {
231
242
  }
232
243
  const authOptions = {
233
244
  ...userConfig,
245
+ ...rateLimit ? { rateLimit } : {},
234
246
  ...database ? { database } : {},
235
- ...hubSecondaryStorage === true ? { secondaryStorage: createSecondaryStorage() } : {},
236
247
  secret: betterAuthSecret,
237
248
  baseURL: siteUrl,
238
249
  trustedOrigins
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtjs/better-auth",
3
3
  "type": "module",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "Nuxt module for Better Auth integration with NuxtHub, route protection, session management, and role-based access",
6
6
  "author": "onmax",
7
7
  "license": "MIT",