@mastra/auth-studio 1.3.3-alpha.0 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,864 +1,878 @@
1
- import { createHash } from 'crypto';
2
-
3
- // src/index.ts
4
-
5
- // ../../packages/_internals/auth/dist/chunk-LZCWL5CT.js
6
- var RESOURCE_EXPANSIONS = {
7
- stored: [
8
- "stored-agents",
9
- "stored-mcp-clients",
10
- "stored-prompt-blocks",
11
- "stored-scorers",
12
- "stored-skills",
13
- "stored-workspaces"
14
- ]
15
- };
1
+ import { createHash } from "crypto";
2
+ //#region ../../packages/_internals/auth/dist/ee-C8o0LZ3r.js
3
+ /**
4
+ * FGA enforcement utility for checking fine-grained authorization.
5
+ *
6
+ * @license Mastra Enterprise License - see ee/LICENSE
7
+ */
8
+ /**
9
+ * Compound resource keys that expand to a set of per-family resources.
10
+ * A granted `stored:<action>` is treated as matching any `stored-<family>:<action>`
11
+ * (and `stored:*` matches any `stored-<family>:*`).
12
+ */
13
+ const RESOURCE_EXPANSIONS = { stored: [
14
+ "stored-agents",
15
+ "stored-mcp-clients",
16
+ "stored-prompt-blocks",
17
+ "stored-scorers",
18
+ "stored-skills",
19
+ "stored-workspaces"
20
+ ] };
21
+ /**
22
+ * Check if a permission matches (including wildcard support).
23
+ *
24
+ * Permission format: `{resource}:{action}[:{resource-id}]`
25
+ *
26
+ * Examples:
27
+ * - `*` matches everything
28
+ * - `agents:*` matches `agents:read`, `agents:read:my-agent`
29
+ * - `*:read` matches `agents:read`, `workflows:read` (action across all resources)
30
+ * - `agents:read` matches `agents:read`, `agents:read:my-agent`
31
+ * - `agents:read:my-agent` matches only `agents:read:my-agent`
32
+ * - `agents:*:my-agent` matches `agents:read:my-agent`, `agents:write:my-agent`
33
+ *
34
+ * @param userPermission - Permission the user has
35
+ * @param requiredPermission - Permission being checked
36
+ * @returns True if permission matches
37
+ */
16
38
  function matchesPermission(userPermission, requiredPermission) {
17
- if (userPermission === "*") {
18
- return true;
19
- }
20
- const grantedParts = userPermission.split(":");
21
- const requiredParts = requiredPermission.split(":");
22
- const expandedFamilies = RESOURCE_EXPANSIONS[grantedParts[0] ?? ""];
23
- if (expandedFamilies && expandedFamilies.includes(requiredParts[0] ?? "")) {
24
- const aliased = [requiredParts[0], ...grantedParts.slice(1)].join(":");
25
- return matchesPermission(aliased, requiredPermission);
26
- }
27
- if (grantedParts.length < 2 || requiredParts.length < 2) {
28
- return userPermission === requiredPermission;
29
- }
30
- const [grantedResource, grantedAction, grantedId] = grantedParts;
31
- const [requiredResource, requiredAction, requiredId] = requiredParts;
32
- if (grantedResource === "*") {
33
- if (grantedAction === "*") {
34
- if (grantedId === void 0) {
35
- return true;
36
- }
37
- return grantedId === requiredId;
38
- }
39
- if (grantedAction !== requiredAction) {
40
- return false;
41
- }
42
- if (grantedId === void 0) {
43
- return true;
44
- }
45
- return grantedId === requiredId;
46
- }
47
- if (grantedResource !== requiredResource) {
48
- return false;
49
- }
50
- if (grantedAction === "*") {
51
- if (grantedId === void 0) {
52
- return true;
53
- }
54
- return grantedId === requiredId;
55
- }
56
- if (grantedAction !== requiredAction) {
57
- return false;
58
- }
59
- if (grantedId === void 0) {
60
- return true;
61
- }
62
- return grantedId === requiredId;
39
+ if (userPermission === "*") return true;
40
+ const grantedParts = userPermission.split(":");
41
+ const requiredParts = requiredPermission.split(":");
42
+ const expandedFamilies = RESOURCE_EXPANSIONS[grantedParts[0] ?? ""];
43
+ if (expandedFamilies && expandedFamilies.includes(requiredParts[0] ?? "")) return matchesPermission([requiredParts[0], ...grantedParts.slice(1)].join(":"), requiredPermission);
44
+ if (grantedParts.length < 2 || requiredParts.length < 2) return userPermission === requiredPermission;
45
+ const [grantedResource, grantedAction, grantedId] = grantedParts;
46
+ const [requiredResource, requiredAction, requiredId] = requiredParts;
47
+ if (grantedResource === "*") {
48
+ if (grantedAction === "*") {
49
+ if (grantedId === void 0) return true;
50
+ return grantedId === requiredId;
51
+ }
52
+ if (grantedAction !== requiredAction) return false;
53
+ if (grantedId === void 0) return true;
54
+ return grantedId === requiredId;
55
+ }
56
+ if (grantedResource !== requiredResource) return false;
57
+ if (grantedAction === "*") {
58
+ if (grantedId === void 0) return true;
59
+ return grantedId === requiredId;
60
+ }
61
+ if (grantedAction !== requiredAction) return false;
62
+ if (grantedId === void 0) return true;
63
+ return grantedId === requiredId;
63
64
  }
65
+ /**
66
+ * Resolve permissions from user roles using a role mapping.
67
+ *
68
+ * This function translates provider-defined roles (from WorkOS, Okta, etc.)
69
+ * to Mastra permissions using a configurable mapping.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const roleMapping = {
74
+ * "Engineering": ["agents:*", "workflows:*"],
75
+ * "Product": ["agents:read"],
76
+ * "_default": [],
77
+ * };
78
+ *
79
+ * // User has "Engineering" and "QA" roles
80
+ * const permissions = resolvePermissionsFromMapping(
81
+ * ["Engineering", "QA"],
82
+ * roleMapping
83
+ * );
84
+ * // Result: ["agents:*", "workflows:*"] (QA is unmapped, gets _default)
85
+ * ```
86
+ *
87
+ * @param roles - User's roles from the identity provider
88
+ * @param mapping - Role to permission mapping
89
+ * @returns Array of resolved permissions
90
+ */
64
91
  function resolvePermissionsFromMapping(roles, mapping) {
65
- const permissions = /* @__PURE__ */ new Set();
66
- const defaultPerms = mapping["_default"] ?? [];
67
- for (const role of roles) {
68
- const rolePerms = mapping[role];
69
- if (rolePerms) {
70
- for (const perm of rolePerms) {
71
- permissions.add(perm);
72
- }
73
- } else {
74
- for (const perm of defaultPerms) {
75
- permissions.add(perm);
76
- }
77
- }
78
- }
79
- return Array.from(permissions);
92
+ const permissions = /* @__PURE__ */ new Set();
93
+ const defaultPerms = mapping["_default"] ?? [];
94
+ for (const role of roles) {
95
+ const rolePerms = mapping[role];
96
+ if (rolePerms) for (const perm of rolePerms) permissions.add(perm);
97
+ else for (const perm of defaultPerms) permissions.add(perm);
98
+ }
99
+ return Array.from(permissions);
80
100
  }
