@nuxtjs/better-auth 0.3.9 → 0.3.11

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
@@ -30,24 +30,27 @@ interface BetterAuthDatabaseProviderDefinition {
30
30
  isEnabled?: (ctx: BetterAuthDatabaseProviderEnabledContext) => boolean;
31
31
  priority?: number;
32
32
  }
33
+ interface BetterAuthNuxtHooks {
34
+ /** Register absolute plugin module paths during module setup. Sources are additive and are not deduplicated. */
35
+ 'better-auth:plugins:extend': (sources: BetterAuthPluginSources) => void | Promise<void>;
36
+ /**
37
+ * Add plugin schemas to generated Better Auth database tables.
38
+ * This build-time hook does not install plugins in the runtime auth instance.
39
+ * @param config - Plugins to include during schema generation
40
+ * @param config.plugins - Better Auth plugins whose schemas should be generated
41
+ */
42
+ 'better-auth:config:extend': (config: {
43
+ plugins?: BetterAuthPlugin[];
44
+ }) => void | Promise<void>;
45
+ /** Register or override Better Auth database providers. */
46
+ 'better-auth:database:providers': (providers: Record<string, BetterAuthDatabaseProviderDefinition>) => void | Promise<void>;
47
+ }
33
48
  declare module '@nuxt/schema' {
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>;
37
- /**
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
42
- */
43
- 'better-auth:config:extend': (config: {
44
- plugins?: BetterAuthPlugin[];
45
- }) => void | Promise<void>;
46
- /**
47
- * Register or override Better Auth database providers.
48
- * Providers are auto-selected via `isEnabled` + `priority`.
49
- */
50
- 'better-auth:database:providers': (providers: Record<string, BetterAuthDatabaseProviderDefinition>) => void | Promise<void>;
49
+ interface NuxtHooks extends BetterAuthNuxtHooks {
50
+ }
51
+ }
52
+ declare module 'nuxt/schema' {
53
+ interface NuxtHooks extends BetterAuthNuxtHooks {
51
54
  }
52
55
  }
