import { deleteSession, getSession, updateSession, type SessionData, } from "./session-store"; import { exchangeFeedToken, refreshProfileTokens } from "./minnect"; // Reusable server-side session logic the route handlers share. The feed route // (a later task) calls `ensureFreshSession` before using a token, and // `refreshSession` once after a feed call returns 401. Minnect tokens never leave the server; nothing here is logged. /** Refresh when a token expires within this window (spec: two minutes). */ export const REFRESH_WINDOW_MS = 2 * 60 * 1000; export interface RefreshOptions { /** Injectable fetch, for testing. Defaults to the global fetch. */ fetchImpl?: typeof fetch; /** Injectable clock for the expiry decision, for testing. Defaults to Date.now(). */ now?: number; } /** True when the expiry is known and falls within the refresh window. */ function expiresSoon(expiresAt: number | null, now: number): boolean { return expiresAt !== null && expiresAt - now <= REFRESH_WINDOW_MS; } /** * Refresh the profile and feed tokens of a session and persist them, keeping the * same session id (and the same person). Uses the stored profile refresh token, * never a password. On any failure the session is deleted and `null` is * returned, so the browser is sent back to the sign-in screen. */ export async function refreshSession( sessionId: string, options: RefreshOptions = {}, ): Promise { const session = getSession(sessionId); if (!session) return null; try { const profile = await refreshProfileTokens( session.profileRefreshToken, options, ); const feed = await exchangeFeedToken(profile.profileAccessToken, options); const updated: SessionData = { ...session, profileAccessToken: profile.profileAccessToken, profileAccessTokenExpiresAt: profile.profileAccessTokenExpiresAt, profileRefreshToken: profile.profileRefreshToken, feedAccessToken: feed.feedAccessToken, feedAccessTokenExpiresAt: feed.feedAccessTokenExpiresAt, }; updateSession(sessionId, updated); return updated; } catch { // Refresh failed: drop the session. The caller returns 401 { error: "sign_in" }. deleteSession(sessionId); return null; } } /** * Load a session and, when its profile or feed access token is within the * refresh window, refresh it first. Returns the usable session, or `null` when * there is no session or the refresh failed. Call this before using a token. */ export async function ensureFreshSession( sessionId: string | undefined, options: RefreshOptions = {}, ): Promise { if (!sessionId) return null; const session = getSession(sessionId); if (!session) return null; const now = options.now ?? Date.now(); if ( expiresSoon(session.profileAccessTokenExpiresAt, now) || expiresSoon(session.feedAccessTokenExpiresAt, now) ) { return refreshSession(sessionId, options); } return session; }