81
-
82
- // ../../packages/_internal-core/dist/chunk-3M4SEWMI.js
83
- var RegisteredLogger = {
84
- LLM: "LLM"};
85
- var LogLevel = {
86
- DEBUG: "debug",
87
- INFO: "info",
88
- WARN: "warn",
89
- ERROR: "error"};
101
+ /**
102
+ * @mastra/core/auth/ee
103
+ *
104
+ * Enterprise authentication capabilities for Mastra.
105
+ * This code is licensed under the Mastra Enterprise License - see ee/LICENSE.
106
+ *
107
+ * @license Mastra Enterprise License - see ee/LICENSE
108
+ * @packageDocumentation
109
+ */
110
+ //#endregion
111
+ //#region ../../packages/_internal-core/dist/logger/index.js
112
+ const RegisteredLogger = {
113
+ AGENT: "AGENT",
114
+ OBSERVABILITY: "OBSERVABILITY",
115
+ AUTH: "AUTH",
116
+ BROWSER: "BROWSER",
117
+ NETWORK: "NETWORK",
118
+ WORKFLOW: "WORKFLOW",
119
+ LLM: "LLM",
120
+ TTS: "TTS",
121
+ VOICE: "VOICE",
122
+ VECTOR: "VECTOR",
123
+ BUNDLER: "BUNDLER",
124
+ DEPLOYER: "DEPLOYER",
125
+ MEMORY: "MEMORY",
126
+ STORAGE: "STORAGE",
127
+ EMBEDDINGS: "EMBEDDINGS",
128
+ MCP_SERVER: "MCP_SERVER",
129
+ SERVER_CACHE: "SERVER_CACHE",
130
+ SERVER: "SERVER",
131
+ WORKSPACE: "WORKSPACE",
132
+ CHANNEL: "CHANNEL"
133
+ };
134
+ const LogLevel = {
135
+ DEBUG: "debug",
136
+ INFO: "info",
137
+ WARN: "warn",
138
+ ERROR: "error",
139
+ NONE: "silent"
140
+ };
90
141
  var MastraLogger = class {
91
- name;
92
- level;
93
- transports;
94
- constructor(options = {}) {
95
- this.name = options.name || "Mastra";
96
- this.level = options.level || LogLevel.ERROR;
97
- this.transports = new Map(Object.entries(options.transports || {}));
98
- }
99
- getTransports() {
100
- return this.transports;
101
- }
102
- trackException(_error, _metadata) {
103
- }
104
- async listLogs(transportId, params) {
105
- if (!transportId || !this.transports.has(transportId)) {
106
- return { logs: [], total: 0, page: params?.page ?? 1, perPage: params?.perPage ?? 100, hasMore: false };
107
- }
108
- return this.transports.get(transportId).listLogs?.(params) ?? {
109
- logs: [],
110
- total: 0,
111
- page: params?.page ?? 1,
112
- perPage: params?.perPage ?? 100,
113
- hasMore: false
114
- };
115
- }
116
- async listLogsByRunId({
117
- transportId,
118
- runId,
119
- fromDate,
120
- toDate,
121
- logLevel,
122
- filters,
123
- page,
124
- perPage
125
- }) {
126
- if (!transportId || !this.transports.has(transportId) || !runId) {
127
- return { logs: [], total: 0, page: page ?? 1, perPage: perPage ?? 100, hasMore: false };
128
- }
129
- return this.transports.get(transportId).listLogsByRunId?.({ runId, fromDate, toDate, logLevel, filters, page, perPage }) ?? {
130
- logs: [],
131
- total: 0,
132
- page: page ?? 1,
133
- perPage: perPage ?? 100,
134
- hasMore: false
135
- };
136
- }
142
+ name;
143
+ level;
144
+ transports;
145
+ constructor(options = {}) {
146
+ this.name = options.name || "Mastra";
147
+ this.level = options.level || LogLevel.ERROR;
148
+ this.transports = new Map(Object.entries(options.transports || {}));
149
+ }
150
+ getTransports() {
151
+ return this.transports;
152
+ }
153
+ trackException(_error, _metadata) {}
154
+ async listLogs(transportId, params) {
155
+ if (!transportId || !this.transports.has(transportId)) return {
156
+ logs: [],
157
+ total: 0,
158
+ page: params?.page ?? 1,
159
+ perPage: params?.perPage ?? 100,
160
+ hasMore: false
161
+ };
162
+ return this.transports.get(transportId).listLogs?.(params) ?? {
163
+ logs: [],
164
+ total: 0,
165
+ page: params?.page ?? 1,
166
+ perPage: params?.perPage ?? 100,
167
+ hasMore: false
168
+ };
169
+ }
170
+ async listLogsByRunId({ transportId, runId, fromDate, toDate, logLevel, filters, page, perPage }) {
171
+ if (!transportId || !this.transports.has(transportId) || !runId) return {
172
+ logs: [],
173
+ total: 0,
174
+ page: page ?? 1,
175
+ perPage: perPage ?? 100,
176
+ hasMore: false
177
+ };
178
+ return this.transports.get(transportId).listLogsByRunId?.({
179
+ runId,
180
+ fromDate,
181
+ toDate,
182
+ logLevel,
183
+ filters,
184
+ page,
185
+ perPage
186
+ }) ?? {
187
+ logs: [],
188
+ total: 0,
189
+ page: page ?? 1,
190
+ perPage: perPage ?? 100,
191
+ hasMore: false
192
+ };
193
+ }
137
194
  };
138
- var ConsoleLogger = class _ConsoleLogger extends MastraLogger {
139
- component;
140
- filter;
141
- constructor(options = {}) {
142
- super(options);
143
- this.component = options.component;
144
- this.filter = options.filter;
145
- }
146
- child(componentOrBindings) {
147
- const component = typeof componentOrBindings === "string" ? componentOrBindings : componentOrBindings?.component ?? this.component;
148
- return new _ConsoleLogger({
149
- name: this.name,
150
- level: this.level,
151
- component,
152
- filter: this.filter
153
- });
154
- }
155
- shouldLog(level, message, args) {
156
- if (!this.filter) return true;
157
- try {
158
- return this.filter({ component: this.component, level, message, args });
159
- } catch (e) {
160
- console.error(`[Logger] Filter error for component=${this.component} level=${level}:`, e);
161
- return true;
162
- }
163
- }
164
- prefix() {
165
- return this.component ? `[${this.component}] ` : "";
166
- }
167
- debug(message, ...args) {
168
- if (this.level === LogLevel.DEBUG && this.shouldLog(LogLevel.DEBUG, message, args)) {
169
- console.info(`${this.prefix()}${message}`, ...args);
170
- }
171
- }
172
- info(message, ...args) {
173
- if ((this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.INFO, message, args)) {
174
- console.info(`${this.prefix()}${message}`, ...args);
175
- }
176
- }
177
- warn(message, ...args) {
178
- if ((this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.WARN, message, args)) {
179
- console.warn(`${this.prefix()}${message}`, ...args);
180
- }
181
- }
182
- error(message, ...args) {
183
- if ((this.level === LogLevel.ERROR || this.level === LogLevel.WARN || this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.ERROR, message, args)) {
184
- console.error(`${this.prefix()}${message}`, ...args);
185
- }
186
- }
187
- async listLogs(_transportId, _params) {
188
- return { logs: [], total: 0, page: _params?.page ?? 1, perPage: _params?.perPage ?? 100, hasMore: false };
189
- }
190
- async listLogsByRunId(_args) {
191
- return { logs: [], total: 0, page: _args.page ?? 1, perPage: _args.perPage ?? 100, hasMore: false };
192
- }
195
+ var ConsoleLogger = class ConsoleLogger extends MastraLogger {
196
+ component;
197
+ filter;
198
+ constructor(options = {}) {
199
+ super(options);
200
+ this.component = options.component;
201
+ this.filter = options.filter;
202
+ }
203
+ child(componentOrBindings) {
204
+ const component = typeof componentOrBindings === "string" ? componentOrBindings : componentOrBindings?.component ?? this.component;
205
+ return new ConsoleLogger({
206
+ name: this.name,
207
+ level: this.level,
208
+ component,
209
+ filter: this.filter
210
+ });
211
+ }
212
+ shouldLog(level, message, args) {
213
+ if (!this.filter) return true;
214
+ try {
215
+ return this.filter({
216
+ component: this.component,
217
+ level,
218
+ message,
219
+ args
220
+ });
221
+ } catch (e) {
222
+ console.error(`[Logger] Filter error for component=${this.component} level=${level}:`, e);
223
+ return true;
224
+ }
225
+ }
226
+ prefix() {
227
+ return this.component ? `[${this.component}] ` : "";
228
+ }
229
+ debug(message, ...args) {
230
+ if (this.level === LogLevel.DEBUG && this.shouldLog(LogLevel.DEBUG, message, args)) console.info(`${this.prefix()}${message}`, ...args);
231
+ }
232
+ info(message, ...args) {
233
+ if ((this.level === LogLevel.INFO || this.level === LogLevel.DEBUG) && this.shouldLog(LogLevel.INFO, message, args)) console.info(`${this.prefix()}${message}`, ...args);
234
+ }
235
+ warn(message, ...args) {
236
+ 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);
237
+ }
238
+ error(message, ...args) {
239
+ 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);
240
+ }
241
+ async listLogs(_transportId, _params) {
242
+ return {
243
+ logs: [],
244
+ total: 0,
245
+ page: _params?.page ?? 1,
246
+ perPage: _params?.perPage ?? 100,
247
+ hasMore: false
248
+ };
249
+ }
250
+ async listLogsByRunId(_args) {
251
+ return {
252
+ logs: [],
253
+ total: 0,
254
+ page: _args.page ?? 1,
255
+ perPage: _args.perPage ?? 100,
256
+ hasMore: false
257
+ };
258
+ }
193
259
  };
