import type { IncomingMessage, ServerResponse } from 'http'; import { getProviderService } from '../../services/providers/provider-service'; import { proxyAwareFetch } from '../../utils/proxy-fetch'; import { normalizeUserSyncBootstrap, toServerUserSyncPartner, toServerUserSyncPreferences, toServerUserSyncProviderAccount, type UserSyncBootstrap, } from '../../../shared/user-sync'; import type { HostApiContext } from '../context'; import { parseJsonBody, sendJson } from '../route-utils'; import { WORKS_SQUARE_CONFIG } from '../works-config'; class UserSyncProxyError extends Error { constructor( public readonly statusCode: number, message: string, ) { super(message); } } function normalizeWorksBase(value = WORKS_SQUARE_CONFIG.apiBaseUrl): string { const apiBase = value.replace(/\/+$/, ''); if (!/^https?:\/\//i.test(apiBase)) { throw new Error('Works Square API base URL must start with http:// or https://'); } return apiBase; } function createWorksUrl(pathname: string): URL { return new URL(`${normalizeWorksBase()}${pathname}`); } function readRequiredAccessToken(req: IncomingMessage): string { const header = req.headers['x-niancode-access-token']; const token = Array.isArray(header) ? header[0] : header; if (!token || !token.trim()) { throw new UserSyncProxyError(400, 'Missing x-niancode-access-token'); } return token.trim(); } async function readResponsePayload(response: Response): Promise { const text = await response.text(); if (!text.trim()) return null; try { return JSON.parse(text) as unknown; } catch { return text; } } function getErrorMessage(payload: unknown, fallback: string): string { if (payload && typeof payload === 'object') { const record = payload as Record; for (const field of ['msg', 'message', 'error_description', 'error', 'detail']) { const value = record[field]; if (typeof value === 'string' && value.trim()) { return value; } } } if (typeof payload === 'string' && payload.trim()) { return payload; } return fallback; } async function fetchUserSync( pathname: string, accessToken: string, init: RequestInit, ): Promise { const response = await proxyAwareFetch(createWorksUrl(pathname).toString(), { ...init, headers: { Authorization: `Bearer ${accessToken}`, ...(init.headers ?? {}), }, }); const payload = await readResponsePayload(response); if (!response.ok) { throw new UserSyncProxyError( response.status >= 400 && response.status < 500 ? response.status : 502, getErrorMessage(payload, `Works Square user sync failed (${response.status})`), ); } return payload; } async function normalizeAndApplyProviderMetadata(payload: unknown): Promise<{ bootstrap: UserSyncBootstrap; applied: { providerAccounts: number }; }> { const bootstrap = normalizeUserSyncBootstrap(payload); const providerService = getProviderService(); let appliedProviderAccounts = 0; for (const account of bootstrap.providerAccounts) { await providerService.upsertSyncedAccountMetadata(account); appliedProviderAccounts += 1; } return { bootstrap, applied: { providerAccounts: appliedProviderAccounts }, }; } export async function handleUserSyncRoutes( req: IncomingMessage, res: ServerResponse, url: URL, _ctx: HostApiContext, ): Promise { if (!url.pathname.startsWith('/api/user-sync')) { return false; } try { const accessToken = readRequiredAccessToken(req); if (url.pathname === '/api/user-sync/bootstrap' && req.method === 'GET') { const result = await normalizeAndApplyProviderMetadata( await fetchUserSync('/api/me/bootstrap', accessToken, { method: 'GET' }), ); sendJson(res, 200, { success: true, ...result }); return true; } if (url.pathname === '/api/user-sync/preferences' && req.method === 'PUT') { const body = await parseJsonBody(req); const result = await normalizeAndApplyProviderMetadata( await fetchUserSync('/api/me/preferences', accessToken, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(toServerUserSyncPreferences(body)), }), ); sendJson(res, 200, { success: true, ...result }); return true; } if (url.pathname === '/api/user-sync/provider-accounts' && req.method === 'POST') { const body = await parseJsonBody(req); const result = await normalizeAndApplyProviderMetadata( await fetchUserSync('/api/me/provider-accounts', accessToken, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(toServerUserSyncProviderAccount(body)), }), ); sendJson(res, 200, { success: true, ...result }); return true; } if (url.pathname === '/api/user-sync/partners' && req.method === 'POST') { const body = await parseJsonBody(req); const result = await normalizeAndApplyProviderMetadata( await fetchUserSync('/api/me/partners', accessToken, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(toServerUserSyncPartner(body)), }), ); sendJson(res, 200, { success: true, ...result }); return true; } if (url.pathname.startsWith('/api/user-sync/partners/') && req.method === 'DELETE') { const partnerId = decodeURIComponent(url.pathname.slice('/api/user-sync/partners/'.length)); const result = await normalizeAndApplyProviderMetadata( await fetchUserSync(`/api/me/partners/${encodeURIComponent(partnerId)}`, accessToken, { method: 'DELETE', }), ); sendJson(res, 200, { success: true, ...result }); return true; } sendJson(res, 404, { success: false, error: `No route for ${req.method} ${url.pathname}` }); return true; } catch (error) { sendJson(res, error instanceof UserSyncProxyError ? error.statusCode : 500, { success: false, error: error instanceof Error ? error.message : String(error), }); return true; } }