@rebasepro/server 0.10.1-canary.31c773c → 0.10.1-canary.811b3da

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.
@@ -119,6 +119,33 @@ export interface RefreshTokenInfo {
119
119
  createdAt: Date;
120
120
  userAgent?: string | null;
121
121
  ipAddress?: string | null;
122
+ /**
123
+ * The sign-in this token descends from. Every token minted by rotating
124
+ * this one carries the same id.
125
+ *
126
+ * Optional because a custom {@link TokenRepository} written against an
127
+ * older release does not supply it; the refresh endpoint then treats the
128
+ * token as a session of one, which costs reuse tolerance but still works.
129
+ */
130
+ sessionId?: string;
131
+ /**
132
+ * Set when this token was superseded by a rotation. Being superseded is
133
+ * not an error — a client whose response was lost still holds it — so it
134
+ * stays usable to mint a sibling for a short window after this instant.
135
+ */
136
+ rotatedAt?: Date | null;
137
+ /** Hard kill (logout, remote session revoke). Never usable again. */
138
+ revoked?: boolean;
139
+ /** When the sign-in happened; carried across rotations, unlike createdAt. */
140
+ sessionStartedAt?: Date;
141
+ }
142
+ /**
143
+ * Identity of the sign-in a refresh token belongs to, threaded through
144
+ * rotation so descendants stay grouped.
145
+ */
146
+ export interface RefreshTokenSession {
147
+ id: string;
148
+ startedAt: Date;
122
149
  }
123
150
  /**
124
151
  * Password reset token info
@@ -278,9 +305,45 @@ export interface RoleRepository {
278
305
  */
