@mastra/auth-better-auth 1.1.2 → 1.1.3

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/index.cjs CHANGED
@@ -1,729 +1,758 @@
1
- 'use strict';
2
-
3
- var kyselyLibsql = require('@libsql/kysely-libsql');
4
- var betterAuth = require('better-auth');
5
- var crypto$1 = require('better-auth/crypto');
6
- var migration = require('better-auth/db/migration');
7
- var plugins = require('better-auth/plugins');
8
-
9
- // ../../packages/_internal-core/dist/chunk-3M4SEWMI.js
10
- var RegisteredLogger = {
11
- LLM: "LLM"};
12
- var LogLevel = {
13
- DEBUG: "debug",
14
- INFO: "info",
15
- WARN: "warn",
16
- ERROR: "error"};
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _libsql_kysely_libsql = require("@libsql/kysely-libsql");
3
+ let better_auth = require("better-auth");
4
+ let better_auth_crypto = require("better-auth/crypto");
5
+ let better_auth_db_migration = require("better-auth/db/migration");
6
+ let better_auth_plugins = require("better-auth/plugins");
7
+ //#region ../../packages/_internal-core/dist/logger/index.js
8
+ const RegisteredLogger = {
9
+ AGENT: "AGENT",
10
+ OBSERVABILITY: "OBSERVABILITY",
11
+ AUTH: "AUTH",
12
+ BROWSER: "BROWSER",
13
+ NETWORK: "NETWORK",
14
+ WORKFLOW: "WORKFLOW",
15
+ LLM: "LLM",
16
+ TTS: "TTS",
17
+ VOICE: "VOICE",
18
+ VECTOR: "VECTOR",
19
+ BUNDLER: "BUNDLER",
20
+ DEPLOYER: "DEPLOYER",
21
+ MEMORY: "MEMORY",
22
+ STORAGE: "STORAGE",
23
+ EMBEDDINGS: "EMBEDDINGS",
24
+ MCP_SERVER: "MCP_SERVER",
25
+ SERVER_CACHE: "SERVER_CACHE",
26
+ SERVER: "SERVER",
27
+ WORKSPACE: "WORKSPACE",
28
+ CHANNEL: "CHANNEL"
29
+ };
30
+ const LogLevel = {
31
+ DEBUG: "debug",
32
+ INFO: "info",
33
+ WARN: "warn",
34
+ ERROR: "error",
35
+ NONE: "silent"
36
+ };
17
37
  var MastraLogger = class {
18
- name;
19
- level;
20
- transports;
21
- constructor(options = {}) {
22
- this.name = options.name || "Mastra";
23
- this.level = options.level || LogLevel.ERROR;
24
- this.transports = new Map(Object.entries(options.transports || {}));
25
- }
26
- getTransports() {
27
- return this.transports;
28
- }
29
- trackException(_error, _metadata) {
30
- }
31
- async listLogs(transportId, params) {
32
- if (!transportId || !this.transports.has(transportId)) {
33
- return { logs: [], total: 0, page: params?.page ?? 1, perPage: params?.perPage ?? 100, hasMore: false };
34
- }
35
- return this.transports.get(transportId).listLogs?.(params) ?? {
36
- logs: [],
37
- total: 0,
38
- page: params?.page ?? 1,
39
- perPage: params?.perPage ?? 100,
40
- hasMore: false
41
- };
42
- }
43
- async listLogsByRunId({
44
- transportId,
45
- runId,
46
- fromDate,
47
- toDate,
48
- logLevel,
49
- filters,
50
- page,
51
- perPage
52
- }) {
53
- if (!transportId || !this.transports.has(transportId) || !runId) {
54
- return { logs: [], total: 0, page: page ?? 1, perPage: perPage ?? 100, hasMore: false };
55
- }
56
- return this.transports.get(transportId).listLogsByRunId?.({ runId, fromDate, toDate, logLevel, filters, page, perPage }) ?? {
57
- logs: [],
58
- total: 0,
59
- page: page ?? 1,
60
- perPage: perPage ?? 100,
61
- hasMore: false
62
- };
63
- }
38
+ name;
39
+ level;
40
+ transports;
41
+ constructor(options = {}) {
42
+ this.name = options.name || "Mastra";
43
+ this.level = options.level || LogLevel.ERROR;
44
+ this.transports = new Map(Object.entries(options.transports || {}));
45
+ }
46
+ getTransports() {
47
+ return this.transports;
48
+ }
49
+ trackException(_error, _metadata) {}
50
+ async listLogs(transportId, params) {
51
+ if (!transportId || !this.transports.has(transportId)) return {
52
+ logs: [],
53
+ total: 0,
54
+ page: params?.page ?? 1,
55
+ perPage: params?.perPage ?? 100,
56
+ hasMore: false
57
+ };
58
+ return this.transports.get(transportId).listLogs?.(params) ?? {
59
+ logs: [],
60
+ total: 0,
61
+ page: params?.page ?? 1,
62
+ perPage: params?.perPage ?? 100,
63
+ hasMore: false
64
+ };
65
+ }
66
+ async listLogsByRunId({ transportId, runId, fromDate, toDate, logLevel, filters, page, perPage }) {
67
+ if (!transportId || !this.transports.has(transportId) || !runId) return {
68
+ logs: [],
69
+ total: 0,
70
+ page: page ?? 1,
71
+ perPage: perPage ?? 100,
72
+ hasMore: false
73
+ };
74
+ return this.transports.get(transportId).listLogsByRunId?.({
75
+ runId,
76
+ fromDate,
77
+ toDate,
78
+ logLevel,
79
+ filters,
80
+ page,
81
+ perPage
82
+ }) ?? {
83
+ logs: [],
84
+ total: 0,
85
+ page: page ?? 1,
86
+ perPage: perPage ?? 100,
87
+ hasMore: false
88
+ };
89
+ }
64
90
  };
