@nuxtjs/better-auth 0.3.7 → 0.3.8

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.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/better-auth",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "configKey": "auth",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.0.0"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
- import { getLayerDirectories, updateTemplates, addServerImports, addServerHandler, addImports, addPlugin, addComponentsDir, addRouteMiddleware, extendPages, addTemplate, hasNuxtModule, addTypeTemplate, defineNuxtModule, createResolver } from '@nuxt/kit';
3
+ import { getLayerDirectories, resolveServerVariant, hasNuxtCompatibility, updateTemplates, addServerImports, addServerHandler, addNitroPlugin, addImports, addPlugin, addComponentsDir, addRouteMiddleware, extendPages, addTemplate, hasNuxtModule, addTypeTemplate, defineNuxtModule, createResolver, getNitroVersion } from '@nuxt/kit';
4
4
  import { consola as consola$1 } from 'consola';
5
5
  import { defineDiagnostics, Diagnostic, formatDiagnostic } from 'nostics';
6
6
  import { isAbsolute, join, resolve, relative, dirname } from 'pathe';
@@ -13,7 +13,7 @@ import { isCI, isTest } from 'std-env';
13
13
  import { isInternalAssetPath } from '../dist/runtime/internal/auth-route-rules.js';
14
14
  export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
15
15
 
16
- const version = "0.3.7";
16
+ const version = "0.3.8";
17
17
 
18
18
  const CONFIG_EXTENSIONS = [".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"];
19
19
  const CONFIG_EXTENSION_RE = /\.[cm]?[jt]s$/;
@@ -198,13 +198,26 @@ function resolveNitro3RouteRulesTarget(nuxtRootDir) {
198
198
  return fallback;
199
199
  }
200
200
  }
201
- function resolveNitroCompatibilityImports(nuxtVersion) {
202
- const major = Number.parseInt(nuxtVersion, 10);
203
- const nitroV3 = Number.isFinite(major) && major >= 5;
204
- return {
205
- h3: nitroV3 ? "nitro/h3" : "h3",
206
- runtime: nitroV3 ? "nitro3" : "nitro2"
207
- };
201
+ function resolveServerRuntimeShim(resolve) {
202
+ const nitro2 = resolve("./runtime/server/internal/nitro2");
203
+ return resolveServerVariant({
204
+ nuxt: resolve("./runtime/server/internal/portable"),
205
+ nitro2
206
+ }) ?? nitro2;
207
+ }
208
+ function resolveH3TypesPath(nitroMajor) {
209
+ return nitroMajor === 3 ? "nitro/h3" : "h3";
210
+ }
211
+ async function resolveAppSecretShim(resolve) {
212
+ const canDeriveAppSecret = await hasNuxtCompatibility({ nuxt: ">=4.6.0-0" });
213
+ return resolve(`./runtime/server/internal/${canDeriveAppSecret ? "app-secret" : "app-secret-unavailable"}`);
214
+ }
215
+ async function resolveAuthSecretPlugin(resolve) {
216
+ if (!await hasNuxtCompatibility({ nuxt: ">=4.6.0-0" }))
217
+ return;
218
+ if (!resolveServerVariant({ nitro2: true, nitro3: true, nuxt: false }))
219
+ return;
220
+ return resolve("./runtime/server/plugins/auth-secret");
208
221
  }
209
222
 
210
223
  function configFilename(file) {
@@ -286,15 +299,18 @@ function registerTemplateHmrHook(nuxt) {
286
299
  });
287
300
  }
