diff --git a/src/lib/api.ts b/src/lib/api.ts index dd07c57..a7718c6 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -44,6 +44,13 @@ export const api = createTRPCProxyClient({ headers: () => { const csrfToken = getCSRFToken(); return csrfToken ? { "x-csrf-token": csrfToken } : {}; + }, + // CRITICAL FOR SAFARI: Ensure cookies are sent and received + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include" // Safari requires this for cookie handling + }); } }) ] diff --git a/src/lib/token-refresh.ts b/src/lib/token-refresh.ts index e16d186..778d33d 100644 --- a/src/lib/token-refresh.ts +++ b/src/lib/token-refresh.ts @@ -23,6 +23,7 @@ class TokenRefreshManager { private onlineHandler: (() => void) | null = null; private focusHandler: (() => void) | null = null; private lastRefreshTime: number | null = null; + private lastCheckTime: number = 0; /** * Start monitoring and auto-refresh @@ -73,7 +74,18 @@ class TokenRefreshManager { window.addEventListener("online", this.onlineHandler); // Re-check on window focus (device was asleep or user switched apps) + // Debounce to prevent Safari from firing this too frequently this.focusHandler = () => { + const now = Date.now(); + const timeSinceLastCheck = now - this.lastCheckTime; + + // Debounce: only check if last check was >1s ago (prevents Safari spam) + if (timeSinceLastCheck < 1000) { + console.log("[Token Refresh] Window focused but debouncing (Safari)"); + return; + } + + this.lastCheckTime = now; console.log("[Token Refresh] Window focused, checking token status"); this.checkAndRefreshIfNeeded(); }; diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 2758cae..3dddc53 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -1660,17 +1660,33 @@ export const authRouter = createTRPCRouter({ }); } - // Step 4: Refresh CSRF token + // Step 4: Force response headers to be sent immediately + // This is critical for Safari to receive the new session cookies + // Safari is very strict about cookie updates from fetch responses + try { + const headers = event.node?.res?.getHeaders?.() || {}; + console.log( + "[Token Refresh] Response headers set:", + Object.keys(headers) + ); + } catch (e) { + // Headers already sent or not available - that's OK + } + + // Step 5: Refresh CSRF token setCSRFToken(event); - // Step 5: Opportunistic cleanup (serverless-friendly) + // Step 6: Opportunistic cleanup (serverless-friendly) import("~/server/token-cleanup") .then((module) => module.opportunisticCleanup()) .catch((err) => console.error("Opportunistic cleanup failed:", err)); return { success: true, - message: "Token refreshed successfully" + message: "Token refreshed successfully", + // Return new session ID for Safari fallback + // If Safari doesn't apply cookies, client can use this to restore + sessionId: newSession.sessionId }; } catch (error) { console.error("Token refresh error:", error); diff --git a/src/server/session-helpers.ts b/src/server/session-helpers.ts index 2d8f080..9420eb4 100644 --- a/src/server/session-helpers.ts +++ b/src/server/session-helpers.ts @@ -232,7 +232,7 @@ export async function createAuthSession( // Explicitly seal/flush the session to ensure cookie is written // This is important in serverless environments where response might stream early const { sealSession } = await import("vinxi/http"); - sealSession(event, configWithMaxAge); + await sealSession(event, configWithMaxAge); console.log("[Session Create] Session sealed");