65
- var ConsoleLogger = class _ConsoleLogger extends MastraLogger {
66
- component;
67
- filter;
68
- constructor(options = {}) {
69
- super(options);
70
- this.component = options.component;
71
- this.filter = options.filter;
72
- }
73
- child(componentOrBindings) {
74
- const component = typeof componentOrBindings === "string" ? componentOrBindings : componentOrBindings?.component ?? this.component;
75
- return new _ConsoleLogger({
76
- name: this.name,
77
- level: this.level,
78
- component,
79
- filter: this.filter
80
- });
81
- }
82
- shouldLog(level, message, args) {
83
- if (!this.filter) return true;
84
- try {
85
- return this.filter({ component: this.component, level, message, args });
86
- } catch (e) {
87
- console.error(`[Logger] Filter error for component=${this.component} level=${level}:`, e);
88
- return true;
89
- }
90
- }
91
- prefix() {
92
- return this.component ? `[${this.component}] ` : "";
93
- }
94
- debug(message, ...args) {
95
- if (this.level === LogLevel.DEBUG && this.shouldLog(LogLevel.DEBUG, message, args)) {
96
- console.info(`${this.prefix()}${message}`, ...args);
97
- }
98
- }
99
- info(message, ...args) {
100
- if ((this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.INFO, message, args)) {
101
- console.info(`${this.prefix()}${message}`, ...args);
102
- }
103
- }
104
- warn(message, ...args) {
105
- if ((this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.WARN, message, args)) {
106
- console.warn(`${this.prefix()}${message}`, ...args);
107
- }
108
- }
109
- error(message, ...args) {
110
- if ((this.level === LogLevel.ERROR || this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.ERROR, message, args)) {
111
- console.error(`${this.prefix()}${message}`, ...args);
112
- }
113
- }
114
- async listLogs(_transportId, _params) {
115
- return { logs: [], total: 0, page: _params?.page ?? 1, perPage: _params?.perPage ?? 100, hasMore: false };
116
- }
117
- async listLogsByRunId(_args) {
118
- return { logs: [], total: 0, page: _args.page ?? 1, perPage: _args.perPage ?? 100, hasMore: false };
119
- }
91
+ var ConsoleLogger = class ConsoleLogger extends MastraLogger {
92
+ component;
93
+ filter;
94
+ constructor(options = {}) {
95
+ super(options);
96
+ this.component = options.component;
97
+ this.filter = options.filter;
98
+ }
99
+ child(componentOrBindings) {
100
+ const component = typeof componentOrBindings === "string" ? componentOrBindings : componentOrBindings?.component ?? this.component;
101
+ return new ConsoleLogger({
102
+ name: this.name,
103
+ level: this.level,
104
+ component,
105
+ filter: this.filter
106
+ });
107
+ }
108
+ shouldLog(level, message, args) {
109
+ if (!this.filter) return true;
110
+ try {
111
+ return this.filter({
112
+ component: this.component,
113
+ level,
114
+ message,
115
+ args
116
+ });
117
+ } catch (e) {
118
+ console.error(`[Logger] Filter error for component=${this.component} level=${level}:`, e);
119
+ return true;
120
+ }
121
+ }
122
+ prefix() {
123
+ return this.component ? `[${this.component}] ` : "";
124
+ }
125
+ debug(message, ...args) {
126
+ if (this.level === LogLevel.DEBUG && this.shouldLog(LogLevel.DEBUG, message, args)) console.info(`${this.prefix()}${message}`, ...args);
127
+ }
128
+ info(message, ...args) {
129
+ if ((this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.INFO, message, args)) console.info(`${this.prefix()}${message}`, ...args);
130
+ }
131
+ warn(message, ...args) {
132
+ if ((this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.WARN, message, args)) console.warn(`${this.prefix()}${message}`, ...args);
133
+ }
134
+ error(message, ...args) {
135
+ if ((this.level === LogLevel.ERROR || this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.ERROR, message, args)) console.error(`${this.prefix()}${message}`, ...args);
136
+ }
137
+ async listLogs(_transportId, _params) {
138
+ return {
139
+ logs: [],
140
+ total: 0,
141
+ page: _params?.page ?? 1,
142
+ perPage: _params?.perPage ?? 100,
143
+ hasMore: false
144
+ };
145
+ }
146
+ async listLogsByRunId(_args) {
147
+ return {
148
+ logs: [],
149
+ total: 0,
150
+ page: _args.page ?? 1,
151
+ perPage: _args.perPage ?? 100,
152
+ hasMore: false
153
+ };
154
+ }
120
155
  };
121
-
122
- // ../../packages/_internal-core/dist/base/index.js
156
+ //#endregion
157
+ //#region ../../packages/_internal-core/dist/base/index.js
123
158
  var MastraBase = class {
124
- component = RegisteredLogger.LLM;
125
- logger;
126
- name;
127
- #rawConfig;
128
- constructor({
129
- component,
130
- name,
131
- rawConfig
132
- }) {
133
- this.component = component || RegisteredLogger.LLM;
134
- this.name = name;
135
- this.#rawConfig = rawConfig;
136
- this.logger = new ConsoleLogger({ name: `${this.component} - ${this.name}` });
137
- }
138
- /**
139
- * Returns the raw storage configuration this primitive was created from,
140
- * or undefined if it was created from code.
141
- */
142
- toRawConfig() {
143
- return this.#rawConfig;
144
- }
145
- /**
146
- * Sets the raw storage configuration for this primitive.
147
- * @internal
148
- */
149
- __setRawConfig(rawConfig) {
150
- this.#rawConfig = rawConfig;
151
- }
152
- /**
153
- * Set the logger for the agent
154
- * @param logger
155
- */
156
- __setLogger(logger) {
157
- this.logger = "child" in logger && typeof logger.child === "function" ? logger.child({ component: this.component }) : logger;
158
- }
159
+ component = RegisteredLogger.LLM;
160
+ logger;
161
+ name;
162
+ #rawConfig;
163
+ constructor({ component, name, rawConfig }) {
164
+ this.component = component || RegisteredLogger.LLM;
165
+ this.name = name;
166
+ this.#rawConfig = rawConfig;
167
+ this.logger = new ConsoleLogger({ name: `${this.component} - ${this.name}` });
168
+ }
169
+ /**
170
+ * Returns the raw storage configuration this primitive was created from,
171
+ * or undefined if it was created from code.
172
+ */
173
+ toRawConfig() {
174
+ return this.#rawConfig;
175
+ }
176
+ /**
177
+ * Sets the raw storage configuration for this primitive.
178
+ * @internal
179
+ */
180
+ __setRawConfig(rawConfig) {
181
+ this.#rawConfig = rawConfig;
182
+ }
183
+ /**
184
+ * Set the logger for the agent
185
+ * @param logger
186
+ */
187
+ __setLogger(logger) {
188
+ this.logger = "child" in logger && typeof logger.child === "function" ? logger.child({ component: this.component }) : logger;
189
+ }
159
190
  };
160
-
161
- // ../../packages/_internals/auth/dist/chunk-NJOKT6V5.js
191
+ //#endregion
192
+ //#region ../../packages/_internals/auth/dist/provider/index.js
162
193
  var MastraAuthProvider = class extends MastraBase {
163
- protected;
164
- public;
165
- constructor(options) {
166
- super({ component: "AUTH", name: options?.name });
167
- if (options?.authorizeUser) {
168
- this.authorizeUser = options.authorizeUser.bind(this);
169
- }
170
- this.protected = options?.protected;
171
- this.public = options?.public;
172
- this.mapUserToResourceId = options?.mapUserToResourceId;
173
- }
174
- registerOptions(opts) {
175
- if (opts?.authorizeUser) {
176
- this.authorizeUser = opts.authorizeUser.bind(this);
177
- }
178
- if (opts?.mapUserToResourceId) {
179
- this.mapUserToResourceId = opts.mapUserToResourceId;
180
- }
181
- if (opts?.protected) {
182
- this.protected = opts.protected;
183
- }
184
- if (opts?.public) {
185
- this.public = opts.public;
186
- }
187
- }
194
+ protected;
195
+ public;
196
+ constructor(options) {
197
+ super({
198
+ component: "AUTH",
199
+ name: options?.name
200
+ });
201
+ if (options?.authorizeUser) this.authorizeUser = options.authorizeUser.bind(this);
202
+ this.protected = options?.protected;
203
+ this.public = options?.public;
204
+ this.mapUserToResourceId = options?.mapUserToResourceId;
205
+ }
206
+ registerOptions(opts) {
207
+ if (opts?.authorizeUser) this.authorizeUser = opts.authorizeUser.bind(this);
208
+ if (opts?.mapUserToResourceId) this.mapUserToResourceId = opts.mapUserToResourceId;
209
+ if (opts?.protected) this.protected = opts.protected;
210
+ if (opts?.public) this.public = opts.public;
211
+ }
188
212
  };
213
+ //#endregion
214
+ //#region src/index.ts
189
215
  function tryDecode(value) {
190
- try {
191
- return decodeURIComponent(value);
192
- } catch {
193
- return value;
194
- }
216
+ try {
217
+ return decodeURIComponent(value);
218
+ } catch {
219
+ return value;
220
+ }
195
221
  }
196
- function getRequestHeader2(request, name) {
197
- if (request instanceof Request) {
198
- return request.headers.get(name);
199
- }
200
- return request.raw?.headers.get(name) ?? request.headers?.get(name) ?? request.header(name) ?? null;
222
+ function getRequestHeader(request, name) {
223
+ if (request instanceof Request) return request.headers.get(name);
224
+ return request.raw?.headers.get(name) ?? request.headers?.get(name) ?? request.header(name) ?? null;
201
225
  }
226
+ /**
227
+ * Maps Better Auth User to EE User format.
228
+ */
202
229
  function mapBetterAuthUserToEEUser(user) {
203
- return {
204
- id: user.id,
205
- email: user.email,
206
- name: user.name,
207
- avatarUrl: user.image ?? void 0,
208
- metadata: {
209
- emailVerified: user.emailVerified,
210
- createdAt: user.createdAt,
211
- updatedAt: user.updatedAt
212
- }
213
- };
230
+ return {
231
+ id: user.id,
232
+ email: user.email,
233
+ name: user.name,
234
+ avatarUrl: user.image ?? void 0,
235
+ metadata: {
236
+ emailVerified: user.emailVerified,
237
+ createdAt: user.createdAt,
238
+ updatedAt: user.updatedAt
239
+ }
240
+ };
214
241
  }
242
+ /**
243
+ * Mastra authentication provider for Better Auth.
244
+ *
245
+ * Better Auth is a self-hosted, open-source authentication framework
246
+ * that gives you full control over your authentication system.
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * import { betterAuth } from 'better-auth';
251
+ * import { MastraAuthBetterAuth } from '@mastra/auth-better-auth';
252
+ *
253
+ * // Create your Better Auth instance
254
+ * const auth = betterAuth({
255
+ * database: {
256
+ * provider: 'postgresql',
257
+ * url: process.env.DATABASE_URL!,
258
+ * },
259
+ * emailAndPassword: {
260
+ * enabled: true,
261
+ * },
262
+ * });
263
+ *
264
+ * // Create the Mastra auth provider
265
+ * const mastraAuth = new MastraAuthBetterAuth({
266
+ * auth,
267
+ * });
268
+ *
269
+ * // Use with Mastra
270
+ * const mastra = new Mastra({
271
+ * server: {
272
+ * auth: mastraAuth,
273
+ * },
274
+ * });
275
+ * ```
276
+ *
277
+ * @see https://better-auth.com for Better Auth documentation
278
+ */
215
279
  var MastraAuthBetterAuth = class extends MastraAuthProvider {
216
- #auth;
217
- #secret;
218
- /** True when `init()` built the instance — then we also own its migrations. */
219
- #ownsInstance = false;
220
- /** Once-per-process migration latch; reset on failure so a later call retries. */
221
- #migrated;
222
- /** In-process `userId → orgId` cache so hosts can call `ensureOrganization` per request. */
223
- #orgCache = /* @__PURE__ */ new Map();
224
- /** Set from `init()`: cross-origin SPA deploys need SameSite=None; Secure cookies. */
225
- #crossSite = false;
226
- signUpEnabledConfig;
227
- constructor(options) {
228
- super({ name: options?.name ?? "better-auth" });
229
- if (!options.auth && !options.secret) {
230
- throw new Error(
231
- "Better Auth instance is required. Please provide the auth option with your Better Auth instance created via betterAuth({ ... }), or provide `secret` so the provider can build its own instance in init() on the host database."
232
- );
233
- }
234
- this.#auth = options.auth;
235
- this.#secret = options.secret;
236
- this.signUpEnabledConfig = options.signUpEnabled ?? true;
237
- this.registerOptions(options);
238
- }
239
- /**
240
- * The active Better Auth instance. Throws before `init()` in deferred
241
- * instance mode (constructed with `secret` instead of `auth`).
242
- */
243
- get auth() {
244
- if (!this.#auth) {
245
- throw new Error(
246
- "MastraAuthBetterAuth is not initialized \u2014 init() must run first (or pass a configured `auth` instance)."
247
- );
248
- }
249
- return this.#auth;
250
- }
251
- /**
252
- * Session cookie name, honoring Better Auth's `cookiePrefix`, a caller
253
- * override via `advanced.cookies.session_token.name`, and the `__Secure-`
254
- * prefix Better Auth applies when secure cookies are active.
255
- */
256
- get sessionCookieName() {
257
- const options = this.#auth?.options;
258
- const advanced = options?.advanced;
259
- const prefix = advanced?.cookiePrefix ?? "better-auth";
260
- const secure = advanced?.useSecureCookies ?? options?.baseURL?.startsWith("https://") ?? false;
261
- const baseName = advanced?.cookies?.session_token?.name ?? `${prefix}.session_token`;
262
- return `${secure ? "__Secure-" : ""}${baseName}`;
263
- }
264
- // ============================================
265
- // IAuthInit implementation
266
- // ============================================
267
- /**
268
- * One-time host initialization. In deferred instance mode (no `auth` in the
269
- * options) this builds the provider-owned `betterAuth()` instance on the
270
- * host's auth database; schema migrations then run lazily behind a
271
- * once-per-process latch on first use.
272
- *
273
- * Bring-your-own `auth` instances skip construction entirely — the caller
274
- * owns their database and migrations.
275
- */
276
- async init(ctx) {
277
- this.#crossSite = (ctx.allowedOrigins?.length ?? 0) > 0;
278
- if (this.#auth) return;
279
- const authDb = ctx.database;
280
- if (!authDb) {
281
- throw new Error(
282
- "MastraAuthBetterAuth needs a database to build its own better-auth instance, but the host passed none. Use a storage backend that exposes an auth database, or pass your own configured `auth` instance."
283
- );
284
- }
285
- const database = authDb.dialect === "postgres" ? authDb.pool : authDb.dialect === "libsql" ? {
286
- dialect: new kyselyLibsql.LibsqlDialect({ client: authDb.client }),
287
- type: "sqlite"
288
- } : authDb.database;
289
- const allowedOrigins = ctx.allowedOrigins ?? [];
290
- const options = {
291
- database,
292
- secret: this.#secret,
293
- // All provider endpoints (sign-in/up/out/session) live under /auth/api/*,
294
- // where hosts mount handleAuthRequest.
295
- basePath: "/auth/api",
296
- ...ctx.publicUrl ? { baseURL: ctx.publicUrl } : {},
297
- // Cross-origin SPA deploys: SameSite=None only lets the browser SEND the
298
- // cookie — better-auth still rejects requests from origins outside its
299
- // own allow-list, so the SPA origins must be trusted too.
300
- ...allowedOrigins.length ? { trustedOrigins: allowedOrigins } : {},
301
- emailAndPassword: { enabled: true, disableSignUp: !this.signUpEnabledConfig },
302
- plugins: [plugins.organization()],
303
- // Cross-origin SPA deploys need SameSite=None; Secure for the browser to
304
- // send the session cookie.
305
- ...this.#crossSite ? { advanced: { defaultCookieAttributes: { sameSite: "none", secure: true } } } : {}
306
- };
307
- this.#auth = betterAuth.betterAuth(options);
308
- this.#ownsInstance = true;
309
- }
310
- /**
311
- * Ensure better-auth's tables exist in the host database. Only for instances
312
- * this provider built — bring-your-own instances manage their own migrations.
313
- */
314
- async #ensureDbReady() {
315
- if (!this.#ownsInstance) return;
316
- this.#migrated ??= (async () => {
317
- const { runMigrations } = await migration.getMigrations(this.auth.options);
318
- await runMigrations();
319
- })();
320
- try {
321
- await this.#migrated;
322
- } catch (error) {
323
- this.#migrated = void 0;
324
- console.warn("[BetterAuth] Failed to run auth schema migrations; auth stays unavailable until this succeeds.");
325
- throw error;
326
- }
327
- }
328
- // ============================================
329
- // IAuthHttpHandler implementation
330
- // ============================================
331
- /**
332
- * Proxy a raw HTTP request to Better Auth's own API surface
333
- * (sign-in/up/out/session). Hosts mount this under `/auth/api/*`.
334
- */
335
- async handleAuthRequest(request) {
336
- try {
337
- await this.#ensureDbReady();
338
- } catch {
339
- return new Response(JSON.stringify({ error: "auth_unavailable" }), {
340
- status: 503,
341
- headers: { "content-type": "application/json" }
342
- });
343
- }
344
- return this.auth.handler(request);
345
- }
346
- // ============================================
347
- // IOrganizationsProvider implementation
348
- // ============================================
349
- /**
350
- * Ensure the user belongs to an organization, mirroring the WorkOS
351
- * personal-org bootstrap on better-auth's organization tables:
352
- * ≥1 membership → first org id; 0 → create a personal org with an
353
- * idempotent slug derived from the user id.
354
- *
355
- * Concurrent/retried first logins recover via the unique slug instead of
356
- * creating duplicates. Slug alone is NOT proof of ownership though: the
357
- * organization API is reachable by any authenticated user, so an attacker
358
- * could squat `personal-<victimId>`. The slug-matched org is only adopted
359
- * when nobody else is a member; otherwise a fresh org with an unguessable
360
- * slug is created instead.
361
- *
362
- * Best-effort: any failure is swallowed and leaves the user no-org.
363
- */
364
- async ensureOrganization(userId) {
365
- const cached = this.#orgCache.get(userId);
366
- if (cached) return cached;
367
- try {
368
- await this.#ensureDbReady();
369
- const ctx = await this.getAuthContext();
370
- if (!ctx) return void 0;
371
- const memberships = await ctx.adapter.findMany({
372
- model: "member",
373
- where: [{ field: "userId", value: userId }]
374
- });
375
- const firstExisting = memberships.find((m) => m.organizationId)?.organizationId;
376
- if (firstExisting) {
377
- this.#orgCache.set(userId, firstExisting);
378
- return firstExisting;
379
- }
380
- const userRecord = await ctx.internalAdapter.findUserById(userId).catch(() => null);
381
- const label = userRecord?.email ?? userRecord?.name ?? userId;
382
- const orgName = `${label}'s org`;
383
- const orgData = () => ({
384
- name: orgName,
385
- createdAt: /* @__PURE__ */ new Date(),
386
- metadata: JSON.stringify({ mastraPersonalOrg: "true" })
387
- });
388
- const slug = `personal-${userId}`;
389
- let organizationId;
390
- try {
391
- const created = await ctx.adapter.create({
392
- model: "organization",
393
- data: { ...orgData(), slug }
394
- });
395
- organizationId = created.id;
396
- } catch (error) {
397
- const existing = await ctx.adapter.findOne({
398
- model: "organization",
399
- where: [{ field: "slug", value: slug }]
400
- });
401
- if (!existing) throw error;
402
- const existingMembers = await ctx.adapter.findMany({
403
- model: "member",
404
- where: [{ field: "organizationId", value: existing.id }]
405
- });
406
- const foreignMember = existingMembers.some((m) => m.userId !== userId);
407
- if (foreignMember) {
408
- const fallback = await ctx.adapter.create({
409
- model: "organization",
410
- data: { ...orgData(), slug: `personal-${userId}-${crypto.randomUUID()}` }
411
- });
412
- organizationId = fallback.id;
413
- } else {
414
- organizationId = existing.id;
415
- }
416
- }
417
- try {
418
- await ctx.adapter.create({
419
- model: "member",
420
- data: { organizationId, userId, role: "owner", createdAt: /* @__PURE__ */ new Date() }
421
- });
422
- } catch (error) {
423
- const member = await ctx.adapter.findOne({
424
- model: "member",
425
- where: [
426
- { field: "organizationId", value: organizationId },
427
- { field: "userId", value: userId }
428
- ]
429
- });
430
- if (!member) throw error;
431
- }
432
- this.#orgCache.set(userId, organizationId);
433
- return organizationId;
434
- } catch (error) {
435
- console.warn(
436
- `[BetterAuth] Failed to bootstrap personal organization for user ${userId}. The user will see organization_required until this succeeds.`,
437
- error
438
- );
439
- return void 0;
440
- }
441
- }
442
- /**
443
- * Whether the user holds an admin-equivalent role (`owner` or `admin`) in
444
- * the organization. Provider errors resolve to `false`.
445
- */
446
- async isOrganizationAdmin(organizationId, userId) {
447
- try {
448
- await this.#ensureDbReady();
449
- const ctx = await this.getAuthContext();
450
- if (!ctx) return false;
451
- const membership = await ctx.adapter.findOne({
452
- model: "member",
453
- where: [
454
- { field: "organizationId", value: organizationId },
455
- { field: "userId", value: userId }
456
- ]
457
- });
458
- return membership?.role === "owner" || membership?.role === "admin";
459
- } catch {
460
- return false;
461
- }
462
- }
463
- /**
464
- * Check if sign-up is enabled.
465
- * Implements ICredentialsProvider.isSignUpEnabled.
466
- */
467
- isSignUpEnabled() {
468
- return this.signUpEnabledConfig;
469
- }
470
- // ============================================
471
- // IUserProvider implementation (EE capability)
472
- // License check happens in buildCapabilities()
473
- // ============================================
474
- /**
475
- * Get current user from request.
476
- * Implements IUserProvider for EE user awareness in Studio.
477
- *
478
- * Supports both cookie-based sessions and `Authorization: Bearer <token>`
479
- * requests (e.g. API clients using the token returned by `signIn`).
480
- *
481
- * @param request - Incoming HTTP request
482
- * @returns EE User object or null if not authenticated
483
- */
484
- async getCurrentUser(request) {
485
- try {
486
- await this.#ensureDbReady();
487
- const headers = new Headers(request.headers);
488
- const authHeader = headers.get("Authorization");
489
- const bearerToken = authHeader && authHeader.slice(0, 7).toLowerCase() === "bearer " ? authHeader.slice(7).trim() : void 0;
490
- await this.ensureSessionCookie(headers, bearerToken);
491
- const result = await this.auth.api.getSession({
492
- headers
493
- });
494
- if (!result?.user) return null;
495
- return mapBetterAuthUserToEEUser(result.user);
496
- } catch {
497
- return null;
498
- }
499
- }
500
- /**
501
- * Get user by ID.
502
- * Implements IUserProvider for EE user awareness.
503
- *
504
- * Uses Better Auth's internal adapter to look up the user record,
505
- * so it works with whichever database Better Auth is configured with.
506
- *
507
- * @param userId - User identifier
508
- * @returns EE User object or null if not found
509
- */
510
- async getUser(userId) {
511
- try {
512
- const ctx = await this.getAuthContext();
513
- if (!ctx) return null;
514
- const user = await ctx.internalAdapter.findUserById(userId);
515
- if (!user) return null;
516
- return mapBetterAuthUserToEEUser(user);
517
- } catch {
518
- return null;
519
- }
520
- }
521
- /**
522
- * Get multiple users by ID.
523
- * Optional IUserProvider method used for batch author enrichment.
524
- *
525
- * @param userIds - User identifiers
526
- * @returns EE User objects (null for missing users, order preserved)
527
- */
528
- async getUsers(userIds) {
529
- return Promise.all(userIds.map((userId) => this.getUser(userId)));
530
- }
531
- /**
532
- * Get URL to user's profile page.
533
- * Optional IUserProvider method.
534
- */
535
- getUserProfileUrl(user) {
536
- return `/profile/${user.id}`;
537
- }
538
- /**
539
- * Get the resolved Better Auth context, or null when unavailable
540
- * (e.g. a partial mock without `$context`).
541
- */
542
- async getAuthContext() {
543
- try {
544
- return await Promise.resolve(this.auth.$context) ?? null;
545
- } catch {
546
- return null;
547
- }
548
- }
549
- /**
550
- * Ensure the Cookie header on `headers` carries a Better Auth session cookie
551
- * for the given token.
552
- *
553
- * Better Auth's `getSession` reads the session from a *signed* cookie
554
- * (`<token>.<HMAC-SHA256 signature>`), while `signIn`/`signUp` return the
555
- * raw unsigned token. Mirroring Better Auth's bearer plugin, unsigned tokens
556
- * are signed with the instance secret before being set as the session cookie.
557
- */
558
- async ensureSessionCookie(headers, token) {
559
- if (!token) return;
560
- const ctx = await this.getAuthContext();
561
- const cookieName = ctx?.authCookies.sessionToken.name ?? this.sessionCookieName;
562
- const secret = ctx?.secret;
563
- const cookieHeader = headers.get("Cookie");
564
- const hasSessionCookie = !!cookieHeader?.split(";").some((pair) => {
565
- const [key] = pair.trim().split("=");
566
- return key?.trim() === cookieName;
567
- });
568
- if (hasSessionCookie) return;
569
- let cookieValue = token.includes(".") ? token.includes("%") ? tryDecode(token) : token : token;
570
- if (!token.includes(".") && secret) {
571
- cookieValue = `${token}.${await crypto$1.makeSignature(token, secret)}`;
572
- }
573
- const existingCookies = cookieHeader ? `${cookieHeader}; ` : "";
574
- headers.set("Cookie", `${existingCookies}${cookieName}=${encodeURIComponent(cookieValue)}`);
575
- }
576
- /**
577
- * Authenticate a bearer token by verifying the session with Better Auth.
578
- *
579
- * This method extracts the session from the request headers using
580
- * Better Auth's `api.getSession()` endpoint. Raw (unsigned) session tokens —
581
- * as returned by `signIn`/`signUp` — are signed before being passed to
582
- * Better Auth as a session cookie, matching the bearer plugin's semantics.
583
- *
584
- * @param token - The bearer token (session token) to authenticate
585
- * @param request - The request containing headers
586
- * @returns The authenticated user and session, or null if authentication fails
587
- */
588
- async authenticateToken(token, request) {
589
- try {
590
- await this.#ensureDbReady();
591
- const headers = new Headers();
592
- const cookieHeader = getRequestHeader2(request, "Cookie");
593
- if (cookieHeader) {
594
- headers.set("Cookie", cookieHeader);
595
- }
596
- await this.ensureSessionCookie(headers, token);
597
- const result = await this.auth.api.getSession({
598
- headers
599
- });
600
- if (!result || !result.session || !result.user) {
601
- return null;
602
- }
603
- return {
604
- session: result.session,
605
- user: result.user
606
- };
607
- } catch {
608
- return null;
609
- }
610
- }
611
- /**
612
- * Authorize a user for access.
613
- *
614
- * By default, any authenticated user with a valid session is authorized.
615
- * You can override this behavior by providing a custom `authorizeUser` function
616
- * in the constructor options.
617
- *
618
- * @param user - The authenticated user and session
619
- * @returns True if the user is authorized, false otherwise
620
- */
621
- async authorizeUser(user) {
622
- return !!user?.session?.id && !!user?.user?.id;
623
- }
624
- // ============================================
625
- // ICredentialsProvider implementation (EE capability)
626
- // License check happens in buildCapabilities()
627
- // ============================================
628
- /**
629
- * Sign in with email and password.
630
- * Implements ICredentialsProvider for EE credentials auth.
631
- *
632
- * @param email - User email
633
- * @param password - User password
634
- * @param request - Incoming HTTP request
635
- * @returns Result with user and session cookies
636
- * @throws Error if credentials are invalid
637
- */
638
- async signIn(email, password, request) {
639
- const headers = request?.headers ?? new Headers();
640
- const response = await this.auth.api.signInEmail({
641
- body: { email, password },
642
- headers,
643
- asResponse: true
644
- });
645
- if (!response.ok) {
646
- const errorData = await response.json().catch(() => ({}));
647
- throw new Error(errorData.message || "Invalid email or password");
648
- }
649
- const result = await response.json();
650
- if (!result?.user) {
651
- throw new Error("Invalid email or password");
652
- }
653
- const cookies = [];
654
- const setCookieHeader = response.headers.get("set-cookie");
655
- if (setCookieHeader) {
656
- cookies.push(...setCookieHeader.split(/,(?=\s*\w+=)/));
657
- }
658
- return {
659
- user: mapBetterAuthUserToEEUser(result.user),
660
- token: result.token ?? void 0,
661
- cookies
662
- };
663
- }
664
- /**
665
- * Sign up with email and password.
666
- * Implements ICredentialsProvider for EE credentials auth.
667
- *
668
- * @param email - User email
669
- * @param password - User password
670
- * @param name - Optional display name
671
- * @param request - Incoming HTTP request
672
- * @returns Result with new user and session cookies
673
- * @throws Error if sign up fails
674
- */
675
- async signUp(email, password, name, request) {
676
- const displayName = name ?? email.split("@")[0] ?? "User";
677
- const headers = request?.headers ?? new Headers();
678
- const response = await this.auth.api.signUpEmail({
679
- body: { email, password, name: displayName },
680
- headers,
681
- asResponse: true
682
- });
683
- if (!response.ok) {
684
- const errorData = await response.json().catch(() => ({}));
685
- throw new Error(errorData.message || "Failed to create account");
686
- }
687
- const result = await response.json();
688
- if (!result?.user) {
689
- throw new Error("Failed to create account");
690
- }
691
- const cookies = [];
692
- const setCookieHeader = response.headers.get("set-cookie");
693
- if (setCookieHeader) {
694
- cookies.push(...setCookieHeader.split(/,(?=\s*\w+=)/));
695
- }
696
- return {
697
- user: mapBetterAuthUserToEEUser(result.user),
698
- token: result.token ?? void 0,
699
- cookies
700
- };
701
- }
702
- /**
703
- * Get the underlying Better Auth instance.
704
- * Useful for accessing Better Auth APIs directly.
705
- */
706
- getAuth() {
707
- return this.auth;
708
- }
709
- /**
710
- * Get headers to clear the session cookies on logout.
711
- * Partial ISessionProvider implementation for logout support.
712
- *
713
- * Clears Better Auth's default session cookies.
714
- */
715
- getClearSessionHeaders() {
716
- const sameSite = this.#crossSite ? "None; Secure" : "Lax";
717
- const cookies = [
718
- `${this.sessionCookieName}=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`,
719
- `${this.sessionCookieName}_sig=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`
720
- ];
721
- return {
722
- "Set-Cookie": cookies.join(", ")
723
- };
724
- }
280
+ #auth;
281
+ #secret;
282
+ /** True when `init()` built the instance — then we also own its migrations. */
283
+ #ownsInstance = false;
284
+ /** Once-per-process migration latch; reset on failure so a later call retries. */
285
+ #migrated;
286
+ /** In-process `userId → orgId` cache so hosts can call `ensureOrganization` per request. */
287
+ #orgCache = /* @__PURE__ */ new Map();
288
+ /** Set from `init()`: cross-origin SPA deploys need SameSite=None; Secure cookies. */
289
+ #crossSite = false;
290
+ signUpEnabledConfig;
291
+ constructor(options) {
292
+ super({ name: options?.name ?? "better-auth" });
293
+ if (!options.auth && !options.secret) throw new Error("Better Auth instance is required. Please provide the auth option with your Better Auth instance created via betterAuth({ ... }), or provide `secret` so the provider can build its own instance in init() on the host database.");
294
+ this.#auth = options.auth;
295
+ this.#secret = options.secret;
296
+ this.signUpEnabledConfig = options.signUpEnabled ?? true;
297
+ this.registerOptions(options);
298
+ }
299
+ /**
300
+ * The active Better Auth instance. Throws before `init()` in deferred
301
+ * instance mode (constructed with `secret` instead of `auth`).
302
+ */
303
+ get auth() {
304
+ if (!this.#auth) throw new Error("MastraAuthBetterAuth is not initialized — init() must run first (or pass a configured `auth` instance).");
305
+ return this.#auth;
306
+ }
307
+ /**
308
+ * Session cookie name, honoring Better Auth's `cookiePrefix`, a caller
309
+ * override via `advanced.cookies.session_token.name`, and the `__Secure-`
310
+ * prefix Better Auth applies when secure cookies are active.
311
+ */
312
+ get sessionCookieName() {
313
+ const options = this.#auth?.options;
314
+ const advanced = options?.advanced;
315
+ const prefix = advanced?.cookiePrefix ?? "better-auth";
316
+ const secure = advanced?.useSecureCookies ?? options?.baseURL?.startsWith("https://") ?? false;
317
+ const baseName = advanced?.cookies?.session_token?.name ?? `${prefix}.session_token`;
318
+ return `${secure ? "__Secure-" : ""}${baseName}`;
319
+ }
320
+ /**
321
+ * One-time host initialization. In deferred instance mode (no `auth` in the
322
+ * options) this builds the provider-owned `betterAuth()` instance on the
323
+ * host's auth database; schema migrations then run lazily behind a
324
+ * once-per-process latch on first use.
325
+ *
326
+ * Bring-your-own `auth` instances skip construction entirely — the caller
327
+ * owns their database and migrations.
328
+ */
329
+ async init(ctx) {
330
+ this.#crossSite = (ctx.allowedOrigins?.length ?? 0) > 0;
331
+ if (this.#auth) return;
332
+ const authDb = ctx.database;
333
+ if (!authDb) throw new Error("MastraAuthBetterAuth needs a database to build its own better-auth instance, but the host passed none. Use a storage backend that exposes an auth database, or pass your own configured `auth` instance.");
334
+ const database = authDb.dialect === "postgres" ? authDb.pool : authDb.dialect === "libsql" ? {
335
+ dialect: new _libsql_kysely_libsql.LibsqlDialect({ client: authDb.client }),
336
+ type: "sqlite"
337
+ } : authDb.database;
338
+ const allowedOrigins = ctx.allowedOrigins ?? [];
339
+ const options = {
340
+ database,
341
+ secret: this.#secret,
342
+ basePath: "/auth/api",
343
+ ...ctx.publicUrl ? { baseURL: ctx.publicUrl } : {},
344
+ ...allowedOrigins.length ? { trustedOrigins: allowedOrigins } : {},
345
+ emailAndPassword: {
346
+ enabled: true,
347
+ disableSignUp: !this.signUpEnabledConfig
348
+ },
349
+ plugins: [(0, better_auth_plugins.organization)()],
350
+ ...this.#crossSite ? { advanced: { defaultCookieAttributes: {
351
+ sameSite: "none",
352
+ secure: true
353
+ } } } : {}
354
+ };
355
+ this.#auth = (0, better_auth.betterAuth)(options);
356
+ this.#ownsInstance = true;
357
+ }
358
+ /**
359
+ * Ensure better-auth's tables exist in the host database. Only for instances
360
+ * this provider built — bring-your-own instances manage their own migrations.
361
+ */
362
+ async #ensureDbReady() {
363
+ if (!this.#ownsInstance) return;
364
+ this.#migrated ??= (async () => {
365
+ const { runMigrations } = await (0, better_auth_db_migration.getMigrations)(this.auth.options);
366
+ await runMigrations();
367
+ })();
368
+ try {
369
+ await this.#migrated;
370
+ } catch (error) {
371
+ this.#migrated = void 0;
372
+ console.warn("[BetterAuth] Failed to run auth schema migrations; auth stays unavailable until this succeeds.");
373
+ throw error;
374
+ }
375
+ }
376
+ /**
377
+ * Proxy a raw HTTP request to Better Auth's own API surface
378
+ * (sign-in/up/out/session). Hosts mount this under `/auth/api/*`.
379
+ */
380
+ async handleAuthRequest(request) {
381
+ try {
382
+ await this.#ensureDbReady();
383
+ } catch {
384
+ return new Response(JSON.stringify({ error: "auth_unavailable" }), {
385
+ status: 503,
386
+ headers: { "content-type": "application/json" }
387
+ });
388
+ }
389
+ return this.auth.handler(request);
390
+ }
391
+ /**
392
+ * Ensure the user belongs to an organization, mirroring the WorkOS
393
+ * personal-org bootstrap on better-auth's organization tables:
394
+ * ≥1 membership → first org id; 0 → create a personal org with an
395
+ * idempotent slug derived from the user id.
396
+ *
397
+ * Concurrent/retried first logins recover via the unique slug instead of
398
+ * creating duplicates. Slug alone is NOT proof of ownership though: the
399
+ * organization API is reachable by any authenticated user, so an attacker
400
+ * could squat `personal-<victimId>`. The slug-matched org is only adopted
401
+ * when nobody else is a member; otherwise a fresh org with an unguessable
402
+ * slug is created instead.
403
+ *
404
+ * Best-effort: any failure is swallowed and leaves the user no-org.
405
+ */
406
+ async ensureOrganization(userId) {
407
+ const cached = this.#orgCache.get(userId);
408
+ if (cached) return cached;
409
+ try {
410
+ await this.#ensureDbReady();
411
+ const ctx = await this.getAuthContext();
412
+ if (!ctx) return void 0;
413
+ const firstExisting = (await ctx.adapter.findMany({
414
+ model: "member",
415
+ where: [{
416
+ field: "userId",
417
+ value: userId
418
+ }]
419
+ })).find((m) => m.organizationId)?.organizationId;
420
+ if (firstExisting) {
421
+ this.#orgCache.set(userId, firstExisting);
422
+ return firstExisting;
423
+ }
424
+ const userRecord = await ctx.internalAdapter.findUserById(userId).catch(() => null);
425
+ const orgName = `${userRecord?.email ?? userRecord?.name ?? userId}'s org`;
426
+ const orgData = () => ({
427
+ name: orgName,
428
+ createdAt: /* @__PURE__ */ new Date(),
429
+ metadata: JSON.stringify({ mastraPersonalOrg: "true" })
430
+ });
431
+ const slug = `personal-${userId}`;
432
+ let organizationId;
433
+ try {
434
+ organizationId = (await ctx.adapter.create({
435
+ model: "organization",
436
+ data: {
437
+ ...orgData(),
438
+ slug
439
+ }
440
+ })).id;
441
+ } catch (error) {
442
+ const existing = await ctx.adapter.findOne({
443
+ model: "organization",
444
+ where: [{
445
+ field: "slug",
446
+ value: slug
447
+ }]
448
+ });
449
+ if (!existing) throw error;
450
+ if ((await ctx.adapter.findMany({
451
+ model: "member",
452
+ where: [{
453
+ field: "organizationId",
454
+ value: existing.id
455
+ }]
456
+ })).some((m) => m.userId !== userId)) organizationId = (await ctx.adapter.create({
457
+ model: "organization",
458
+ data: {
459
+ ...orgData(),
460
+ slug: `personal-${userId}-${crypto.randomUUID()}`
461
+ }
462
+ })).id;
463
+ else organizationId = existing.id;
464
+ }
465
+ try {
466
+ await ctx.adapter.create({
467
+ model: "member",
468
+ data: {
469
+ organizationId,
470
+ userId,
471
+ role: "owner",
472
+ createdAt: /* @__PURE__ */ new Date()
473
+ }
474
+ });
475
+ } catch (error) {
476
+ if (!await ctx.adapter.findOne({
477
+ model: "member",
478
+ where: [{
479
+ field: "organizationId",
480
+ value: organizationId
481
+ }, {
482
+ field: "userId",
483
+ value: userId
484
+ }]
485
+ })) throw error;
486
+ }
487
+ this.#orgCache.set(userId, organizationId);
488
+ return organizationId;
489
+ } catch (error) {
490
+ console.warn(`[BetterAuth] Failed to bootstrap personal organization for user ${userId}. The user will see organization_required until this succeeds.`, error);
491
+ return;
492
+ }
493
+ }
494
+ /**
495
+ * Whether the user holds an admin-equivalent role (`owner` or `admin`) in
496
+ * the organization. Provider errors resolve to `false`.
497
+ */
498
+ async isOrganizationAdmin(organizationId, userId) {
499
+ try {
500
+ await this.#ensureDbReady();
501
+ const ctx = await this.getAuthContext();
502
+ if (!ctx) return false;
503
+ const membership = await ctx.adapter.findOne({
504
+ model: "member",
505
+ where: [{
506
+ field: "organizationId",
507
+ value: organizationId
508
+ }, {
509
+ field: "userId",
510
+ value: userId
511
+ }]
512
+ });
513
+ return membership?.role === "owner" || membership?.role === "admin";
514
+ } catch {
515
+ return false;
516
+ }
517
+ }
518
+ /**
519
+ * Check if sign-up is enabled.
520
+ * Implements ICredentialsProvider.isSignUpEnabled.
521
+ */
522
+ isSignUpEnabled() {
523
+ return this.signUpEnabledConfig;
524
+ }
525
+ /**
526
+ * Get current user from request.
527
+ * Implements IUserProvider for EE user awareness in Studio.
528
+ *
529
+ * Supports both cookie-based sessions and `Authorization: Bearer <token>`
530
+ * requests (e.g. API clients using the token returned by `signIn`).
531
+ *
532
+ * @param request - Incoming HTTP request
533
+ * @returns EE User object or null if not authenticated
534
+ */
535
+ async getCurrentUser(request) {
536
+ try {
537
+ await this.#ensureDbReady();
538
+ const headers = new Headers(request.headers);
539
+ const authHeader = headers.get("Authorization");
540
+ const bearerToken = authHeader && authHeader.slice(0, 7).toLowerCase() === "bearer " ? authHeader.slice(7).trim() : void 0;
541
+ await this.ensureSessionCookie(headers, bearerToken);
542
+ const result = await this.auth.api.getSession({ headers });
543
+ if (!result?.user) return null;
544
+ return mapBetterAuthUserToEEUser(result.user);
545
+ } catch {
546
+ return null;
547
+ }
548
+ }
549
+ /**
550
+ * Get user by ID.
551
+ * Implements IUserProvider for EE user awareness.
552
+ *
553
+ * Uses Better Auth's internal adapter to look up the user record,
554
+ * so it works with whichever database Better Auth is configured with.
555
+ *
556
+ * @param userId - User identifier
557
+ * @returns EE User object or null if not found
558
+ */
559
+ async getUser(userId) {
560
+ try {
561
+ const ctx = await this.getAuthContext();
562
+ if (!ctx) return null;
563
+ const user = await ctx.internalAdapter.findUserById(userId);
564
+ if (!user) return null;
565
+ return mapBetterAuthUserToEEUser(user);
566
+ } catch {
567
+ return null;
568
+ }
569
+ }
570
+ /**
571
+ * Get multiple users by ID.
572
+ * Optional IUserProvider method used for batch author enrichment.
573
+ *
574
+ * @param userIds - User identifiers
575
+ * @returns EE User objects (null for missing users, order preserved)
576
+ */
577
+ async getUsers(userIds) {
578
+ return Promise.all(userIds.map((userId) => this.getUser(userId)));
579
+ }
580
+ /**
581
+ * Get URL to user's profile page.
582
+ * Optional IUserProvider method.
583
+ */
584
+ getUserProfileUrl(user) {
585
+ return `/profile/${user.id}`;
586
+ }
587
+ /**
588
+ * Get the resolved Better Auth context, or null when unavailable
589
+ * (e.g. a partial mock without `$context`).
590
+ */
591
+ async getAuthContext() {
592
+ try {
593
+ return await Promise.resolve(this.auth.$context) ?? null;
594
+ } catch {
595
+ return null;
596
+ }
597
+ }
598
+ /**
599
+ * Ensure the Cookie header on `headers` carries a Better Auth session cookie
600
+ * for the given token.
601
+ *
602
+ * Better Auth's `getSession` reads the session from a *signed* cookie
603
+ * (`<token>.<HMAC-SHA256 signature>`), while `signIn`/`signUp` return the
604
+ * raw unsigned token. Mirroring Better Auth's bearer plugin, unsigned tokens
605
+ * are signed with the instance secret before being set as the session cookie.
606
+ */
607
+ async ensureSessionCookie(headers, token) {
608
+ if (!token) return;
609
+ const ctx = await this.getAuthContext();
610
+ const cookieName = ctx?.authCookies.sessionToken.name ?? this.sessionCookieName;
611
+ const secret = ctx?.secret;
612
+ const cookieHeader = headers.get("Cookie");
613
+ if (!!cookieHeader?.split(";").some((pair) => {
614
+ const [key] = pair.trim().split("=");
615
+ return key?.trim() === cookieName;
616
+ })) return;
617
+ let cookieValue = token.includes(".") ? token.includes("%") ? tryDecode(token) : token : token;
618
+ if (!token.includes(".") && secret) cookieValue = `${token}.${await (0, better_auth_crypto.makeSignature)(token, secret)}`;
619
+ const existingCookies = cookieHeader ? `${cookieHeader}; ` : "";
620
+ headers.set("Cookie", `${existingCookies}${cookieName}=${encodeURIComponent(cookieValue)}`);
621
+ }
622
+ /**
623
+ * Authenticate a bearer token by verifying the session with Better Auth.
624
+ *
625
+ * This method extracts the session from the request headers using
626
+ * Better Auth's `api.getSession()` endpoint. Raw (unsigned) session tokens —
627
+ * as returned by `signIn`/`signUp` — are signed before being passed to
628
+ * Better Auth as a session cookie, matching the bearer plugin's semantics.
629
+ *
630
+ * @param token - The bearer token (session token) to authenticate
631
+ * @param request - The request containing headers
632
+ * @returns The authenticated user and session, or null if authentication fails
633
+ */
634
+ async authenticateToken(token, request) {
635
+ try {
636
+ await this.#ensureDbReady();
637
+ const headers = new Headers();
638
+ const cookieHeader = getRequestHeader(request, "Cookie");
639
+ if (cookieHeader) headers.set("Cookie", cookieHeader);
640
+ await this.ensureSessionCookie(headers, token);
641
+ const result = await this.auth.api.getSession({ headers });
642
+ if (!result || !result.session || !result.user) return null;
643
+ return {
644
+ session: result.session,
645
+ user: result.user
646
+ };
647
+ } catch {
648
+ return null;
649
+ }
650
+ }
651
+ /**
652
+ * Authorize a user for access.
653
+ *
654
+ * By default, any authenticated user with a valid session is authorized.
655
+ * You can override this behavior by providing a custom `authorizeUser` function
656
+ * in the constructor options.
657
+ *
658
+ * @param user - The authenticated user and session
659
+ * @returns True if the user is authorized, false otherwise
660
+ */
661
+ async authorizeUser(user) {
662
+ return !!user?.session?.id && !!user?.user?.id;
663
+ }
664
+ /**
665
+ * Sign in with email and password.
666
+ * Implements ICredentialsProvider for EE credentials auth.
667
+ *
668
+ * @param email - User email
669
+ * @param password - User password
670
+ * @param request - Incoming HTTP request
671
+ * @returns Result with user and session cookies
672
+ * @throws Error if credentials are invalid
673
+ */
674
+ async signIn(email, password, request) {
675
+ const headers = request?.headers ?? new Headers();
676
+ const response = await this.auth.api.signInEmail({
677
+ body: {
678
+ email,
679
+ password
680
+ },
681
+ headers,
682
+ asResponse: true
683
+ });
684
+ if (!response.ok) {
685
+ const errorData = await response.json().catch(() => ({}));
686
+ throw new Error(errorData.message || "Invalid email or password");
687
+ }
688
+ const result = await response.json();
689
+ if (!result?.user) throw new Error("Invalid email or password");
690
+ const cookies = [];
691
+ const setCookieHeader = response.headers.get("set-cookie");
692
+ if (setCookieHeader) cookies.push(...setCookieHeader.split(/,(?=\s*\w+=)/));
693
+ return {
694
+ user: mapBetterAuthUserToEEUser(result.user),
695
+ token: result.token ?? void 0,
696
+ cookies
697
+ };
698
+ }
699
+ /**
700
+ * Sign up with email and password.
701
+ * Implements ICredentialsProvider for EE credentials auth.
702
+ *
703
+ * @param email - User email
704
+ * @param password - User password
705
+ * @param name - Optional display name
706
+ * @param request - Incoming HTTP request
707
+ * @returns Result with new user and session cookies
708
+ * @throws Error if sign up fails
709
+ */
710
+ async signUp(email, password, name, request) {
711
+ const displayName = name ?? email.split("@")[0] ?? "User";
712
+ const headers = request?.headers ?? new Headers();
713
+ const response = await this.auth.api.signUpEmail({
714
+ body: {
715
+ email,
716
+ password,
717
+ name: displayName
718
+ },
719
+ headers,
720
+ asResponse: true
721
+ });
722
+ if (!response.ok) {
723
+ const errorData = await response.json().catch(() => ({}));
724
+ throw new Error(errorData.message || "Failed to create account");
725
+ }
726
+ const result = await response.json();
727
+ if (!result?.user) throw new Error("Failed to create account");
728
+ const cookies = [];
729
+ const setCookieHeader = response.headers.get("set-cookie");
730
+ if (setCookieHeader) cookies.push(...setCookieHeader.split(/,(?=\s*\w+=)/));
731
+ return {
732
+ user: mapBetterAuthUserToEEUser(result.user),
733
+ token: result.token ?? void 0,
734
+ cookies
735
+ };
736
+ }
737
+ /**
738
+ * Get the underlying Better Auth instance.
739
+ * Useful for accessing Better Auth APIs directly.
740
+ */
741
+ getAuth() {
742
+ return this.auth;
743
+ }
744
+ /**
745
+ * Get headers to clear the session cookies on logout.
746
+ * Partial ISessionProvider implementation for logout support.
747
+ *
748
+ * Clears Better Auth's default session cookies.
749
+ */
750
+ getClearSessionHeaders() {
751
+ const sameSite = this.#crossSite ? "None; Secure" : "Lax";
752
+ return { "Set-Cookie": [`${this.sessionCookieName}=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`, `${this.sessionCookieName}_sig=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`].join(", ") };
753
+ }
725
754
  };
726
-
755
+ //#endregion
727
756
  exports.MastraAuthBetterAuth = MastraAuthBetterAuth;
728
- //# sourceMappingURL=index.cjs.map
757
+
729
758
  //# sourceMappingURL=index.cjs.map