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

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @mastra/auth-better-auth
2
2
 
3
+ ## 1.1.2-alpha.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Added a deferred instance mode and organization management to MastraAuthBetterAuth so it can be passed directly to a server host without a wrapper adapter. The provider can now be constructed with just a secret and will build its Better Auth instance (including running migrations) against the host database during init. It also bootstraps a personal organization for new users (ensureOrganization), checks organization admin roles (isOrganizationAdmin), and exposes the Better Auth HTTP handler (handleAuthRequest) so hosts can mount it under /auth/api/*. ([#19765](https://github.com/mastra-ai/mastra/pull/19765))
8
+
9
+ **Before**
10
+
11
+ ```ts
12
+ import { betterAuth } from 'better-auth';
13
+ import { MastraAuthBetterAuth } from '@mastra/auth-better-auth';
14
+
15
+ const auth = new MastraAuthBetterAuth({ auth: betterAuth({/* ... */}) });
16
+ ```
17
+
18
+ **After** (bring-your-own instance still works)
19
+
20
+ ```ts
21
+ import { MastraAuthBetterAuth } from '@mastra/auth-better-auth';
22
+
23
+ const auth = new MastraAuthBetterAuth({ secret: process.env.BETTER_AUTH_SECRET! });
24
+ // host calls auth.init({ database, publicUrl, allowedOrigins }) during startup
25
+ ```
26
+
27
+ ## 1.1.2-alpha.1
28
+
29
+ ### Patch Changes
30
+
31
+ - Fixed Better Auth bearer token authentication and implemented user lookup in `@mastra/auth-better-auth` (fixes [#19110](https://github.com/mastra-ai/mastra/issues/19110)). ([#19629](https://github.com/mastra-ai/mastra/pull/19629))
32
+
33
+ **Bearer tokens now work after credentials sign-in**
34
+
35
+ `signIn`/`signUp` return Better Auth's raw session token, but Better Auth only accepts _signed_ session cookies. Sending that token as `Authorization: Bearer <token>` previously always failed authentication. The provider now signs unsigned tokens with the Better Auth secret before verifying the session — matching the semantics of Better Auth's bearer plugin. The session cookie name is also resolved from the Better Auth instance, so secure-cookie setups (`__Secure-` prefix) work too.
36
+
37
+ **`getUser()` and `getUsers()` are now implemented**
38
+
39
+ Previously `getUser()` was a stub that always returned `null`, breaking Studio user lookup and author enrichment. It now resolves users by ID through Better Auth's internal database adapter, and `getUsers()` supports batch lookups.
40
+
3
41
  ## 1.1.2-alpha.0
4
42
 
5
43
  ### Patch Changes
@@ -3,12 +3,9 @@
3
3
  *
4
4
  * @license Mastra Enterprise License - see ee/LICENSE
5
5
  */
6
- import type { FGACheckContext, IFGAProvider } from './interfaces/fga.js';
6
+ import type { ActorSignal, FGACheckContext, IFGAProvider } from './interfaces/fga.js';
7
7
  import type { MastraFGAPermissionInput } from './interfaces/permissions.generated.js';
8
- export type ActorSignal = true | {
9
- actorKind: 'system';
10
- sourceWorkflow?: string;
11
- };
8
+ export type { ActorSignal };
12
9
  export interface CheckFGAOptions {
13
10
  fgaProvider: IFGAProvider | undefined;
14
11
  user: any;
@@ -8,6 +8,34 @@
8
8
  * @license Mastra Enterprise License - see ee/LICENSE
9
9
  */
10
10
  import type { MastraFGAPermissionInput } from './permissions.generated.js';
11
+ /**
12
+ * Signals that a call is made by a trusted non-user actor rather than an
13
+ * authenticated end user.
14
+ *
15
+ * - `true` is the anonymous system shorthand.
16
+ * - The object form additionally names the acting agent (`agentId`) and carries
17
+ * the permission grants / scope a provider can enforce for least privilege.
18
+ */
19
+ export type ActorSignal = true | {
20
+ actorKind: 'system';
21
+ sourceWorkflow?: string;
22
+ /**
23
+ * Identity of the acting system agent. Unlike the check `resource` (the
24
+ * target), this names the actor itself so a provider can enforce
25
+ * per-agent least privilege.
26
+ */
27
+ agentId?: string;
28
+ /**
29
+ * Permission grants *claimed* for this actor — the actor analog of a
30
+ * user's resolved permissions. This is an untrusted, self-asserted hint:
31
+ * a provider enforcing real least privilege should resolve the actor's
32
+ * authoritative grants from a trusted source (e.g. a manifest or FGA,
33
+ * keyed by `agentId`) rather than trusting these values directly.
34
+ */
35
+ permissions?: MastraFGAPermissionInput[];
36
+ /** Additional provider-specific scope for the actor (e.g. tenant, environment). */
37
+ scope?: Record<string, string>;
38
+ };
11
39
  /**
12
40
  * Optional context for an authorization check.
13
41
  */
@@ -306,6 +334,25 @@ export interface IFGAProvider<TUser = unknown> {
306
334
  filterAccessible<T extends {
307
335
  id: string;
308
336
  }>(user: TUser, resources: T[], resourceType: string, permission: MastraFGAPermissionInput): Promise<T[]>;
337
+ /**
338
+ * Authorize a non-user (system / autonomous agent) actor.
339
+ *
340
+ * System actors bypass the user-centric `require()` path. Implement this
341
+ * method to opt in to provider-driven least privilege for those actors:
342
+ * decide whether the actor (identified by `actor.agentId` and constrained by
343
+ * `actor.permissions` / `actor.scope`) may perform `permission` on
344
+ * `resource`, and throw {@link FGADeniedError} to deny.
345
+ *
346
+ * When this method is not implemented, Mastra preserves the legacy
347
+ * trusted-actor bypass (allow after the tenant-scope check). Adding it is
348
+ * therefore fully backward compatible.
349
+ *
350
+ * @param actor - The system actor signal (`true` shorthand, or an object with
351
+ * `agentId`, `permissions`, `scope`).
352
+ * @param params - The resource and permission being attempted.
353
+ * @throws FGADeniedError if the actor may not perform the action.
354
+ */
355
+ requireActor?(actor: ActorSignal, params: FGACheckParams): Promise<void>;
309
356
  }
310
357
  /**
311
358
  * Extended FGA interface with write operations for managing resources and role assignments.
@@ -270,6 +270,72 @@ export interface ICredentialsProvider<TUser extends User = User> {
270
270
  */
271
271
  isSignUpEnabled?(): boolean;
272
272
  }
273
+ /**
274
+ * Provider interface for organization membership management.
275
+ *
276
+ * Implement this interface to enable multi-tenant hosts (e.g. the Mastra Code
277
+ * web factory) to bootstrap a personal organization for new users and to
278
+ * authorize organization-level administrative mutations.
279
+ */
280
+ export interface IOrganizationsProvider {
281
+ /**
282
+ * Ensure the user belongs to an organization, bootstrapping a personal org
283
+ * on first use when they have none. Must be idempotent under
284
+ * concurrent/retried first logins.
285
+ *
286
+ * @param userId - Stable provider user id
287
+ * @returns The organization id, or `undefined` when the user genuinely
288
+ * stays no-org (bootstrap is best-effort)
289
+ */
290
+ ensureOrganization(userId: string): Promise<string | undefined>;
291
+ /**
292
+ * Whether the user holds an admin-equivalent role in the organization.
293
+ * Provider errors should resolve to `false` rather than throw.
294
+ *
295
+ * @param organizationId - Organization id
296
+ * @param userId - Stable provider user id
297
+ */
298
+ isOrganizationAdmin(organizationId: string, userId: string): Promise<boolean>;
299
+ }
300
+ /**
301
+ * Host-level context handed to {@link IAuthInit.init} once during server
302
+ * preparation. Storage-agnostic: hosts pass whatever database handle their
303
+ * storage backend exposes.
304
+ */
305
+ export interface AuthInitContext {
306
+ /**
307
+ * Database handle for providers that persist their own auth tables
308
+ * (e.g. better-auth). Shape is provider-defined; hosts pass their storage
309
+ * backend's auth database as-is.
310
+ */
311
+ database?: unknown;
312
+ /** Browser-facing origin (no trailing slash), e.g. `https://factory.acme.com`. */
313
+ publicUrl?: string;
314
+ /**
315
+ * Extra browser origins allowed to talk to the API (cross-origin SPA
316
+ * deploys). Providers that enforce their own origin allow-list (e.g.
317
+ * better-auth `trustedOrigins`) must honor these.
318
+ */
319
+ allowedOrigins?: string[];
320
+ }
321
+ /**
322
+ * Optional one-time initialization hook for auth providers. Hosts call
323
+ * `init()` once during preparation with host-level context (database, public
324
+ * origin) so providers can consume it without the deploy entry passing it
325
+ * twice. Providers should fail fast here for requirements only satisfiable at
326
+ * prepare time.
327
+ */
328
+ export interface IAuthInit {
329
+ init(ctx: AuthInitContext): Promise<void>;
330
+ }
331
+ /**
332
+ * Provider interface for handling raw auth HTTP requests. Providers that ship
333
+ * their own HTTP API surface (e.g. better-auth's `/api/auth/*` handler)
334
+ * implement this so hosts can mount it (typically under `/auth/api/*`).
335
+ */
336
+ export interface IAuthHttpHandler {
337
+ handleAuthRequest(request: Request): Promise<Response>;
338
+ }
273
339
  export * from './session/index.js';
274
340
  export * from './provider/index.js';
275
341
  export * from './types/index.js';
@@ -1,5 +1,5 @@
1
1
  import { MastraBase } from '../_types/@internal_core/dist/base/index.d.ts';
2
- import type { CredentialsResult, ISSOProvider, ISessionProvider, IUserProvider, Session, SSOCallbackResult, SSOLoginConfig, User } from '..';
2
+ import type { CredentialsResult, IAuthHttpHandler, IAuthInit, ICredentialsProvider, IOrganizationsProvider, ISSOProvider, ISessionProvider, IUserProvider, Session, SSOCallbackResult, SSOLoginConfig, User } from '..';
3
3
  import type { AuthorizeUserFn, MastraAuthConfig, MastraAuthRequest } from '../types/index.js';
4
4
  export interface MastraAuthProviderOptions<TUser = unknown> {
5
5
  name?: string;
@@ -72,6 +72,13 @@ export declare abstract class MastraAuthProvider<TUser = unknown> extends Mastra
72
72
  abstract authorizeUser(user: TUser, request: MastraAuthRequest): Promise<boolean> | boolean;
73
73
  protected registerOptions(opts?: MastraAuthProviderOptions<TUser>): void;
74
74
  }
75
+ export declare function isSSOProvider(p: unknown): p is ISSOProvider;
76
+ export declare function isSessionProvider(p: unknown): p is ISessionProvider;
77
+ export declare function isUserProvider(p: unknown): p is IUserProvider;
78
+ export declare function isCredentialsProvider(p: unknown): p is ICredentialsProvider;
79
+ export declare function isOrganizationsProvider(p: unknown): p is IOrganizationsProvider;
80
+ export declare function isAuthHttpHandler(p: unknown): p is IAuthHttpHandler;
81
+ export declare function hasAuthInit(p: unknown): p is IAuthInit;
75
82
  export declare class CompositeAuth extends MastraAuthProvider implements ISSOProvider<User>, ISessionProvider<Session>, IUserProvider<User> {
76
83
  private providers;
77
84
  private authenticatedProviderByObject;
package/dist/index.cjs CHANGED
@@ -1,5 +1,11 @@
1
1
  'use strict';
2
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
+
3
9
  // ../../packages/_internal-core/dist/chunk-3M4SEWMI.js
4
10
  var RegisteredLogger = {
5
11
  LLM: "LLM"};
@@ -152,7 +158,7 @@ var MastraBase = class {
152
158
  }
153
159
  };
154
160
 
155
- // ../../packages/_internals/auth/dist/chunk-7GLKLOU3.js
161
+ // ../../packages/_internals/auth/dist/chunk-NJOKT6V5.js
156
162
  var MastraAuthProvider = class extends MastraBase {
157
163
  protected;
158
164
  public;
@@ -180,8 +186,13 @@ var MastraAuthProvider = class extends MastraBase {
180
186
  }
181
187
  }
182
188
  };
183
-
184
- // src/index.ts
189
+ function tryDecode(value) {
190
+ try {
191
+ return decodeURIComponent(value);
192
+ } catch {
193
+ return value;
194
+ }
195
+ }
185
196
  function getRequestHeader2(request, name) {
186
197
  if (request instanceof Request) {
187
198
  return request.headers.get(name);
@@ -202,23 +213,253 @@ function mapBetterAuthUserToEEUser(user) {
202
213
  };
203
214
  }
204
215
  var MastraAuthBetterAuth = class extends MastraAuthProvider {
205
- auth;
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;
206
226
  signUpEnabledConfig;
207
- sessionCookieName;
208
227
  constructor(options) {
209
228
  super({ name: options?.name ?? "better-auth" });
210
- if (!options.auth) {
229
+ if (!options.auth && !options.secret) {
211
230
  throw new Error(
212
- "Better Auth instance is required. Please provide the auth option with your Better Auth instance created via betterAuth({ ... })"
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."
213
232
  );
214
233
  }
215
- this.auth = options.auth;
234
+ this.#auth = options.auth;
235
+ this.#secret = options.secret;
216
236
  this.signUpEnabledConfig = options.signUpEnabled ?? true;
217
- const authWithOptions = this.auth;
218
- const prefix = authWithOptions.options?.advanced?.cookiePrefix ?? "better-auth";
219
- this.sessionCookieName = `${prefix}.session_token`;
220
237
  this.registerOptions(options);
221
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
+ }
222
463
  /**
223
464
  * Check if sign-up is enabled.
224
465
  * Implements ICredentialsProvider.isSignUpEnabled.
@@ -234,13 +475,21 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
234
475
  * Get current user from request.
235
476
  * Implements IUserProvider for EE user awareness in Studio.
236
477
  *
478
+ * Supports both cookie-based sessions and `Authorization: Bearer <token>`
479
+ * requests (e.g. API clients using the token returned by `signIn`).
480
+ *
237
481
  * @param request - Incoming HTTP request
238
482
  * @returns EE User object or null if not authenticated
239
483
  */
240
484
  async getCurrentUser(request) {
241
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);
242
491
  const result = await this.auth.api.getSession({
243
- headers: request.headers
492
+ headers
244
493
  });
245
494
  if (!result?.user) return null;
246
495
  return mapBetterAuthUserToEEUser(result.user);
@@ -252,18 +501,32 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
252
501
  * Get user by ID.
253
502
  * Implements IUserProvider for EE user awareness.
254
503
  *
255
- * Note: Better Auth doesn't expose a direct getUser API.
256
- * For full functionality, you may need to implement this using
257
- * direct database access in a subclass.
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.
258
506
  *
259
507
  * @param userId - User identifier
260
508
  * @returns EE User object or null if not found
261
509
  */
262
- async getUser(_userId) {
263
- console.warn(
264
- "[MastraAuthBetterAuth] getUser() requires direct database access. Override this method in a subclass for full user lookup support."
265
- );
266
- return null;
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)));
267
530
  }
268
531
  /**
269
532
  * Get URL to user's profile page.
@@ -272,11 +535,51 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
272
535
  getUserProfileUrl(user) {
273
536
  return `/profile/${user.id}`;
274
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
+ }
275
576
  /**
276
577
  * Authenticate a bearer token by verifying the session with Better Auth.
277
578
  *
278
579
  * This method extracts the session from the request headers using
279
- * Better Auth's `api.getSession()` endpoint.
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.
280
583
  *
281
584
  * @param token - The bearer token (session token) to authenticate
282
585
  * @param request - The request containing headers
@@ -284,19 +587,13 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
284
587
  */
285
588
  async authenticateToken(token, request) {
286
589
  try {
590
+ await this.#ensureDbReady();
287
591
  const headers = new Headers();
288
592
  const cookieHeader = getRequestHeader2(request, "Cookie");
289
593
  if (cookieHeader) {
290
594
  headers.set("Cookie", cookieHeader);
291
595
  }
292
- const hasSessionCookie = !!cookieHeader?.split(";").some((pair) => {
293
- const [key] = pair.trim().split("=");
294
- return key?.trim() === this.sessionCookieName;
295
- });
296
- if (token && !hasSessionCookie) {
297
- const existingCookies = cookieHeader ? `${cookieHeader}; ` : "";
298
- headers.set("Cookie", `${existingCookies}${this.sessionCookieName}=${token}`);
299
- }
596
+ await this.ensureSessionCookie(headers, token);
300
597
  const result = await this.auth.api.getSession({
301
598
  headers
302
599
  });
@@ -416,9 +713,10 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
416
713
  * Clears Better Auth's default session cookies.
417
714
  */
418
715
  getClearSessionHeaders() {
716
+ const sameSite = this.#crossSite ? "None; Secure" : "Lax";
419
717
  const cookies = [
420
- `${this.sessionCookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`,
421
- `${this.sessionCookieName}_sig=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`
718
+ `${this.sessionCookieName}=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`,
719
+ `${this.sessionCookieName}_sig=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`
422
720
  ];
423
721
  return {
424
722
  "Set-Cookie": cookies.join(", ")