@mastra/auth-studio 1.3.8-alpha.0 → 1.3.9-alpha.0

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.
@@ -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() + 1440 * 60 * 1e3),
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() + 1440 * 60 * 1e3),
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() + 1440 * 60 * 1e3),
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
- "Max-Age=86400"
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=86400"
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(";");