@mastra/auth-studio 1.3.1 → 1.3.2-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // ../../packages/_internals/auth/dist/chunk-7SVOHBXQ.js
1
+ // ../../packages/_internals/auth/dist/chunk-LZCWL5CT.js
2
2
  var RESOURCE_EXPANSIONS = {
3
3
  stored: [
4
4
  "stored-agents",
@@ -227,7 +227,7 @@ var MastraBase = class {
227
227
  }
228
228
  };
229
229
 
230
- // ../../packages/_internals/auth/dist/chunk-XG6GH2VJ.js
230
+ // ../../packages/_internals/auth/dist/chunk-NJOKT6V5.js
231
231
  var MastraAuthProvider = class extends MastraBase {
232
232
  protected;
233
233
  public;
@@ -270,20 +270,40 @@ var MastraAuthStudio = class extends MastraAuthProvider {
270
270
  organizationId;
271
271
  useProductionCookies;
272
272
  cookieDomain;
273
+ /**
274
+ * `userId → sealed session cookie` cache. The `IOrganizationsProvider`
275
+ * interface only hands us a `userId`, but the shared API's org endpoints are
276
+ * cookie-authenticated — so we remember the cookie last seen for a user
277
+ * inside `verifySessionCookie` and reuse it here. Kept small: bounded to
278
+ * the last 1000 users, LRU-evicted on insert.
279
+ */
280
+ userSessionCookies = /* @__PURE__ */ new Map();
281
+ maxCachedSessions = 1e3;
282
+ /**
283
+ * In-flight `ensureOrganization` promises keyed by userId. Concurrent calls
284
+ * for the same brand-new user (multiple tabs, parallel requests) would
285
+ * otherwise all see "no org" from `GET /auth/me` and each fire
286
+ * `POST /auth/orgs`, creating duplicate personal organizations. The first
287
+ * caller's promise is reused by every follower until it settles.
288
+ */
289
+ organizationBootstrapInFlight = /* @__PURE__ */ new Map();
273
290
  constructor(options) {
274
291
  super({ name: "mastra-studio", ...options });
275
- this.sharedApiUrl = options?.sharedApiUrl || process.env.MASTRA_SHARED_API_URL || "http://localhost:3010/v1";
292
+ const explicitSharedApiUrl = options?.sharedApiUrl || process.env.MASTRA_SHARED_API_URL;
293
+ this.sharedApiUrl = explicitSharedApiUrl || "https://platform.mastra.ai/v1";
276
294
  this.organizationId = options?.organizationId || process.env.MASTRA_ORGANIZATION_ID;
277
295
  if (this.sharedApiUrl.endsWith("/")) {
278
296
  this.sharedApiUrl = this.sharedApiUrl.slice(0, -1);
279
297
  }
280
298
  this.cookieDomain = options?.cookieDomain || process.env.MASTRA_COOKIE_DOMAIN;
281
299
  let autoDetectMastraAi = false;
282
- try {
283
- const hostname = new URL(this.sharedApiUrl).hostname.toLowerCase();
284
- autoDetectMastraAi = hostname === "mastra.ai" || hostname.endsWith(".mastra.ai");
285
- } catch {
286
- autoDetectMastraAi = false;
300
+ if (explicitSharedApiUrl) {
301
+ try {
302
+ const hostname = new URL(this.sharedApiUrl).hostname.toLowerCase();
303
+ autoDetectMastraAi = hostname === "mastra.ai" || hostname.endsWith(".mastra.ai");
304
+ } catch {
305
+ autoDetectMastraAi = false;
306
+ }
287
307
  }
288
308
  this.useProductionCookies = !!this.cookieDomain || autoDetectMastraAi;
289
309
  if (!this.cookieDomain && autoDetectMastraAi) {
@@ -508,8 +528,127 @@ var MastraAuthStudio = class extends MastraAuthProvider {
508
528
  return null;
509
529
  }
510
530
  // ---------------------------------------------------------------------------
531
+ // IOrganizationsProvider
532
+ // ---------------------------------------------------------------------------
533
+ /**
534
+ * Ensure the user belongs to an organization, bootstrapping a personal org
535
+ * on first use when they have none.
536
+ *
537
+ * Because the shared API's org endpoints are cookie-authenticated but this
538
+ * method only receives a userId, we look up the user's sealed session cookie
539
+ * from the {@link userSessionCookies} cache populated by
540
+ * {@link verifySessionCookie}. If we have never seen a cookie for this user
541
+ * (e.g. bearer-token flow, or the cache was evicted), we skip bootstrap and
542
+ * return `undefined` — the caller keeps the user in their current no-org
543
+ * state and the next authenticated request retries.
544
+ *
545
+ * Best-effort: any shared-API failure returns `undefined` rather than
546
+ * throwing, mirroring `MastraAuthWorkos.ensureOrganization`.
547
+ */
548
+ async ensureOrganization(userId) {
549
+ const inFlight = this.organizationBootstrapInFlight.get(userId);
550
+ if (inFlight) return inFlight;
551
+ const bootstrap = this.doEnsureOrganization(userId).finally(() => {
552
+ this.organizationBootstrapInFlight.delete(userId);
553
+ });
554
+ this.organizationBootstrapInFlight.set(userId, bootstrap);
555
+ return bootstrap;
556
+ }
557
+ async doEnsureOrganization(userId) {
558
+ const sessionCookie = this.userSessionCookies.get(userId);
559
+ if (!sessionCookie) {
560
+ this.logger.debug("ensureOrganization: no cached session cookie for user; skipping bootstrap", { userId });
561
+ return void 0;
562
+ }
563
+ try {
564
+ const me = await this.fetchMe(sessionCookie);
565
+ if (me?.organizationId) return me.organizationId;
566
+ if (me?.memberOrgIds && me.memberOrgIds.length > 0) return me.memberOrgIds[0];
567
+ const orgName = me?.user?.email ? `${me.user.email}'s org` : `Personal (${userId})`;
568
+ const res = await fetch(`${this.sharedApiUrl}/auth/orgs`, {
569
+ method: "POST",
570
+ headers: {
571
+ "Content-Type": "application/json",
572
+ Cookie: `${COOKIE_NAME}=${sessionCookie}`
573
+ },
574
+ body: JSON.stringify({ name: orgName })
575
+ });
576
+ if (!res.ok) {
577
+ this.logger.warn("ensureOrganization: shared API POST /auth/orgs returned non-OK", {
578
+ status: res.status,
579
+ userId
580
+ });
581
+ return void 0;
582
+ }
583
+ const data = await res.json();
584
+ return data.organization?.id;
585
+ } catch (error) {
586
+ this.logger.error("ensureOrganization: fetch to shared API failed", {
587
+ userId,
588
+ error: error instanceof Error ? { message: error.message, stack: error.stack } : String(error)
589
+ });
590
+ return void 0;
591
+ }
592
+ }
593
+ /**
594
+ * Whether the user holds an admin-equivalent role in the organization.
595
+ *
596
+ * Fast path: if the org matches the user's currently-active session org, we
597
+ * read the role directly from `/auth/me`. Cross-org path: we call
598
+ * `/auth/orgs` (which returns per-membership roles) and look up the target
599
+ * org. Any shared-API failure resolves to `false`.
600
+ */
601
+ async isOrganizationAdmin(organizationId, userId) {
602
+ const sessionCookie = this.userSessionCookies.get(userId);
603
+ if (!sessionCookie) return false;
604
+ try {
605
+ const me = await this.fetchMe(sessionCookie);
606
+ if (me?.organizationId === organizationId) {
607
+ return isAdminRole(me.role);
608
+ }
609
+ const res = await fetch(`${this.sharedApiUrl}/auth/orgs`, {
610
+ headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` }
611
+ });
612
+ if (!res.ok) return false;
613
+ const data = await res.json();
614
+ const membership = data.organizations?.find((o) => o.id === organizationId);
615
+ return isAdminRole(membership?.role ?? void 0);
616
+ } catch {
617
+ return false;
618
+ }
619
+ }
620
+ // ---------------------------------------------------------------------------
511
621
  // Internal helpers
512
622
  // ---------------------------------------------------------------------------
623
+ /**
624
+ * Record the sealed session cookie last seen for a user so
625
+ * {@link ensureOrganization} / {@link isOrganizationAdmin} can act on their
626
+ * behalf. LRU-evicted at {@link maxCachedSessions} entries.
627
+ */
628
+ rememberUserSession(userId, sessionCookie) {
629
+ this.userSessionCookies.delete(userId);
630
+ this.userSessionCookies.set(userId, sessionCookie);
631
+ if (this.userSessionCookies.size > this.maxCachedSessions) {
632
+ const oldest = this.userSessionCookies.keys().next().value;
633
+ if (oldest !== void 0) this.userSessionCookies.delete(oldest);
634
+ }
635
+ }
636
+ /**
637
+ * Fetch the shared API's `/auth/me` and return the raw response body, or
638
+ * `null` on any non-OK / network error. Split out so `ensureOrganization`
639
+ * and `isOrganizationAdmin` can reuse it without duplicating the shape.
640
+ */
641
+ async fetchMe(sessionCookie) {
642
+ try {
643
+ const res = await fetch(`${this.sharedApiUrl}/auth/me`, {
644
+ headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` }
645
+ });
646
+ if (!res.ok) return null;
647
+ return await res.json();
648
+ } catch {
649
+ return null;
650
+ }
651
+ }
513
652
  /**
514
653
  * Forward a sealed session cookie to the shared API's /auth/me endpoint
515
654
  * to validate it and get user info.
@@ -530,6 +669,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
530
669
  return null;
531
670
  }
532
671
  const data = await res.json();
672
+ this.rememberUserSession(data.user.id, sessionCookie);
533
673
  return {
534
674
  id: data.user.id,
535
675
  email: data.user.email,
@@ -589,6 +729,9 @@ function parseCookie(cookieHeader, name) {
589
729
  const match = cookieHeader.match(new RegExp(`${name}=([^;]+)`));
590
730
  return match?.[1] ?? null;
591
731
  }
732
+ function isAdminRole(role) {
733
+ return role === "admin" || role === "owner";
734
+ }
592
735
  function parseCookieFromHeader(setCookieHeader, name) {
593
736
  const parts = setCookieHeader.split(";");
594
737
  if (parts.length === 0) return null;