The webmail session now lasts 30 days and is swapped for a fresh token once a day while in use; the console keeps its fixed 8 hours. Each sign-in is also filed under lm_acct_<mailbox id>, with new endpoints to list and switch accounts, and sign-out now leaves the other accounts signed in. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
57 lines
2.4 KiB
TypeScript
57 lines
2.4 KiB
TypeScript
/**
|
|
* Several webmail accounts signed in at once, and a session that stays alive
|
|
* for as long as it is used.
|
|
*
|
|
* Each signed-in account keeps its own cookie, lm_acct_<mailbox id>, next to
|
|
* lm_user_session. lm_user_session stays the *active* account: every existing
|
|
* route, the Next middleware and the protocols service keep reading that one
|
|
* cookie and need no change. Switching accounts is copying one of the
|
|
* lm_acct_ tokens into lm_user_session.
|
|
*/
|
|
|
|
/** A webmail session lasts this long without use, and is renewed while in use. */
|
|
export const USER_SESSION_TTL_SECONDS = 30 * 24 * 3600;
|
|
/** The console is the sensitive surface: it keeps the short, fixed lifetime. */
|
|
export const ADMIN_SESSION_TTL_SECONDS = 8 * 3600;
|
|
/** A token is replaced once it is older than this, so a busy day costs one write. */
|
|
export const RENEW_AFTER_SECONDS = 24 * 3600;
|
|
/** How long a replaced token keeps working, for requests already in flight. */
|
|
export const REPLACED_GRACE_SECONDS = 120;
|
|
|
|
const ACCOUNT_COOKIE_PREFIX = "lm_acct_";
|
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
/** More than this and the Cookie header risks the proxies' 8 KB limit. */
|
|
export const MAX_ACCOUNTS = 6;
|
|
|
|
export function accountCookieName(mailboxId: string): string {
|
|
return `${ACCOUNT_COOKIE_PREFIX}${mailboxId}`;
|
|
}
|
|
|
|
export function parseCookies(header: string | undefined): Record<string, string> {
|
|
const out: Record<string, string> = {};
|
|
if (!header) return out;
|
|
for (const part of header.split(";")) {
|
|
const trimmed = part.trim();
|
|
const eq = trimmed.indexOf("=");
|
|
// sliced rather than split so a value containing "=" survives intact.
|
|
if (eq > 0) out[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** The account cookies a browser sent, as {mailbox id, token}. Names that are not a UUID are ignored. */
|
|
export function accountTokens(header: string | undefined): { id: string; token: string }[] {
|
|
const found: { id: string; token: string }[] = [];
|
|
for (const [name, token] of Object.entries(parseCookies(header))) {
|
|
if (!name.startsWith(ACCOUNT_COOKIE_PREFIX) || !token) continue;
|
|
const id = name.slice(ACCOUNT_COOKIE_PREFIX.length);
|
|
if (UUID.test(id)) found.push({ id: id.toLowerCase(), token });
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/** Whether a token has aged enough to be replaced with a fresh one. */
|
|
export function shouldRenew(iat: number, nowSeconds: number): boolean {
|
|
return nowSeconds - iat >= RENEW_AFTER_SECONDS;
|
|
}
|