All files / lib ho-session-cache.ts

100% Statements 29/29
93.75% Branches 15/16
100% Functions 4/4
100% Lines 23/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82                  17x 17x   14x 14x                     9x 9x     8x 8x     7x 7x 7x     5x 5x 5x 3x           5x                   4x 4x   3x 3x                               4x 4x   3x    
// Session caching layer for Homeowner Better Auth sessions
// Same pattern as session-cache.ts but reads ho.session_token cookie
import type { DualCache } from "./cache";
import { CACHE_TTL } from "./cache";
import type { HomeownerAuth } from "./homeowner-auth";
 
type HoSessionResult = Awaited<ReturnType<HomeownerAuth["api"]["getSession"]>>;
 
function extractHoSessionToken(headers: Headers): string | null {
	const cookie = headers.get("cookie");
	if (!cookie) return null;
 
	const match = cookie.match(/ho\.session_token=([^;]+)/);
	return match?.[1] ?? null;
}
 
// Takes a FACTORY, not an instance — see the note on getCachedSession in
// session-cache.ts. The homeowner auth instance drags the same ~2.4 MB Better
// Auth tail, so it is loaded lazily and only when the cache misses.
export async function getCachedHoSession(
	cache: DualCache,
	getAuth: () => HomeownerAuth | Promise<HomeownerAuth>,
	headers: Headers,
): Promise<HoSessionResult> {
	const token = extractHoSessionToken(headers);
	if (!token) return null;
 
	// Check blocklist first
	const isBlocked = await cache.get<boolean>(`ho-deleted-session:${token}`);
	if (isBlocked) return null;
 
	// Check cache
	const cacheKey = `ho-session:${token}`;
	const cached = await cache.get<NonNullable<HoSessionResult>>(cacheKey);
	if (cached) return cached;
 
	// Cache miss — fetch from Better Auth (hits D1)
	const auth = await getAuth();
	const session = await auth.api.getSession({ headers });
	if (session) {
		await cache.put(cacheKey, session, {
			l1Ttl: CACHE_TTL.SESSION_L1,
			l2Ttl: CACHE_TTL.SESSION_L2,
		});
	}
 
	return session;
}
 
// Revokes the session outright: drops the cache entry AND blocklists the
// token so a stale cached copy can never be served again. Use only where the
// session itself must stop working — sign-out, account deletion.
export async function invalidateHoSession(
	cache: DualCache,
	headers: Headers,
): Promise<void> {
	const token = extractHoSessionToken(headers);
	if (!token) return;
 
	await cache.delete(`ho-session:${token}`);
	await cache.put(`ho-deleted-session:${token}`, true, {
		l1Ttl: CACHE_TTL.SESSION_L1,
		l2Ttl: CACHE_TTL.SESSION_L2,
	});
}
 
// Drops the cached copy of a still-valid session so the next request rebuilds
// it from D1 with fresh fields (name, email, emailVerified, ...). Unlike
// invalidateHoSession, this does NOT blocklist the token — the session stays
// logged in. Use after mutating the ho_users row for the current session
// (displayName mirror, change-email verify) where invalidateHoSession would
// incorrectly sign the user out of their own still-valid session.
export async function refreshHoSessionCache(
	cache: DualCache,
	headers: Headers,
): Promise<void> {
	const token = extractHoSessionToken(headers);
	if (!token) return;
 
	await cache.delete(`ho-session:${token}`);
}