/** * NomGap backend client — typed HTTP wrappers for the nomgap-backend (port 4013). * * Auth: Bearer token from the caller's JWT (same JWT_SECRET as platform-service). */ import { config } from './config.js'; export interface NomGapClientOptions { token?: string; requestId?: string; productId?: string; } async function nomgapFetch( path: string, init: RequestInit, opts: NomGapClientOptions ): Promise { const headers: Record = { 'Content-Type': 'application/json', ...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}), ...(opts.requestId ? { 'x-request-id': opts.requestId } : {}), ...(opts.productId ? { 'x-product-id': opts.productId } : { 'x-product-id': 'nomgap' }), }; const res = await fetch(`${config.NOMGAP_BACKEND_URL}/api${path}`, { ...init, headers: { ...((init.headers as Record) ?? {}), ...headers }, signal: AbortSignal.timeout(15_000), }); if (!res.ok) { const body = await res.text().catch(() => ''); throw new Error(`nomgap-backend ${init.method ?? 'GET'} ${path} → ${res.status}: ${body}`); } return res.json() as Promise; } // ── Fasting sessions ─────────────────────────────────────────────────────── export interface FastingSessionDoc { id: string; userId: string; productId: string; protocolId: string; startedAt: number; targetDurationMs: number; status: 'active' | 'completed' | 'broken'; stages: unknown[]; moodCheckins: unknown[]; waterIntake: unknown[]; metrics: { actualDurationMs: number; completionRatio: number; peakAutophagyConfidence: number; totalPausedMs: number; moodCheckinCount: number; averageEnergy: number | null; averageMood: number | null; }; createdAt: string; updatedAt: string; } export function nomgapFastingCreateSession( input: { protocolId: string; variables?: Record }, opts: NomGapClientOptions ): Promise { return nomgapFetch('/fasting/sessions', { method: 'POST', body: JSON.stringify(input) }, opts); } export function nomgapFastingSessionsList( params: { limit?: number; offset?: number; status?: string; from?: string; to?: string }, opts: NomGapClientOptions ): Promise<{ items: FastingSessionDoc[]; total: number }> { const qs = new URLSearchParams(); if (params.limit !== undefined) qs.set('limit', String(params.limit)); if (params.offset !== undefined) qs.set('offset', String(params.offset)); if (params.status) qs.set('status', params.status); if (params.from) qs.set('from', params.from); if (params.to) qs.set('to', params.to); const q = qs.toString(); return nomgapFetch(`/fasting/sessions${q ? `?${q}` : ''}`, { method: 'GET' }, opts); } export function nomgapFastingSessionGet( sessionId: string, opts: NomGapClientOptions ): Promise { return nomgapFetch(`/fasting/sessions/${sessionId}`, { method: 'GET' }, opts); } export function nomgapFastingGetStats(opts: NomGapClientOptions): Promise> { return nomgapFetch('/fasting/stats', { method: 'GET' }, opts); } export function nomgapFastingGetWeeklyStats( opts: NomGapClientOptions ): Promise> { return nomgapFetch('/fasting/stats/weekly', { method: 'GET' }, opts); } // ── Protocols ───────────────────────────────────────────────────────────── export function nomgapProtocolsList( opts: NomGapClientOptions ): Promise<{ protocols: unknown[]; total: number }> { return nomgapFetch('/fasting/protocols', { method: 'GET' }, opts); } export function nomgapProtocolGet( protocolId: string, opts: NomGapClientOptions ): Promise> { return nomgapFetch(`/fasting/protocols/${protocolId}`, { method: 'GET' }, opts); } // ── Body stages (public — no auth required) ──────────────────────────────── export function nomgapBodyStagesList( opts: Pick ): Promise<{ stages: unknown[]; total: number }> { return nomgapFetch('/fasting/stages', { method: 'GET' }, opts); } // ── Autophagy confidence ────────────────────────────────────────────────── export interface AutophagyConfidenceInput { durationHours: number; activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active'; lastMealCarbs?: number; sleepHours?: number; completionHistory?: { totalFasts: number; completionRate: number }; hrvData?: { restingHR?: number; hrv?: number }; } export interface AutophagyConfidenceResult { confidence: number; label: 'unlikely' | 'possible' | 'likely' | 'very_likely' | 'near_certain'; breakdown: { duration: number; meal: number; activity: number; sleep: number; history: number; hrv: number; }; currentStage: string; } export function nomgapAutophagyConfidence( input: AutophagyConfidenceInput, opts: Pick ): Promise { return nomgapFetch( '/fasting/autophagy-confidence', { method: 'POST', body: JSON.stringify(input) }, opts ); } // ── Push triggers ────────────────────────────────────────────────────────── export type PushTriggerType = | 'streak_risk' | 'fast_milestone' | 'stage_transition' | 'social_invite' | 'weekly_digest' | 'achievement_unlocked' | 'refeeding_reminder' | 'electrolyte_reminder' | 'extended_fast_warning'; export interface PushTriggerDoc { id: string; productId: string; type: PushTriggerType; userId: string; variables: Record; status: 'pending' | 'sent' | 'skipped' | 'failed'; scheduledAt: string; createdAt: string; } // ── Social fasting ──────────────────────────────────────────────────────── export interface GroupFastDoc { id: string; userId: string; productId: string; sessionId: string; memberCount: number; status: 'active' | 'completed' | 'cancelled'; createdAt: string; } export function nomgapSocialListGroupFasts( params: { limit?: number; offset?: number }, opts: NomGapClientOptions ): Promise<{ items: GroupFastDoc[]; total: number }> { const qs = new URLSearchParams(); if (params.limit !== undefined) qs.set('limit', String(params.limit)); if (params.offset !== undefined) qs.set('offset', String(params.offset)); const q = qs.toString(); return nomgapFetch(`/social/group-fasts${q ? `?${q}` : ''}`, { method: 'GET' }, opts); } export function nomgapPushFire( input: { type: PushTriggerType; userId: string; variables?: Record; scheduledFor?: string; }, opts: NomGapClientOptions ): Promise { return nomgapFetch('/push-triggers', { method: 'POST', body: JSON.stringify(input) }, opts); } export function nomgapPushGetStats(opts: NomGapClientOptions): Promise> { return nomgapFetch('/push-triggers/stats', { method: 'GET' }, opts); } export function nomgapPushGetPending(opts: NomGapClientOptions): Promise { return nomgapFetch('/push-triggers/pending', { method: 'GET' }, opts); }