288
301
  function registerServerRuntime(input) {
289
- const { clientOnly, resolve } = input;
302
+ const { authSecretPlugin, clientOnly, resolve } = input;
290
303
  if (!clientOnly) {
291
304
  addServerImports([
292
305
  { name: "defineServerAuth", from: resolve("./runtime/config") },
293
306
  { name: "serverAuth", from: resolve("./runtime/server/utils/auth") },
307
+ { name: "ensureServerAuth", from: resolve("./runtime/server/utils/auth") },
294
308
  ...["getRequestSession", "getUserSession", "setRequestSession", "refreshSessionCookieCache", "setSessionCookie", "createSession", "requireUserSession"].map((name) => ({ name, from: resolve("./runtime/server/utils/session") }))
295
309
  ]);
296
310
  addServerHandler({ middleware: true, handler: resolve("./runtime/server/middleware/route-access") });
297
311
  addServerHandler({ route: "/api/auth/**", handler: resolve("./runtime/server/api/auth/[...all]") });
312
+ if (authSecretPlugin)
313
+ addNitroPlugin({ nitro2: authSecretPlugin, nitro3: authSecretPlugin });
298
314
  }
299
315
  addImports([
300
316
  "runWithSessionRefresh",
@@ -643,45 +659,62 @@ async function setupBetterAuthSchema(nuxt, serverConfigPath, options, consola, h
643
659
  const DEFAULT_SECRET_ENV = "NUXT_BETTER_AUTH_SECRET";
644
660
  const FALLBACK_SECRET_ENV = "BETTER_AUTH_SECRET";
645
661
  const VERSIONED_SECRET_ENV = "BETTER_AUTH_SECRETS";
662
+ const APP_SECRET_ENV = "NUXT_APP_SECRET";
646
663
  const generateSecret = () => randomBytes(32).toString("hex");
647
664
  function readEnvFile(rootDir) {
648
665
  const envPath = join(rootDir, ".env");
649
666
  return existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
650
667
  }
651
- function hasEnvSecret(rootDir) {
668
+ function hasEnvSecret(rootDir, names) {
652
669
  const envFile = readEnvFile(rootDir);
653
- return [DEFAULT_SECRET_ENV, FALLBACK_SECRET_ENV, VERSIONED_SECRET_ENV].some((name) => {
670
+ return names.some((name) => {
654
671
  const match = envFile.match(new RegExp(`^${name}=(.+)$`, "m"));
655
672
  return !!match && !!match[1] && match[1].trim().length > 0;
656
673
  });
657
674
  }
658
- function appendSecretToEnv(rootDir, secret) {
675
+ function appendSecretToEnv(rootDir, name, secret) {
659
676
  const envPath = join(rootDir, ".env");
660
677
  let content = readEnvFile(rootDir);
661
678
  if (content.length > 0 && !content.endsWith("\n"))
662
679
  content += "\n";
663
- content += `${DEFAULT_SECRET_ENV}=${secret}
680
+ content += `${name}=${secret}
664
681
  `;
665
682
  writeFileSync(envPath, content, "utf-8");
666
683
  }
684
+ function getSecretEnvName(appSecret) {
685
+ return appSecret ? APP_SECRET_ENV : DEFAULT_SECRET_ENV;
686
+ }
687
+ function applyPromptedAppSecret(secret) {
688
+ process.env[APP_SECRET_ENV] = secret;
689
+ delete process.env.NUXT_APP_SECRET_GENERATED;
690
+ }
667
691
  async function promptForSecret(rootDir, consola, options = {}) {
668
692
  const configuredSecret = options.configuredSecret?.trim();
669
693
  if (configuredSecret)
670
694
  return void 0;
671
- if (process.env.NUXT_BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRETS || hasEnvSecret(rootDir))
695
+ if (process.env.NUXT_BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRETS)
696
+ return void 0;
697
+ const secretEnvNames = [DEFAULT_SECRET_ENV, FALLBACK_SECRET_ENV, VERSIONED_SECRET_ENV];
698
+ if (options.appSecret)
699
+ secretEnvNames.push(APP_SECRET_ENV);
700
+ if (hasEnvSecret(rootDir, secretEnvNames))
701
+ return void 0;
702
+ const generatedAppSecret = process.env.NUXT_APP_SECRET_GENERATED === "1";
703
+ if (options.appSecret && !generatedAppSecret && (process.env.NUXT_APP_SECRET || options.configuredAppSecret?.trim()))
672
704
  return void 0;
673
705
  const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
674
706
  if (options.prepare || !hasTty) {
675
- consola.warn("[nuxt-better-auth] Skipping auth secret prompt (non-interactive). Set NUXT_BETTER_AUTH_SECRET, BETTER_AUTH_SECRET, or BETTER_AUTH_SECRETS.");
707
+ consola.warn(options.appSecret ? "[nuxt-better-auth] Skipping auth secret prompt (non-interactive). Set NUXT_APP_SECRET, or NUXT_BETTER_AUTH_SECRET, BETTER_AUTH_SECRET, or BETTER_AUTH_SECRETS." : "[nuxt-better-auth] Skipping auth secret prompt (non-interactive). Set NUXT_BETTER_AUTH_SECRET, BETTER_AUTH_SECRET, or BETTER_AUTH_SECRETS.");
676
708
  return void 0;
677
709
  }
710
+ const envName = getSecretEnvName(options.appSecret);
678
711
  if (isCI || isTest) {
679
712
  const secret2 = generateSecret();
680
- appendSecretToEnv(rootDir, secret2);
681
- consola.info("Generated NUXT_BETTER_AUTH_SECRET and added to .env (CI/test mode)");
713
+ appendSecretToEnv(rootDir, envName, secret2);
714
+ consola.info(`Generated ${envName} and added to .env (CI/test mode)`);
682
715
  return secret2;
683
716
  }
684
- consola.box("An auth secret is required for authentication.\nThis will add NUXT_BETTER_AUTH_SECRET to your .env file.\nBETTER_AUTH_SECRET and BETTER_AUTH_SECRETS are also supported.");
717
+ consola.box(options.appSecret ? "An auth secret is required for authentication.\nThis will add NUXT_APP_SECRET to your .env file. The auth secret is derived from it.\nNUXT_BETTER_AUTH_SECRET, BETTER_AUTH_SECRET and BETTER_AUTH_SECRETS are also supported." : "An auth secret is required for authentication.\nThis will add NUXT_BETTER_AUTH_SECRET to your .env file.\nBETTER_AUTH_SECRET and BETTER_AUTH_SECRETS are also supported.");
685
718
  const choice = await consola.prompt("How do you want to set it?", {
686
719
  type: "select",
687
720
  options: [
@@ -708,14 +741,14 @@ async function promptForSecret(rootDir, consola, options = {}) {
708
741
  }
709
742
  const preview = `${secret.slice(0, 8)}...${secret.slice(-4)}`;
710
743
  const confirm = await consola.prompt(`Add to .env:
711
- ${DEFAULT_SECRET_ENV}=${preview}
744
+ ${envName}=${preview}
712
745
  Proceed?`, { type: "confirm", initial: true, cancel: "null" });
713
746
  if (typeof confirm === "symbol" || !confirm) {
714
747
  consola.info("Cancelled. Secret not written.");
715
748
  return void 0;
716
749
  }
717
- appendSecretToEnv(rootDir, secret);
718
- consola.success("Added NUXT_BETTER_AUTH_SECRET to .env");
750
+ appendSecretToEnv(rootDir, envName, secret);
751
+ consola.success(`Added ${envName} to .env`);
719
752
  return secret;
720
753
  }
721
754
 
@@ -1645,16 +1678,25 @@ const module$1 = defineNuxtModule({
1645
1678
  },
1646
1679
  async onInstall(nuxt) {
1647
1680
  const configuredSecret = nuxt.options.runtimeConfig?.betterAuthSecret;
1648
- const generatedSecret = await promptForSecret(nuxt.options.rootDir, consola, { configuredSecret, prepare: Boolean(nuxt.options._prepare) });
1649
- if (generatedSecret)
1681
+ const appSecret = await hasNuxtCompatibility({ nuxt: ">=4.6.0-0" }, nuxt);
1682
+ const generatedSecret = await promptForSecret(nuxt.options.rootDir, consola, {
1683
+ configuredSecret,
1684
+ prepare: Boolean(nuxt.options._prepare),
1685
+ appSecret,
1686
+ configuredAppSecret: nuxt.options.runtimeConfig?.appSecret
1687
+ });
1688
+ if (generatedSecret && appSecret)
1689
+ applyPromptedAppSecret(generatedSecret);
1690
+ else if (generatedSecret)
1650
1691
  process.env.NUXT_BETTER_AUTH_SECRET = generatedSecret;
1651
1692
  await createDefaultAuthConfigFiles(nuxt);
1652
1693
  },
1653
1694
  setup(options, nuxt) {
1654
1695
  const finishSetup = async () => {
1655
1696
  const resolver = createResolver(import.meta.url);
1656
- const nitroImports = resolveNitroCompatibilityImports(nuxt._version);
1657
- nuxt.options.alias["#better-auth/nitro-compat"] = resolver.resolve(`./runtime/server/internal/${nitroImports.runtime}`);
1697
+ const h3TypesPath = resolveH3TypesPath(getNitroVersion(nuxt));
1698
+ nuxt.options.alias["#better-auth/nitro-compat"] = resolveServerRuntimeShim(resolver.resolve);
1699
+ nuxt.options.alias["#better-auth/app-secret"] = await resolveAppSecretShim(resolver.resolve);
1658
1700
  const registeredPluginSources = {};
1659
1701
  await nuxt.callHook("better-auth:plugins:extend", registeredPluginSources);
1660
1702
  for (const source of [...registeredPluginSources.server || [], ...registeredPluginSources.client || []]) {
@@ -1740,8 +1782,8 @@ const module$1 = defineNuxtModule({
1740
1782
  hasHubDb: setup.serverTypes.hasHubDb,
1741
1783
  runtimeTypesPath: resolver.resolve("./runtime/types"),
1742
1784
  sharedServerConfigSafe: [setup.configs.server.path, ...setup.pluginSources.server].every(isServerConfigSharedTypeSafe),
1743
- h3TypesPath: nitroImports.h3,
1744
- nitro3RouteRulesTarget: nitroImports.runtime === "nitro3" ? resolveNitro3RouteRulesTarget(nuxt.options.rootDir) : void 0
1785
+ h3TypesPath,
1786
+ nitro3RouteRulesTarget: h3TypesPath === "nitro/h3" ? resolveNitro3RouteRulesTarget(nuxt.options.rootDir) : void 0
1745
1787
  });
1746
1788
  }
1747
1789
  if (setup.schemaGeneration) {
@@ -1759,11 +1801,15 @@ const module$1 = defineNuxtModule({
1759
1801
  runtimeTypesAugmentPath: setup.sharedTypes.runtimeTypesAugmentPath,
1760
1802
  runtimeTypesPath: resolver.resolve("./runtime/types"),
1761
1803
  clientConfigPath,
1762
- h3TypesPath: nitroImports.h3,
1804
+ h3TypesPath,
1763
1805
  clientOnly: setup.clientOnly
1764
1806
  });
1765
1807
  registerTemplateHmrHook(nuxt);
1766
- registerServerRuntime({ clientOnly: setup.clientOnly, resolve: resolver.resolve });
1808
+ registerServerRuntime({
1809
+ clientOnly: setup.clientOnly,
1810
+ resolve: resolver.resolve,
1811
+ authSecretPlugin: setup.clientOnly ? void 0 : await resolveAuthSecretPlugin(resolver.resolve)
1812
+ });
1767
1813
  registerAuthMiddleware(resolver.resolve);
1768
1814
  await registerDevtools({ nuxt, clientOnly: setup.clientOnly, hasHubDb: setup.database.hasHubDb, resolve: resolver.resolve });
1769
1815
  registerRouteRulesMetaHook(nuxt);
@@ -1,6 +1,8 @@
1
+ import { prepareAuthSecret } from "../../internal/auth-secret.js";
1
2
  import { defineEventHandler, toWebRequest } from "../../internal/nitro-compat.js";
2
3
  import { serverAuth } from "../../utils/auth.js";
3
4
  export default defineEventHandler(async (event) => {
5
+ await prepareAuthSecret();
4
6
  const auth = serverAuth(event);
5
7
  return auth.handler(toWebRequest(event));
6
8
  });
@@ -0,0 +1 @@
1
+ export * from '#better-auth/app-secret';
@@ -0,0 +1 @@
1
+ export * from "#better-auth/app-secret";
@@ -0,0 +1,2 @@
1
+ /** Nuxt < 4.6 has no `appSecret` to derive the Better Auth secret from. */
2
+ export declare const deriveAuthSecret: (() => Promise<string>) | undefined;
@@ -0,0 +1 @@
1
+ export const deriveAuthSecret = void 0;
@@ -0,0 +1,4 @@
1
+ /** The `deriveSecret()` purpose for the Better Auth secret. Changing it rotates every derived secret. */
2
+ export declare const AUTH_SECRET_PURPOSE = "better-auth:secret";
3
+ /** Derives the Better Auth secret from Nuxt's `appSecret` (`NUXT_APP_SECRET`), on Nuxt 4.6+. */
4
+ export declare const deriveAuthSecret: (() => Promise<string>) | undefined;
@@ -0,0 +1,3 @@
1
+ import { deriveSecret } from "nuxt/server";
2
+ export const AUTH_SECRET_PURPOSE = "better-auth:secret";
3
+ export const deriveAuthSecret = () => deriveSecret(AUTH_SECRET_PURPOSE);
@@ -0,0 +1,25 @@
1
+ /** The singular secret configured for the module: runtime config (`NUXT_BETTER_AUTH_SECRET`) or `BETTER_AUTH_SECRET`. */
2
+ export declare function getConfiguredAuthSecret(runtimeConfig: object): string;
3
+ /** Whether a secret source that predates `appSecret` derivation is configured. These always win. */
4
+ export declare function hasExplicitAuthSecret(runtimeConfig: object): boolean;
5
+ /**
6
+ * Whether the auth secret is derived from `appSecret`: Nuxt provides `deriveSecret()` (4.6+) and
7
+ * the `appSecret` is the user's. In development Nuxt generates an `appSecret` when none is set
8
+ * (`NUXT_APP_SECRET_GENERATED`); deriving from that would replace Better Auth's development
9
+ * default secret and sign out existing development sessions, so the default is kept.
10
+ */
11
+ export declare function canDeriveAuthSecret(): boolean;
12
+ /**
13
+ * Derives the Better Auth secret from Nuxt's `appSecret` once per server process, when no auth
14
+ * secret is configured and the `appSecret` is the user's (see `canDeriveAuthSecret`).
15
+ * `serverAuth()` is synchronous, so request entry points and the startup plugin start this, and
16
+ * the request entry points await it before they create the auth instance.
17
+ */
18
+ export declare function prepareAuthSecret(): Promise<void> | undefined;
19
+ export interface DerivedAuthSecretState {
20
+ /** The secret is derived from the user's `appSecret` (Nuxt 4.6+). */
21
+ available: boolean;
22
+ secret?: string;
23
+ error?: unknown;
24
+ }
25
+ export declare function getDerivedAuthSecret(): DerivedAuthSecretState;
@@ -0,0 +1,40 @@
1
+ import { env } from "better-auth";
2
+ import { deriveAuthSecret } from "./app-secret-compat.js";
3
+ import { useRuntimeConfig } from "./nitro-compat.js";
4
+ let derivedSecret;
5
+ let derivedSecretPromise;
6
+ let derivedSecretError;
7
+ export function getConfiguredAuthSecret(runtimeConfig) {
8
+ const secret = runtimeConfig.betterAuthSecret;
9
+ return typeof secret === "string" && secret || env.BETTER_AUTH_SECRET || "";
10
+ }
11
+ export function hasExplicitAuthSecret(runtimeConfig) {
12
+ return Boolean(getConfiguredAuthSecret(runtimeConfig) || env.BETTER_AUTH_SECRETS || env.AUTH_SECRET);
13
+ }
14
+ export function canDeriveAuthSecret() {
15
+ if (!deriveAuthSecret)
16
+ return false;
17
+ return !(import.meta.dev && globalThis.process?.env?.NUXT_APP_SECRET_GENERATED === "1");
18
+ }
19
+ export function prepareAuthSecret() {
20
+ if (derivedSecret !== void 0 || derivedSecretError !== void 0 || !canDeriveAuthSecret())
21
+ return;
22
+ if (hasExplicitAuthSecret(useRuntimeConfig()))
23
+ return;
24
+ derivedSecretPromise ||= deriveAuthSecret().then(
25
+ (secret) => {
26
+ derivedSecret = secret;
27
+ },
28
+ (error) => {
29
+ derivedSecretError = error ?? new Error("deriveSecret failed");
30
+ }
31
+ );
32
+ return derivedSecretPromise;
33
+ }
34
+ export function getDerivedAuthSecret() {
35
+ return {
36
+ available: canDeriveAuthSecret(),
37
+ secret: derivedSecret,
38
+ error: derivedSecretError
39
+ };
40
+ }
@@ -1,10 +1,9 @@
1
- import type { H3Event } from 'nitro/h3';
1
+ import type { RequestEvent } from 'nuxt/server';
2
2
  import type { AuthRouteRules } from '../../types.js';
3
3
  import { splitSetCookieString } from 'cookie-es';
4
- import { defineEventHandler, getQuery, getRequestHost, getRequestProtocol, getRequestURL, readBody } from 'nitro/h3';
5
- import { useRuntimeConfig } from 'nitro/runtime-config';
4
+ import { defineEventHandler, getQuery, getRequestHost, getRequestProtocol, getRequestURL, readBody, useRuntimeConfig } from 'nuxt/server';
6
5
  export { defineEventHandler, getQuery, getRequestHost, getRequestProtocol, getRequestURL, readBody, useRuntimeConfig, };
7
- export type ServerEvent = H3Event;
6
+ export type ServerEvent = RequestEvent;
8
7
  export { splitSetCookieString as splitCookiesString };
9
8
  export declare function getAuthRouteRules(event: ServerEvent): AuthRouteRules;
10
9
  export declare function createAuthError(status: number, statusText: string): Error;
@@ -1,15 +1,15 @@
1
1
  import { splitSetCookieString } from "cookie-es";
2
- import { getRouteRules } from "nitro/app";
3
2
  import {
3
+ createError,
4
4
  defineEventHandler,
5
5
  getQuery,
6
6
  getRequestHost,
7
7
  getRequestProtocol,
8
8
  getRequestURL,
9
- HTTPError,
10
- readBody
11
- } from "nitro/h3";
12
- import { useRuntimeConfig } from "nitro/runtime-config";
9
+ getRouteRules,
10
+ readBody,
11
+ useRuntimeConfig
12
+ } from "nuxt/server";
13
13
  import { normalizeAuthRouteRule } from "../../internal/auth-route-rules.js";
14
14
  export {
15
15
  defineEventHandler,
@@ -22,11 +22,11 @@ export {
22
22
  };
23
23
  export { splitSetCookieString as splitCookiesString };
24
24
  export function getAuthRouteRules(event) {
25
- const auth = getRouteRules(event.req.method, getRequestURL(event).pathname).routeRules?.auth;
26
- return { auth: normalizeAuthRouteRule(auth) };
25
+ const rules = getRouteRules(event);
26
+ return { auth: normalizeAuthRouteRule(rules.auth) };
27
27
  }
28
28
  export function createAuthError(status, statusText) {
29
- return HTTPError.status(status, statusText);
29
+ return createError({ status, statusText });
30
30
  }
31
31
  export function toWebRequest(event) {
32
32
  return event.req;
@@ -1,8 +1,10 @@
1
1
  import { normalizeAuthRoutePath, shouldSkipAuthRouteRules } from "../../internal/auth-route-rules.js";
2
2
  import { matchesUser } from "../../utils/match-user.js";
3
+ import { prepareAuthSecret } from "../internal/auth-secret.js";
3
4
  import { createAuthError, defineEventHandler, getAuthRouteRules, getRequestURL, useRuntimeConfig } from "../internal/nitro-compat.js";
4
5
  import { getUserSession, requireUserSession } from "../utils/session.js";
5
6
  export default defineEventHandler(async (event) => {
7
+ await prepareAuthSecret();
6
8
  const path = normalizeAuthRoutePath(
7
9
  getRequestURL(event).pathname,
8
10
  useRuntimeConfig().app?.baseURL
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Starts deriving the auth secret from Nuxt's `appSecret` when the server starts, so a
3
+ * `serverAuth()` call made before the first request, such as from a task, can use it.
4
+ * Nitro does not await plugins: startup code that needs the secret right away awaits
5
+ * `ensureServerAuth()` instead.
6
+ */
7
+ export default function authSecretPlugin(): void;
@@ -0,0 +1,4 @@
1
+ import { prepareAuthSecret } from "../internal/auth-secret.js";
2
+ export default function authSecretPlugin() {
3
+ void prepareAuthSecret();
4
+ }
@@ -4,6 +4,7 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
6
  "paths": {
7
+ "#better-auth/app-secret": ["./internal/app-secret"],
7
8
  "#better-auth/nitro-compat": ["./internal/nitro2"],
8
9
  "#nuxt-better-auth": ["../types/augment"]
9
10
  },
@@ -16,6 +16,12 @@ type ResolvedAuthOptions = Omit<UserAuthConfig, 'secret' | 'baseURL' | 'trustedO
16
16
  database?: BetterAuthOptions['database'];
17
17
  };
18
18
  type AuthInstance = ReturnType<typeof betterAuth<ResolvedAuthOptions>>;
19
+ /**
20
+ * Returns the Better Auth instance like `serverAuth()`, after waiting for the auth secret that
21
+ * Nuxt 4.6+ derives from `appSecret` (`NUXT_APP_SECRET`). Use it in code that can run before the
22
+ * first request, such as a Nitro plugin or task at server startup.
23
+ */
24
+ export declare function ensureServerAuth(event?: ServerEvent): Promise<AuthInstance>;
19
25
  /** Returns Better Auth instance. Caches per resolved host (or single instance when siteUrl is explicit). */
20
26
  export declare function serverAuth(event?: ServerEvent): AuthInstance;
21
27
  export {};
@@ -1,6 +1,7 @@
1
1
  import { betterAuth, env } from "better-auth";
2
2
  import { createDatabase, db } from "#auth/database";
3
3
  import createServerAuth from "#auth/server";
4
+ import { getConfiguredAuthSecret, getDerivedAuthSecret, hasExplicitAuthSecret, prepareAuthSecret } from "../internal/auth-secret.js";
4
5
  import { getRequestHost, getRequestProtocol, useRuntimeConfig } from "../internal/nitro-compat.js";
5
6
  import { resolveAuthBaseURL } from "../../internal/auth-base-url.js";
6
7
  import { resolveCustomSecondaryStorageRequirement } from "./custom-secondary-storage.js";
@@ -10,6 +11,7 @@ let _baseURLInferenceLogged = false;
10
11
  let _customSecondaryStorageMisconfigWarned = false;
11
12
  let _unsupportedHubSecondaryStorageWarned = false;
12
13
  let _secondaryStorageRateLimitFallbackWarned = false;
14
+ let _derivedSecretFallbackWarned = false;
13
15
  const fallbackRequestAuthContext = /* @__PURE__ */ new WeakMap();
14
16
  function getRequestAuthContext(event) {
15
17
  const eventWithContext = event;
@@ -193,17 +195,39 @@ function withDevTrustedOrigins(trustedOrigins) {
193
195
  return mergeOrigins([], request);
194
196
  };
195
197
  }
198
+ function missingAuthSecretMessage(derived) {
199
+ const message = "[nuxt-better-auth] An auth secret is required in production. Set NUXT_BETTER_AUTH_SECRET, BETTER_AUTH_SECRET, BETTER_AUTH_SECRETS, or defineServerAuth({ secrets })";
200
+ if (!derived.available)
201
+ return `${message}.`;
202
+ if (derived.error !== void 0)
203
+ return `${message}, or set NUXT_APP_SECRET (at least 32 characters) to derive one from Nuxt's appSecret.`;
204
+ return `${message}. The secret derived from NUXT_APP_SECRET is resolved asynchronously, and serverAuth() was called before it was ready, outside a request. Use \`await ensureServerAuth()\` in code that runs at server startup.`;
205
+ }
206
+ function warnDerivedSecretFallback(derived) {
207
+ if (_derivedSecretFallbackWarned)
208
+ return;
209
+ _derivedSecretFallbackWarned = true;
210
+ const reason = derived.error !== void 0 ? "could not be derived from NUXT_APP_SECRET. Set NUXT_APP_SECRET (at least 32 characters) or NUXT_BETTER_AUTH_SECRET." : "derived from NUXT_APP_SECRET was not ready yet. Use `await ensureServerAuth()` in code that runs at server startup.";
211
+ console.warn(`[nuxt-better-auth] Better Auth's development default secret is used for this call: the auth secret ${reason}`);
212
+ }
213
+ export async function ensureServerAuth(event) {
214
+ await prepareAuthSecret();
215
+ return serverAuth(event);
216
+ }
196
217
  export function serverAuth(event) {
197
218
  const runtimeConfig = useRuntimeConfig();
198
- const betterAuthSecret = runtimeConfig.betterAuthSecret || env.BETTER_AUTH_SECRET || "";
199
- if (betterAuthSecret && betterAuthSecret.length < 32)
219
+ const configuredSecret = getConfiguredAuthSecret(runtimeConfig);
220
+ if (configuredSecret && configuredSecret.length < 32)
200
221
  throw new Error("[nuxt-better-auth] Singular auth secret must be at least 32 characters for security");
222
+ const derived = getDerivedAuthSecret();
201
223
  const requestOrigin = resolveEventOrigin(event);
202
224
  let userConfig;
203
- if (!import.meta.dev && !betterAuthSecret && !env.BETTER_AUTH_SECRETS) {
225
+ if (!import.meta.dev && !configuredSecret && !env.BETTER_AUTH_SECRETS && !derived.secret) {
204
226
  userConfig = createServerAuth({ runtimeConfig, db, requestOrigin });
205
- if (userConfig.secrets === void 0)
206
- throw new Error("[nuxt-better-auth] An auth secret is required in production. Set NUXT_BETTER_AUTH_SECRET, BETTER_AUTH_SECRET, BETTER_AUTH_SECRETS, or defineServerAuth({ secrets }).");
227
+ if (userConfig.secrets === void 0) {
228
+ void prepareAuthSecret();
229
+ throw new Error(missingAuthSecretMessage(derived));
230
+ }
207
231
  }
208
232
  const siteUrl = getBaseURL(event);
209
233
  const hasExplicitSiteUrl = runtimeConfig.public.siteUrl && typeof runtimeConfig.public.siteUrl === "string";
@@ -212,6 +236,13 @@ export function serverAuth(event) {
212
236
  if (requestContext?.[requestAuthKey])
213
237
  return requestContext[requestAuthKey];
214
238
  userConfig ??= createServerAuth({ runtimeConfig, db, requestOrigin });
239
+ const usesDerivedSecret = derived.available && userConfig.secrets === void 0 && !hasExplicitAuthSecret(runtimeConfig);
240
+ const derivedSecretMissing = usesDerivedSecret && !derived.secret;
241
+ if (derivedSecretMissing) {
242
+ void prepareAuthSecret();
243
+ warnDerivedSecretFallback(derived);
244
+ }
245
+ const betterAuthSecret = configuredSecret || (usesDerivedSecret ? derived.secret ?? "" : "");
215
246
  const database = createDatabase(event);
216
247
  const trustedOrigins = withDevTrustedOrigins(userConfig.trustedOrigins);
217
248
  const hubSecondaryStorage = runtimeConfig.auth?.hubSecondaryStorage;
@@ -232,7 +263,8 @@ export function serverAuth(event) {
232
263
  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
264
  }
234
265
  const rateLimit = useMemoryRateLimit ? { ...userConfig.rateLimit, storage: "memory" } : userConfig.rateLimit;
235
- if (!database) {
266
+ const cacheable = !database && !derivedSecretMissing;
267
+ if (cacheable) {
236
268
  const cached = _authCache.get(cacheKey);
237
269
  if (cached) {
238
270
  if (requestContext)
@@ -251,7 +283,7 @@ export function serverAuth(event) {
251
283
  const auth = betterAuth(authOptions);
252
284
  if (requestContext)
253
285
  requestContext[requestAuthKey] = auth;
254
- if (!database)
286
+ if (cacheable)
255
287
  _authCache.set(cacheKey, auth);
256
288
  return auth;
257
289
  }
@@ -1,5 +1,6 @@
1
1
  import { matchesUser } from "../../utils/match-user.js";
2
2
  import { createAuthError } from "../internal/nitro-compat.js";
3
+ import { prepareAuthSecret } from "../internal/auth-secret.js";
3
4
  import { appendCookieHeader, appendSetCookieHeaders, getSetCookieHeaders } from "../internal/cookie-headers.js";
4
5
  import { serverAuth } from "./auth.js";
5
6
  const requestSessionLoadKey = Symbol.for("nuxt-better-auth.requestSessionLoad");
@@ -26,6 +27,9 @@ function getRequestHeaders(event) {
26
27
  return getRequestSessionContext(event).requestHeaders ?? getIncomingRequestHeaders(event);
27
28
  }
28
29
  async function loadSession(event) {
30
+ const pendingSecret = prepareAuthSecret();
31
+ if (pendingSecret)
32
+ await pendingSecret;
29
33
  const auth = serverAuth(event);
30
34
  const result = await auth.api.getSession({
31
35
  headers: getRequestHeaders(event),
@@ -36,6 +40,9 @@ async function loadSession(event) {
36
40
  return { headers: new Headers(), response: result };
37
41
  }
38
42
  function loadFreshSession(event) {
43
+ const pendingSecret = prepareAuthSecret();
44
+ if (pendingSecret)
45
+ return pendingSecret.then(() => loadFreshSession(event));
39
46
  const auth = serverAuth(event);
40
47
  return auth.api.getSession({
41
48
  headers: getRequestHeaders(event),
@@ -44,6 +51,9 @@ function loadFreshSession(event) {
44
51
  });
45
52
  }
46
53
  function getServerAuthContext(event) {
54
+ const pendingSecret = prepareAuthSecret();
55
+ if (pendingSecret)
56
+ return pendingSecret.then(() => getServerAuthContext(event));
47
57
  const auth = serverAuth(event);
48
58
  return auth.$context;
49
59
  }
@@ -9,7 +9,7 @@ const requestSchema = z.discriminatedUnion("action", [
9
9
  ]);
10
10
  const ownedUsers = /* @__PURE__ */ new Set();
11
11
  export default defineEventHandler(async (event) => {
12
- const token = useRuntimeConfig(event).betterAuthTestToken;
12
+ const token = useRuntimeConfig().betterAuthTestToken;
13
13
  if (!token || toWebRequest(event).headers.get("x-nuxt-auth-test") !== token)
14
14
  throw createAuthError(404, "Not found");
15
15
  const body = requestSchema.parse(parseJSON(await toWebRequest(event).text()));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtjs/better-auth",
3
3
  "type": "module",
4
- "version": "0.3.7",
4
+ "version": "0.3.8",
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",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  "dependencies": {
75
75
  "@nuxt/devtools-kit": "^3.4.1",
76
- "@nuxt/kit": "^4.5.2",
76
+ "@nuxt/kit": "^4.6.0",
77
77
  "@types/node": "^24.13.3",
78
78
  "auth": "1.7.7",
79
79
  "consola": "^3.4.2",
@@ -91,36 +91,36 @@
91
91
  },
92
92
  "devDependencies": {
93
93
  "@antfu/eslint-config": "^9.3.0",
94
- "@libsql/client": "^0.17.4",
94
+ "@libsql/client": "^0.18.0",
95
95
  "@libsql/linux-x64-gnu": "0.5.29",
96
96
  "@nuxt/devtools": "^3.4.1",
97
97
  "@nuxt/fonts": "0.14.0",
98
98
  "@nuxt/module-builder": "^1.0.3",
99
- "@nuxt/schema": "^4.5.2",
100
- "@nuxt/test-utils": "^4.1.0",
99
+ "@nuxt/schema": "^4.6.0",
100
+ "@nuxt/test-utils": "^4.3.3",
101
101
  "@nuxthub/core": "^0.10.8",
102
102
  "@pinia/nuxt": "^1.0.2",
103
103
  "@types/better-sqlite3": "^9.6.0",
104
104
  "@vue/test-utils": "^2.4.6",
105
105
  "better-auth": "1.7.7",
106
106
  "better-call": "1.4.0",
107
- "better-sqlite3": "^12.11.1",
107
+ "better-sqlite3": "^13.0.0",
108
108
  "drizzle-kit": "^0.31.10",
109
109
  "drizzle-orm": "^0.45.2",
110
110
  "eslint": "^10.8.1",
111
111
  "happy-dom": "^20.8.9",
112
112
  "nitropack": "2.13.4",
113
- "nuxt": "^4.5.2",
113
+ "nuxt": "^4.6.0",
114
114
  "pinia": "^4.0.3",
115
115
  "pkg-pr-new": "0.0.88",
116
- "playwright-core": "1.58.2",
116
+ "playwright-core": "1.63.0",
117
117
  "postgres": "^3.4.9",
118
118
  "typescript": "~5.9.3",
119
119
  "unstorage": "^1.17.5",
120
- "vitest": "^4.1.11",
121
- "vue": "3.5.41",
120
+ "vitest": "^5.0.0",
121
+ "vue": "3.5.43",
122
122
  "vue-tsc": "^3.3.10",
123
- "wrangler": "4.124.0",
123
+ "wrangler": "4.147.0",
124
124
  "yaml": "^2.9.0"
125
125
  },
126
126
  "scripts": {