158 lines
5.7 KiB
JavaScript
158 lines
5.7 KiB
JavaScript
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;
|
|
}
|