279
306
  export interface TokenRepository {
280
307
  /**
281
- * Create a new refresh token
308
+ * Create a new refresh token.
309
+ *
310
+ * `session` groups this token with the sign-in it descends from. It is
311
+ * optional so that repositories written against an older release keep
312
+ * satisfying this interface; implementations that ignore it degrade to one
313
+ * session per token.
314
+ */
315
+ createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
316
+ /**
317
+ * Mark a token as superseded by a rotation, WITHOUT making it unusable.
318
+ *
319
+ * The distinction from deletion is the entire point: a client that never
320
+ * received the rotated response still holds this token, and must be able
321
+ * to present it and be recognised. Implementations that omit this method
322
+ * fall back to {@link TokenRepository.deleteRefreshToken}, which restores
323
+ * the old, lossy behaviour.
324
+ */
325
+ markRefreshTokenRotated?(tokenHash: string): Promise<void>;
326
+ /**
327
+ * Hard-kill every token of one sign-in (logout, remote session revoke).
328
+ * Unlike rotation this is final — no grace, no replay.
329
+ */
330
+ revokeRefreshTokenSession?(sessionId: string): Promise<void>;
331
+ /**
332
+ * Drop tokens of a session that were superseded before `supersededBefore`,
333
+ * plus anything already expired. Keeps rotation from growing a row per
334
+ * refresh forever; called opportunistically, never load-bearing.
335
+ */
336
+ pruneRefreshTokens?(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
337
+ /**
338
+ * The instant before which every session of this user is void, or null if
339
+ * none is set. See `users.tokens_valid_after`.
340
+ */
341
+ getTokensValidAfter?(uid: string): Promise<Date | null>;
342
+ /**
343
+ * Void every session that began before `at`. Set alongside deleting the
344
+ * user's tokens so a rotation racing the delete cannot survive it.
282
345
  */
283
- createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
346
+ setTokensValidAfter?(uid: string, at: Date): Promise<void>;
284
347
  /**
285
348
  * Find a refresh token by hash
286
349
  */
@@ -65,6 +65,23 @@ export declare function generateRefreshToken(): string;
65
65
  * Hash a refresh token for database storage (don't store raw tokens)
66
66
  */
67
67
  export declare function hashRefreshToken(token: string): string;
68
+ /**
69
+ * The longest a cookie can live. Chrome (since 104) and RFC 6265bis silently
70
+ * rewrite any `Max-Age` beyond 400 days down to 400 days, so promising a
71
+ * browser more is not a stricter policy — it is a policy that differs from
72
+ * what is actually enforced, which is worse than knowing the ceiling.
73
+ */
74
+ export declare const MAX_COOKIE_AGE_MS: number;
75
+ /**
76
+ * How long a refresh token is valid for, in milliseconds.
77
+ *
78
+ * Every rotation issues a token with a fresh TTL, so this is a sliding window:
79
+ * a user who visits at all keeps their session indefinitely, and one who
80
+ * disappears loses it this long after their last visit. That is what both
81
+ * Firebase and Supabase do by default, and it is the behaviour people mean
82
+ * when they say they expect to still be signed in.
83
+ */
84
+ export declare function getRefreshTokenTtlMs(): number;
68
85
  /**
69
86
  * Calculate refresh token expiration date
70
87
  */
@@ -47,6 +47,22 @@ export interface AuthModuleConfig {
47
47
  * auth endpoints, and CORS must allow credentials (no `origin: "*"`).
48
48
  */
49
49
  cookieAuth?: CookieAuthConfig;
50
+ /**
51
+ * How long a refresh token stays usable after it has been rotated away,
52
+ * in seconds. Default 10, matching GoTrue's `refresh_token_reuse_interval`.
53
+ *
54
+ * Rotation is only safe if the client is guaranteed to receive the
55
+ * replacement, and no network guarantees that. A pod rolls mid-response, a
56
+ * laptop suspends, a second tab boots at the same instant — and the client
57
+ * is left holding a token the database has moved past. Within this window
58
+ * that client is handed a fresh token of the same session instead of a
59
+ * 401, which is the difference between a hiccup and being silently signed
60
+ * out of an app you were using.
61
+ *
62
+ * Widen it if your clients are flaky or your deploys are long; the cost is
63
+ * how long a captured token stays useful to someone who copied it.
64
+ */
65
+ refreshTokenReuseIntervalSeconds?: number;
50
66
  }
51
67
  /**
52
68
  * Configuration for httpOnly refresh-token cookies.
package/dist/index.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  import process from "process";
3
3
  __createRequire(import.meta.url);
4
- import { a as getAccessTokenExpiry, d as require_jsonwebtoken, f as __commonJSMin, h as __toESM, i as generateRefreshToken, l as verifyAccessToken, m as __require, n as generateAccessToken, o as getRefreshTokenExpiry, p as __exportAll, r as generateDownloadToken, s as hashRefreshToken, t as configureJwt, u as verifyDownloadToken } from "./jwt-B3zjddCa.js";
4
+ import { _ as __toESM, a as generateRefreshToken, c as getRefreshTokenTtlMs, d as verifyAccessToken, f as verifyDownloadToken, g as __require, h as __exportAll, i as generateDownloadToken, l as hashRefreshToken, m as __commonJSMin, n as configureJwt, o as getAccessTokenExpiry, p as require_jsonwebtoken, r as generateAccessToken, s as getRefreshTokenExpiry, t as MAX_COOKIE_AGE_MS } from "./jwt-Dj7r7QX7.js";
5
5
  import { _ as RebaseApiError, a as policy, c as isPostgresCollectionConfig, d as REST_TO_CANONICAL, f as toCanonicalOp, g as Vector, h as GeoPoint, i as isSQLAdmin, l as CANONICAL_TO_REST, m as EntityRelation, o as getCollectionDataPath, p as EntityReference, r as getDataSourceCapabilities, s as getDeclaredSubcollections, t as DEFAULT_STORAGE_SOURCE_KEY, u as NULL_OPS, v as RebaseClientError } from "./src-CsHhSKbi.js";
6
6
  import { t as logger } from "./logger-BYU66ENZ.js";
7
7
  import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
@@ -9888,7 +9888,10 @@ function mountMfaRoutes(router, config, ops, parseBody, applyTransformHook) {
9888
9888
  const roleIds = (await authRepo.getUserRoles(userCtx.uid)).map((r) => r.id);
9889
9889
  const accessToken = generateAccessToken(userCtx.uid, roleIds, "aal2");
9890
9890
  const refreshToken = generateRefreshToken();
9891
- await authRepo.createRefreshToken(userCtx.uid, hashRefreshToken(refreshToken), getRefreshTokenExpiry(), c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown");
9891
+ await authRepo.createRefreshToken(userCtx.uid, hashRefreshToken(refreshToken), getRefreshTokenExpiry(), c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown", {
9892
+ id: randomUUID$1(),
9893
+ startedAt: /* @__PURE__ */ new Date()
9894
+ });
9892
9895
  if (ops.onMfaVerified) ops.onMfaVerified(userCtx.uid, factor.id).catch((err) => {
9893
9896
  logger.error("[AuthHooks] onMfaVerified error", { error: err instanceof Error ? err.message : err });
9894
9897
  });
@@ -9959,7 +9962,8 @@ function setRefreshCookie(c, refreshToken, config) {
9959
9962
  let cookie = `${settings.name}=${encodeURIComponent(refreshToken)}; Path=${settings.path}; HttpOnly; SameSite=${settings.sameSite}`;
9960
9963
  if (isSecure) cookie += "; Secure";
9961
9964
  if (settings.domain) cookie += `; Domain=${settings.domain}`;
9962
- cookie += `; Max-Age=${720 * 60 * 60}`;
9965
+ const maxAgeSeconds = Math.floor(Math.min(getRefreshTokenTtlMs(), MAX_COOKIE_AGE_MS) / 1e3);
9966
+ cookie += `; Max-Age=${maxAgeSeconds}`;
9963
9967
  c.header("Set-Cookie", cookie, { append: true });
9964
9968
  }
9965
9969
  /**
@@ -10025,12 +10029,14 @@ function mountSessionRoutes(opts) {
10025
10029
  const refreshToken = readRefreshToken(c, parseBody(logoutSchema, await c.req.json().catch(() => ({}))), config.cookieAuth);
10026
10030
  if (refreshToken) {
10027
10031
  const tokenHash = hashRefreshToken(refreshToken);
10028
- await authRepo.deleteRefreshToken(tokenHash);
10032
+ const sessionId = (await authRepo.findRefreshTokenByHash(tokenHash).catch(() => null))?.sessionId;
10033
+ if (sessionId && authRepo.revokeRefreshTokenSession) await authRepo.revokeRefreshTokenSession(sessionId);
10034
+ else await authRepo.deleteRefreshToken(tokenHash);
10029
10035
  }
10030
10036
  clearRefreshCookie(c, config.cookieAuth);
10031
10037
  const authHeader = c.req.header("authorization");
10032
10038
  if (ops.afterLogout && authHeader?.startsWith("Bearer ")) {
10033
- const { verifyAccessToken } = await import("./jwt-B3zjddCa.js").then((n) => n.c);
10039
+ const { verifyAccessToken } = await import("./jwt-Dj7r7QX7.js").then((n) => n.u);
10034
10040
  const payload = verifyAccessToken(authHeader.substring(7));
10035
10041
  if (payload) ops.afterLogout(payload.uid).catch((err) => {
10036
10042
  logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
@@ -10047,12 +10053,20 @@ function mountSessionRoutes(opts) {
10047
10053
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
10048
10054
  const currentRefreshToken = c.req.header("x-refresh-token");
10049
10055
  const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
10050
- const mappedSessions = (await authRepo.listRefreshTokensForUser(userCtx.uid)).map((s) => ({
10056
+ const tokens = await authRepo.listRefreshTokensForUser(userCtx.uid);
10057
+ const bySession = /* @__PURE__ */ new Map();
10058
+ for (const token of tokens) {
10059
+ if (token.revoked) continue;
10060
+ const key = token.sessionId ?? token.id;
10061
+ const existing = bySession.get(key);
10062
+ if (!existing || existing.createdAt < token.createdAt) bySession.set(key, token);
10063
+ }
10064
+ const mappedSessions = [...bySession.values()].map((s) => ({
10051
10065
  id: s.id,
10052
10066
  userAgent: s.userAgent,
10053
10067
  ipAddress: s.ipAddress,
10054
- createdAt: s.createdAt,
10055
- isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
10068
+ createdAt: s.sessionStartedAt ?? s.createdAt,
10069
+ isCurrentSession: currentTokenHash ? tokens.some((t) => t.tokenHash === currentTokenHash && (t.sessionId ?? t.id) === (s.sessionId ?? s.id)) : false
10056
10070
  }));
10057
10071
  return c.json({ sessions: mappedSessions });
10058
10072
  });
