@mastra/auth-studio 1.3.8 → 1.3.9
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/_types/@internal_auth/dist/provider/index.d.ts +9 -0
- package/dist/index.cjs +84 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -50,6 +50,14 @@ export interface IMastraAuthProvider<TUser = unknown> {
|
|
|
50
50
|
* Map an authenticated user to a memory resource id
|
|
51
51
|
*/
|
|
52
52
|
mapUserToResourceId?(user: TUser): string | undefined | null;
|
|
53
|
+
/**
|
|
54
|
+
* Return response headers (typically `Set-Cookie` for a rotated session)
|
|
55
|
+
* that the provider accumulated while handling this request and clear them
|
|
56
|
+
* from the provider's per-request state. The server middleware appends them
|
|
57
|
+
* to the outgoing response so that server-to-server session refreshes
|
|
58
|
+
* reach the browser.
|
|
59
|
+
*/
|
|
60
|
+
consumePendingResponseHeaders?(request: MastraAuthRequest): Record<string, string> | undefined;
|
|
53
61
|
}
|
|
54
62
|
export declare abstract class MastraAuthProvider<TUser = unknown> extends MastraBase implements IMastraAuthProvider<TUser> {
|
|
55
63
|
protected?: MastraAuthConfig['protected'];
|
|
@@ -97,6 +105,7 @@ export declare class CompositeAuth extends MastraAuthProvider implements ISSOPro
|
|
|
97
105
|
*/
|
|
98
106
|
get isSimpleAuth(): boolean;
|
|
99
107
|
authenticateToken(token: string, request: MastraAuthRequest): Promise<unknown | null>;
|
|
108
|
+
consumePendingResponseHeaders(request: MastraAuthRequest): Record<string, string> | undefined;
|
|
100
109
|
authorizeUser(user: unknown, request: MastraAuthRequest): Promise<boolean>;
|
|
101
110
|
/**
|
|
102
111
|
* Forward cookie header to SSO provider for PKCE validation.
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,10 @@ function getRequestHeader(request, name) {
|
|
|
21
21
|
if (headers && typeof headers === "object") return headerFromPlainObject(headers, name);
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
+
function getWebRequest(request) {
|
|
25
|
+
if (request instanceof Request) return request;
|
|
26
|
+
return request.raw instanceof Request ? request.raw : void 0;
|
|
27
|
+
}
|
|
24
28
|
//#endregion
|
|
25
29
|
//#region ../../packages/_internal-core/dist/logger/index.js
|
|
26
30
|
/**
|
|
@@ -424,6 +428,7 @@ function resolvePermissionsFromMapping(roles, mapping) {
|
|
|
424
428
|
*/
|
|
425
429
|
//#endregion
|
|
426
430
|
//#region src/index.ts
|
|
431
|
+
const DEFAULT_SESSION_MAX_AGE_SECONDS = 336 * 60 * 60;
|
|
427
432
|
const COOKIE_NAME = "wos-session";
|
|
428
433
|
const PROJECT_CONFIG_FILE = ".mastra-project.json";
|
|
429
434
|
/**
|
|
@@ -474,6 +479,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
474
479
|
organizationId;
|
|
475
480
|
useProductionCookies;
|
|
476
481
|
cookieDomain;
|
|
482
|
+
sessionMaxAgeSeconds;
|
|
477
483
|
/**
|
|
478
484
|
* `userId → sealed session cookie` cache. The `IOrganizationsProvider`
|
|
479
485
|
* interface only hands us a `userId`, but the shared API's org endpoints are
|
|
@@ -500,6 +506,19 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
500
506
|
* caller's promise is reused by every follower until it settles.
|
|
501
507
|
*/
|
|
502
508
|
organizationBootstrapInFlight = /* @__PURE__ */ new Map();
|
|
509
|
+
/**
|
|
510
|
+
* Per-request response headers to forward to the browser — specifically a
|
|
511
|
+
* rotated `wos-session` `Set-Cookie` returned by the shared API when the
|
|
512
|
+
* platform `sessionAuth` middleware transparently refreshes an expired
|
|
513
|
+
* access token. Without this, the browser's sealed cookie would never
|
|
514
|
+
* rotate: platform would succeed server-to-server by refreshing on every
|
|
515
|
+
* request, but the next refresh would hit `invalid_grant` once WorkOS
|
|
516
|
+
* invalidated the previous refresh token.
|
|
517
|
+
*
|
|
518
|
+
* Keyed by the underlying `Request` (via `getWebRequest`) so we don't retain
|
|
519
|
+
* requests after the response is written.
|
|
520
|
+
*/
|
|
521
|
+
pendingResponseHeaders = /* @__PURE__ */ new WeakMap();
|
|
503
522
|
constructor(options) {
|
|
504
523
|
super({
|
|
505
524
|
name: "mastra-studio",
|
|
@@ -510,6 +529,9 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
510
529
|
this.organizationId = options?.organizationId || process.env.MASTRA_ORGANIZATION_ID || readOrganizationIdFromProjectConfig();
|
|
511
530
|
if (this.sharedApiUrl.endsWith("/")) this.sharedApiUrl = this.sharedApiUrl.slice(0, -1);
|
|
512
531
|
this.cookieDomain = options?.cookieDomain || process.env.MASTRA_COOKIE_DOMAIN;
|
|
532
|
+
const envMaxAge = Number(process.env.MASTRA_SESSION_MAX_AGE);
|
|
533
|
+
const maxAge = options?.sessionMaxAgeSeconds ?? (envMaxAge > 0 ? envMaxAge : void 0);
|
|
534
|
+
this.sessionMaxAgeSeconds = maxAge && Number.isFinite(maxAge) && maxAge > 0 ? Math.floor(maxAge) : DEFAULT_SESSION_MAX_AGE_SECONDS;
|
|
513
535
|
let autoDetectMastraAi = false;
|
|
514
536
|
if (explicitSharedApiUrl) try {
|
|
515
537
|
const hostname = new URL(this.sharedApiUrl).hostname.toLowerCase();
|
|
@@ -528,7 +550,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
528
550
|
async authenticateToken(token, request) {
|
|
529
551
|
let user = null;
|
|
530
552
|
const sessionCookie = parseCookie(getRequestHeader(request, "Cookie"), COOKIE_NAME);
|
|
531
|
-
if (sessionCookie) user = await this.verifySessionCookie(sessionCookie);
|
|
553
|
+
if (sessionCookie) user = await this.verifySessionCookie(sessionCookie, request);
|
|
532
554
|
if (!user && token) user = await this.verifyBearerToken(token);
|
|
533
555
|
if (!user) return null;
|
|
534
556
|
if (this.organizationId && !user.memberOrgIds?.includes(this.organizationId)) return null;
|
|
@@ -602,7 +624,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
602
624
|
return {
|
|
603
625
|
id: metadata?.accessToken || crypto.randomUUID(),
|
|
604
626
|
userId,
|
|
605
|
-
expiresAt: new Date(now.getTime() +
|
|
627
|
+
expiresAt: new Date(now.getTime() + this.sessionMaxAgeSeconds * 1e3),
|
|
606
628
|
createdAt: now,
|
|
607
629
|
metadata
|
|
608
630
|
};
|
|
@@ -614,7 +636,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
614
636
|
return {
|
|
615
637
|
id: sessionId,
|
|
616
638
|
userId: user.id,
|
|
617
|
-
expiresAt: new Date(now.getTime() +
|
|
639
|
+
expiresAt: new Date(now.getTime() + this.sessionMaxAgeSeconds * 1e3),
|
|
618
640
|
createdAt: now
|
|
619
641
|
};
|
|
620
642
|
}
|
|
@@ -651,7 +673,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
651
673
|
return {
|
|
652
674
|
id: newSessionId,
|
|
653
675
|
userId: user.id,
|
|
654
|
-
expiresAt: new Date(now.getTime() +
|
|
676
|
+
expiresAt: new Date(now.getTime() + this.sessionMaxAgeSeconds * 1e3),
|
|
655
677
|
createdAt: now
|
|
656
678
|
};
|
|
657
679
|
} catch (error) {
|
|
@@ -674,7 +696,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
674
696
|
"HttpOnly",
|
|
675
697
|
"SameSite=Lax",
|
|
676
698
|
"Path=/",
|
|
677
|
-
|
|
699
|
+
`Max-Age=${this.sessionMaxAgeSeconds}`
|
|
678
700
|
];
|
|
679
701
|
if (this.useProductionCookies && this.cookieDomain) {
|
|
680
702
|
parts.push("Secure");
|
|
@@ -698,7 +720,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
698
720
|
}
|
|
699
721
|
async getCurrentUser(request) {
|
|
700
722
|
const sessionCookie = parseCookie(request.headers.get("Cookie"), COOKIE_NAME);
|
|
701
|
-
if (sessionCookie) return this.verifySessionCookie(sessionCookie);
|
|
723
|
+
if (sessionCookie) return this.verifySessionCookie(sessionCookie, request);
|
|
702
724
|
const authHeader = request.headers.get("Authorization");
|
|
703
725
|
if (authHeader?.startsWith("Bearer ")) return this.verifyBearerToken(authHeader.slice(7));
|
|
704
726
|
return null;
|
|
@@ -851,6 +873,57 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
851
873
|
}
|
|
852
874
|
}
|
|
853
875
|
/**
|
|
876
|
+
* Read every `Set-Cookie` header from a response, falling back to the
|
|
877
|
+
* single-value accessor on runtimes without `Headers.getSetCookie()`.
|
|
878
|
+
*/
|
|
879
|
+
readSetCookieHeaders(res) {
|
|
880
|
+
const headers = res.headers;
|
|
881
|
+
if (typeof headers.getSetCookie === "function") return headers.getSetCookie();
|
|
882
|
+
const single = res.headers.get("Set-Cookie");
|
|
883
|
+
return single ? [single] : [];
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* If the shared API rotated the sealed `wos-session` cookie on this
|
|
887
|
+
* response, stash the `Set-Cookie` header against the inbound request so
|
|
888
|
+
* the server middleware can forward it to the browser, and invalidate the
|
|
889
|
+
* short-TTL verification cache entry for the old cookie value. Returns the
|
|
890
|
+
* rotated sealed value, or null when the cookie was not rotated.
|
|
891
|
+
*/
|
|
892
|
+
captureRotatedCookie(res, oldSessionCookie, oldCacheKey, request) {
|
|
893
|
+
const setCookies = this.readSetCookieHeaders(res);
|
|
894
|
+
if (setCookies.length === 0) return null;
|
|
895
|
+
const rotatedCookieHeader = setCookies.find((h) => parseCookieFromHeader(h, COOKIE_NAME) !== null);
|
|
896
|
+
if (!rotatedCookieHeader) return null;
|
|
897
|
+
const rotatedValue = parseCookieFromHeader(rotatedCookieHeader, COOKIE_NAME);
|
|
898
|
+
if (!rotatedValue || rotatedValue === oldSessionCookie) return null;
|
|
899
|
+
const reissued = this.getSessionHeaders({
|
|
900
|
+
id: rotatedValue,
|
|
901
|
+
userId: "",
|
|
902
|
+
expiresAt: /* @__PURE__ */ new Date(),
|
|
903
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
904
|
+
});
|
|
905
|
+
this.verifiedCredentials.delete(oldCacheKey);
|
|
906
|
+
if (request) {
|
|
907
|
+
const rawRequest = getWebRequest(request);
|
|
908
|
+
if (rawRequest) this.pendingResponseHeaders.set(rawRequest, reissued);
|
|
909
|
+
else this.logger.warn("captureRotatedCookie: renewed session cookie cannot be forwarded (no web Request)");
|
|
910
|
+
}
|
|
911
|
+
return rotatedValue;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Server middleware hook: emit any response headers the provider
|
|
915
|
+
* accumulated for this request (currently a rotated `Set-Cookie`) and
|
|
916
|
+
* clear them from the provider's per-request state.
|
|
917
|
+
*/
|
|
918
|
+
consumePendingResponseHeaders(request) {
|
|
919
|
+
const rawRequest = getWebRequest(request);
|
|
920
|
+
if (!rawRequest) return void 0;
|
|
921
|
+
const headers = this.pendingResponseHeaders.get(rawRequest);
|
|
922
|
+
if (!headers) return void 0;
|
|
923
|
+
this.pendingResponseHeaders.delete(rawRequest);
|
|
924
|
+
return headers;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
854
927
|
* Fetch the shared API's `/auth/me` and return the raw response body, or
|
|
855
928
|
* `null` on any non-OK / network error. Split out so `ensureOrganization`
|
|
856
929
|
* and `isOrganizationAdmin` can reuse it without duplicating the shape.
|
|
@@ -871,7 +944,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
871
944
|
* Forward a sealed session cookie to the shared API's /auth/me endpoint
|
|
872
945
|
* to validate it and get user info.
|
|
873
946
|
*/
|
|
874
|
-
async verifySessionCookie(sessionCookie) {
|
|
947
|
+
async verifySessionCookie(sessionCookie, request) {
|
|
875
948
|
const cacheKey = await this.verificationKey("cookie", sessionCookie);
|
|
876
949
|
const cached = this.getCachedVerification(cacheKey);
|
|
877
950
|
if (cached) {
|
|
@@ -883,6 +956,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
883
956
|
headers: { Cookie: `${COOKIE_NAME}=${sessionCookie}` },
|
|
884
957
|
signal: AbortSignal.timeout(VERIFY_FETCH_TIMEOUT_MS)
|
|
885
958
|
});
|
|
959
|
+
const rotated = this.captureRotatedCookie(res, sessionCookie, cacheKey, request);
|
|
886
960
|
if (!res.ok) {
|
|
887
961
|
this.logger.warn("verifySessionCookie: shared API returned non-OK status", {
|
|
888
962
|
status: res.status,
|
|
@@ -892,7 +966,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
892
966
|
return null;
|
|
893
967
|
}
|
|
894
968
|
const data = await res.json();
|
|
895
|
-
this.rememberUserSession(data.user.id, sessionCookie);
|
|
969
|
+
this.rememberUserSession(data.user.id, rotated ?? sessionCookie);
|
|
896
970
|
const sessionUser = {
|
|
897
971
|
id: data.user.id,
|
|
898
972
|
email: data.user.email,
|
|
@@ -904,7 +978,7 @@ var MastraAuthStudio = class extends MastraAuthProvider {
|
|
|
904
978
|
memberOrgIds: data.memberOrgIds
|
|
905
979
|
};
|
|
906
980
|
const user = await this.asPinnedOrganizationMember(sessionUser, sessionCookie);
|
|
907
|
-
if (user.organizationId) this.cacheVerification(cacheKey, user);
|
|
981
|
+
if (user.organizationId && !rotated) this.cacheVerification(cacheKey, user);
|
|
908
982
|
return user;
|
|
909
983
|
} catch (error) {
|
|
910
984
|
this.logger.error("verifySessionCookie: fetch to shared API failed", {
|
|
@@ -976,7 +1050,7 @@ function isAdminRole(role) {
|
|
|
976
1050
|
}
|
|
977
1051
|
/**
|
|
978
1052
|
* Parse a cookie value from a Set-Cookie header.
|
|
979
|
-
* Set-Cookie format: "name=value; HttpOnly; SameSite=Lax; Path=/; Max-Age=
|
|
1053
|
+
* Set-Cookie format: "name=value; HttpOnly; SameSite=Lax; Path=/; Max-Age=1209600"
|
|
980
1054
|
*/
|
|
981
1055
|
function parseCookieFromHeader(setCookieHeader, name) {
|
|
982
1056
|
const parts = setCookieHeader.split(";");
|