@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 +38 -0
- package/dist/_types/@internal_auth/dist/ee/fga-check.d.ts +2 -5
- package/dist/_types/@internal_auth/dist/ee/interfaces/fga.d.ts +47 -0
- package/dist/_types/@internal_auth/dist/index.d.ts +66 -0
- package/dist/_types/@internal_auth/dist/provider/index.d.ts +8 -1
- package/dist/index.cjs +329 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +92 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +329 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
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
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
256
|
-
*
|
|
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(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
421
|
-
`${this.sessionCookieName}_sig=; Path=/; HttpOnly; SameSite
|
|
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(", ")
|