@onmax/nuxt-better-auth 0.0.2-alpha.13 → 0.0.2-alpha.14

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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "0.0.2-alpha.13",
7
+ "version": "0.0.2-alpha.14",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -5,7 +5,7 @@ import { consola as consola$1 } from 'consola';
5
5
  import { defu } from 'defu';
6
6
  import { join } from 'pathe';
7
7
  import { toRouteMatcher, createRouter } from 'radix3';
8
- import pluralize from 'pluralize';
8
+ import { generateDrizzleSchema as generateDrizzleSchema$1 } from '@better-auth/cli/api';
9
9
  export { defineClientAuth, defineServerAuth } from '../dist/runtime/config.js';
10
10
 
11
11
  function setupDevTools(nuxt) {
@@ -23,160 +23,51 @@ function setupDevTools(nuxt) {
23
23
  });
24
24
  }
25
25
 
26
- function toSnakeCase(str) {
27
- return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
26
+ function dialectToProvider(dialect) {
27
+ return dialect === "postgresql" ? "pg" : dialect;
28
28
  }
29
- function generateDrizzleSchema(tables, dialect, options) {
30
- const typedTables = tables;
31
- const imports = getImports(dialect, options);
32
- const tableDefinitions = Object.entries(typedTables).map(([tableName, table]) => generateTable(tableName, table, dialect, typedTables, options)).join("\n\n");
33
- return `${imports}
34
-
35
- ${tableDefinitions}
36
- `;
37
- }
38
- function getImports(dialect, options) {
39
- switch (dialect) {
40
- case "sqlite":
41
- return `import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'`;
42
- case "postgresql":
43
- return options?.useUuid ? `import { boolean, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'` : `import { boolean, integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core'`;
44
- case "mysql":
45
- return `import { boolean, int, mysqlTable, text, timestamp, varchar } from 'drizzle-orm/mysql-core'`;
46
- }
47
- }
48
- function generateTable(tableName, table, dialect, allTables, options) {
49
- const tableFunc = dialect === "sqlite" ? "sqliteTable" : dialect === "postgresql" ? "pgTable" : "mysqlTable";
50
- const hasCustomModelName = table.modelName && table.modelName !== tableName;
51
- let dbTableName = hasCustomModelName ? table.modelName : tableName;
52
- if (options?.casing === "snake_case" && !hasCustomModelName)
53
- dbTableName = toSnakeCase(dbTableName);
54
- if (options?.usePlural && !hasCustomModelName)
55
- dbTableName = pluralize(dbTableName);
56
- const fields = Object.entries(table.fields).map(([fieldName, field]) => generateField(fieldName, field, dialect, allTables, options)).join(",\n ");
57
- const idField = generateIdField(dialect, options);
58
- return `export const ${tableName} = ${tableFunc}('${dbTableName}', {
59
- ${idField},
60
- ${fields}
61
- })`;
62
- }
63
- function generateIdField(dialect, options) {
64
- switch (dialect) {
65
- case "sqlite":
66
- if (options?.useUuid)
67
- consola$1.warn("[@onmax/nuxt-better-auth] useUuid ignored for SQLite (no native uuid type). Using text.");
68
- return `id: text('id').primaryKey()`;
69
- case "postgresql":
70
- return options?.useUuid ? `id: uuid('id').defaultRandom().primaryKey()` : `id: text('id').primaryKey()`;
71
- case "mysql":
72
- return `id: varchar('id', { length: 36 }).primaryKey()`;
73
- }
74
- }
75
- function generateField(fieldName, field, dialect, allTables, options) {
76
- const dbFieldName = options?.casing === "snake_case" ? toSnakeCase(fieldName) : fieldName;
77
- const isFkToId = options?.useUuid && field.references?.field === "id";
78
- let fieldDef;
79
- if (isFkToId && dialect === "postgresql")
80
- fieldDef = `uuid('${dbFieldName}')`;
81
- else if (isFkToId && dialect === "mysql")
82
- fieldDef = `varchar('${dbFieldName}', { length: 36 })`;
83
- else
84
- fieldDef = getFieldType(field.type, dialect, dbFieldName);
85
- if (field.required && field.defaultValue === void 0)
86
- fieldDef += ".notNull()";
87
- if (field.unique)
88
- fieldDef += ".unique()";
89
- if (field.defaultValue !== void 0) {
90
- if (typeof field.defaultValue === "boolean")
91
- fieldDef += `.default(${field.defaultValue})`;
92
- else if (typeof field.defaultValue === "string")
93
- fieldDef += `.default('${field.defaultValue}')`;
94
- else if (typeof field.defaultValue === "function")
95
- fieldDef += `.$defaultFn(${field.defaultValue})`;
96
- else
97
- fieldDef += `.default(${field.defaultValue})`;
98
- if (field.required)
99
- fieldDef += ".notNull()";
100
- }
101
- if (typeof field.onUpdate === "function" && field.type === "date")
102
- fieldDef += `.$onUpdate(${field.onUpdate})`;
103
- if (field.references) {
104
- const refTable = field.references.model;
105
- if (allTables[refTable])
106
- fieldDef += `.references(() => ${refTable}.${field.references.field})`;
107
- }
108
- return `${fieldName}: ${fieldDef}`;
109
- }
110
- function getFieldType(type, dialect, fieldName) {
111
- const normalizedType = Array.isArray(type) ? "string" : type;
112
- switch (dialect) {
113
- case "sqlite":
114
- return getSqliteType(normalizedType, fieldName);
115
- case "postgresql":
116
- return getPostgresType(normalizedType, fieldName);
117
- case "mysql":
118
- return getMysqlType(normalizedType, fieldName);
119
- }
120
- }
121
- function getSqliteType(type, fieldName) {
122
- switch (type) {
123
- case "string":
124
- return `text('${fieldName}')`;
125
- case "boolean":
126
- return `integer('${fieldName}', { mode: 'boolean' })`;
127
- case "date":
128
- return `integer('${fieldName}', { mode: 'timestamp' })`;
129
- case "number":
130
- return `integer('${fieldName}')`;
131
- default:
132
- return `text('${fieldName}')`;
133
- }
134
- }
135
- function getPostgresType(type, fieldName) {
136
- switch (type) {
137
- case "string":
138
- return `text('${fieldName}')`;
139
- case "boolean":
140
- return `boolean('${fieldName}')`;
141
- case "date":
142
- return `timestamp('${fieldName}')`;
143
- case "number":
144
- return `integer('${fieldName}')`;
145
- default:
146
- return `text('${fieldName}')`;
147
- }
148
- }
149
- function getMysqlType(type, fieldName) {
150
- switch (type) {
151
- case "string":
152
- return `text('${fieldName}')`;
153
- case "boolean":
154
- return `boolean('${fieldName}')`;
155
- case "date":
156
- return `timestamp('${fieldName}')`;
157
- case "number":
158
- return `int('${fieldName}')`;
159
- default:
160
- return `text('${fieldName}')`;
29
+ async function generateDrizzleSchema(authOptions, dialect, schemaOptions) {
30
+ const provider = dialectToProvider(dialect);
31
+ const options = {
32
+ ...authOptions,
33
+ advanced: {
34
+ ...authOptions.advanced,
35
+ database: {
36
+ ...authOptions.advanced?.database,
37
+ ...schemaOptions?.useUuid && { generateId: "uuid" }
38
+ }
39
+ }
40
+ };
41
+ const adapter = {
42
+ id: "drizzle",
43
+ options: {
44
+ provider,
45
+ camelCase: schemaOptions?.casing !== "snake_case",
46
+ adapterConfig: { usePlural: schemaOptions?.usePlural ?? false }
47
+ }
48
+ };
49
+ const result = await generateDrizzleSchema$1({ adapter, options });
50
+ if (!result.code) {
51
+ throw new Error(`Schema generation returned empty result for ${dialect}`);
161
52
  }
53
+ return result.code;
162
54
  }
163
55
  async function loadUserAuthConfig(configPath, throwOnError = false) {
164
56
  const { createJiti } = await import('jiti');
165
57
  const { defineServerAuth } = await import('../dist/runtime/config.js');
166
58
  const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
167
- const key = "defineServerAuth";
168
- const g = globalThis;
169
- if (!g[key]) {
59
+ if (!globalThis.defineServerAuth) {
170
60
  defineServerAuth._count = 0;
171
- g[key] = defineServerAuth;
61
+ globalThis.defineServerAuth = defineServerAuth;
172
62
  }
173
- g[key]._count++;
63
+ globalThis.defineServerAuth._count++;
174
64
  try {
175
65
  const mod = await jiti.import(configPath);
176
66
  const configFn = typeof mod === "object" && mod !== null && "default" in mod ? mod.default : mod;
177
67
  if (typeof configFn === "function") {
178
68
  return configFn({ runtimeConfig: {}, db: null });
179
69
  }
70
+ consola$1.warn("[@onmax/nuxt-better-auth] auth.config.ts does not export a function. Expected: export default defineServerAuth(...)");
180
71
  if (throwOnError) {
181
72
  throw new Error("auth.config.ts must export default defineServerAuth(...)");
182
73
  }
@@ -188,9 +79,9 @@ async function loadUserAuthConfig(configPath, throwOnError = false) {
188
79
  consola$1.error("[@onmax/nuxt-better-auth] Failed to load auth config for schema generation. Schema may be incomplete:", error);
189
80
  return {};
190
81
  } finally {
191
- g[key]._count--;
192
- if (!g[key]._count) {
193
- delete g[key];
82
+ globalThis.defineServerAuth._count--;
83
+ if (!globalThis.defineServerAuth._count) {
84
+ globalThis.defineServerAuth = void 0;
194
85
  }
195
86
  }
196
87
  }
@@ -257,14 +148,15 @@ const module$1 = defineNuxtModule({
257
148
  consola.info("clientOnly mode enabled - server utilities (serverAuth, getUserSession, requireUserSession) are not available");
258
149
  }
259
150
  if (!clientOnly) {
260
- const betterAuthSecret = process.env.BETTER_AUTH_SECRET || process.env.NUXT_BETTER_AUTH_SECRET || nuxt.options.runtimeConfig.betterAuthSecret || "";
151
+ const currentSecret = nuxt.options.runtimeConfig.betterAuthSecret;
152
+ nuxt.options.runtimeConfig.betterAuthSecret = currentSecret || process.env.BETTER_AUTH_SECRET || "";
153
+ const betterAuthSecret = nuxt.options.runtimeConfig.betterAuthSecret;
261
154
  if (!nuxt.options.dev && !betterAuthSecret) {
262
155
  throw new Error("[nuxt-better-auth] BETTER_AUTH_SECRET is required in production. Set BETTER_AUTH_SECRET or NUXT_BETTER_AUTH_SECRET environment variable.");
263
156
  }
264
157
  if (betterAuthSecret && betterAuthSecret.length < 32) {
265
158
  throw new Error("[nuxt-better-auth] BETTER_AUTH_SECRET must be at least 32 characters for security");
266
159
  }
267
- nuxt.options.runtimeConfig.betterAuthSecret = betterAuthSecret;
268
160
  nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth, {
269
161
  secondaryStorage: secondaryStorageEnabled
270
162
  });
@@ -465,27 +357,22 @@ async function setupBetterAuthSchema(nuxt, serverConfigPath, options) {
465
357
  const extendedConfig = {};
466
358
  await nuxt.callHook("better-auth:config:extend", extendedConfig);
467
359
  const plugins = [...userConfig.plugins || [], ...extendedConfig.plugins || []];
468
- const { getAuthTables } = await import('better-auth/db');
469
- const tables = getAuthTables({
360
+ const authOptions = {
361
+ ...userConfig,
470
362
  plugins,
471
- secondaryStorage: options.secondaryStorage ? {
472
- get: async (_key) => null,
473
- set: async (_key, _value, _ttl) => {
474
- },
475
- delete: async (_key) => {
476
- }
477
- } : void 0
478
- });
479
- const useUuid = userConfig.advanced?.database?.generateId === "uuid";
363
+ secondaryStorage: options.secondaryStorage ? { get: async (_key) => null, set: async (_key, _value, _ttl) => {
364
+ }, delete: async (_key) => {
365
+ } } : void 0
366
+ };
480
367
  const hubCasing = getHubCasing(hub);
481
- const schemaOptions = { ...options.schema, useUuid, casing: options.schema?.casing ?? hubCasing };
482
- const schemaCode = generateDrizzleSchema(tables, dialect, schemaOptions);
368
+ const schemaOptions = { ...options.schema, useUuid: userConfig.advanced?.database?.generateId === "uuid", casing: options.schema?.casing ?? hubCasing };
369
+ const schemaCode = await generateDrizzleSchema(authOptions, dialect, schemaOptions);
483
370
  const schemaDir = join(nuxt.options.buildDir, "better-auth");
484
371
  const schemaPath = join(schemaDir, `schema.${dialect}.ts`);
485
372
  await mkdir(schemaDir, { recursive: true });
486
373
  await writeFile(schemaPath, schemaCode);
487
374
  addTemplate({ filename: `better-auth/schema.${dialect}.ts`, getContents: () => schemaCode, write: true });
488
- consola.info(`Generated ${dialect} schema with ${Object.keys(tables).length} tables`);
375
+ consola.info(`Generated ${dialect} schema`);
489
376
  } catch (error) {
490
377
  if (isProduction) {
491
378
  throw error;
@@ -2,8 +2,9 @@ import { createDatabase, db } from "#auth/database";
2
2
  import { createSecondaryStorage } from "#auth/secondary-storage";
3
3
  import createServerAuth from "#auth/server";
4
4
  import { betterAuth } from "better-auth";
5
- import { getRequestURL } from "h3";
5
+ import { getRequestHost, getRequestProtocol } from "h3";
6
6
  import { useRuntimeConfig } from "nitropack/runtime";
7
+ import { withoutProtocol } from "ufo";
7
8
  let _auth = null;
8
9
  function validateURL(url) {
9
10
  try {
@@ -12,12 +13,46 @@ function validateURL(url) {
12
13
  throw new Error(`Invalid siteUrl: "${url}". Must be a valid URL.`);
13
14
  }
14
15
  }
16
+ function getNitroOrigin(e) {
17
+ const cert = process.env.NITRO_SSL_CERT;
18
+ const key = process.env.NITRO_SSL_KEY;
19
+ let host = process.env.NITRO_HOST || process.env.HOST;
20
+ let port;
21
+ if (import.meta.dev)
22
+ port = process.env.NITRO_PORT || process.env.PORT || "3000";
23
+ let protocol = cert && key || !import.meta.dev ? "https" : "http";
24
+ try {
25
+ if ((import.meta.dev || import.meta.prerender) && process.env.__NUXT_DEV__) {
26
+ const origin = JSON.parse(process.env.__NUXT_DEV__).proxy.url;
27
+ host = withoutProtocol(origin);
28
+ protocol = origin.includes("https") ? "https" : "http";
29
+ } else if ((import.meta.dev || import.meta.prerender) && process.env.NUXT_VITE_NODE_OPTIONS) {
30
+ const origin = JSON.parse(process.env.NUXT_VITE_NODE_OPTIONS).baseURL.replace("/__nuxt_vite_node__", "");
31
+ host = withoutProtocol(origin);
32
+ protocol = origin.includes("https") ? "https" : "http";
33
+ } else if (e) {
34
+ host = getRequestHost(e, { xForwardedHost: true }) || host;
35
+ protocol = getRequestProtocol(e, { xForwardedProto: true }) || protocol;
36
+ }
37
+ } catch {
38
+ }
39
+ if (!host)
40
+ return void 0;
41
+ if (host.includes(":") && !host.startsWith("[")) {
42
+ const hostParts = host.split(":");
43
+ port = hostParts.pop();
44
+ host = hostParts.join(":");
45
+ }
46
+ const portSuffix = port ? `:${port}` : "";
47
+ return `${protocol}://${host}${portSuffix}`;
48
+ }
15
49
  function getBaseURL(event) {
16
50
  const config = useRuntimeConfig();
17
51
  if (config.public.siteUrl && typeof config.public.siteUrl === "string")
18
52
  return validateURL(config.public.siteUrl);
19
- if (event)
20
- return getRequestURL(event).origin;
53
+ const nitroOrigin = getNitroOrigin(event);
54
+ if (nitroOrigin)
55
+ return validateURL(nitroOrigin);
21
56
  if (process.env.VERCEL_URL)
22
57
  return validateURL(`https://${process.env.VERCEL_URL}`);
23
58
  if (process.env.CF_PAGES_URL)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@onmax/nuxt-better-auth",
3
3
  "type": "module",
4
- "version": "0.0.2-alpha.13",
4
+ "version": "0.0.2-alpha.14",
5
5
  "packageManager": "pnpm@10.15.1",
6
6
  "description": "Nuxt module for Better Auth integration with NuxtHub, route protection, session management, and role-based access",
7
7
  "author": "onmax",
@@ -10,6 +10,14 @@
10
10
  "type": "git",
11
11
  "url": "https://github.com/onmax/nuxt-better-auth"
12
12
  },
13
+ "agents": {
14
+ "skills": [
15
+ {
16
+ "name": "nuxt-better-auth",
17
+ "path": "./skills/nuxt-better-auth"
18
+ }
19
+ ]
20
+ },
13
21
  "exports": {
14
22
  ".": {
15
23
  "types": "./dist/types.d.mts",
@@ -32,7 +40,8 @@
32
40
  }
33
41
  },
34
42
  "files": [
35
- "dist"
43
+ "dist",
44
+ "skills"
36
45
  ],
37
46
  "scripts": {
38
47
  "prepack": "nuxt-module-build build",
@@ -59,16 +68,15 @@
59
68
  }
60
69
  },
61
70
  "dependencies": {
71
+ "@better-auth/cli": "^1.5.0-beta.3",
62
72
  "@nuxt/kit": "^4.2.2",
63
73
  "defu": "^6.1.4",
64
74
  "jiti": "^2.4.2",
65
75
  "pathe": "^2.0.3",
66
- "pluralize": "^8.0.0",
67
76
  "radix3": "^1.1.2"
68
77
  },
69
78
  "devDependencies": {
70
79
  "@antfu/eslint-config": "^4.12.0",
71
- "@better-auth/cli": "^1.4.6",
72
80
  "@libsql/client": "^0.15.15",
73
81
  "@nuxt/devtools": "^3.1.1",
74
82
  "@nuxt/devtools-kit": "^3.1.1",
@@ -78,8 +86,7 @@
78
86
  "@nuxthub/core": "^0.10.3",
79
87
  "@types/better-sqlite3": "^7.6.13",
80
88
  "@types/node": "latest",
81
- "@types/pluralize": "^0.0.33",
82
- "better-auth": "^1.4.7",
89
+ "better-auth": "^1.5.0-beta.3",
83
90
  "better-sqlite3": "^11.9.1",
84
91
  "bumpp": "^10.3.2",
85
92
  "changelogen": "^0.6.2",
@@ -87,6 +94,7 @@
87
94
  "drizzle-kit": "^0.31.8",
88
95
  "drizzle-orm": "^0.38.4",
89
96
  "eslint": "^9.39.1",
97
+ "npm-agentskills": "https://pkg.pr.new/onmax/npm-agentskills@394499e",
90
98
  "nuxt": "^4.2.2",
91
99
  "tinyexec": "^1.0.2",
92
100
  "typescript": "~5.9.3",
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: nuxt-better-auth
3
+ description: Use when implementing auth in Nuxt apps with @onmax/nuxt-better-auth - provides useUserSession composable, server auth helpers, route protection, and Better Auth plugins integration.
4
+ license: MIT
5
+ ---
6
+
7
+ # Nuxt Better Auth
8
+
9
+ Authentication module for Nuxt 4+ built on [Better Auth](https://www.better-auth.com/). Provides composables, server utilities, and route protection.
10
+
11
+ > **Alpha Status**: This module is currently in alpha (v0.0.2-alpha.12) and not recommended for production use. APIs may change.
12
+
13
+ ## When to Use
14
+
15
+ - Installing/configuring `@onmax/nuxt-better-auth`
16
+ - Implementing login/signup/signout flows
17
+ - Protecting routes (client and server)
18
+ - Accessing user session in API routes
19
+ - Integrating Better Auth plugins (admin, passkey, 2FA)
20
+ - Setting up database with NuxtHub
21
+ - Using clientOnly mode for external auth backends
22
+
23
+ **For Nuxt patterns:** use `nuxt` skill
24
+ **For NuxtHub database:** use `nuxthub` skill
25
+
26
+ ## Available Guidance
27
+
28
+ | File | Topics |
29
+ | -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
30
+ | **[references/installation.md](references/installation.md)** | Module setup, env vars, config files |
31
+ | **[references/client-auth.md](references/client-auth.md)** | useUserSession, signIn/signUp/signOut, BetterAuthState, safe redirects |
32
+ | **[references/server-auth.md](references/server-auth.md)** | serverAuth, getUserSession, requireUserSession |
33
+ | **[references/route-protection.md](references/route-protection.md)** | routeRules, definePageMeta, middleware |
34
+ | **[references/plugins.md](references/plugins.md)** | Better Auth plugins (admin, passkey, 2FA) |
35
+ | **[references/database.md](references/database.md)** | NuxtHub integration, Drizzle schema |
36
+ | **[references/client-only.md](references/client-only.md)** | External auth backend, clientOnly mode, CORS |
37
+ | **[references/types.md](references/types.md)** | AuthUser, AuthSession, type augmentation |
38
+
39
+ ## Usage Pattern
40
+
41
+ **Load based on context:**
42
+
43
+ - Installing module? → [references/installation.md](references/installation.md)
44
+ - Login/signup forms? → [references/client-auth.md](references/client-auth.md)
45
+ - API route protection? → [references/server-auth.md](references/server-auth.md)
46
+ - Route rules/page meta? → [references/route-protection.md](references/route-protection.md)
47
+ - Using plugins? → [references/plugins.md](references/plugins.md)
48
+ - Database setup? → [references/database.md](references/database.md)
49
+ - External auth backend? → [references/client-only.md](references/client-only.md)
50
+ - TypeScript types? → [references/types.md](references/types.md)
51
+
52
+ **DO NOT read all files at once.** Load based on context.
53
+
54
+ ## Key Concepts
55
+
56
+ | Concept | Description |
57
+ | ---------------------- | --------------------------------------------------------------- |
58
+ | `useUserSession()` | Client composable - user, session, loggedIn, signIn/Out methods |
59
+ | `requireUserSession()` | Server helper - throws 401/403 if not authenticated |
60
+ | `auth` route mode | `'user'`, `'guest'`, `{ user: {...} }`, or `false` |
61
+ | `serverAuth()` | Get Better Auth instance in server routes |
62
+
63
+ ## Quick Reference
64
+
65
+ ```ts
66
+ // Client: useUserSession()
67
+ const { user, loggedIn, signIn, signOut } = useUserSession()
68
+ await signIn.email({ email, password }, { onSuccess: () => navigateTo('/') })
69
+ ```
70
+
71
+ ```ts
72
+ // Server: requireUserSession()
73
+ const { user } = await requireUserSession(event, { user: { role: 'admin' } })
74
+ ```
75
+
76
+ ```ts
77
+ // nuxt.config.ts: Route protection
78
+ routeRules: {
79
+ '/admin/**': { auth: { user: { role: 'admin' } } },
80
+ '/login': { auth: 'guest' },
81
+ '/app/**': { auth: 'user' }
82
+ }
83
+ ```
84
+
85
+ ## Resources
86
+
87
+ - [Module Docs](https://github.com/onmax/nuxt-better-auth)
88
+ - [Better Auth Docs](https://www.better-auth.com/)
89
+
90
+ ---
91
+
92
+ _Token efficiency: Main skill ~300 tokens, each sub-file ~800-1200 tokens_
@@ -0,0 +1,153 @@
1
+ # Client-Side Authentication
2
+
3
+ ## useUserSession()
4
+
5
+ Main composable for auth state and methods.
6
+
7
+ ```ts
8
+ const {
9
+ user, // Ref<AuthUser | null>
10
+ session, // Ref<AuthSession | null>
11
+ loggedIn, // ComputedRef<boolean>
12
+ ready, // ComputedRef<boolean> - session fetch complete
13
+ client, // Better Auth client (client-side only)
14
+ signIn, // Proxy to client.signIn
15
+ signUp, // Proxy to client.signUp
16
+ signOut, // Sign out and clear session
17
+ fetchSession, // Manually refresh session
18
+ updateUser // Optimistic local user update
19
+ } = useUserSession()
20
+ ```
21
+
22
+ ## Sign In
23
+
24
+ ```ts
25
+ // Email/password
26
+ await signIn.email({
27
+ email: 'user@example.com',
28
+ password: 'password123'
29
+ }, {
30
+ onSuccess: () => navigateTo('/dashboard')
31
+ })
32
+
33
+ // OAuth
34
+ await signIn.social({ provider: 'github' })
35
+ ```
36
+
37
+ ## Sign Up
38
+
39
+ ```ts
40
+ await signUp.email({
41
+ email: 'user@example.com',
42
+ password: 'password123',
43
+ name: 'John Doe'
44
+ }, {
45
+ onSuccess: () => navigateTo('/welcome')
46
+ })
47
+ ```
48
+
49
+ ## Sign Out
50
+
51
+ ```ts
52
+ await signOut()
53
+ // or with redirect
54
+ await signOut({ redirect: '/login' })
55
+ ```
56
+
57
+ ## Check Auth State
58
+
59
+ ```vue
60
+ <script setup>
61
+ const { user, loggedIn, ready } = useUserSession()
62
+ </script>
63
+
64
+ <template>
65
+ <div v-if="!ready">Loading...</div>
66
+ <div v-else-if="loggedIn">Welcome, {{ user?.name }}</div>
67
+ <div v-else>Please log in</div>
68
+ </template>
69
+ ```
70
+
71
+ ## Safe Redirects
72
+
73
+ Always validate redirect URLs from query params to prevent open redirects:
74
+
75
+ ```ts
76
+ function getSafeRedirect() {
77
+ const redirect = route.query.redirect as string
78
+ // Must start with / and not // (prevents protocol-relative URLs)
79
+ if (!redirect?.startsWith('/') || redirect.startsWith('//')) {
80
+ return '/'
81
+ }
82
+ return redirect
83
+ }
84
+
85
+ await signIn.email({
86
+ email, password
87
+ }, {
88
+ onSuccess: () => navigateTo(getSafeRedirect())
89
+ })
90
+ ```
91
+
92
+ ## Wait for Session
93
+
94
+ Useful when needing session before rendering:
95
+
96
+ ```ts
97
+ await waitForSession() // 5s timeout
98
+ if (loggedIn.value) {
99
+ // Session is ready
100
+ }
101
+ ```
102
+
103
+ ## Manual Session Refresh
104
+
105
+ ```ts
106
+ // Refetch from server
107
+ await fetchSession({ force: true })
108
+ ```
109
+
110
+ ## Session Management
111
+
112
+ Additional session management via Better Auth client:
113
+
114
+ ```ts
115
+ const { client } = useUserSession()
116
+
117
+ // List all active sessions for current user
118
+ const sessions = await client.listSessions()
119
+
120
+ // Revoke a specific session
121
+ await client.revokeSession({ sessionId: 'xxx' })
122
+
123
+ // Revoke all sessions except current
124
+ await client.revokeOtherSessions()
125
+
126
+ // Revoke all sessions (logs out everywhere)
127
+ await client.revokeSessions()
128
+ ```
129
+
130
+ These methods require the user to be authenticated.
131
+
132
+ ## BetterAuthState Component
133
+
134
+ Renders once session hydration completes (`ready === true`), with loading placeholder support.
135
+
136
+ ```vue
137
+ <BetterAuthState>
138
+ <template #default="{ loggedIn, user, session, signOut }">
139
+ <p v-if="loggedIn">Hi {{ user?.name }}</p>
140
+ <button v-else @click="navigateTo('/login')">Sign in</button>
141
+ </template>
142
+ <template #placeholder>
143
+ <p>Loading…</p>
144
+ </template>
145
+ </BetterAuthState>
146
+ ```
147
+
148
+ **Slots:**
149
+
150
+ - `default` - Renders when `ready === true`, provides `{ loggedIn, user, session, signOut }`
151
+ - `placeholder` - Renders while session hydrates
152
+
153
+ Useful in clientOnly mode or for graceful SSR loading states.