Files
memind/scripts/local-test-proxy.mjs
Your Name b0f5d6a51c Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:39:39 -07:00

169 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Unified HTTPS reverse proxy for local test domains.
*
* test.tkmind.cn -> Vite :5173 (H5 + HMR)
* testadm.tkmind.cn -> admin :8082
* testpla.tkmind.cn -> Plaza :3001
* testops.tkmind.cn -> Ops :3002
*
* Requires root on macOS for :443:
* sudo pnpm dev:local-proxy
*/
import http from 'node:http';
import https from 'node:https';
import fs from 'node:fs';
import {
ADMIN_HOST,
H5_HOST,
OPS_HOST,
PLAZA_HOST,
PUBLIC_PORT,
PUBLIC_SCHEME,
publicUrl,
} from './local-test-config.mjs';
import { ensureLocalTestTls } from './local-test-tls.mjs';
const listenHost = process.env.LOCAL_TEST_PROXY_BIND ?? '0.0.0.0';
const targetHost = process.env.LOCAL_TEST_PROXY_TARGET ?? '127.0.0.1';
const vitePort = Number(process.env.VITE_PORT ?? 5173);
const adminPort = Number(process.env.ADMIN_PORT ?? 8082);
const plazaPort = Number(process.env.PLAZA_PORT ?? 3001);
const opsPort = Number(process.env.OPS_PORT ?? 3002);
const useHttps = PUBLIC_SCHEME === 'https';
const ROUTES = {
[H5_HOST.toLowerCase()]: { port: vitePort },
[ADMIN_HOST.toLowerCase()]: { port: adminPort },
[PLAZA_HOST.toLowerCase()]: { port: plazaPort, rewriteHost: true },
[OPS_HOST.toLowerCase()]: { port: opsPort, rewriteHost: true },
};
function routeFor(req) {
const hostname = String(req.headers.host ?? '').split(':')[0].toLowerCase();
return ROUTES[hostname] ? { hostname, ...ROUTES[hostname] } : null;
}
function proxyRequest(clientReq, clientRes) {
const route = routeFor(clientReq);
if (!route) {
clientRes.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
clientRes.end(`Unknown host: ${clientReq.headers.host ?? '(missing)'}`);
return;
}
const headers = { ...clientReq.headers };
if (route.rewriteHost) {
headers.host = `${route.hostname}:${route.port}`;
}
const proxyReq = http.request(
{
hostname: targetHost,
port: route.port,
method: clientReq.method,
path: clientReq.url,
headers,
},
(proxyRes) => {
clientRes.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
proxyRes.pipe(clientRes);
},
);
proxyReq.on('error', (err) => {
if (!clientRes.headersSent) {
clientRes.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' });
}
clientRes.end(
`Upstream unavailable (${route.hostname} -> ${targetHost}:${route.port}): ${err.message}`,
);
});
clientReq.pipe(proxyReq);
}
function proxyUpgrade(clientReq, clientSocket, head) {
const route = routeFor(clientReq);
if (!route) {
clientSocket.destroy();
return;
}
const headers = { ...clientReq.headers };
if (route.rewriteHost) {
headers.host = `${route.hostname}:${route.port}`;
}
const proxyReq = http.request({
hostname: targetHost,
port: route.port,
method: clientReq.method,
path: clientReq.url,
headers,
});
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
clientSocket.write(
`HTTP/1.1 ${proxyRes.statusCode ?? 101} ${proxyRes.statusMessage ?? 'Switching Protocols'}\r\n` +
Object.entries(proxyRes.headers)
.filter(([, value]) => value != null)
.map(([key, value]) => `${key}: ${value}`)
.join('\r\n') +
'\r\n\r\n',
);
if (proxyHead.length) proxySocket.write(proxyHead);
proxySocket.pipe(clientSocket);
clientSocket.pipe(proxySocket);
});
proxyReq.on('error', () => clientSocket.destroy());
proxyReq.end(head);
}
function bindErrorHandler(server) {
server.on('error', (err) => {
if (err.code === 'EACCES') {
console.error(`无法监听 ${PUBLIC_PORT} 端口。若使用 443 需 sudo;默认 8443 无需 root。`);
process.exit(1);
}
if (err.code === 'EADDRINUSE') {
console.error(`端口 ${PUBLIC_PORT} 已被占用。`);
process.exit(1);
}
throw err;
});
}
let server;
if (useHttps) {
const { keyPath, certPath } = ensureLocalTestTls();
server = https.createServer(
{
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
},
proxyRequest,
);
} else {
server = http.createServer(proxyRequest);
}
server.on('upgrade', proxyUpgrade);
bindErrorHandler(server);
server.listen(PUBLIC_PORT, listenHost, () => {
const scheme = useHttps ? 'https' : 'http';
console.log(`Local test proxy (${scheme}:${PUBLIC_PORT})`);
console.log(` ${publicUrl(H5_HOST)} -> :${vitePort} (H5)`);
console.log(` ${publicUrl(ADMIN_HOST)} -> :${adminPort} (memind_adm)`);
console.log(` ${publicUrl(PLAZA_HOST)}/plaza -> :${plazaPort} (Plaza)`);
console.log(` ${publicUrl(OPS_HOST)}/ops/ -> :${opsPort} (Ops)`);
});
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
server.close(() => process.exit(0));
});
}