@mastra/auth-better-auth 1.1.2-alpha.1 → 1.1.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/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
+ import { LibsqlDialect } from '@libsql/kysely-libsql';
2
+ import { betterAuth } from 'better-auth';
1
3
  import { makeSignature } from 'better-auth/crypto';
4
+ import { getMigrations } from 'better-auth/db/migration';
5
+ import { organization } from 'better-auth/plugins';
2
6
 
3
7
  // ../../packages/_internal-core/dist/chunk-3M4SEWMI.js
4
8
  var RegisteredLogger = {
@@ -152,7 +156,7 @@ var MastraBase = class {
152
156
  }
153
157
  };
154
158
 
155
- // ../../packages/_internals/auth/dist/chunk-7GLKLOU3.js
159
+ // ../../packages/_internals/auth/dist/chunk-NJOKT6V5.js
156
160
  var MastraAuthProvider = class extends MastraBase {
157
161
  protected;
158
162
  public;
@@ -207,23 +211,253 @@ function mapBetterAuthUserToEEUser(user) {
207
211
  };
208
212
  }
209
213
  var MastraAuthBetterAuth = class extends MastraAuthProvider {
210
- auth;
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;
211
224
  signUpEnabledConfig;
212
- sessionCookieName;
213
225
  constructor(options) {
214
226
  super({ name: options?.name ?? "better-auth" });
215
- if (!options.auth) {
227
+ if (!options.auth && !options.secret) {
216
228
  throw new Error(
217
- "Better Auth instance is required. Please provide the auth option with your Better Auth instance created via betterAuth({ ... })"
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."
218
230
  );
219
231
  }
220
- this.auth = options.auth;
232
+ this.#auth = options.auth;
233
+ this.#secret = options.secret;
221
234
  this.signUpEnabledConfig = options.signUpEnabled ?? true;
222
- const authWithOptions = this.auth;
223
- const prefix = authWithOptions.options?.advanced?.cookiePrefix ?? "better-auth";
224
- this.sessionCookieName = `${prefix}.session_token`;
225
235
  this.registerOptions(options);
226
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
+ }
227
461
  /**
228
462
  * Check if sign-up is enabled.
229
463
  * Implements ICredentialsProvider.isSignUpEnabled.
@@ -247,6 +481,7 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
247
481
  */
248
482
  async getCurrentUser(request) {
249
483
  try {
484
+ await this.#ensureDbReady();
250
485
  const headers = new Headers(request.headers);
251
486
  const authHeader = headers.get("Authorization");
252
487
  const bearerToken = authHeader && authHeader.slice(0, 7).toLowerCase() === "bearer " ? authHeader.slice(7).trim() : void 0;
@@ -350,6 +585,7 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
350
585
  */
351
586
  async authenticateToken(token, request) {
352
587
  try {
588
+ await this.#ensureDbReady();
353
589
  const headers = new Headers();
354
590
  const cookieHeader = getRequestHeader2(request, "Cookie");
355
591
  if (cookieHeader) {
@@ -475,9 +711,10 @@ var MastraAuthBetterAuth = class extends MastraAuthProvider {
475
711
  * Clears Better Auth's default session cookies.
476
712
  */
477
713
  getClearSessionHeaders() {
714
+ const sameSite = this.#crossSite ? "None; Secure" : "Lax";
478
715
  const cookies = [
479
- `${this.sessionCookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`,
480
- `${this.sessionCookieName}_sig=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`
716
+ `${this.sessionCookieName}=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`,
717
+ `${this.sessionCookieName}_sig=; Path=/; HttpOnly; SameSite=${sameSite}; Max-Age=0`
481
718
  ];
482
719
  return {
483
720
  "Set-Cookie": cookies.join(", ")