@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/CHANGELOG.md +14 -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/ee/interfaces/permissions.generated.d.ts +5 -1
- package/dist/_types/@internal_auth/dist/index.d.ts +66 -0
- package/dist/_types/@internal_auth/dist/provider/index.d.ts +47 -3
- package/dist/index.cjs +151 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +57 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -8
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ../../packages/_internals/auth/dist/chunk-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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;
|