import { FEED_BASE_URL } from "./env"; import { ensureFreshSession, refreshSession } from "./session-refresh"; // Home-feed proxy logic. Reads one page of the person's home feed from Minnect // with their server-held feed token. Tokens never leave the server and are // never logged. Card-field extraction is a later task; items pass through raw. export const DEFAULT_LIMIT = 20; export const MAX_LIMIT = 50; export type FeedPageResult = | { ok: true; items: unknown[]; nextCursor: string | null } | { ok: false; status: 401 | 502; error: "sign_in" | "feed_unavailable" }; interface FeedOptions { fetchImpl?: typeof fetch; now?: number; } /** Clamp a requested page size to 1..50, defaulting to 20. */ export function clampLimit(raw: string | number | null | undefined): number { const n = typeof raw === "number" ? raw : Number.parseInt(raw ?? "", 10); if (!Number.isFinite(n) || n < 1) return DEFAULT_LIMIT; return Math.min(Math.floor(n), MAX_LIMIT); } function buildUrl(cursor: string | null, limit: number): string { const params = new URLSearchParams({ limit: String(limit) }); if (cursor) params.set("cursor", cursor); return `${FEED_BASE_URL}/api/v1/feeds/home/items?${params.toString()}`; } async function callFeed( token: string, cursor: string | null, limit: number, doFetch: typeof fetch, ): Promise { return doFetch(buildUrl(cursor, limit), { method: "GET", headers: { Accept: "application/json", Authorization: `Bearer ${token}` }, }); } export async function getHomeFeedPage( sessionId: string | undefined, cursor: string | null, limit: number, options: FeedOptions = {}, ): Promise { const signIn = { ok: false, status: 401, error: "sign_in" } as const; const doFetch = options.fetchImpl ?? fetch; let session = await ensureFreshSession(sessionId, options); if (!session || !sessionId) return signIn; let res: Response; try { res = await callFeed(session.feedAccessToken, cursor, limit, doFetch); if (res.status === 401) { // One refresh, then one retry. A second 401 means sign in again. session = await refreshSession(sessionId, options); if (!session) return signIn; res = await callFeed(session.feedAccessToken, cursor, limit, doFetch); if (res.status === 401) return signIn; } } catch { return { ok: false, status: 502, error: "feed_unavailable" }; } if (res.status !== 200) { return { ok: false, status: 502, error: "feed_unavailable" }; } let body: unknown; try { body = await res.json(); } catch { return { ok: false, status: 502, error: "feed_unavailable" }; } const root = body && typeof body === "object" ? (body as Record) : {}; const meta = root.metadata && typeof root.metadata === "object" ? (root.metadata as Record) : {}; return { ok: true, items: Array.isArray(root.data) ? root.data : [], nextCursor: typeof meta.nextCursor === "string" && meta.nextCursor ? meta.nextCursor : null, }; }