mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 17:08:10 +00:00
fix(proxy): route pilot-agent HTTP via PilotTunnelBridge loopback (#989)
The remote-node HTTP proxy resolved targets by reading nodes.api_url and
nodes.api_token directly from the database. Both fields are empty for
pilot-agent nodes by design, which produced a misleading 503 ("no API URL
or token configured. Update it in Settings, Nodes.") for any API call
targeting a healthy pilot-agent: stack creation, log retrieval, and every
other resource a pilot-agent should serve.
NodeRegistry.getProxyTarget already encapsulates the correct dispatch.
For proxy mode it returns the persisted api_url and api_token. For
pilot-agent it returns the loopback URL of the active PilotTunnelBridge
with an empty token, since the bridge re-authenticates implicitly via the
pre-verified tunnel socket.
Switch all three lookup sites in remoteNodeProxy to this helper, cache
the resolved target on req.proxyTarget so the http-proxy router and
proxyReq callbacks do not re-resolve, and split the 503 message so
pilot-agent operators see "Pilot tunnel to X is disconnected" instead of
the proxy-mode hint.
This commit is contained in:
@@ -20,17 +20,13 @@ export function createRemoteProxyMiddleware(): RequestHandler {
|
||||
const proxy = createProxyMiddleware<Request, Response>({
|
||||
target: 'http://localhost:0', // placeholder - overridden per-request by router
|
||||
changeOrigin: true,
|
||||
router: (req) => {
|
||||
const node = NodeRegistry.getInstance().getNode(req.nodeId);
|
||||
return node?.api_url?.replace(/\/$/, '');
|
||||
},
|
||||
router: (req) => req.proxyTarget?.apiUrl.replace(/\/$/, ''),
|
||||
// When mounted at app.use('/api/', ...), Express strips the '/api/' prefix from
|
||||
// req.url before the middleware sees it. Re-add it so the remote Sencho instance
|
||||
// receives the full path (e.g. '/stats' becomes '/api/stats').
|
||||
pathRewrite: (path) => '/api' + path,
|
||||
on: {
|
||||
proxyReq: (proxyReq, req) => {
|
||||
const node = NodeRegistry.getInstance().getNode(req.nodeId);
|
||||
// Strip headers that must not reach the remote instance:
|
||||
// - x-node-id: remote Sencho treats all requests as local
|
||||
// - cookie: the browser's sencho_token is signed with THIS instance's JWT secret;
|
||||
@@ -38,8 +34,9 @@ export function createRemoteProxyMiddleware(): RequestHandler {
|
||||
// Authentication is handled exclusively via the Bearer token below.
|
||||
proxyReq.removeHeader('x-node-id');
|
||||
proxyReq.removeHeader('cookie');
|
||||
if (node?.api_token) {
|
||||
proxyReq.setHeader('Authorization', `Bearer ${node.api_token}`);
|
||||
// Pilot-agent targets carry an empty token; see NodeRegistry.getProxyTarget.
|
||||
if (req.proxyTarget?.apiToken) {
|
||||
proxyReq.setHeader('Authorization', `Bearer ${req.proxyTarget.apiToken}`);
|
||||
}
|
||||
// Distributed License Enforcement: assert the main instance's license
|
||||
// tier to the remote node so tier-gated routes honor the main's
|
||||
@@ -107,13 +104,21 @@ export function createRemoteProxyMiddleware(): RequestHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node.api_url || !node.api_token) {
|
||||
res.status(503).json({
|
||||
error: `Remote node "${node.name}" has no API URL or token configured. Update it in Settings → Nodes.`,
|
||||
});
|
||||
const target = NodeRegistry.getInstance().getProxyTarget(req.nodeId);
|
||||
if (!target) {
|
||||
if (node.mode === 'pilot_agent') {
|
||||
res.status(503).json({
|
||||
error: `Pilot tunnel to "${node.name}" is disconnected. Operations will resume when the agent reconnects.`,
|
||||
});
|
||||
} else {
|
||||
res.status(503).json({
|
||||
error: `Remote node "${node.name}" has no API URL or token configured. Update it in Settings → Nodes.`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
req.proxyTarget = target;
|
||||
proxy(req, res, next);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user