@@ -10064,6 +10078,7 @@ function mountSessionRoutes(opts) {
10064
10078
  const userCtx = c.get("user");
10065
10079
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
10066
10080
  await authRepo.deleteAllRefreshTokensForUser(userCtx.uid);
10081
+ await authRepo.setTokensValidAfter?.(userCtx.uid, /* @__PURE__ */ new Date()).catch(() => void 0);
10067
10082
  return c.json({
10068
10083
  success: true,
10069
10084
  message: "All sessions revoked successfully"
@@ -10078,7 +10093,9 @@ function mountSessionRoutes(opts) {
10078
10093
  if (!userCtx) throw ApiError.unauthorized("Not authenticated");
10079
10094
  const id = c.req.param("id");
10080
10095
  if (!id) throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
10081
- await authRepo.deleteRefreshTokenById(id, userCtx.uid);
10096
+ const target = (await authRepo.listRefreshTokensForUser(userCtx.uid).catch(() => [])).find((t) => t.id === id);
10097
+ if (target?.sessionId && authRepo.revokeRefreshTokenSession) await authRepo.revokeRefreshTokenSession(target.sessionId);
10098
+ else await authRepo.deleteRefreshTokenById(id, userCtx.uid);
10082
10099
  return c.json({
10083
10100
  success: true,
10084
10101
  message: "Session revoked successfully"
@@ -10442,7 +10459,10 @@ function createAuthRoutes(config) {
10442
10459
  }
10443
10460
  const accessToken = generateAccessToken(uid, roleIds, "aal1", customClaims);
10444
10461
  const refreshToken = generateRefreshToken();
10445
- await authRepo.createRefreshToken(uid, hashRefreshToken(refreshToken), getRefreshTokenExpiry(), userAgent, ipAddress);
10462
+ await authRepo.createRefreshToken(uid, hashRefreshToken(refreshToken), getRefreshTokenExpiry(), userAgent, ipAddress, {
10463
+ id: randomUUID$1(),
10464
+ startedAt: /* @__PURE__ */ new Date()
10465
+ });
10446
10466
  return {
10447
10467
  roleIds,
10448
10468
  accessToken,
@@ -10675,6 +10695,7 @@ function createAuthRoutes(config) {
10675
10695
  await authRepo.updatePassword(storedToken.uid, passwordHash);
10676
10696
  await authRepo.markPasswordResetTokenUsed(tokenHash);
10677
10697
  await authRepo.deleteAllRefreshTokensForUser(storedToken.uid);
10698
+ await authRepo.setTokensValidAfter?.(storedToken.uid, /* @__PURE__ */ new Date()).catch(() => void 0);
10678
10699
  if (ops.onPasswordReset) ops.onPasswordReset(storedToken.uid).catch((err) => {
10679
10700
  logger.error("[AuthHooks] onPasswordReset error", { error: err instanceof Error ? err.message : err });
10680
10701
  });
@@ -10699,6 +10720,7 @@ function createAuthRoutes(config) {
10699
10720
  const passwordHash = await ops.hashPassword(newPassword);
10700
10721
  await authRepo.updatePassword(user.id, passwordHash);
10701
10722
  await authRepo.deleteAllRefreshTokensForUser(user.id);
10723
+ await authRepo.setTokensValidAfter?.(user.id, /* @__PURE__ */ new Date()).catch(() => void 0);
10702
10724
  return c.json({
10703
10725
  success: true,
10704
10726
  message: "Password has been changed successfully"
@@ -10762,15 +10784,34 @@ function createAuthRoutes(config) {
10762
10784
  if (!refreshToken) throw ApiError.unauthenticated("No refresh token presented", "NO_SESSION");
10763
10785
  const tokenHash = hashRefreshToken(refreshToken);
10764
10786
  const storedToken = await authRepo.findRefreshTokenByHash(tokenHash);
10765
- if (!storedToken) {
10787
+ if (!storedToken) throw ApiError.unauthorized("Invalid refresh token", "INVALID_TOKEN");
10788
+ if (storedToken.revoked) {
10766
10789
  clearRefreshCookie(c, config.cookieAuth);
10767
- throw ApiError.unauthorized("Invalid refresh token", "INVALID_TOKEN");
10790
+ throw ApiError.unauthorized("Session has been revoked", "SESSION_REVOKED");
10768
10791
  }
10769
10792
  if (/* @__PURE__ */ new Date() > storedToken.expiresAt) {
10770
10793
  await authRepo.deleteRefreshToken(tokenHash);
10771
10794
  clearRefreshCookie(c, config.cookieAuth);
10772
10795
  throw ApiError.unauthorized("Refresh token expired", "TOKEN_EXPIRED");
10773
10796
  }
10797
+ const sessionStartedAt = storedToken.sessionStartedAt ?? storedToken.createdAt;
10798
+ const tokensValidAfter = await authRepo.getTokensValidAfter?.(storedToken.uid).catch(() => null);
10799
+ if (tokensValidAfter && sessionStartedAt < tokensValidAfter) {
10800
+ await authRepo.deleteRefreshToken(tokenHash);
10801
+ clearRefreshCookie(c, config.cookieAuth);
10802
+ throw ApiError.unauthorized("Session has been revoked", "SESSION_REVOKED");
10803
+ }
10804
+ const reuseWindowMs = Math.max(0, config.refreshTokenReuseIntervalSeconds ?? 10) * 1e3;
10805
+ const supersededAt = storedToken.rotatedAt ? new Date(storedToken.rotatedAt) : null;
10806
+ if (supersededAt && Date.now() - supersededAt.getTime() > reuseWindowMs) {
10807
+ logger.warn("[Auth] Refresh token replayed after the reuse window", {
10808
+ uid: storedToken.uid,
10809
+ sessionId: storedToken.sessionId,
10810
+ supersededSecondsAgo: Math.round((Date.now() - supersededAt.getTime()) / 1e3),
10811
+ userAgent: c.req.header("user-agent") || "unknown"
10812
+ });
10813
+ throw ApiError.unauthorized("Refresh token already used", "TOKEN_ALREADY_USED");
10814
+ }
10774
10815
  const roleIds = (await authRepo.getUserRoles(storedToken.uid)).map((r) => r.id);
10775
10816
  const user = await authRepo.getUserById(storedToken.uid).catch((err) => {
10776
10817
  logger.warn("[Auth] Could not load user during token refresh; returning tokens only", {
@@ -10792,8 +10833,16 @@ function createAuthRoutes(config) {
10792
10833
  const newRefreshToken = generateRefreshToken();
10793
10834
  const userAgent = c.req.header("user-agent") || "unknown";
10794
10835
  const ipAddress = c.req.header("x-forwarded-for") || "unknown";
10795
- await authRepo.deleteRefreshToken(tokenHash);
10796
- await authRepo.createRefreshToken(storedToken.uid, hashRefreshToken(newRefreshToken), getRefreshTokenExpiry(), userAgent, ipAddress);
10836
+ const session = {
10837
+ id: storedToken.sessionId ?? storedToken.id,
10838
+ startedAt: sessionStartedAt
10839
+ };
10840
+ if (!supersededAt) if (authRepo.markRefreshTokenRotated) await authRepo.markRefreshTokenRotated(tokenHash);
10841
+ else await authRepo.deleteRefreshToken(tokenHash);
10842
+ await authRepo.createRefreshToken(storedToken.uid, hashRefreshToken(newRefreshToken), getRefreshTokenExpiry(), userAgent, ipAddress, session);
10843
+ authRepo.pruneRefreshTokens?.(storedToken.uid, session.id, new Date(Date.now() - reuseWindowMs)).catch((err) => {
10844
+ logger.warn("[Auth] Refresh token prune failed", { error: err instanceof Error ? err.message : String(err) });
10845
+ });
10797
10846
  const tokensOnlyResponse = { tokens: {
10798
10847
  accessToken: newAccessToken,
10799
10848
  refreshToken: newRefreshToken,
@@ -15032,19 +15081,39 @@ function createAuth(transport, options) {
15032
15081
  */
15033
15082
  function isFatalRefreshError(err) {
15034
15083
  if (!(err instanceof RebaseApiError)) return false;
15084
+ if (err.code === "TOKEN_ALREADY_USED") return false;
15035
15085
  if (err.code === "INVALID_TOKEN" || err.code === "TOKEN_EXPIRED") return true;
15036
15086
  return err.status === 401 || err.status === 403;
15037
15087
  }
15088
+ /**
15089
+ * Drop this client's session without telling the server.
15090
+ *
15091
+ * `signOut()` is a user action: it POSTs /logout, which revokes the whole
15092
+ * sign-in. That is the wrong hammer for a refresh that failed. Our token
15093
+ * may be stale precisely because a sibling tab holds a live one, and
15094
+ * logging out on its behalf would turn one tab's bad luck into everybody
15095
+ * being signed out — the exact failure this work exists to remove.
15096
+ */
15097
+ function abandonSessionLocally() {
15098
+ currentSession = null;
15099
+ clearStoredSession();
15100
+ if (refreshTimeout) {
15101
+ clearTimeout(refreshTimeout);
15102
+ refreshTimeout = null;
15103
+ }
15104
+ transport.setToken(null);
15105
+ emit("SIGNED_OUT", null);
15106
+ }
15038
15107
  async function attemptScheduledRefresh(attempt) {
15039
15108
  try {
15040
15109
  await refreshSession();
15041
15110
  } catch (err) {
15042
15111
  if (isFatalRefreshError(err)) {
15043
- signOut();
15112
+ abandonSessionLocally();
15044
15113
  return;
15045
15114
  }
15046
15115
  if (attempt >= MAX_REFRESH_RETRIES) {
15047
- signOut();
15116
+ abandonSessionLocally();
15048
15117
  return;
15049
15118
  }
15050
15119
  const backoff = Math.min(REFRESH_RETRY_BASE_MS * 2 ** attempt, REFRESH_RETRY_MAX_MS);
@@ -15265,9 +15334,42 @@ function createAuth(transport, options) {
15265
15334
  transport.setToken(null);
15266
15335
  emit("SIGNED_OUT", null);
15267
15336
  }
15337
+ /**
15338
+ * Serialise refreshes across TABS, not just within one.
15339
+ *
15340
+ * The in-flight promise below covers callers inside a single JavaScript
15341
+ * context. It does nothing about the far more common case: two tabs of the
15342
+ * same app booting together, each firing its own /refresh with the same
15343
+ * cookie. The server tolerates that now (superseded tokens stay usable for
15344
+ * a grace window), but tolerating a stampede is not the same as avoiding
15345
+ * one, and every extra rotation is another chance to end up holding a
15346
+ * token whose response never arrived.
15347
+ *
15348
+ * Web Locks are best-effort on purpose. supabase-js shipped this and then
15349
+ * spent a year fielding deadlock reports — a lock held by a crashed or
15350
+ * frozen tab must never be able to wedge sign-in — so a lock we cannot
15351
+ * take within the timeout is simply not taken, and the refresh proceeds
15352
+ * unserialised, exactly as it did before.
15353
+ */
15354
+ const REFRESH_LOCK_NAME = "rebase-auth-refresh";
15355
+ const REFRESH_LOCK_TIMEOUT_MS = 5e3;
15356
+ async function withRefreshLock(fn) {
15357
+ const locks = globalThis.navigator?.locks;
15358
+ if (!locks?.request) return fn();
15359
+ const controller = new AbortController();
15360
+ const giveUp = setTimeout(() => controller.abort(), REFRESH_LOCK_TIMEOUT_MS);
15361
+ try {
15362
+ return await locks.request(REFRESH_LOCK_NAME, { signal: controller.signal }, async () => fn());
15363
+ } catch (e) {
15364
+ if (e?.name !== "AbortError") throw e;
15365
+ return fn();
15366
+ } finally {
15367
+ clearTimeout(giveUp);
15368
+ }
15369
+ }
15268
15370
  function refreshSession() {
15269
15371
  if (inFlightRefresh) return inFlightRefresh;
15270
- inFlightRefresh = doRefreshSession().finally(() => {
15372
+ inFlightRefresh = withRefreshLock(() => doRefreshSession()).finally(() => {
15271
15373
  inFlightRefresh = null;
15272
15374
  });
15273
15375
  return inFlightRefresh;
@@ -21782,7 +21884,7 @@ var rebaseEnvSchema = object({
21782
21884
  ADMIN_CONNECTION_STRING: string().url().optional(),
21783
21885
  JWT_SECRET: string().min(32, "JWT_SECRET must be at least 32 characters long"),
21784
21886
  JWT_ACCESS_EXPIRES_IN: string().default("1h"),
21785
- JWT_REFRESH_EXPIRES_IN: string().default("30d"),
21887
+ JWT_REFRESH_EXPIRES_IN: string().default("400d"),
21786
21888
  GOOGLE_CLIENT_ID: string().optional(),
21787
21889
  GOOGLE_CLIENT_SECRET: string().optional(),
21788
21890
  REBASE_SERVICE_KEY: string().optional(),