// YOUreka Auth cookie session → PocketBase client. Ported from // one-school-chat (src/lib/server/yrka-auth.ts): the portal returns a // PocketBase session cookie from `GET /api/web/auth/{sessionId}/cookie`; we // store its encoded value under our own `session` cookie and reload it into a // PocketBase SDK client (pb.itcass.net), refreshing when invalid or near // expiry. `createPb` is injectable for tests; production always uses the // hardcoded pbUrl default. import PocketBase from 'pocketbase'; export const PB_URL = 'https://pb.itcass.net'; /** Refresh proactively when the token expires within this window. */ const REFRESH_BUFFER_MS = 5 * 60 * 1000; /** The auth cookie name used by the YRKA-auth PocketBase instance. */ const PB_COOKIE_KEY = 'pb_auth'; /** Extract the bare `pb_auth` value from a Set-Cookie-style string. */ export function extractPbCookieValue(raw) { const match = new RegExp(`${PB_COOKIE_KEY}=([^;]+)`).exec(raw); if (match) return match[1]; // Already a bare value (no attributes / no name prefix). return raw.split(';')[0]; } /** Decode a JWT's `exp` (seconds) → ms, or 0 if unreadable. */ export function tokenExpiresAtMs(token) { if (!token) return 0; try { const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')); return typeof payload.exp === 'number' ? payload.exp * 1000 : 0; } catch { return 0; } } function rebuildCookieHeader(encodedValue) { return `${PB_COOKIE_KEY}=${encodedValue}`; } function recordCollectionName(pb) { return pb.authStore.record?.collectionName ?? 'users'; } function exportCookieValue(pb) { return extractPbCookieValue(pb.authStore.exportToCookie({}, PB_COOKIE_KEY)); } async function tryRefresh(pb) { try { await pb.collection(recordCollectionName(pb)).authRefresh(); return true; } catch { return false; } } /** * Load a YRKA-auth PocketBase session from the persisted cookie value, * refreshing the token when it is expired or about to expire. Returns * { pb, refreshed, cookie? } or null when the session cannot be recovered * (caller treats the operator as anonymous). */ export async function loadYrkaSession( encodedCookieValue, { pbUrl = PB_URL, createPb = (url) => new PocketBase(url) } = {} ) { if (!encodedCookieValue) return null; // Normalize: tolerate stale values that still carry a `pb_auth=` prefix. const normalized = extractPbCookieValue(encodedCookieValue); const pb = createPb(pbUrl); pb.autoCancellation(false); try { pb.authStore.loadFromCookie(rebuildCookieHeader(normalized)); } catch { return null; } if (!pb.authStore.token) return null; // Invalid / expired → attempt one refresh (the stored token may redeem). if (!pb.authStore.isValid) { if (await tryRefresh(pb)) { return { pb, refreshed: true, cookie: exportCookieValue(pb) }; } return null; } // Valid but nearing expiry → refresh proactively and persist the new token. const expiresAt = tokenExpiresAtMs(pb.authStore.token); if (expiresAt > 0 && Date.now() + REFRESH_BUFFER_MS >= expiresAt) { if (await tryRefresh(pb)) { return { pb, refreshed: true, cookie: exportCookieValue(pb) }; } } return { pb, refreshed: false }; }