refactor: node tools and canvas host url

This commit is contained in:
Peter Steinberger
2025-12-27 01:36:24 +01:00
parent 52ca5c4aa2
commit c54e4d0900
19 changed files with 448 additions and 128 deletions

View File

@@ -60,6 +60,8 @@ import {
NodePairRequestParamsSchema,
type NodePairVerifyParams,
NodePairVerifyParamsSchema,
type NodeRenameParams,
NodeRenameParamsSchema,
PROTOCOL_VERSION,
type PresenceEntry,
PresenceEntrySchema,
@@ -135,6 +137,9 @@ export const validateNodePairRejectParams = ajv.compile<NodePairRejectParams>(
export const validateNodePairVerifyParams = ajv.compile<NodePairVerifyParams>(
NodePairVerifyParamsSchema,
);
export const validateNodeRenameParams = ajv.compile<NodeRenameParams>(
NodeRenameParamsSchema,
);
export const validateNodeListParams =
ajv.compile<NodeListParams>(NodeListParamsSchema);
export const validateNodeDescribeParams = ajv.compile<NodeDescribeParams>(

View File

@@ -254,6 +254,11 @@ export const NodePairVerifyParamsSchema = Type.Object(
{ additionalProperties: false },
);
export const NodeRenameParamsSchema = Type.Object(
{ nodeId: NonEmptyString, displayName: NonEmptyString },
{ additionalProperties: false },
);
export const NodeListParamsSchema = Type.Object(
{},
{ additionalProperties: false },
@@ -652,6 +657,7 @@ export const ProtocolSchemas: Record<string, TSchema> = {
NodePairApproveParams: NodePairApproveParamsSchema,
NodePairRejectParams: NodePairRejectParamsSchema,
NodePairVerifyParams: NodePairVerifyParamsSchema,
NodeRenameParams: NodeRenameParamsSchema,
NodeListParams: NodeListParamsSchema,
NodeDescribeParams: NodeDescribeParamsSchema,
NodeInvokeParams: NodeInvokeParamsSchema,
@@ -707,6 +713,7 @@ export type NodePairListParams = Static<typeof NodePairListParamsSchema>;
export type NodePairApproveParams = Static<typeof NodePairApproveParamsSchema>;
export type NodePairRejectParams = Static<typeof NodePairRejectParamsSchema>;
export type NodePairVerifyParams = Static<typeof NodePairVerifyParamsSchema>;
export type NodeRenameParams = Static<typeof NodeRenameParamsSchema>;
export type NodeListParams = Static<typeof NodeListParamsSchema>;
export type NodeDescribeParams = Static<typeof NodeDescribeParamsSchema>;
export type NodeInvokeParams = Static<typeof NodeInvokeParamsSchema>;

View File

@@ -76,6 +76,7 @@ import { isVerbose } from "../globals.js";
import { onAgentEvent } from "../infra/agent-events.js";
import { startGatewayBonjourAdvertiser } from "../infra/bonjour.js";
import { startNodeBridgeServer } from "../infra/bridge/server.js";
import { resolveCanvasHostUrl } from "../infra/canvas-host-url.js";
import { GatewayLockError } from "../infra/gateway-lock.js";
import {
getLastHeartbeatEvent,
@@ -90,6 +91,7 @@ import { getMachineDisplayName } from "../infra/machine-name.js";
import {
approveNodePairing,
listNodePairing,
renamePairedNode,
rejectNodePairing,
requestNodePairing,
verifyNodeToken,
@@ -379,6 +381,7 @@ import {
validateNodePairRejectParams,
validateNodePairRequestParams,
validateNodePairVerifyParams,
validateNodeRenameParams,
validateProvidersStatusParams,
validateRequestFrame,
validateSendParams,
@@ -485,6 +488,7 @@ const METHODS = [
"node.pair.approve",
"node.pair.reject",
"node.pair.verify",
"node.rename",
"node.list",
"node.describe",
"node.invoke",
@@ -609,39 +613,6 @@ function buildSnapshot(): Snapshot {
const MAX_PAYLOAD_BYTES = 512 * 1024; // cap incoming frame size
const MAX_BUFFERED_BYTES = 1.5 * 1024 * 1024; // per-connection send buffer limit
function deriveCanvasHostUrl(
req: IncomingMessage | undefined,
canvasPort: number | undefined,
hostOverride?: string,
) {
if (!req || !canvasPort) return undefined;
const hostHeader = req.headers.host?.trim();
const forwardedProto =
typeof req.headers["x-forwarded-proto"] === "string"
? req.headers["x-forwarded-proto"]
: Array.isArray(req.headers["x-forwarded-proto"])
? req.headers["x-forwarded-proto"][0]
: undefined;
const scheme = forwardedProto === "https" ? "https" : "http";
let host = (hostOverride ?? "").trim();
if (host === "0.0.0.0" || host === "::") host = "";
if (!host && hostHeader) {
try {
const parsed = new URL(`http://${hostHeader}`);
host = parsed.hostname;
} catch {
host = "";
}
}
if (!host) {
host = req.socket?.localAddress?.trim() ?? "";
}
if (!host) return undefined;
const formattedHost = host.includes(":") ? `[${host}]` : host;
return `${scheme}://${formattedHost}:${canvasPort}`;
}
const MAX_CHAT_HISTORY_MESSAGES_BYTES = 6 * 1024 * 1024; // keep history responses comfortably under client WS limits
const HANDSHAKE_TIMEOUT_MS = 10_000;
const TICK_INTERVAL_MS = 30_000;
@@ -3519,11 +3490,13 @@ export async function startGatewayServer(
bridgeHost && bridgeHost !== "0.0.0.0" && bridgeHost !== "::"
? bridgeHost
: undefined;
const canvasHostUrl = deriveCanvasHostUrl(
upgradeReq,
canvasHostPortForWs,
canvasHostServer ? canvasHostOverride : undefined,
);
const canvasHostUrl = resolveCanvasHostUrl({
canvasPort: canvasHostPortForWs,
hostOverride: canvasHostServer ? canvasHostOverride : undefined,
requestHost: upgradeReq.headers.host,
forwardedProto: upgradeReq.headers["x-forwarded-proto"],
localAddress: upgradeReq.socket?.localAddress,
});
logWs("in", "open", { connId, remoteAddr });
const isWebchatConnect = (params: ConnectParams | null | undefined) =>
params?.client?.mode === "webchat" ||
@@ -5438,6 +5411,59 @@ export async function startGatewayServer(
}
break;
}
case "node.rename": {
const params = (req.params ?? {}) as Record<string, unknown>;
if (!validateNodeRenameParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid node.rename params: ${formatValidationErrors(validateNodeRenameParams.errors)}`,
),
);
break;
}
const { nodeId, displayName } = params as {
nodeId: string;
displayName: string;
};
try {
const trimmed = displayName.trim();
if (!trimmed) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"displayName required",
),
);
break;
}
const updated = await renamePairedNode(nodeId, trimmed);
if (!updated) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "unknown nodeId"),
);
break;
}
respond(
true,
{ nodeId: updated.nodeId, displayName: updated.displayName },
undefined,
);
} catch (err) {
respond(
false,
undefined,
errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)),
);
}
break;
}
case "node.list": {
const params = (req.params ?? {}) as Record<string, unknown>;
if (!validateNodeListParams(params)) {