mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 16:12:02 +00:00
79 lines
3.3 KiB
TypeScript
79 lines
3.3 KiB
TypeScript
import { configManager } from '@/lib/admin/config-manager';
|
|
import { DEFAULT_OAUTH_SCOPES } from './scopes';
|
|
|
|
/**
|
|
* Resolve the OAuth scopes to request at authorize time.
|
|
*
|
|
* Reads admin override / OAUTH_SCOPES / OAUTH_EXTRA_SCOPES at call time so
|
|
* runtime env vars (and admin dashboard changes) take effect without a rebuild.
|
|
* Server-only: callers in the browser must read `oauthScopes` from /api/config.
|
|
*/
|
|
export function getOauthScopes(): string {
|
|
const explicit = configManager.get<string>('oauthScopes', '');
|
|
if (explicit) return explicit;
|
|
const extra = configManager.get<string>('oauthExtraScopes', '');
|
|
return extra ? `${DEFAULT_OAUTH_SCOPES} ${extra}`.trim() : DEFAULT_OAUTH_SCOPES;
|
|
}
|
|
export const REFRESH_TOKEN_COOKIE = 'jmap_rt';
|
|
export const REFRESH_TOKEN_SERVER_COOKIE = 'jmap_rts';
|
|
export const ACCESS_TOKEN_COOKIE = 'jmap_at';
|
|
|
|
/** Get the cookie name for a given account slot. Slot 0 uses the legacy name. */
|
|
export function refreshTokenCookieName(slot: number): string {
|
|
return slot === 0 ? REFRESH_TOKEN_COOKIE : `${REFRESH_TOKEN_COOKIE}_${slot}`;
|
|
}
|
|
|
|
/** Companion cookie storing which server entry id minted the refresh token at this slot. */
|
|
export function refreshTokenServerCookieName(slot: number): string {
|
|
return slot === 0 ? REFRESH_TOKEN_SERVER_COOKIE : `${REFRESH_TOKEN_SERVER_COOKIE}_${slot}`;
|
|
}
|
|
|
|
/**
|
|
* Cookie caching the *current* access token for a slot, so a page reload can
|
|
* resume with the token it already had instead of burning a refresh.
|
|
*/
|
|
export function accessTokenCookieName(slot: number): string {
|
|
return slot === 0 ? ACCESS_TOKEN_COOKIE : `${ACCESS_TOKEN_COOKIE}_${slot}`;
|
|
}
|
|
|
|
/**
|
|
* Seconds of remaining life an access token must have for the cache to serve
|
|
* it. Mirrors the client's refresh-ahead margin: inside this window the token
|
|
* is due for renewal anyway, and IdPs that gate refresh on an `nbf` claim
|
|
* (Rauthy sets `iat + access_token_lifetime - 60`) have opened by then.
|
|
*/
|
|
export const ACCESS_TOKEN_MIN_REMAINING_SECONDS = 60;
|
|
|
|
/**
|
|
* Cap on the cached value. Browsers cap a cookie at ~4096 bytes including the
|
|
* name and attributes; an access token above this is dropped from the cache
|
|
* rather than silently truncated, degrading to a refresh-on-reload.
|
|
*/
|
|
const MAX_CACHED_ACCESS_TOKEN_BYTES = 3300;
|
|
|
|
/** Serialise an access token plus its absolute expiry for cookie storage. */
|
|
export function encodeCachedAccessToken(token: string, expiresInSeconds: number): string | null {
|
|
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
|
|
const value = `${expiresAt}.${token}`;
|
|
if (value.length > MAX_CACHED_ACCESS_TOKEN_BYTES) return null;
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Parse a cached access token, returning it only when it still has at least
|
|
* {@link ACCESS_TOKEN_MIN_REMAINING_SECONDS} of life left.
|
|
*/
|
|
export function decodeCachedAccessToken(
|
|
value: string | undefined,
|
|
): { accessToken: string; expiresIn: number } | null {
|
|
if (!value) return null;
|
|
const sep = value.indexOf('.');
|
|
if (sep <= 0) return null;
|
|
const expiresAt = parseInt(value.slice(0, sep), 10);
|
|
const accessToken = value.slice(sep + 1);
|
|
if (!Number.isFinite(expiresAt) || !accessToken) return null;
|
|
const expiresIn = expiresAt - Math.floor(Date.now() / 1000);
|
|
if (expiresIn < ACCESS_TOKEN_MIN_REMAINING_SECONDS) return null;
|
|
return { accessToken, expiresIn };
|
|
}
|