53
56
 
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuxtjs/better-auth",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
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, resolveServerVariant, hasNuxtCompatibility, updateTemplates, addServerImports, addServerHandler, addNitroPlugin, addImports, addPlugin, addComponentsDir, addRouteMiddleware, extendPages, addTemplate, hasNuxtModule, addTypeTemplate, defineNuxtModule, createResolver, getNitroVersion } from '@nuxt/kit';
3
+ import { getLayerDirectories, resolveServerVariant, hasNuxtCompatibility, hasNuxtModuleCompatibility, 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.9";
16
+ const version = "0.3.11";
17
17
 
18
18
  const CONFIG_EXTENSIONS = [".ts", ".js", ".mts", ".cts", ".mjs", ".cjs"];
19
19
  const CONFIG_EXTENSION_RE = /\.[cm]?[jt]s$/;
@@ -152,6 +152,17 @@ function resolveAuthPluginSources(nuxt) {
152
152
  return resolved;
153
153
  }
154
154
 
155
+ function exportedNames(declaration) {
156
+ const names = /* @__PURE__ */ new Set();
157
+ for (const [, specifiers] of declaration.matchAll(/\bexport\s+(?:type\s+)?\{([^}]*)\}/g)) {
158
+ for (const specifier of specifiers.split(",")) {
159
+ const name = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/).pop()?.trim();
160
+ if (name)
161
+ names.add(name);
162
+ }
163
+ }
164
+ return names;
165
+ }
155
166
  function selectNitro3RouteRulesTarget(nitroTypesDeclaration, resolveH3Rules) {
156
167
  if (/\binterface\s+NitroRouteConfig\b/.test(nitroTypesDeclaration)) {
157
168
  return {
@@ -160,6 +171,14 @@ function selectNitro3RouteRulesTarget(nitroTypesDeclaration, resolveH3Rules) {
160
171
  rulesInterface: "NitroRouteRules"
161
172
  };
162
173
  }
174
+ const exported = exportedNames(nitroTypesDeclaration);
175
+ if (exported.has("RouteRuleConfig") && exported.has("RouteRules")) {
176
+ return {
177
+ moduleName: "nitro/types",
178
+ configInterface: "RouteRuleConfig",
179
+ rulesInterface: "RouteRules"
180
+ };
181
+ }
163
182
  return {
164
183
  moduleName: resolveH3Rules(),
165
184
  configInterface: "RouteRuleConfig",
@@ -208,6 +227,9 @@ function resolveServerRuntimeShim(resolve) {
208
227
  function resolveH3TypesPath(nitroMajor) {
209
228
  return nitroMajor === 3 ? "nitro/h3" : "h3";
210
229
  }
230
+ function resolveServerImportsTypesFile(nitroMajor) {
231
+ return nitroMajor === 3 ? "server-imports.d.ts" : "nitro-imports.d.ts";
232
+ }
211
233
  async function resolveAppSecretShim(resolve) {
212
234
  const canDeriveAppSecret = await hasNuxtCompatibility({ nuxt: ">=4.6.0-0" });
213
235
  return resolve(`./runtime/server/internal/${canDeriveAppSecret ? "app-secret" : "app-secret-unavailable"}`);
@@ -273,8 +295,22 @@ const diagnostics = defineDiagnostics({
273
295
  }
274
296
  });
275
297
 
276
- function setupDevTools(nuxt) {
298
+ async function setupDevTools(nuxt) {
277
299
  const hookable = nuxt;
300
+ if (await hasNuxtModuleCompatibility("@nuxt/devtools", ">=4.0.0-alpha.8", nuxt)) {
301
+ hookable.hook("devtools:ready", (ctx) => {
302
+ ctx.docks.register({
303
+ id: "better-auth",
304
+ title: "Auth",
305
+ icon: "simple-icons:betterauth",
306
+ type: "iframe",
307
+ url: "/__better-auth-devtools",
308
+ groupId: "nuxt",
309
+ category: "server"
310
+ });
311
+ });
312
+ return;
313
+ }
278
314
  hookable.hook("devtools:customTabs", (tabs) => {
279
315
  tabs.push({
280
316
  category: "server",
@@ -342,12 +378,12 @@ function registerAuthMiddleware(resolve) {
342
378
  });
343
379
  }
344
380
  function registerPrepareTypesHook(input) {
345
- const { nuxt, serverDir, hasHubDb } = input;
381
+ const { nuxt, serverDir, hasHubDb, serverImportsTypesFile } = input;
346
382
  nuxt.hook("prepare:types", ({ nodeTsConfig, nodeReferences, sharedReferences }) => {
347
383
  nodeTsConfig.compilerOptions ||= {};
348
384
  nodeTsConfig.compilerOptions.paths ||= {};
349
385
  const projectReferenceTypePaths = [
350
- join(nuxt.options.buildDir, "types/nitro-imports.d.ts"),
386
+ join(nuxt.options.buildDir, "types", serverImportsTypesFile),
351
387
  join(nuxt.options.buildDir, "types/auth-database.d.ts")
352
388
  ];
353
389
  if (hasHubDb)
@@ -394,7 +430,7 @@ async function registerDevtools(input) {
394
430
  const isProduction = process.env.NODE_ENV === "production" || !nuxt.options.dev;
395
431
  if (isProduction || clientOnly)
396
432
  return;
397
- setupDevTools(nuxt);
433
+ await setupDevTools(nuxt);
398
434
  addServerHandler({ route: "/api/_better-auth/config", method: "get", handler: resolve("./runtime/server/api/_better-auth/config.get") });
399
435
  if (hasHubDb) {
400
436
  const handlers = [
@@ -1209,7 +1245,7 @@ function buildNuxtFetchFallback(name) {
1209
1245
  `).join("");
1210
1246
  }
1211
1247
  function registerServerTypeTemplates(input) {
1212
- const { serverConfigPath, hasHubDb, runtimeTypesPath, sharedServerConfigSafe, h3TypesPath, nitro3RouteRulesTarget } = input;
1248
+ const { serverConfigPath, hasHubDb, runtimeTypesPath, sharedServerConfigSafe, h3TypesPath, serverImportsTypesFile, nitro3RouteRulesTarget } = input;
1213
1249
  const fetchRoutingTypeDeclarations = h3TypesPath === "nitro/h3" ? `type NitroFetchRequest = string & {}
1214
1250
  type AvailableRouterMethod<_ReqT extends NitroFetchRequest> = 'get' | 'head' | 'patch' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace'` : `import type { AvailableRouterMethod, NitroFetchRequest } from 'nitropack/types'`;
1215
1251
  const resolvedNitro3RouteRulesTarget = nitro3RouteRulesTarget ?? {
@@ -1232,7 +1268,7 @@ type AvailableRouterMethod<_ReqT extends NitroFetchRequest> = 'get' | 'head' | '
1232
1268
  auth?: import('${runtimeTypesPath}').AuthMeta
1233
1269
  }
1234
1270
  }`).join("\n");
1235
- const serverConfigTypeTemplateOptions = sharedServerConfigSafe ? { nuxt: true, nitro: true, node: true, shared: true } : { nuxt: true, nitro: true, node: true };
1271
+ const serverConfigTypeTemplateOptions = sharedServerConfigSafe || h3TypesPath === "nitro/h3" ? { nuxt: true, nitro: true, node: true, shared: true } : { nuxt: true, nitro: true, node: true };
1236
1272
  addTypeTemplate({
1237
1273
  filename: "types/auth-database.d.ts",
1238
1274
  getContents: () => `
@@ -1246,7 +1282,7 @@ declare module '#auth/database' {
1246
1282
  addTypeTemplate({
1247
1283
  filename: "types/nuxt-better-auth-server-context.d.ts",
1248
1284
  getContents: () => `
1249
- /// <reference path="./nitro-imports.d.ts" />
1285
+ /// <reference path="./${serverImportsTypesFile}" />
1250
1286
  /// <reference path="./auth-database.d.ts" />
1251
1287
  ${hasHubDb ? '/// <reference path="../hub/db.d.ts" />' : ""}
1252
1288
 
@@ -1537,6 +1573,8 @@ declare module '#nuxt-better-auth' {
1537
1573
  emailVerified: boolean
1538
1574
  name: string
1539
1575
  image?: string | null
1576
+ /** Whether the user has enabled two-factor authentication. */
1577
+ twoFactorEnabled?: boolean
1540
1578
  }
1541
1579
 
1542
1580
  export interface AuthSession {
@@ -1694,7 +1732,9 @@ const module$1 = defineNuxtModule({
1694
1732
  setup(options, nuxt) {
1695
1733
  const finishSetup = async () => {
1696
1734
  const resolver = createResolver(import.meta.url);
1697
- const h3TypesPath = resolveH3TypesPath(getNitroVersion(nuxt));
1735
+ const nitroMajor = getNitroVersion(nuxt);
1736
+ const h3TypesPath = resolveH3TypesPath(nitroMajor);
1737
+ const serverImportsTypesFile = resolveServerImportsTypesFile(nitroMajor);
1698
1738
  nuxt.options.alias["#better-auth/nitro-compat"] = resolveServerRuntimeShim(resolver.resolve);
1699
1739
  nuxt.options.alias["#better-auth/app-secret"] = await resolveAppSecretShim(resolver.resolve);
1700
1740
  const registeredPluginSources = {};
@@ -1758,7 +1798,8 @@ const module$1 = defineNuxtModule({
1758
1798
  registerPrepareTypesHook({
1759
1799
  nuxt,
1760
1800
  serverDir: setup.prepareTypes.serverDir,
1761
- hasHubDb: setup.prepareTypes.hasHubDb
1801
+ hasHubDb: setup.prepareTypes.hasHubDb,
1802
+ serverImportsTypesFile
1762
1803
  });
1763
1804
  }
1764
1805
  if (setup.database.providerDefinition) {
@@ -1783,6 +1824,7 @@ const module$1 = defineNuxtModule({
1783
1824
  runtimeTypesPath: resolver.resolve("./runtime/types"),
1784
1825
  sharedServerConfigSafe: [setup.configs.server.path, ...setup.pluginSources.server].every(isServerConfigSharedTypeSafe),
1785
1826
  h3TypesPath,
1827
+ serverImportsTypesFile,
1786
1828
  nitro3RouteRulesTarget: h3TypesPath === "nitro/h3" ? resolveNitro3RouteRulesTarget(nuxt.options.rootDir) : void 0
1787
1829
  });
1788
1830
  }
@@ -2,7 +2,7 @@ import { computed, navigateTo, nextTick, useNuxtApp, useRequestURL, useRuntimeCo
2
2
  import { normalizeAuthActionError } from "../internal/auth-action-error.js";
3
3
  import { resolvePostAuthSuccessRedirect, withFallbackSocialCallbackURL } from "../internal/redirect-helpers.js";
4
4
  import { fetchSessionClient, fetchSessionServer, isExpectedSignedOutSessionError, stripToken } from "../internal/session-fetch.js";
5
- import { isRecord } from "../internal/utils.js";
5
+ import { isRecord, setIfChanged } from "../internal/utils.js";
6
6
  import { createVueSafeAuthFacade, isAuthProxyProbeKey } from "../internal/vue-safe-auth-proxy.js";
7
7
  import { wrapAuthMethod } from "../internal/wrap-auth-method.js";
8
8
  import { getAuthRuntimeFlags, useRawAuthClient } from "./useAuthClient.js";
@@ -152,8 +152,8 @@ export function useUserSession() {
152
152
  if (newSession?.data?.session && newSession?.data?.user) {
153
153
  sessionExpiredRedirecting = false;
154
154
  hydrationSessionWasActive = false;
155
- session.value = stripToken(newSession.data.session);
156
- user.value = newSession.data.user;
155
+ setIfChanged(session, stripToken(newSession.data.session));
156
+ setIfChanged(user, newSession.data.user);
157
157
  } else if (!newSession?.isPending && !newSession?.isRefetching) {
158
158
  const isHydrationEmptySnapshot = nuxtApp.isHydrating && nuxtApp.payload.serverRendered && Boolean(session.value && user.value) && !newSession?.data?.session && !newSession?.data?.user;
159
159
  if (isHydrationEmptySnapshot) {
@@ -1,6 +1,7 @@
1
1
  import { parseJSON } from "better-auth/client";
2
2
  import { useRequestFetch, useRequestHeaders } from "#imports";
3
3
  import { normalizeAuthActionError } from "./auth-action-error.js";
4
+ import { setIfChanged } from "./utils.js";
4
5
  export function stripToken(session) {
5
6
  const { token: _, ...safe } = session;
6
7
  return safe;
@@ -35,8 +36,8 @@ export async function fetchSessionServer(session, user, authReady, options = {})
35
36
  ...options.force ? { query: { disableCookieCache: true } } : {}
36
37
  });
37
38
  if (data?.session && data?.user) {
38
- session.value = stripToken(data.session);
39
- user.value = data.user;
39
+ setIfChanged(session, stripToken(data.session));
40
+ setIfChanged(user, data.user);
40
41
  } else {
41
42
  session.value = null;
42
43
  user.value = null;
@@ -60,8 +61,8 @@ export async function fetchSessionClient(client, session, user, authReady, optio
60
61
  }
61
62
  const data = result.data;
62
63
  if (data?.session && data?.user) {
63
- session.value = stripToken(data.session);
64
- user.value = data.user;
64
+ setIfChanged(session, stripToken(data.session));
65
+ setIfChanged(user, data.user);
65
66
  } else {
66
67
  session.value = null;
67
68
  user.value = null;
@@ -1 +1,9 @@
1
1
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ /**
3
+ * Better Auth refetches the session on window focus and returns fresh objects even
4
+ * when nothing changed. Keeping the current object avoids retriggering every
5
+ * computed and watcher that depends on the session or user.
6
+ */
7
+ export declare function setIfChanged<T>(target: {
8
+ value: T;
9
+ }, value: T): void;
@@ -1,3 +1,19 @@
1
1
  export function isRecord(value) {
2
2
  return Boolean(value && typeof value === "object");
3
3
  }
4
+ function isSameValue(a, b) {
5
+ if (Object.is(a, b))
6
+ return true;
7
+ if (a instanceof Date || b instanceof Date)
8
+ return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
9
+ if (Array.isArray(a) || Array.isArray(b))
10
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => isSameValue(item, b[index]));
11
+ if (!isRecord(a) || !isRecord(b))
12
+ return false;
13
+ const keys = Object.keys(a);
14
+ return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && isSameValue(a[key], b[key]));
15
+ }
16
+ export function setIfChanged(target, value) {
17
+ if (!isSameValue(target.value, value))
18
+ target.value = value;
19
+ }
@@ -2,13 +2,15 @@ import { defu } from "defu";
2
2
  import { createRouter, toRouteMatcher } from "radix3";
3
3
  import { parsePath, parseQuery } from "ufo";
4
4
  import { createError, defineNuxtRouteMiddleware, getRouteRules, navigateTo, useNuxtApp, useRequestHeaders, useRuntimeConfig } from "#imports";
5
- import { shouldSkipAuthRouteRules } from "../../internal/auth-route-rules.js";
5
+ import { normalizeAuthRoutePath, shouldSkipAuthRouteRules } from "../../internal/auth-route-rules.js";
6
6
  import { matchesUser } from "../../utils/match-user.js";
7
7
  import { useUserSession } from "../composables/useUserSession.js";
8
8
  let authRouteRulesPromise = null;
9
9
  let routeRulesMatcherPromise = null;
10
10
  export default defineNuxtRouteMiddleware(async (to) => {
11
- if (shouldSkipAuthRouteRules(to.path))
11
+ const runtimeConfig = useRuntimeConfig();
12
+ const path = normalizeAuthRoutePath(to.path, runtimeConfig.app?.baseURL);
13
+ if (shouldSkipAuthRouteRules(to.path) || shouldSkipAuthRouteRules(path))
12
14
  return;
13
15
  if (to.matched?.length === 0)
14
16
  return;
@@ -30,7 +32,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
30
32
  const auth = to.meta.auth;
31
33
  if (auth === void 0 || auth === false)
32
34
  return;
33
- const config = useRuntimeConfig().public.auth;
35
+ const config = runtimeConfig.public.auth;
34
36
  if (import.meta.server && config?.clientOnly)
35
37
  return;
36
38
  const { fetchSession, user, loggedIn } = useUserSession();
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
- import { useFetch, useRuntimeConfig } from "#imports";
2
+ import { useFetch, useHead, useRuntimeConfig } from "#imports";
3
3
  import { useDevtoolsClient } from "@nuxt/devtools-kit/iframe-client";
4
- import { computed, ref, watch, watchEffect } from "vue";
4
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, triggerRef, watch } from "vue";
5
5
  const pageSize = 20;
6
6
  const tabLabels = {
7
7
  sessions: "Sessions",
@@ -10,13 +10,23 @@ const tabLabels = {
10
10
  config: "Config"
11
11
  };
12
12
  const devtoolsClient = useDevtoolsClient();
13
+ const nativeHost = shallowRef();
14
+ let stopHostUpdates;
15
+ onMounted(() => {
16
+ if (window.parent === window)
17
+ return;
18
+ try {
19
+ nativeHost.value = window.parent.__NUXT_DEVTOOLS_HOST__;
20
+ } catch {
21
+ return;
22
+ }
23
+ stopHostUpdates = nativeHost.value?.hooks.hook("host:update:reactivity", () => triggerRef(nativeHost));
24
+ });
25
+ onBeforeUnmount(() => stopHostUpdates?.());
13
26
  const runtimeConfig = useRuntimeConfig();
14
27
  const hasDb = computed(() => runtimeConfig.public.auth?.useDatabase ?? false);
15
- const isDark = computed(() => devtoolsClient.value?.host?.app?.colorMode?.value === "dark");
16
- watchEffect(() => {
17
- if (import.meta.client)
18
- document.documentElement.classList.toggle("dark", isDark.value);
19
- });
28
+ const isDark = computed(() => (nativeHost.value ?? devtoolsClient.value?.host)?.app?.colorMode?.value === "dark");
29
+ useHead({ style: [{ key: "better-auth-devtools-body", textContent: "body { margin: 0 !important; }" }] });
20
30
  const activeTab = ref(hasDb.value ? "sessions" : "config");
21
31
  const tabs = computed(() => hasDb.value ? ["sessions", "users", "accounts", "config"] : ["config"]);
22
32
  watch(hasDb, (enabled) => {
@@ -125,7 +135,7 @@ async function deleteConfirmedSession() {
125
135
  </script>
126
136
 
127
137
  <template>
128
- <div class="devtools-shell">
138
+ <div class="devtools-shell" :class="{ dark: isDark }">
129
139
  <header class="devtools-header">
130
140
  <div class="brand">
131
141
  <svg width="60" height="45" viewBox="0 0 60 45" fill="none" class="brand-mark" xmlns="http://www.w3.org/2000/svg">
@@ -375,6 +385,6 @@ async function deleteConfirmedSession() {
375
385
  </div>
376
386
  </template>
377
387
 
378
- <style>
379
- :root{--ba-bg:#fff;--ba-fg:#1f1f1f;--ba-muted:#707070;--ba-border:#e5e5e5;--ba-soft:#f7f7f7;--ba-hover:#efefef;--ba-good:#047857;--ba-bad:#b91c1c}.dark{--ba-bg:#111;--ba-fg:#f4f4f5;--ba-muted:#a1a1aa;--ba-border:#2d2d2d;--ba-soft:#191919;--ba-hover:#242424;--ba-good:#34d399;--ba-bad:#f87171}*{box-sizing:border-box}body{margin:0}button,input{font:inherit}.devtools-shell{background:var(--ba-bg);color:var(--ba-fg);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;min-height:100vh}.devtools-header{border-bottom:1px solid var(--ba-border);gap:16px;justify-content:space-between;min-height:48px;padding:0 16px}.actions,.brand,.confirm,.devtools-header,.links,.pagination,.toolbar,.toolbar-meta{align-items:center;display:flex}.brand{font-weight:600;gap:10px}.brand-mark{height:auto;width:24px}.links a{color:var(--ba-muted);padding:8px 10px;text-decoration:none}.links a:hover{color:var(--ba-fg)}.tabs{border-bottom:1px solid var(--ba-border);display:flex;gap:4px;padding:8px 12px 0}.actions button,.confirm button,.pagination button,.tabs button,.toolbar button{background:transparent;border:1px solid var(--ba-border);color:var(--ba-fg);cursor:pointer;min-height:30px;padding:5px 10px}.tabs button{border-bottom:0}.tabs button.active,button:hover:not(:disabled){background:var(--ba-hover)}button:disabled{color:var(--ba-muted);cursor:default}button.danger{color:var(--ba-bad)}.panel{padding:16px}.section{display:grid;gap:14px}.toolbar{gap:12px;justify-content:space-between}.actions,.confirm,.pagination,.toolbar-meta{gap:8px}input{background:var(--ba-bg);border:1px solid var(--ba-border);color:var(--ba-fg);padding:7px 10px;width:min(340px,100%)}table{border:1px solid var(--ba-border);border-collapse:collapse;width:100%}td,th{border-bottom:1px solid var(--ba-border);padding:9px 10px;text-align:left;vertical-align:top}th{background:var(--ba-soft);font-weight:500}td small,th{color:var(--ba-muted)}td small{display:block;margin-top:3px}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace}.badge{background:var(--ba-soft);border:1px solid var(--ba-border);color:var(--ba-muted);display:inline-flex;padding:2px 8px}.badge.good{color:var(--ba-good)}.badge.bad,.error{color:var(--ba-bad)}.confirm,.empty,.notice{background:var(--ba-soft);border:1px solid var(--ba-border);padding:10px 12px}.empty,.notice{color:var(--ba-muted)}.config-grid{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.config-grid .error,.config-grid .toolbar{grid-column:1/-1}article{background:var(--ba-soft);border:1px solid var(--ba-border)}h2{border-bottom:1px solid var(--ba-border);font-size:13px;padding:10px 12px}h2,pre{margin:0}pre{font-size:12px;line-height:1.5;overflow:auto;padding:12px}.capitalize{text-transform:capitalize}@media (max-width:720px){.devtools-header,.toolbar,.toolbar-meta{align-items:stretch;flex-direction:column}.actions,.tabs{flex-wrap:wrap}table{display:block;overflow-x:auto}}
388
+ <style scoped>
389
+ .devtools-shell{--ba-bg:#fff;--ba-fg:#1f1f1f;--ba-muted:#707070;--ba-border:#e5e5e5;--ba-soft:#f7f7f7;--ba-hover:#efefef;--ba-good:#047857;--ba-bad:#b91c1c}.devtools-shell.dark{--ba-bg:#111;--ba-fg:#f4f4f5;--ba-muted:#a1a1aa;--ba-border:#2d2d2d;--ba-soft:#191919;--ba-hover:#242424;--ba-good:#34d399;--ba-bad:#f87171}*{box-sizing:border-box}button,input{font:inherit}.devtools-shell{background:var(--ba-bg);color:var(--ba-fg);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:13px;min-height:100vh}.devtools-header{border-bottom:1px solid var(--ba-border);gap:16px;justify-content:space-between;min-height:48px;padding:0 16px}.actions,.brand,.confirm,.devtools-header,.links,.pagination,.toolbar,.toolbar-meta{align-items:center;display:flex}.brand{font-weight:600;gap:10px}.brand-mark{height:auto;width:24px}.links a{color:var(--ba-muted);padding:8px 10px;text-decoration:none}.links a:hover{color:var(--ba-fg)}.tabs{border-bottom:1px solid var(--ba-border);display:flex;gap:4px;padding:8px 12px 0}.actions button,.confirm button,.pagination button,.tabs button,.toolbar button{background:transparent;border:1px solid var(--ba-border);color:var(--ba-fg);cursor:pointer;min-height:30px;padding:5px 10px}.tabs button{border-bottom:0}.tabs button.active,button:hover:not(:disabled){background:var(--ba-hover)}button:disabled{color:var(--ba-muted);cursor:default}button.danger{color:var(--ba-bad)}.panel{padding:16px}.section{display:grid;gap:14px}.toolbar{gap:12px;justify-content:space-between}.actions,.confirm,.pagination,.toolbar-meta{gap:8px}input{background:var(--ba-bg);border:1px solid var(--ba-border);color:var(--ba-fg);padding:7px 10px;width:min(340px,100%)}table{border:1px solid var(--ba-border);border-collapse:collapse;width:100%}td,th{border-bottom:1px solid var(--ba-border);padding:9px 10px;text-align:left;vertical-align:top}th{background:var(--ba-soft);font-weight:500}td small,th{color:var(--ba-muted)}td small{display:block;margin-top:3px}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace}.badge{background:var(--ba-soft);border:1px solid var(--ba-border);color:var(--ba-muted);display:inline-flex;padding:2px 8px}.badge.good{color:var(--ba-good)}.badge.bad,.error{color:var(--ba-bad)}.confirm,.empty,.notice{background:var(--ba-soft);border:1px solid var(--ba-border);padding:10px 12px}.empty,.notice{color:var(--ba-muted)}.config-grid{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.config-grid .error,.config-grid .toolbar{grid-column:1/-1}article{background:var(--ba-soft);border:1px solid var(--ba-border)}h2{border-bottom:1px solid var(--ba-border);font-size:13px;padding:10px 12px}h2,pre{margin:0}pre{font-size:12px;line-height:1.5;overflow:auto;padding:12px}.capitalize{text-transform:capitalize}@media (max-width:720px){.devtools-header,.toolbar,.toolbar-meta{align-items:stretch;flex-direction:column}.actions,.tabs{flex-wrap:wrap}table{display:block;overflow-x:auto}}
380
390
  </style>
@@ -12,7 +12,7 @@ export interface ClientAuthContext {
12
12
  export type ServerAuthConfig = Omit<BetterAuthOptions, 'secret' | 'baseURL' | 'basePath'> & {
13
13
  /** The module registers its server handler at /api/auth. */
14
14
  basePath?: '/api/auth';
15
- plugins?: readonly BetterAuthPlugin[];
15
+ plugins?: readonly unknown[];
16
16
  };
17
17
  export type ClientAuthConfig = Omit<BetterAuthClientOptions, 'baseURL'> & {
18
18
  baseURL?: string;
@@ -108,8 +108,11 @@ export interface AuthRuntimeConfig {
108
108
  export interface AuthPrivateRuntimeConfig {
109
109
  hubSecondaryStorage: boolean | 'custom';
110
110
  }
111
- export declare function defineServerAuth<const R>(config: (ctx: ServerAuthContext) => R & ServerAuthConfig): (ctx: ServerAuthContext) => R;
112
- export declare function defineServerAuth<const R>(config: R & ServerAuthConfig & (R extends (...args: never[]) => unknown ? never : unknown)): (ctx: ServerAuthContext) => R;
111
+ export declare function defineServerAuth<const R extends ServerAuthConfig>(config: (ctx: ServerAuthContext) => R): (ctx: ServerAuthContext) => R;
112
+ export declare function defineServerAuth<const R>(config: (ctx: ServerAuthContext) => R & ServerAuthConfig & (R extends {
113
+ basePath: infer B;
114
+ } ? B extends '/api/auth' ? unknown : never : unknown)): (ctx: ServerAuthContext) => R;
115
+ export declare function defineServerAuth<const R extends ServerAuthConfig>(config: R & (R extends (...args: never[]) => unknown ? never : unknown)): (ctx: ServerAuthContext) => R;
113
116
  export declare function extendServerAuth<const R extends ServerAuthConfig, const P extends readonly BetterAuthPlugin[]>(createConfig: (ctx: ServerAuthContext) => R, plugins: P): (ctx: ServerAuthContext) => ExtendedServerAuthConfig<R, P>;
114
117
  export declare function defineClientAuth<T extends ClientAuthConfig>(config: T | ((ctx: ClientAuthContext) => T)): ClientAuthFactory<T>;
115
118
  export declare function extendClientAuth<T extends ClientAuthConfig, const P extends BetterAuthClientPlugin[]>(createClient: ClientAuthFactory<T>, plugins: P): ClientAuthFactory<ExtendedClientAuthConfig<T, P>>;
@@ -2,7 +2,9 @@ import { withoutBase } from "ufo";
2
2
  const internalAssetPaths = ["/_fonts", "/_ipx", "/_nuxt", "/api/_nuxt_icon"];
3
3
  export function isInternalAssetPath(path) {
4
4
  const pathname = path.split(/[?#]/, 1)[0] || "/";
5
- return internalAssetPaths.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
5
+ return internalAssetPaths.some((prefix) => {
6
+ return pathname === prefix || pathname.startsWith(`${prefix}/`) || pathname.endsWith(prefix) || pathname.includes(`${prefix}/`);
7
+ });
6
8
  }
7
9
  const internalRouteRuleAuthPaths = /* @__PURE__ */ new Set([
8
10
  "/__better-auth-devtools",
@@ -1,10 +1,9 @@
1
1
  import type { H3Event } from 'h3';
2
2
  import type { AuthRouteRules } from '../../types.js';
3
- import { splitSetCookieString } from 'cookie-es';
4
3
  import { defineEventHandler, getQuery, getRequestHost, getRequestProtocol, getRequestURL, readBody, toWebRequest } from 'h3';
5
4
  import { useRuntimeConfig } from 'nitropack/runtime';
6
5
  export { defineEventHandler, getQuery, getRequestHost, getRequestProtocol, getRequestURL, readBody, toWebRequest, useRuntimeConfig, };
7
6
  export type ServerEvent = H3Event;
8
- export { splitSetCookieString as splitCookiesString };
7
+ export declare function splitCookiesString(cookiesString: string | string[]): string[];
9
8
  export declare function getAuthRouteRules(event: ServerEvent): AuthRouteRules;
10
9
  export declare function createAuthError(status: number, statusText: string): Error;
@@ -20,7 +20,9 @@ export {
20
20
  toWebRequest,
21
21
  useRuntimeConfig
22
22
  };
23
- export { splitSetCookieString as splitCookiesString };
23
+ export function splitCookiesString(cookiesString) {
24
+ return splitSetCookieString(cookiesString);
25
+ }
24
26
  export function getAuthRouteRules(event) {
25
27
  return getRouteRules(event);
26
28
  }
@@ -1,10 +1,9 @@
1
1
  import type { RequestEvent } from 'nuxt/server';
2
2
  import type { AuthRouteRules } from '../../types.js';
3
- import { splitSetCookieString } from 'cookie-es';
4
3
  import { defineEventHandler, getQuery, getRequestHost, getRequestProtocol, getRequestURL, readBody, useRuntimeConfig } from 'nuxt/server';
5
4
  export { defineEventHandler, getQuery, getRequestHost, getRequestProtocol, getRequestURL, readBody, useRuntimeConfig, };
6
5
  export type ServerEvent = RequestEvent;
7
- export { splitSetCookieString as splitCookiesString };
6
+ export declare function splitCookiesString(cookiesString: string | string[]): string[];
8
7
  export declare function getAuthRouteRules(event: ServerEvent): AuthRouteRules;
9
8
  export declare function createAuthError(status: number, statusText: string): Error;
10
9
  export declare function toWebRequest(event: ServerEvent): Request;
@@ -20,7 +20,9 @@ export {
20
20
  readBody,
21
21
  useRuntimeConfig
22
22
  };
23
- export { splitSetCookieString as splitCookiesString };
23
+ export function splitCookiesString(cookiesString) {
24
+ return splitSetCookieString(cookiesString);
25
+ }
24
26
  export function getAuthRouteRules(event) {
25
27
  const rules = getRouteRules(event);
26
28
  return { auth: normalizeAuthRouteRule(rules.auth) };
@@ -9,13 +9,18 @@ type UserAuthConfig = AuthOptions & {
9
9
  trustedOrigins?: BetterAuthOptions['trustedOrigins'];
10
10
  secondaryStorage?: BetterAuthOptions['secondaryStorage'];
11
11
  };
12
- type ResolvedAuthOptions = Omit<UserAuthConfig, 'secret' | 'baseURL' | 'trustedOrigins' | 'database'> & {
12
+ type ResolvedAuthOptions = Omit<UserAuthConfig, 'secret' | 'baseURL' | 'trustedOrigins' | 'database' | 'plugins'> & {
13
+ plugins?: BetterAuthOptions['plugins'];
13
14
  secret: string;
14
15
  baseURL: string;
15
16
  trustedOrigins?: BetterAuthOptions['trustedOrigins'];
16
17
  database?: BetterAuthOptions['database'];
17
18
  };
18
- type AuthInstance = ReturnType<typeof betterAuth<ResolvedAuthOptions>>;
19
+ type AuthInstance = ReturnType<typeof betterAuth<ResolvedAuthOptions>> & {
20
+ api: ReturnType<typeof betterAuth<ResolvedAuthOptions>>['api'] & {
21
+ signInUsername?: (...args: unknown[]) => unknown;
22
+ };
23
+ };
19
24
  /**
20
25
  * Returns the Better Auth instance like `serverAuth()`, after waiting for the auth secret that
21
26
  * Nuxt 4.6+ derives from `appSecret` (`NUXT_APP_SECRET`). Use it in code that can run before the
@@ -274,6 +274,7 @@ export function serverAuth(event) {
274
274
  }
275
275
  const authOptions = {
276
276
  ...userConfig,
277
+ ...userConfig.plugins ? { plugins: [...userConfig.plugins] } : {},
277
278
  ...rateLimit ? { rateLimit } : {},
278
279
  ...database ? { database } : {},
279
280
  secret: betterAuthSecret,
@@ -7,6 +7,8 @@ export interface AuthUser {
7
7
  emailVerified: boolean;
8
8
  name: string;
9
9
  image?: string | null;
10
+ /** Whether the user has enabled two-factor authentication. */
11
+ twoFactorEnabled?: boolean;
10
12
  }
11
13
  export interface AuthSession {
12
14
  id: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtjs/better-auth",
3
3
  "type": "module",
4
- "version": "0.3.9",
4
+ "version": "0.3.11",
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",
@@ -94,7 +94,7 @@
94
94
  "@libsql/client": "^0.18.0",
95
95
  "@libsql/linux-x64-gnu": "0.5.29",
96
96
  "@nuxt/devtools": "^3.4.1",
97
- "@nuxt/fonts": "0.14.0",
97
+ "@nuxt/fonts": "1.0.1",
98
98
  "@nuxt/module-builder": "^1.0.3",
99
99
  "@nuxt/schema": "^4.6.0",
100
100
  "@nuxt/test-utils": "^4.3.3",
@@ -102,25 +102,25 @@
102
102
  "@pinia/nuxt": "^1.0.2",
103
103
  "@types/better-sqlite3": "^9.6.0",
104
104
  "@vue/test-utils": "^2.4.6",
105
- "better-auth": "1.7.7",
106
- "better-call": "1.4.0",
105
+ "better-auth": "1.7.10",
106
+ "better-call": "1.4.2",
107
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
- "happy-dom": "^20.8.9",
111
+ "happy-dom": "^21.0.0",
112
112
  "nitropack": "2.13.4",
113
113
  "nuxt": "^4.6.0",
114
114
  "pinia": "^4.0.3",
115
- "pkg-pr-new": "0.0.88",
116
- "playwright-core": "1.63.0",
115
+ "pkg-pr-new": "0.0.91",
116
+ "playwright-core": "1.64.1",
117
117
  "postgres": "^3.4.9",
118
118
  "typescript": "~5.9.3",
119
119
  "unstorage": "^1.17.5",
120
120
  "vitest": "^5.0.0",
121
- "vue": "3.5.43",
121
+ "vue": "3.5.44",
122
122
  "vue-tsc": "^3.3.10",
123
- "wrangler": "4.147.0",
123
+ "wrangler": "4.151.1",
124
124
  "yaml": "^2.9.0"
125
125
  },
126
126
  "scripts": {