import { Router } from 'express'; import { AppError } from '../lib/errors.js'; const HOP_BY_HOP_HEADERS = new Set([ 'connection', 'content-length', 'host', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ]); const DIRECT_CORE_ALIASES = [ { methods: new Set(['POST']), pattern: /^\/upload-file$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/create-signed-url$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/invoke-llm$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/rapid-orders\/transcribe$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/rapid-orders\/parse$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/rapid-orders\/process$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/staff\/profile\/photo$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST', 'PUT']), pattern: /^\/staff\/profile\/documents\/([^/]+)\/upload$/, targetPath: (_pathname, match) => `/core/staff/documents/${match[1]}/upload`, }, { methods: new Set(['POST', 'PUT']), pattern: /^\/staff\/profile\/attire\/([^/]+)\/upload$/, targetPath: (_pathname, match) => `/core/staff/attire/${match[1]}/upload`, }, { methods: new Set(['POST']), pattern: /^\/staff\/profile\/certificates$/, targetPath: () => '/core/staff/certificates/upload', }, { methods: new Set(['DELETE']), pattern: /^\/staff\/profile\/certificates\/([^/]+)$/, targetPath: (_pathname, match) => `/core/staff/certificates/${match[1]}`, }, { methods: new Set(['POST']), pattern: /^\/staff\/documents\/([^/]+)\/upload$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/staff\/attire\/([^/]+)\/upload$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/staff\/certificates\/upload$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['DELETE']), pattern: /^\/staff\/certificates\/([^/]+)$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/verifications$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['GET']), pattern: /^\/verifications\/([^/]+)$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/verifications\/([^/]+)\/review$/, targetPath: (pathname) => `/core${pathname}` }, { methods: new Set(['POST']), pattern: /^\/verifications\/([^/]+)\/retry$/, targetPath: (pathname) => `/core${pathname}` }, ]; function resolveTarget(pathname, method) { const upperMethod = method.toUpperCase(); if (pathname.startsWith('/core')) { return { baseUrl: process.env.CORE_API_BASE_URL, upstreamPath: pathname, }; } if (pathname.startsWith('/commands')) { return { baseUrl: process.env.COMMAND_API_BASE_URL, upstreamPath: pathname, }; } if (pathname.startsWith('/query')) { return { baseUrl: process.env.QUERY_API_BASE_URL, upstreamPath: pathname, }; } for (const alias of DIRECT_CORE_ALIASES) { if (!alias.methods.has(upperMethod)) continue; const match = pathname.match(alias.pattern); if (!match) continue; return { baseUrl: process.env.CORE_API_BASE_URL, upstreamPath: alias.targetPath(pathname, match), }; } if ((upperMethod === 'GET' || upperMethod === 'HEAD') && (pathname.startsWith('/client') || pathname.startsWith('/staff'))) { return { baseUrl: process.env.QUERY_API_BASE_URL, upstreamPath: `/query${pathname}`, }; } if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(upperMethod) && (pathname.startsWith('/client') || pathname.startsWith('/staff'))) { return { baseUrl: process.env.COMMAND_API_BASE_URL, upstreamPath: `/commands${pathname}`, }; } return null; } function copyHeaders(source, target) { for (const [key, value] of source.entries()) { if (HOP_BY_HOP_HEADERS.has(key.toLowerCase())) continue; target.setHeader(key, value); } } async function forwardRequest(req, res, next, fetchImpl) { try { const requestUrl = new URL(req.originalUrl, 'http://localhost'); const target = resolveTarget(requestUrl.pathname, req.method); if (!target?.baseUrl) { throw new AppError('NOT_FOUND', `No upstream configured for ${requestUrl.pathname}`, 404); } const url = new URL(`${target.upstreamPath}${requestUrl.search}`, target.baseUrl); const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value == null || HOP_BY_HOP_HEADERS.has(key.toLowerCase())) continue; if (Array.isArray(value)) { for (const item of value) headers.append(key, item); } else { headers.set(key, value); } } headers.set('x-request-id', req.requestId); const upstream = await fetchImpl(url, { method: req.method, headers, body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req, duplex: req.method === 'GET' || req.method === 'HEAD' ? undefined : 'half', }); copyHeaders(upstream.headers, res); res.status(upstream.status); const buffer = Buffer.from(await upstream.arrayBuffer()); return res.send(buffer); } catch (error) { return next(error); } } export function createProxyRouter(options = {}) { const router = Router(); const fetchImpl = options.fetchImpl || fetch; router.use((req, res, next) => forwardRequest(req, res, next, fetchImpl)); return router; }