@mastra/auth-better-auth 1.1.2-alpha.2 → 1.1.3-alpha.0

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