194
-
195
- // ../../packages/_internal-core/dist/base/index.js
260
+ //#endregion
261
+ //#region ../../packages/_internal-core/dist/base/index.js
196
262
  var MastraBase = class {
197
- component = RegisteredLogger.LLM;
198
- logger;
199
- name;
200
- #rawConfig;
201
- constructor({
202
- component,
203
- name,
204
- rawConfig
205
- }) {
206
- this.component = component || RegisteredLogger.LLM;
207
- this.name = name;
208
- this.#rawConfig = rawConfig;
209
- this.logger = new ConsoleLogger({ name: `${this.component} - ${this.name}` });
210
- }
211
- /**
212
- * Returns the raw storage configuration this primitive was created from,
213
- * or undefined if it was created from code.
214
- */
215
- toRawConfig() {
216
- return this.#rawConfig;
217
- }
218
- /**
219
- * Sets the raw storage configuration for this primitive.
220
- * @internal
221
- */
222
- __setRawConfig(rawConfig) {
223
- this.#rawConfig = rawConfig;
224
- }
225
- /**
226
- * Set the logger for the agent
227
- * @param logger
228
- */
229
- __setLogger(logger) {
230
- this.logger = "child" in logger && typeof logger.child === "function" ? logger.child({ component: this.component }) : logger;
231
- }
263
+ component = RegisteredLogger.LLM;
264
+ logger;
265
+ name;
266
+ #rawConfig;
267
+ constructor({ component, name, rawConfig }) {
268
+ this.component = component || RegisteredLogger.LLM;
269
+ this.name = name;
270
+ this.#rawConfig = rawConfig;
271
+ this.logger = new ConsoleLogger({ name: `${this.component} - ${this.name}` });
272
+ }
273
+ /**
274
+ * Returns the raw storage configuration this primitive was created from,
275
+ * or undefined if it was created from code.
276
+ */
277
+ toRawConfig() {
278
+ return this.#rawConfig;
279
+ }
280
+ /**
281
+ * Sets the raw storage configuration for this primitive.
282
+ * @internal
283
+ */
284
+ __setRawConfig(rawConfig) {
285
+ this.#rawConfig = rawConfig;
286
+ }
287
+ /**
288
+ * Set the logger for the agent
289
+ * @param logger
290
+ */
291
+ __setLogger(logger) {
292
+ this.logger = "child" in logger && typeof logger.child === "function" ? logger.child({ component: this.component }) : logger;
293
+ }
232
294
  };
233
-
234
- // ../../packages/_internals/auth/dist/chunk-NJOKT6V5.js
295
+ //#endregion
296
+ //#region ../../packages/_internals/auth/dist/provider/index.js
235
297
  var MastraAuthProvider = class extends MastraBase {
236
- protected;
237
- public;
238
- constructor(options) {
239
- super({ component: "AUTH", name: options?.name });
240
- if (options?.authorizeUser) {
241
- this.authorizeUser = options.authorizeUser.bind(this);
242
- }
243
- this.protected = options?.protected;
244
- this.public = options?.public;
245
- this.mapUserToResourceId = options?.mapUserToResourceId;
246
- }
247
- registerOptions(opts) {
248
- if (opts?.authorizeUser) {
249
- this.authorizeUser = opts.authorizeUser.bind(this);
250
- }
251
- if (opts?.mapUserToResourceId) {
252
- this.mapUserToResourceId = opts.mapUserToResourceId;
253
- }
254
- if (opts?.protected) {
255
- this.protected = opts.protected;
256
- }
257
- if (opts?.public) {
258
- this.public = opts.public;
259
- }
260
- }
298
+ protected;
299
+ public;
300
+ constructor(options) {
301
+ super({
302
+ component: "AUTH",
303
+ name: options?.name
304
+ });
305
+ if (options?.authorizeUser) this.authorizeUser = options.authorizeUser.bind(this);
306
+ this.protected = options?.protected;
307
+ this.public = options?.public;
308
+ this.mapUserToResourceId = options?.mapUserToResourceId;
309
+ }
310
+ registerOptions(opts) {
311
+ if (opts?.authorizeUser) this.authorizeUser = opts.authorizeUser.bind(this);
312
+ if (opts?.mapUserToResourceId) this.mapUserToResourceId = opts.mapUserToResourceId;
313
+ if (opts?.protected) this.protected = opts.protected;
314
+ if (opts?.public) this.public = opts.public;
315
+ }
261
316
  };
