feat: lift 5-account cap on HTTP/2
This commit is contained in:
+40
-2
@@ -62,5 +62,43 @@ export function getAccountScopedKey(baseKey: string, accountId: string): string
|
||||
return `${baseKey}::${accountId}`;
|
||||
}
|
||||
|
||||
/** Maximum number of accounts allowed */
|
||||
export const MAX_ACCOUNTS = 5;
|
||||
/**
|
||||
* Hard upper bound on cookie slots. Each slot can hold up to ~3 cookies
|
||||
* (session, refresh token, server id, auth context), so 50 slots ≈ 125
|
||||
* cookies on average — within Firefox's per-domain limit of 150.
|
||||
*/
|
||||
export const MAX_ACCOUNT_SLOTS = 50;
|
||||
|
||||
/**
|
||||
* UX cap for browsers using HTTP/1.1. Each account holds one persistent
|
||||
* SSE connection for JMAP push; HTTP/1.1 caps origins at 6 concurrent
|
||||
* connections, so 5 accounts leave one connection free for normal traffic.
|
||||
* On HTTP/2+ this cap doesn't apply because streams are multiplexed.
|
||||
*/
|
||||
export const MAX_ACCOUNTS_HTTP1 = 5;
|
||||
|
||||
/**
|
||||
* Detect whether the page has observed any HTTP/2 or HTTP/3 traffic.
|
||||
*
|
||||
* We walk recent resource-timing entries and treat a single h2/h3 sighting
|
||||
* as a positive signal. Cross-origin entries may report an empty
|
||||
* `nextHopProtocol` without `Timing-Allow-Origin`, in which case we
|
||||
* under-detect and fall back to the conservative cap — that's safe.
|
||||
*/
|
||||
export function isHttp2Available(): boolean {
|
||||
if (typeof performance === 'undefined') return false;
|
||||
const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const proto = entries[i].nextHopProtocol;
|
||||
if (proto === 'h2' || proto === 'h3') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective per-browser account cap. Lifts to {@link MAX_ACCOUNT_SLOTS}
|
||||
* once HTTP/2+ is observed, otherwise returns {@link MAX_ACCOUNTS_HTTP1}.
|
||||
*/
|
||||
export function getMaxAccounts(): number {
|
||||
return isHttp2Available() ? MAX_ACCOUNT_SLOTS : MAX_ACCOUNTS_HTTP1;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const SESSION_COOKIE = 'jmap_session';
|
||||
export const SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
|
||||
|
||||
/** Get the cookie name for a given account slot (0-4). Slot 0 uses the legacy name. */
|
||||
/** Get the cookie name for a given account slot. Slot 0 uses the legacy name. */
|
||||
export function sessionCookieName(slot: number): string {
|
||||
return slot === 0 ? SESSION_COOKIE : `${SESSION_COOKIE}_${slot}`;
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ export const OAUTH_SCOPES = process.env.OAUTH_SCOPES || (EXTRA_SCOPES ? `${DEFAU
|
||||
export const REFRESH_TOKEN_COOKIE = 'jmap_rt';
|
||||
export const REFRESH_TOKEN_SERVER_COOKIE = 'jmap_rts';
|
||||
|
||||
/** Get the cookie name for a given account slot (0-4). Slot 0 uses the legacy name. */
|
||||
/** 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}`;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { cookies } from 'next/headers';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { sessionCookieName } from '@/lib/auth/session-cookie';
|
||||
import { readStalwartAuthContextFromStore } from '@/lib/stalwart/auth-context';
|
||||
import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils';
|
||||
|
||||
export interface StalwartCredentials {
|
||||
/** URL of the JMAP server (used for JMAP + management method calls) */
|
||||
@@ -15,14 +16,16 @@ export interface StalwartCredentials {
|
||||
function parseSlot(raw: string | null): number | null {
|
||||
if (raw === null) return null;
|
||||
const slot = parseInt(raw, 10);
|
||||
return Number.isNaN(slot) || slot < 0 || slot > 4 ? null : slot;
|
||||
return Number.isNaN(slot) || slot < 0 || slot >= MAX_ACCOUNT_SLOTS ? null : slot;
|
||||
}
|
||||
|
||||
const ALL_SLOTS = Array.from({ length: MAX_ACCOUNT_SLOTS }, (_, i) => i);
|
||||
|
||||
function getCandidateSlots(request: NextRequest): number[] {
|
||||
const requestedSlot = parseSlot(request.headers.get('X-JMAP-Cookie-Slot'))
|
||||
?? parseSlot(request.nextUrl.searchParams.get('slot'));
|
||||
|
||||
return requestedSlot === null ? [0, 1, 2, 3, 4] : [requestedSlot];
|
||||
return requestedSlot === null ? ALL_SLOTS : [requestedSlot];
|
||||
}
|
||||
|
||||
export async function getStalwartCredentials(request: NextRequest): Promise<StalwartCredentials | null> {
|
||||
|
||||
Reference in New Issue
Block a user