262
-
263
- // src/index.ts
264
- function getRequestHeader2(request, name) {
265
- if (request instanceof Request) {
266
- return request.headers.get(name);
267
- }
268
- return request.raw?.headers.get(name) ?? request.headers?.get(name) ?? request.header(name) ?? null;
317
+ //#endregion
318
+ //#region src/index.ts
319
+ function getRequestHeader(request, name) {
320
+ if (request instanceof Request) return request.headers.get(name);
321
+ return request.raw?.headers.get(name) ?? request.headers?.get(name) ?? request.header(name) ?? null;
269
322
  }
270
- var COOKIE_NAME = "wos-session";
271
- var VERIFY_FETCH_TIMEOUT_MS = 15e3;
272
- var VERIFY_CACHE_TTL_MS = 3e4;
323
+ const COOKIE_NAME = "wos-session";
324
+ /**
325
+ * Upper bound for shared-API verification fetches. Matches the platform API
326
+ * client's request budget: a slow shared API must fail a single request in
327
+ * bounded time instead of hanging every authenticated endpoint behind it.
328
+ */
329
+ const VERIFY_FETCH_TIMEOUT_MS = 15e3;
330
+ /**
331
+ * How long a successful credential verification may be reused without
332
+ * re-contacting the shared API. Bounds revocation lag: a signed-out or
333
+ * revoked session can be honored for at most this long.
334
+ */
335
+ const VERIFY_CACHE_TTL_MS = 3e4;
336
+ /**
337
+ * Auth provider for Mastra Studio deployed instances.
338
+ *
339
+ * Proxies all authentication through the shared API, keeping the
340
+ * WorkOS API key safely in the shared API. Deployed instances only
341
+ * need the shared API URL — no secrets required.
342
+ *
343
+ * The shared API's sealed session cookie (`wos-session`) is set with
344
+ * `Domain=.mastra.ai` in production, so it's included in requests
345
+ * to deployed instances and can be forwarded for validation.
346
+ */
273
347
  var MastraAuthStudio = class extends MastraAuthProvider {
274
- isMastraCloudAuth = true;
275
- sharedApiUrl;
276
- organizationId;
277
- useProductionCookies;
278
- cookieDomain;
279
- /**
280
- * `userId → sealed session cookie` cache. The `IOrganizationsProvider`
281
- * interface only hands us a `userId`, but the shared API's org endpoints are
282
- * cookie-authenticated — so we remember the cookie last seen for a user
283
- * inside `verifySessionCookie` and reuse it here. Kept small: bounded to
284
- * the last 1000 users, LRU-evicted on insert.
285
- */
286
- userSessionCookies = /* @__PURE__ */ new Map();
287
- maxCachedSessions = 1e3;
288
- /**
289
- * Short-TTL cache of SUCCESSFUL credential verifications, keyed by a
290
- * sha256 of the credential (never the raw cookie/token). Every protected
291
- * request re-verifies against the shared API otherwise — one network
292
- * round trip per request. Failures are never cached, so a rejected
293
- * credential is always re-checked. Bounded + insert-order evicted.
294
- */
295
- verifiedCredentials = /* @__PURE__ */ new Map();
296
- maxCachedVerifications = 1e3;
297
- /**
298
- * In-flight `ensureOrganization` promises keyed by userId. Concurrent calls
299
- * for the same brand-new user (multiple tabs, parallel requests) would
300
- * otherwise all see "no org" from `GET /auth/me` and each fire
301
- * `POST /auth/orgs`, creating duplicate personal organizations. The first
302
- * caller's promise is reused by every follower until it settles.
303
- */
304
- organizationBootstrapInFlight = /* @__PURE__ */ new Map();
305
- constructor(options) {
306
- super({ name: "mastra-studio", ...options });
307
- const explicitSharedApiUrl = options?.sharedApiUrl || process.env.MASTRA_SHARED_API_URL;
308
- this.sharedApiUrl = explicitSharedApiUrl || "https://platform.mastra.ai/v1";
309
- this.organizationId = options?.organizationId || process.env.MASTRA_ORGANIZATION_ID;
310
- if (this.sharedApiUrl.endsWith("/")) {
311
- this.sharedApiUrl = this.sharedApiUrl.slice(0, -1);
312
- }
313
- this.cookieDomain = options?.cookieDomain || process.env.MASTRA_COOKIE_DOMAIN;
314
- let autoDetectMastraAi = false;
315
- if (explicitSharedApiUrl) {
316
- try {
317
- const hostname = new URL(this.sharedApiUrl).hostname.toLowerCase();
318
- autoDetectMastraAi = hostname === "mastra.ai" || hostname.endsWith(".mastra.ai");
319
- } catch {
320
- autoDetectMastraAi = false;
321
- }
322
- }
323
- this.useProductionCookies = !!this.cookieDomain || autoDetectMastraAi;
324
- if (!this.cookieDomain && autoDetectMastraAi) {
325
- this.cookieDomain = ".mastra.ai";
326
- }
327
- if (options) {
328
- this.registerOptions(options);
329
- }
330
- }
331
- // ---------------------------------------------------------------------------
332
- // MastraAuthProvider abstract methods
333
- // ---------------------------------------------------------------------------
334
- /**
335
- * Authenticate an incoming request by forwarding the sealed session cookie
336
- * to the shared API's /auth/me endpoint, or a Bearer token to /auth/verify.
337
- */
338
- async authenticateToken(token, request) {
339
- let user = null;
340
- const cookieHeader = getRequestHeader2(request, "Cookie");
341
- const sessionCookie = parseCookie(cookieHeader, COOKIE_NAME);
342
- if (sessionCookie) {
343
- user = await this.verifySessionCookie(sessionCookie);
344
- }
345
- if (!user && token) {
346
- user = await this.verifyBearerToken(token);
347
- }
348
- if (!user) return null;
349
- if (this.organizationId && !user.memberOrgIds?.includes(this.organizationId)) {
350
- return null;
351
- }
352
- return user;
353
- }
354
- authorizeUser(user) {
355
- return !!user?.id;
356
- }
357
- // ---------------------------------------------------------------------------
358
- // ISSOProvider
359
- // ---------------------------------------------------------------------------
360
- getLoginUrl(redirectUri, state) {
361
- let postLoginRedirect = "/";
362
- if (state) {
363
- const pipeIndex = state.indexOf("|");
364
- if (pipeIndex !== -1) {
365
- try {
366
- postLoginRedirect = decodeURIComponent(state.slice(pipeIndex + 1));
367
- } catch {
368
- }
369
- }
370
- }
371
- const params = new URLSearchParams({
372
- product: "deploy",
373
- redirect_uri: redirectUri,
374
- post_login_redirect: postLoginRedirect,
375
- // Force re-authentication so AuthKit always shows the account picker
376
- prompt: "login",
377
- ...this.organizationId ? { organization_id: this.organizationId } : {}
378
- });
379
- return `${this.sharedApiUrl}/auth/login?${params.toString()}`;
380
- }
381
- async handleCallback(code, _state) {
382
- this.logger.debug("SSO callback: validating sealed session via shared API", {
383
- sharedApiUrl: this.sharedApiUrl,
384
- codeLength: code?.length
385
- });
386
- const user = await this.verifySessionCookie(code);
387
- if (!user) {
388
- this.logger.error("SSO callback: session validation failed \u2014 verifySessionCookie returned null", {
389
- sharedApiUrl: this.sharedApiUrl,
390
- codeLength: code?.length
391
- });
392
- throw new Error("Session validation failed");
393
- }
394
- return {
395
- user,
396
- tokens: {
397
- accessToken: code
398
- }
399
- };
400
- }
401
- setCallbackCookieHeader(_cookieHeader) {
402
- }
403
- getLoginCookies() {
404
- return void 0;
405
- }
406
- getLoginButtonConfig() {
407
- return {
408
- provider: "mastra-studio",
409
- text: "Sign in with Mastra",
410
- description: "Your deployed Studio is secured by your Mastra account. Sign in with the same email you used to sign up on mastra.ai."
411
- };
412
- }
413
- async getLogoutUrl(_redirectUri, request) {
414
- const cookieHeader = request?.headers.get("Cookie");
415
- const sessionCookie = parseCookie(cookieHeader, COOKIE_NAME);
416
- if (!sessionCookie) return null;
417
- try {
418
- const res = await fetch(`${this.sharedApiUrl}/auth/logout`, {
419
- method: "POST",
420
- headers: {
421
- "Content-Type": "application/json",
422
- Cookie: `${COOKIE_NAME}=${sessionCookie}`
423
- }
424
- });
425
- if (res.ok) {
426
- const data = await res.json();
427
- return data.logoutUrl ?? null;
428
- }
429
- } catch {
430
- }
431
- return null;
432
- }
433
- // ---------------------------------------------------------------------------
434
- // ISessionProvider
435
- // ---------------------------------------------------------------------------
436
- async createSession(userId, metadata) {
437
- const now = /* @__PURE__ */ new Date();
438
- return {
439
- id: metadata?.accessToken || crypto.randomUUID(),
440
- userId,
441
- expiresAt: new Date(now.getTime() + 24 * 60 * 60 * 1e3),
442
- // 24 hours
443
- createdAt: now,
444
- metadata
445
- };
446
- }
447
- async validateSession(sessionId) {
448
- const user = await this.verifySessionCookie(sessionId);
449
- if (!user) return null;
450
- const now = /* @__PURE__ */ new Date();
451
- return {
452
- id: sessionId,
453
- userId: user.id,
454
- expiresAt: new Date(now.getTime() + 24 * 60 * 60 * 1e3),
455
- createdAt: now
456
- };
457
- }
458
- async destroySession(sessionId) {
459
- try {
460
- await fetch(`${this.sharedApiUrl}/auth/logout`, {
461
- method: "POST",
462
- headers: {
463
- Cookie: `${COOKIE_NAME}=${sessionId}`
464
- }
465
- });
466
- } catch {
467
- }
468
- }
469
- async refreshSession(sessionId) {
470
- try {
471
- const res = await fetch(`${this.sharedApiUrl}/auth/refresh`, {
472
- method: "GET",
473
- headers: {
474
- Cookie: `${COOKIE_NAME}=${sessionId}`
475
- }
476
- });
477
- if (!res.ok) {
478
- this.logger.warn("refreshSession: shared API refresh returned non-OK status", {
479
- status: res.status,
480
- url: `${this.sharedApiUrl}/auth/refresh`
481
- });
482
- return this.validateSession(sessionId);
483
- }
484
- const setCookie = res.headers.get("Set-Cookie");
485
- const newSessionId = setCookie ? parseCookieFromHeader(setCookie, COOKIE_NAME) : null;
486
- if (!newSessionId) {
487
- this.logger.warn("refreshSession: no Set-Cookie header in refresh response");
488
- return this.validateSession(sessionId);
489
- }
490
- const user = await this.verifySessionCookie(newSessionId);
491
- if (!user) return null;
492
- const now = /* @__PURE__ */ new Date();
493
- return {
494
- id: newSessionId,
495
- userId: user.id,
496
- expiresAt: new Date(now.getTime() + 24 * 60 * 60 * 1e3),
497
- createdAt: now
498
- };
499
- } catch (error) {
500
- this.logger.error("refreshSession: fetch to shared API failed", {
501
- url: `${this.sharedApiUrl}/auth/refresh`,
502
- error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error)
503
- });
504
- return this.validateSession(sessionId);
505
- }
506
- }
507
- getSessionIdFromRequest(request) {
508
- const cookieHeader = request.headers.get("Cookie");
509
- return parseCookie(cookieHeader, COOKIE_NAME);
510
- }
511
- getSessionHeaders(session) {
512
- const parts = [`${COOKIE_NAME}=${session.id}`, "HttpOnly", "SameSite=Lax", "Path=/", "Max-Age=86400"];
513
- if (this.useProductionCookies && this.cookieDomain) {
514
- parts.push("Secure");
515
- parts.push(`Domain=${this.cookieDomain}`);
516
- }
517
- return { "Set-Cookie": parts.join("; ") };
518
- }
519
- getClearSessionHeaders() {
520
- const parts = [`${COOKIE_NAME}=`, "HttpOnly", "SameSite=Lax", "Path=/", "Max-Age=0"];
521
- if (this.useProductionCookies && this.cookieDomain) {
522
- parts.push("Secure");
523
- parts.push(`Domain=${this.cookieDomain}`);
524
- }
525
- return { "Set-Cookie": parts.join("; ") };
526
- }
527
- // ---------------------------------------------------------------------------
528
- // IUserProvider
529
- // ---------------------------------------------------------------------------
530
- async getCurrentUser(request) {
531
- const cookieHeader = request.headers.get("Cookie");
532
- const sessionCookie = parseCookie(cookieHeader, COOKIE_NAME);
533
- if (sessionCookie) {
534
- return this.verifySessionCookie(sessionCookie);
535
- }
536
- const authHeader = request.headers.get("Authorization");
537
- if (authHeader?.startsWith("Bearer ")) {
538
- return this.verifyBearerToken(authHeader.slice(7));
539
- }
540
- return null;
541
- }
542
- async getUser(_userId) {
543
- return null;
544
- }
545
- // ---------------------------------------------------------------------------
546
- // IOrganizationsProvider
547
- // ---------------------------------------------------------------------------
548
- /**
549
- * Ensure the user belongs to an organization, bootstrapping a personal org
550
- * on first use when they have none.
551
- *
552
- * Because the shared API's org endpoints are cookie-authenticated but this
553
- * method only receives a userId, we look up the user's sealed session cookie
554
- * from the {@link userSessionCookies} cache populated by
555
- * {@link verifySessionCookie}. If we have never seen a cookie for this user
556
- * (e.g. bearer-token flow, or the cache was evicted), we skip bootstrap and
557
- * return `undefined` — the caller keeps the user in their current no-org
558
- * state and the next authenticated request retries.
559
- *
560
- * Best-effort: any shared-API failure returns `undefined` rather than
561
- * throwing, mirroring `MastraAuthWorkos.ensureOrganization`.
562
- */
563
- async ensureOrganization(userId) {
564
- const inFlight = this.organizationBootstrapInFlight.get(userId);
565
- if (inFlight) return inFlight;
566
- const bootstrap = this.doEnsureOrganization(userId).finally(() => {
567
- this.organizationBootstrapInFlight.delete(userId);
568
- });
569
- this.organizationBootstrapInFlight.set(userId, bootstrap);
570
- return bootstrap;
571
- }
572
- async doEnsureOrganization(userId) {
573
- const sessionCookie = this.userSessionCookies.get(userId);
574
- if (!sessionCookie) {
575
- this.logger.debug("ensureOrganization: no cached session cookie for user; skipping bootstrap", { userId });
576
- return void 0;
577
- }
578
- try {
579
- const me = await this.fetchMe(sessionCookie);
580
- if (me?.organizationId) return me.organizationId;
581
- if (me?.memberOrgIds && me.memberOrgIds.length > 0) return me.memberOrgIds[0];
582
- const orgName = me?.user?.email ? `${me.user.email}'s org` : `Personal (${userId})`;
583
- const res = await fetch(`${this.sharedApiUrl}/auth/orgs`, {
584
- method: "POST",
585
- headers: {
586
- "Content-Type": "application/json",
587
- Cookie: `${COOKIE_NAME}=${sessionCookie}`
588
- },
589
- body: JSON.stringify({ name: orgName })
590
- });
591
- if (!res.ok) {
592
- this.logger.warn("ensureOrganization: shared API POST /auth/orgs returned non-OK", {
593
- status: res.status,
594
- userId
595
- });
596
- return void 0;
597
- }
598
- const data = await res.json();
599
- return data.organization?.id;
600
- } catch (error) {
601
- this.logger.error("ensureOrganization: fetch to shared API failed", {
602
- userId,
603
- error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error)
604
- });
605
- return void 0;
606
- }
607
- }
608
- /**
609
- * Whether the user holds an admin-equivalent role in the organization.
610
- *
611
- * Fast path: if the org matches the user's currently-active session org, we
612
- * read the role directly from `/auth/me`. Cross-org path: we call
613
- * `/auth/orgs` (which returns per-membership roles) and look up the target
614
- * org. Any shared-API failure resolves to `false`.
615
- */
616
- async isOrganizationAdmin(organizationId, userId) {
617
- const sessionCookie = this.userSessionCookies.get(userId);
618
- if (!sessionCookie) return false;
619
- try {
620
- const me = await this.fetchMe(sessionCookie);
621
- if (me?.organizationId === organizationId) {
622
- return isAdminRole(me.role);
623
- }
624
- const res = await fetch(`${this.sharedApiUrl}/auth/orgs`, {
625
- headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` }
626
- });
627
- if (!res.ok) return false;
628
- const data = await res.json();
629
- const membership = data.organizations?.find((o) => o.id === organizationId);
630
- return isAdminRole(membership?.role ?? void 0);
631
- } catch {
632
- return false;
633
- }
634
- }
635
- // ---------------------------------------------------------------------------
636
- // Internal helpers
637
- // ---------------------------------------------------------------------------
638
- /**
639
- * Record the sealed session cookie last seen for a user so
640
- * {@link ensureOrganization} / {@link isOrganizationAdmin} can act on their
641
- * behalf. LRU-evicted at {@link maxCachedSessions} entries.
642
- */
643
- rememberUserSession(userId, sessionCookie) {
644
- this.userSessionCookies.delete(userId);
645
- this.userSessionCookies.set(userId, sessionCookie);
646
- if (this.userSessionCookies.size > this.maxCachedSessions) {
647
- const oldest = this.userSessionCookies.keys().next().value;
648
- if (oldest !== void 0) this.userSessionCookies.delete(oldest);
649
- }
650
- }
651
- /** Cache key for a verified credential — hash, never the raw secret. */
652
- verificationKey(kind, credential) {
653
- return createHash("sha256").update(`${kind}:${credential}`).digest("hex");
654
- }
655
- getCachedVerification(key) {
656
- const entry = this.verifiedCredentials.get(key);
657
- if (!entry) return null;
658
- if (entry.expiresAt <= Date.now()) {
659
- this.verifiedCredentials.delete(key);
660
- return null;
661
- }
662
- return entry.user;
663
- }
664
- cacheVerification(key, user) {
665
- this.verifiedCredentials.delete(key);
666
- this.verifiedCredentials.set(key, { user, expiresAt: Date.now() + VERIFY_CACHE_TTL_MS });
667
- if (this.verifiedCredentials.size > this.maxCachedVerifications) {
668
- const oldest = this.verifiedCredentials.keys().next().value;
669
- if (oldest !== void 0) this.verifiedCredentials.delete(oldest);
670
- }
671
- }
672
- /**
673
- * Fetch the shared API's `/auth/me` and return the raw response body, or
674
- * `null` on any non-OK / network error. Split out so `ensureOrganization`
675
- * and `isOrganizationAdmin` can reuse it without duplicating the shape.
676
- */
677
- async fetchMe(sessionCookie) {
678
- try {
679
- const res = await fetch(`${this.sharedApiUrl}/auth/me`, {
680
- headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` },
681
- signal: AbortSignal.timeout(VERIFY_FETCH_TIMEOUT_MS)
682
- });
683
- if (!res.ok) return null;
684
- return await res.json();
685
- } catch {
686
- return null;
687
- }
688
- }
689
- /**
690
- * Forward a sealed session cookie to the shared API's /auth/me endpoint
691
- * to validate it and get user info.
692
- */
693
- async verifySessionCookie(sessionCookie) {
694
- const cacheKey = this.verificationKey("cookie", sessionCookie);
695
- const cached = this.getCachedVerification(cacheKey);
696
- if (cached) {
697
- this.rememberUserSession(cached.id, sessionCookie);
698
- return cached;
699
- }
700
- try {
701
- const res = await fetch(`${this.sharedApiUrl}/auth/me`, {
702
- headers: {
703
- Cookie: `${COOKIE_NAME}=${sessionCookie}`
704
- },
705
- signal: AbortSignal.timeout(VERIFY_FETCH_TIMEOUT_MS)
706
- });
707
- if (!res.ok) {
708
- this.logger.warn("verifySessionCookie: shared API returned non-OK status", {
709
- status: res.status,
710
- statusText: res.statusText,
711
- url: `${this.sharedApiUrl}/auth/me`
712
- });
713
- return null;
714
- }
715
- const data = await res.json();
716
- this.rememberUserSession(data.user.id, sessionCookie);
717
- const user = {
718
- id: data.user.id,
719
- email: data.user.email,
720
- name: [data.user.firstName, data.user.lastName].filter(Boolean).join(" ") || void 0,
721
- avatarUrl: data.user.profilePictureUrl,
722
- organizationId: data.organizationId,
723
- role: data.role,
724
- permissions: data.permissions,
725
- memberOrgIds: data.memberOrgIds
726
- };
727
- if (user.organizationId) this.cacheVerification(cacheKey, user);
728
- return user;
729
- } catch (error) {
730
- this.logger.error("verifySessionCookie: fetch to shared API failed", {
731
- url: `${this.sharedApiUrl}/auth/me`,
732
- error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error)
733
- });
734
- return null;
735
- }
736
- }
737
- /**
738
- * Forward a Bearer token to the shared API's /auth/verify endpoint
739
- * to validate it and get user info (used for CLI tokens).
740
- */
741
- async verifyBearerToken(token) {
742
- const cacheKey = this.verificationKey("bearer", token);
743
- const cached = this.getCachedVerification(cacheKey);
744
- if (cached) return cached;
745
- try {
746
- const res = await fetch(`${this.sharedApiUrl}/auth/verify`, {
747
- headers: {
748
- Authorization: `Bearer ${token}`
749
- },
750
- signal: AbortSignal.timeout(VERIFY_FETCH_TIMEOUT_MS)
751
- });
752
- if (!res.ok) {
753
- this.logger.warn("verifyBearerToken: shared API returned non-OK status", {
754
- status: res.status,
755
- url: `${this.sharedApiUrl}/auth/verify`
756
- });
757
- return null;
758
- }
759
- const data = await res.json();
760
- const user = {
761
- id: data.user.id,
762
- email: data.user.email,
763
- name: [data.user.firstName, data.user.lastName].filter(Boolean).join(" ") || void 0,
764
- organizationId: data.organizationId,
765
- role: data.role,
766
- memberOrgIds: data.memberOrgIds
767
- };
768
- if (user.organizationId) this.cacheVerification(cacheKey, user);
769
- return user;
770
- } catch (error) {
771
- this.logger.error("verifyBearerToken: fetch to shared API failed", {
772
- url: `${this.sharedApiUrl}/auth/verify`,
773
- error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error)
774
- });
775
- return null;
776
- }
777
- }
348
+ isMastraCloudAuth = true;
349
+ sharedApiUrl;
350
+ organizationId;
351
+ useProductionCookies;
352
+ cookieDomain;
353
+ /**
354
+ * `userId → sealed session cookie` cache. The `IOrganizationsProvider`
355
+ * interface only hands us a `userId`, but the shared API's org endpoints are
356
+ * cookie-authenticated — so we remember the cookie last seen for a user
357
+ * inside `verifySessionCookie` and reuse it here. Kept small: bounded to
358
+ * the last 1000 users, LRU-evicted on insert.
359
+ */
360
+ userSessionCookies = /* @__PURE__ */ new Map();
361
+ maxCachedSessions = 1e3;
362
+ /**
363
+ * Short-TTL cache of SUCCESSFUL credential verifications, keyed by a
364
+ * sha256 of the credential (never the raw cookie/token). Every protected
365
+ * request re-verifies against the shared API otherwise — one network
366
+ * round trip per request. Failures are never cached, so a rejected
367
+ * credential is always re-checked. Bounded + insert-order evicted.
368
+ */
369
+ verifiedCredentials = /* @__PURE__ */ new Map();
370
+ maxCachedVerifications = 1e3;
371
+ /**
372
+ * In-flight `ensureOrganization` promises keyed by userId. Concurrent calls
373
+ * for the same brand-new user (multiple tabs, parallel requests) would
374
+ * otherwise all see "no org" from `GET /auth/me` and each fire
375
+ * `POST /auth/orgs`, creating duplicate personal organizations. The first
376
+ * caller's promise is reused by every follower until it settles.
377
+ */
378
+ organizationBootstrapInFlight = /* @__PURE__ */ new Map();
379
+ constructor(options) {
380
+ super({
381
+ name: "mastra-studio",
382
+ ...options
383
+ });
384
+ const explicitSharedApiUrl = options?.sharedApiUrl || process.env.MASTRA_SHARED_API_URL;
385
+ this.sharedApiUrl = explicitSharedApiUrl || "https://platform.mastra.ai/v1";
386
+ this.organizationId = options?.organizationId || process.env.MASTRA_ORGANIZATION_ID;
387
+ if (this.sharedApiUrl.endsWith("/")) this.sharedApiUrl = this.sharedApiUrl.slice(0, -1);
388
+ this.cookieDomain = options?.cookieDomain || process.env.MASTRA_COOKIE_DOMAIN;
389
+ let autoDetectMastraAi = false;
390
+ if (explicitSharedApiUrl) try {
391
+ const hostname = new URL(this.sharedApiUrl).hostname.toLowerCase();
392
+ autoDetectMastraAi = hostname === "mastra.ai" || hostname.endsWith(".mastra.ai");
393
+ } catch {
394
+ autoDetectMastraAi = false;
395
+ }
396
+ this.useProductionCookies = !!this.cookieDomain || autoDetectMastraAi;
397
+ if (!this.cookieDomain && autoDetectMastraAi) this.cookieDomain = ".mastra.ai";
398
+ if (options) this.registerOptions(options);
399
+ }
400
+ /**
401
+ * Authenticate an incoming request by forwarding the sealed session cookie
402
+ * to the shared API's /auth/me endpoint, or a Bearer token to /auth/verify.
403
+ */
404
+ async authenticateToken(token, request) {
405
+ let user = null;
406
+ const sessionCookie = parseCookie(getRequestHeader(request, "Cookie"), COOKIE_NAME);
407
+ if (sessionCookie) user = await this.verifySessionCookie(sessionCookie);
408
+ if (!user && token) user = await this.verifyBearerToken(token);
409
+ if (!user) return null;
410
+ if (this.organizationId && !user.memberOrgIds?.includes(this.organizationId)) return null;
411
+ return user;
412
+ }
413
+ authorizeUser(user) {
414
+ return !!user?.id;
415
+ }
416
+ getLoginUrl(redirectUri, state) {
417
+ let postLoginRedirect = "/";
418
+ if (state) {
419
+ const pipeIndex = state.indexOf("|");
420
+ if (pipeIndex !== -1) try {
421
+ postLoginRedirect = decodeURIComponent(state.slice(pipeIndex + 1));
422
+ } catch {}
423
+ }
424
+ const params = new URLSearchParams({
425
+ product: "deploy",
426
+ redirect_uri: redirectUri,
427
+ post_login_redirect: postLoginRedirect,
428
+ prompt: "login",
429
+ ...this.organizationId ? { organization_id: this.organizationId } : {}
430
+ });
431
+ return `${this.sharedApiUrl}/auth/login?${params.toString()}`;
432
+ }
433
+ async handleCallback(code, _state) {
434
+ this.logger.debug("SSO callback: validating sealed session via shared API", {
435
+ sharedApiUrl: this.sharedApiUrl,
436
+ codeLength: code?.length
437
+ });
438
+ const user = await this.verifySessionCookie(code);
439
+ if (!user) {
440
+ this.logger.error("SSO callback: session validation failed — verifySessionCookie returned null", {
441
+ sharedApiUrl: this.sharedApiUrl,
442
+ codeLength: code?.length
443
+ });
444
+ throw new Error("Session validation failed");
445
+ }
446
+ return {
447
+ user,
448
+ tokens: { accessToken: code }
449
+ };
450
+ }
451
+ setCallbackCookieHeader(_cookieHeader) {}
452
+ getLoginCookies() {}
453
+ getLoginButtonConfig() {
454
+ return {
455
+ provider: "mastra-studio",
456
+ text: "Sign in with Mastra",
457
+ description: "Your deployed Studio is secured by your Mastra account. Sign in with the same email you used to sign up on mastra.ai."
458
+ };
459
+ }
460
+ async getLogoutUrl(_redirectUri, request) {
461
+ const cookieHeader = request?.headers.get("Cookie");
462
+ const sessionCookie = parseCookie(cookieHeader, COOKIE_NAME);
463
+ if (!sessionCookie) return null;
464
+ try {
465
+ const res = await fetch(`${this.sharedApiUrl}/auth/logout`, {
466
+ method: "POST",
467
+ headers: {
468
+ "Content-Type": "application/json",
469
+ Cookie: `${COOKIE_NAME}=${sessionCookie}`
470
+ }
471
+ });
472
+ if (res.ok) return (await res.json()).logoutUrl ?? null;
473
+ } catch {}
474
+ return null;
475
+ }
476
+ async createSession(userId, metadata) {
477
+ const now = /* @__PURE__ */ new Date();
478
+ return {
479
+ id: metadata?.accessToken || crypto.randomUUID(),
480
+ userId,
481
+ expiresAt: new Date(now.getTime() + 1440 * 60 * 1e3),
482
+ createdAt: now,
483
+ metadata
484
+ };
485
+ }
486
+ async validateSession(sessionId) {
487
+ const user = await this.verifySessionCookie(sessionId);
488
+ if (!user) return null;
489
+ const now = /* @__PURE__ */ new Date();
490
+ return {
491
+ id: sessionId,
492
+ userId: user.id,
493
+ expiresAt: new Date(now.getTime() + 1440 * 60 * 1e3),
494
+ createdAt: now
495
+ };
496
+ }
497
+ async destroySession(sessionId) {
498
+ try {
499
+ await fetch(`${this.sharedApiUrl}/auth/logout`, {
500
+ method: "POST",
501
+ headers: { Cookie: `${COOKIE_NAME}=${sessionId}` }
502
+ });
503
+ } catch {}
504
+ }
505
+ async refreshSession(sessionId) {
506
+ try {
507
+ const res = await fetch(`${this.sharedApiUrl}/auth/refresh`, {
508
+ method: "GET",
509
+ headers: { Cookie: `${COOKIE_NAME}=${sessionId}` }
510
+ });
511
+ if (!res.ok) {
512
+ this.logger.warn("refreshSession: shared API refresh returned non-OK status", {
513
+ status: res.status,
514
+ url: `${this.sharedApiUrl}/auth/refresh`
515
+ });
516
+ return this.validateSession(sessionId);
517
+ }
518
+ const setCookie = res.headers.get("Set-Cookie");
519
+ const newSessionId = setCookie ? parseCookieFromHeader(setCookie, COOKIE_NAME) : null;
520
+ if (!newSessionId) {
521
+ this.logger.warn("refreshSession: no Set-Cookie header in refresh response");
522
+ return this.validateSession(sessionId);
523
+ }
524
+ const user = await this.verifySessionCookie(newSessionId);
525
+ if (!user) return null;
526
+ const now = /* @__PURE__ */ new Date();
527
+ return {
528
+ id: newSessionId,
529
+ userId: user.id,
530
+ expiresAt: new Date(now.getTime() + 1440 * 60 * 1e3),
531
+ createdAt: now
532
+ };
533
+ } catch (error) {
534
+ this.logger.error("refreshSession: fetch to shared API failed", {
535
+ url: `${this.sharedApiUrl}/auth/refresh`,
536
+ error: error instanceof Error ? {
537
+ message: error.message,
538
+ stack: error.stack
539
+ } : String(error)
540
+ });
541
+ return this.validateSession(sessionId);
542
+ }
543
+ }
544
+ getSessionIdFromRequest(request) {
545
+ return parseCookie(request.headers.get("Cookie"), COOKIE_NAME);
546
+ }
547
+ getSessionHeaders(session) {
548
+ const parts = [
549
+ `${COOKIE_NAME}=${session.id}`,
550
+ "HttpOnly",
551
+ "SameSite=Lax",
552
+ "Path=/",
553
+ "Max-Age=86400"
554
+ ];
555
+ if (this.useProductionCookies && this.cookieDomain) {
556
+ parts.push("Secure");
557
+ parts.push(`Domain=${this.cookieDomain}`);
558
+ }
559
+ return { "Set-Cookie": parts.join("; ") };
560
+ }
561
+ getClearSessionHeaders() {
562
+ const parts = [
563
+ `${COOKIE_NAME}=`,
564
+ "HttpOnly",
565
+ "SameSite=Lax",
566
+ "Path=/",
567
+ "Max-Age=0"
568
+ ];
569
+ if (this.useProductionCookies && this.cookieDomain) {
570
+ parts.push("Secure");
571
+ parts.push(`Domain=${this.cookieDomain}`);
572
+ }
573
+ return { "Set-Cookie": parts.join("; ") };
574
+ }
575
+ async getCurrentUser(request) {
576
+ const sessionCookie = parseCookie(request.headers.get("Cookie"), COOKIE_NAME);
577
+ if (sessionCookie) return this.verifySessionCookie(sessionCookie);
578
+ const authHeader = request.headers.get("Authorization");
579
+ if (authHeader?.startsWith("Bearer ")) return this.verifyBearerToken(authHeader.slice(7));
580
+ return null;
581
+ }
582
+ async getUser(_userId) {
583
+ return null;
584
+ }
585
+ /**
586
+ * Ensure the user belongs to an organization, bootstrapping a personal org
587
+ * on first use when they have none.
588
+ *
589
+ * Because the shared API's org endpoints are cookie-authenticated but this
590
+ * method only receives a userId, we look up the user's sealed session cookie
591
+ * from the {@link userSessionCookies} cache populated by
592
+ * {@link verifySessionCookie}. If we have never seen a cookie for this user
593
+ * (e.g. bearer-token flow, or the cache was evicted), we skip bootstrap and
594
+ * return `undefined` the caller keeps the user in their current no-org
595
+ * state and the next authenticated request retries.
596
+ *
597
+ * Best-effort: any shared-API failure returns `undefined` rather than
598
+ * throwing, mirroring `MastraAuthWorkos.ensureOrganization`.
599
+ */
600
+ async ensureOrganization(userId) {
601
+ const inFlight = this.organizationBootstrapInFlight.get(userId);
602
+ if (inFlight) return inFlight;
603
+ const bootstrap = this.doEnsureOrganization(userId).finally(() => {
604
+ this.organizationBootstrapInFlight.delete(userId);
605
+ });
606
+ this.organizationBootstrapInFlight.set(userId, bootstrap);
607
+ return bootstrap;
608
+ }
609
+ async doEnsureOrganization(userId) {
610
+ const sessionCookie = this.userSessionCookies.get(userId);
611
+ if (!sessionCookie) {
612
+ this.logger.debug("ensureOrganization: no cached session cookie for user; skipping bootstrap", { userId });
613
+ return;
614
+ }
615
+ try {
616
+ const me = await this.fetchMe(sessionCookie);
617
+ if (me?.organizationId) return me.organizationId;
618
+ if (me?.memberOrgIds && me.memberOrgIds.length > 0) return me.memberOrgIds[0];
619
+ const orgName = me?.user?.email ? `${me.user.email}'s org` : `Personal (${userId})`;
620
+ const res = await fetch(`${this.sharedApiUrl}/auth/orgs`, {
621
+ method: "POST",
622
+ headers: {
623
+ "Content-Type": "application/json",
624
+ Cookie: `${COOKIE_NAME}=${sessionCookie}`
625
+ },
626
+ body: JSON.stringify({ name: orgName })
627
+ });
628
+ if (!res.ok) {
629
+ this.logger.warn("ensureOrganization: shared API POST /auth/orgs returned non-OK", {
630
+ status: res.status,
631
+ userId
632
+ });
633
+ return;
634
+ }
635
+ return (await res.json()).organization?.id;
636
+ } catch (error) {
637
+ this.logger.error("ensureOrganization: fetch to shared API failed", {
638
+ userId,
639
+ error: error instanceof Error ? {
640
+ message: error.message,
641
+ stack: error.stack
642
+ } : String(error)
643
+ });
644
+ return;
645
+ }
646
+ }
647
+ /**
648
+ * Whether the user holds an admin-equivalent role in the organization.
649
+ *
650
+ * Fast path: if the org matches the user's currently-active session org, we
651
+ * read the role directly from `/auth/me`. Cross-org path: we call
652
+ * `/auth/orgs` (which returns per-membership roles) and look up the target
653
+ * org. Any shared-API failure resolves to `false`.
654
+ */
655
+ async isOrganizationAdmin(organizationId, userId) {
656
+ const sessionCookie = this.userSessionCookies.get(userId);
657
+ if (!sessionCookie) return false;
658
+ try {
659
+ const me = await this.fetchMe(sessionCookie);
660
+ if (me?.organizationId === organizationId) return isAdminRole(me.role);
661
+ const res = await fetch(`${this.sharedApiUrl}/auth/orgs`, { headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` } });
662
+ if (!res.ok) return false;
663
+ const membership = (await res.json()).organizations?.find((o) => o.id === organizationId);
664
+ return isAdminRole(membership?.role ?? void 0);
665
+ } catch {
666
+ return false;
667
+ }
668
+ }
669
+ /**
670
+ * Record the sealed session cookie last seen for a user so
671
+ * {@link ensureOrganization} / {@link isOrganizationAdmin} can act on their
672
+ * behalf. LRU-evicted at {@link maxCachedSessions} entries.
673
+ */
674
+ rememberUserSession(userId, sessionCookie) {
675
+ this.userSessionCookies.delete(userId);
676
+ this.userSessionCookies.set(userId, sessionCookie);
677
+ if (this.userSessionCookies.size > this.maxCachedSessions) {
678
+ const oldest = this.userSessionCookies.keys().next().value;
679
+ if (oldest !== void 0) this.userSessionCookies.delete(oldest);
680
+ }
681
+ }
682
+ /** Cache key for a verified credential — hash, never the raw secret. */
683
+ verificationKey(kind, credential) {
684
+ return createHash("sha256").update(`${kind}:${credential}`).digest("hex");
685
+ }
686
+ getCachedVerification(key) {
687
+ const entry = this.verifiedCredentials.get(key);
688
+ if (!entry) return null;
689
+ if (entry.expiresAt <= Date.now()) {
690
+ this.verifiedCredentials.delete(key);
691
+ return null;
692
+ }
693
+ return entry.user;
694
+ }
695
+ cacheVerification(key, user) {
696
+ this.verifiedCredentials.delete(key);
697
+ this.verifiedCredentials.set(key, {
698
+ user,
699
+ expiresAt: Date.now() + VERIFY_CACHE_TTL_MS
700
+ });
701
+ if (this.verifiedCredentials.size > this.maxCachedVerifications) {
702
+ const oldest = this.verifiedCredentials.keys().next().value;
703
+ if (oldest !== void 0) this.verifiedCredentials.delete(oldest);
704
+ }
705
+ }
706
+ /**
707
+ * Fetch the shared API's `/auth/me` and return the raw response body, or
708
+ * `null` on any non-OK / network error. Split out so `ensureOrganization`
709
+ * and `isOrganizationAdmin` can reuse it without duplicating the shape.
710
+ */
711
+ async fetchMe(sessionCookie) {
712
+ try {
713
+ const res = await fetch(`${this.sharedApiUrl}/auth/me`, {
714
+ headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` },
715
+ signal: AbortSignal.timeout(VERIFY_FETCH_TIMEOUT_MS)
716
+ });
717
+ if (!res.ok) return null;
718
+ return await res.json();
719
+ } catch {
720
+ return null;
721
+ }
722
+ }
723
+ /**
724
+ * Forward a sealed session cookie to the shared API's /auth/me endpoint
725
+ * to validate it and get user info.
726
+ */
727
+ async verifySessionCookie(sessionCookie) {
728
+ const cacheKey = this.verificationKey("cookie", sessionCookie);
729
+ const cached = this.getCachedVerification(cacheKey);
730
+ if (cached) {
731
+ this.rememberUserSession(cached.id, sessionCookie);
732
+ return cached;
733
+ }
734
+ try {
735
+ const res = await fetch(`${this.sharedApiUrl}/auth/me`, {
736
+ headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` },
737
+ signal: AbortSignal.timeout(VERIFY_FETCH_TIMEOUT_MS)
738
+ });
739
+ if (!res.ok) {
740
+ this.logger.warn("verifySessionCookie: shared API returned non-OK status", {
741
+ status: res.status,
742
+ statusText: res.statusText,
743
+ url: `${this.sharedApiUrl}/auth/me`
744
+ });
745
+ return null;
746
+ }
747
+ const data = await res.json();
748
+ this.rememberUserSession(data.user.id, sessionCookie);
749
+ const user = {
750
+ id: data.user.id,
751
+ email: data.user.email,
752
+ name: [data.user.firstName, data.user.lastName].filter(Boolean).join(" ") || void 0,
753
+ avatarUrl: data.user.profilePictureUrl,
754
+ organizationId: data.organizationId,
755
+ role: data.role,
756
+ permissions: data.permissions,
757
+ memberOrgIds: data.memberOrgIds
758
+ };
759
+ if (user.organizationId) this.cacheVerification(cacheKey, user);
760
+ return user;
761
+ } catch (error) {
762
+ this.logger.error("verifySessionCookie: fetch to shared API failed", {
763
+ url: `${this.sharedApiUrl}/auth/me`,
764
+ error: error instanceof Error ? {
765
+ message: error.message,
766
+ stack: error.stack
767
+ } : String(error)
768
+ });
769
+ return null;
770
+ }
771
+ }
772
+ /**
773
+ * Forward a Bearer token to the shared API's /auth/verify endpoint
774
+ * to validate it and get user info (used for CLI tokens).
775
+ */
776
+ async verifyBearerToken(token) {
777
+ const cacheKey = this.verificationKey("bearer", token);
778
+ const cached = this.getCachedVerification(cacheKey);
779
+ if (cached) return cached;
780
+ try {
781
+ const res = await fetch(`${this.sharedApiUrl}/auth/verify`, {
782
+ headers: { Authorization: `Bearer ${token}` },
783
+ signal: AbortSignal.timeout(VERIFY_FETCH_TIMEOUT_MS)
784
+ });
785
+ if (!res.ok) {
786
+ this.logger.warn("verifyBearerToken: shared API returned non-OK status", {
787
+ status: res.status,
788
+ url: `${this.sharedApiUrl}/auth/verify`
789
+ });
790
+ return null;
791
+ }
792
+ const data = await res.json();
793
+ const user = {
794
+ id: data.user.id,
795
+ email: data.user.email,
796
+ name: [data.user.firstName, data.user.lastName].filter(Boolean).join(" ") || void 0,
797
+ organizationId: data.organizationId,
798
+ role: data.role,
799
+ memberOrgIds: data.memberOrgIds
800
+ };
801
+ if (user.organizationId) this.cacheVerification(cacheKey, user);
802
+ return user;
803
+ } catch (error) {
804
+ this.logger.error("verifyBearerToken: fetch to shared API failed", {
805
+ url: `${this.sharedApiUrl}/auth/verify`,
806
+ error: error instanceof Error ? {
807
+ message: error.message,
808
+ stack: error.stack
809
+ } : String(error)
810
+ });
811
+ return null;
812
+ }
813
+ }
778
814
  };
779
815
  function parseCookie(cookieHeader, name) {
780
- if (!cookieHeader) return null;
781
- const match = cookieHeader.match(new RegExp(`${name}=([^;]+)`));
782
- return match?.[1] ?? null;
816
+ if (!cookieHeader) return null;
817
+ return cookieHeader.match(new RegExp(`${name}=([^;]+)`))?.[1] ?? null;
783
818
  }
819
+ /**
820
+ * WorkOS AuthKit conventionally uses `admin` / `owner` for admin-equivalent
821
+ * roles; the shared API surfaces the WorkOS role slug directly on
822
+ * `/auth/me` and `/auth/orgs`.
823
+ */
784
824
  function isAdminRole(role) {
785
- return role === "admin" || role === "owner";
825
+ return role === "admin" || role === "owner";
786
826
  }
827
+ /**
828
+ * Parse a cookie value from a Set-Cookie header.
829
+ * Set-Cookie format: "name=value; HttpOnly; SameSite=Lax; Path=/; Max-Age=86400"
830
+ */
787
831
  function parseCookieFromHeader(setCookieHeader, name) {
788
- const parts = setCookieHeader.split(";");
789
- if (parts.length === 0) return null;
790
- const [cookieName, ...valueParts] = parts[0].split("=");
791
- if (cookieName?.trim() !== name) return null;
792
- return valueParts.join("=") || null;
832
+ const parts = setCookieHeader.split(";");
833
+ if (parts.length === 0) return null;
834
+ const [cookieName, ...valueParts] = parts[0].split("=");
835
+ if (cookieName?.trim() !== name) return null;
836
+ return valueParts.join("=") || null;
793
837
  }
838
+ /**
839
+ * RBAC provider for Mastra Studio authentication.
840
+ *
841
+ * Maps user roles (from the shared API's /auth/me endpoint) to Mastra permissions
842
+ * using a configurable role mapping.
843
+ */
794
844
  var MastraRBACStudio = class {
795
- options;
796
- get roleMapping() {
797
- return this.options.roleMapping;
798
- }
799
- constructor(options) {
800
- this.options = options;
801
- }
802
- async getRoles(user) {
803
- return user.role ? [user.role] : [];
804
- }
805
- async hasRole(user, role) {
806
- const roles = await this.getRoles(user);
807
- return roles.includes(role);
808
- }
809
- async getPermissions(user) {
810
- const roles = await this.getRoles(user);
811
- if (roles.length === 0) {
812
- return this.options.roleMapping["_default"] ?? [];
813
- }
814
- return resolvePermissionsFromMapping(roles, this.options.roleMapping);
815
- }
816
- async hasPermission(user, permission) {
817
- const permissions = await this.getPermissions(user);
818
- return permissions.some((p) => matchesPermission(p, permission));
819
- }
820
- async hasAllPermissions(user, permissions) {
821
- const userPermissions = await this.getPermissions(user);
822
- return permissions.every((required) => userPermissions.some((p) => matchesPermission(p, required)));
823
- }
824
- async hasAnyPermission(user, permissions) {
825
- const userPermissions = await this.getPermissions(user);
826
- return permissions.some((required) => userPermissions.some((p) => matchesPermission(p, required)));
827
- }
845
+ options;
846
+ get roleMapping() {
847
+ return this.options.roleMapping;
848
+ }
849
+ constructor(options) {
850
+ this.options = options;
851
+ }
852
+ async getRoles(user) {
853
+ return user.role ? [user.role] : [];
854
+ }
855
+ async hasRole(user, role) {
856
+ return (await this.getRoles(user)).includes(role);
857
+ }
858
+ async getPermissions(user) {
859
+ const roles = await this.getRoles(user);
860
+ if (roles.length === 0) return this.options.roleMapping["_default"] ?? [];
861
+ return resolvePermissionsFromMapping(roles, this.options.roleMapping);
862
+ }
863
+ async hasPermission(user, permission) {
864
+ return (await this.getPermissions(user)).some((p) => matchesPermission(p, permission));
865
+ }
866
+ async hasAllPermissions(user, permissions) {
867
+ const userPermissions = await this.getPermissions(user);
868
+ return permissions.every((required) => userPermissions.some((p) => matchesPermission(p, required)));
869
+ }
870
+ async hasAnyPermission(user, permissions) {
871
+ const userPermissions = await this.getPermissions(user);
872
+ return permissions.some((required) => userPermissions.some((p) => matchesPermission(p, required)));
873
+ }
828
874
  };
829
- /**
830
- * EE Authentication Interfaces
831
- *
832
- * Enterprise interfaces for RBAC, ACL, and advanced authorization.
833
- *
834
- * @license Mastra Enterprise License - see ee/LICENSE
835
- * @packageDocumentation
836
- */
837
- /**
838
- * FGA enforcement utility for checking fine-grained authorization.
839
- *
840
- * @license Mastra Enterprise License - see ee/LICENSE
841
- */
842
- /**
843
- * RBAC provider implementations.
844
- *
845
- * @license Mastra Enterprise License - see ee/LICENSE
846
- */
847
- /**
848
- * Default implementations for EE authentication.
849
- *
850
- * @license Mastra Enterprise License - see ee/LICENSE
851
- */
852
- /**
853
- * @mastra/core/auth/ee
854
- *
855
- * Enterprise authentication capabilities for Mastra.
856
- * This code is licensed under the Mastra Enterprise License - see ee/LICENSE.
857
- *
858
- * @license Mastra Enterprise License - see ee/LICENSE
859
- * @packageDocumentation
860
- */
861
-
875
+ //#endregion
862
876
  export { MastraAuthStudio, MastraRBACStudio };
863
- //# sourceMappingURL=index.js.map
877
+
864
878
  //# sourceMappingURL=index.js.map