feat: add provider websocket http data plane

This commit is contained in:
Codex
2026-05-16 16:03:53 +00:00
parent 659d1c6148
commit 28cc2af121
19 changed files with 545 additions and 22 deletions
@@ -1,6 +1,7 @@
import type { JsonValue } from "../../shared/src/index";
import type {
EgressTcpConnection,
HttpTunnelWaiter,
LoggerFn,
MicroserviceAvailabilityEntry,
MicroserviceProxyCacheEntry,
@@ -22,6 +23,7 @@ export const ctx = {
activeProviders: new Map<string, ProviderSocket>(),
activeSshClients: new Map<string, ProviderSocket>(),
activeEgressTcpConnections: new Map<string, EgressTcpConnection>(),
httpTunnelWaiters: new Map<string, HttpTunnelWaiter>(),
taskTerminalWaiters: new Map<string, Set<TaskTerminalWaiter>>(),
microserviceProxyCache: new Map<string, MicroserviceProxyCacheEntry>(),
microserviceProxyRefreshes: new Map<string, Promise<void>>(),
+6
View File
@@ -172,6 +172,12 @@ const providerServer = Bun.serve<WsData>({
logger("warn", "provider_socket_close", { providerId: providerId ?? null });
if (providerId !== undefined) {
closeEgressTcpConnectionsForProvider(providerId);
for (const [requestId, waiter] of ctx.httpTunnelWaiters) {
if (requestId.startsWith(`${providerId}:`)) {
ctx.httpTunnelWaiters.delete(requestId);
waiter(null);
}
}
if (ctx.activeProviders.get(providerId) !== ws) {
logger("info", "provider_socket_close_ignored_replaced", { providerId });
return;
@@ -202,11 +202,15 @@ function responseFromMicroserviceResult(task: RawTaskRow | null): Response {
if (task === null) return jsonResponse({ ok: false, error: "microservice proxy task missing" }, 502);
if (task.status !== "succeeded") return jsonResponse({ ok: false, error: "microservice proxy task failed", task }, 502);
const result = dockerStatusRecord(task.result);
return responseFromProviderMicroserviceResult(result, "provider-task");
}
function responseFromProviderMicroserviceResult(result: Record<string, unknown>, proxyMode: string): Response {
const status = Number(result.status);
const contentType = typeof result.contentType === "string" ? result.contentType : "application/json; charset=utf-8";
const bodyText = typeof result.bodyText === "string" ? result.bodyText : "";
if (!Number.isInteger(status) || status < 100 || status > 599) {
return jsonResponse({ ok: false, error: "microservice proxy returned invalid upstream status", task }, 502);
return jsonResponse({ ok: false, error: "microservice proxy returned invalid upstream status", result }, 502);
}
if (result.truncated === true && contentTypeIsJson(contentType)) {
try {
@@ -215,8 +219,6 @@ function responseFromMicroserviceResult(task: RawTaskRow | null): Response {
return jsonResponse({
ok: false,
error: "microservice proxy response was truncated before a JSON boundary",
providerId: task.provider_id,
command: task.command,
upstreamStatus: status,
upstreamBodyBytes: result.upstreamBodyBytes ?? null,
returnedBodyBytes: result.returnedBodyBytes ?? bodyText.length,
@@ -229,6 +231,8 @@ function responseFromMicroserviceResult(task: RawTaskRow | null): Response {
status,
headers: {
"content-type": contentType,
"x-unidesk-proxy-mode": proxyMode,
"x-unidesk-upstream-proxy-mode": typeof result.proxyMode === "string" ? result.proxyMode : "",
"x-unidesk-response-truncated": result.truncated === true ? "true" : "false",
},
});
@@ -607,6 +611,84 @@ async function k3sctlAdapterMicroserviceResponse(
return fetchMicroserviceUpstreamResponse(adapter, method, adapterTargetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
}
function providerHttpTunnelRequestId(providerId: string): string {
return `${providerId}:http_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}
async function waitForProviderHttpTunnelResponse(
providerId: string,
requestId: string,
timeoutMs: number,
abortSignal?: AbortSignal,
): Promise<{ providerId: string; requestId: string; ok: boolean; result: JsonValue } | null> {
return await new Promise((resolve) => {
let settled = false;
let abortHandler: (() => void) | null = null;
const timer = setTimeout(() => settle(null), Math.max(1, timeoutMs));
const settle = (message: { providerId: string; requestId: string; ok: boolean; result: JsonValue } | null): void => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (abortHandler !== null) abortSignal?.removeEventListener("abort", abortHandler);
ctx.httpTunnelWaiters.delete(requestId);
resolve(message);
};
abortHandler = () => settle(null);
if (abortSignal !== undefined) {
if (abortSignal.aborted) {
settle(null);
return;
}
abortSignal.addEventListener("abort", abortHandler, { once: true });
}
ctx.httpTunnelWaiters.set(requestId, (message) => {
if (message !== null && message.providerId !== providerId) {
logger("warn", "http_tunnel_provider_mismatch", { requestId, expectedProviderId: providerId, actualProviderId: message.providerId });
settle(null);
return;
}
settle(message);
});
});
}
async function providerHttpTunnelMicroserviceResponse(
service: MicroserviceConfig,
method: string,
targetPath: string,
proxyOptions: { query: string; jsonArrayLimits: Record<string, JsonValue> },
requestHeaders: Record<string, JsonValue>,
bodyText: string,
abortSignal?: AbortSignal,
): Promise<Response> {
const socket = ctx.activeProviders.get(service.providerId);
if (socket === undefined) return jsonResponse({ ok: false, error: `provider is offline: ${service.providerId}` }, 503);
const requestId = providerHttpTunnelRequestId(service.providerId);
const timeoutMs = service.backend.timeoutMs + 3000;
const waiter = waitForProviderHttpTunnelResponse(service.providerId, requestId, timeoutMs, abortSignal);
socket.send(JSON.stringify({
type: "http_tunnel_request",
requestId,
payload: {
source: "microservice-frontend-proxy",
serviceId: service.id,
method,
targetBaseUrl: service.backend.nodeBaseUrl,
path: targetPath,
query: proxyOptions.query,
requestHeaders,
bodyText,
jsonArrayLimits: proxyOptions.jsonArrayLimits,
timeoutMs: service.backend.timeoutMs,
cacheTtlMs: providerMicroserviceCacheTtlMs(service.id, targetPath),
},
}));
const message = await waiter;
if (message === null) return jsonResponse({ ok: false, error: "provider HTTP tunnel timed out or disconnected", providerId: service.providerId, requestId }, 504);
if (!message.ok) return jsonResponse({ ok: false, error: "provider HTTP tunnel failed", providerId: service.providerId, requestId, result: message.result }, 502);
return responseFromProviderMicroserviceResult(dockerStatusRecord(message.result), "provider-ws-http-tunnel");
}
async function fetchMicroserviceUpstreamResponse(
service: MicroserviceConfig,
method: string,
@@ -625,6 +707,9 @@ async function fetchMicroserviceUpstreamResponse(
if (!(await providerSupports(service.providerId, "microservice.http"))) {
return jsonResponse({ ok: false, error: `provider does not declare microservice.http capability: ${service.providerId}` }, 409);
}
if (await providerSupports(service.providerId, "microservice.http.tunnel")) {
return providerHttpTunnelMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal);
}
const { taskId, providerOnline } = await createAndSendTask(service.providerId, "microservice.http", {
source: "microservice-frontend-proxy",
serviceId: service.id,
@@ -81,6 +81,22 @@ export async function handleProviderMessage(ws: ProviderSocket, raw: string | Bu
return;
}
if (message.type === "http_tunnel_response") {
const waiter = ctx.httpTunnelWaiters.get(message.requestId);
if (waiter === undefined) {
logger("warn", "http_tunnel_response_without_waiter", { providerId: message.providerId, requestId: message.requestId });
return;
}
ctx.httpTunnelWaiters.delete(message.requestId);
waiter({
providerId: message.providerId,
requestId: message.requestId,
ok: message.ok,
result: message.result,
});
return;
}
if (message.type === "egress_tcp_open") {
handleEgressTcpOpen(ws, message);
return;
+7
View File
@@ -188,4 +188,11 @@ export interface MicroserviceAvailabilityEntry {
probe: Record<string, JsonValue>;
}
export type HttpTunnelWaiter = (message: {
providerId: string;
requestId: string;
ok: boolean;
result: JsonValue;
} | null) => void;
export type LoggerFn = (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void;
@@ -46,7 +46,11 @@ RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 &&
WORKDIR /app/src/components/microservices/code-queue
COPY src/components/microservices/code-queue/package.json ./package.json
RUN test -d node_modules/typescript || bun install
WORKDIR /app
COPY package.json /app/package.json
RUN bun install
COPY src/components/shared /app/src/components/shared
WORKDIR /app/src/components/microservices/code-queue
COPY src/components/microservices/code-queue/tsconfig.json ./tsconfig.json
COPY src/components/microservices/code-queue/src ./src
@@ -31,6 +31,11 @@ services:
K3SCTL_KUBE_API_LOCAL_PORT: "${K3SCTL_KUBE_API_LOCAL_PORT:-6443}"
K3SCTL_KUBE_API_REMOTE_HOST: "${K3SCTL_KUBE_API_REMOTE_HOST:-127.0.0.1}"
K3SCTL_KUBE_API_REMOTE_PORT: "${K3SCTL_KUBE_API_REMOTE_PORT:-6443}"
K3SCTL_NATIVE_SERVICE_PROXY_ENABLED: "${K3SCTL_NATIVE_SERVICE_PROXY_ENABLED:-true}"
K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS: "${K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS:-1200}"
K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS: "${K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS:-10000}"
K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE:-}"
K3SCTL_NATIVE_SERVICE_URL_MDTODO: "${K3SCTL_NATIVE_SERVICE_URL_MDTODO:-}"
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json}"
K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}"
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}"
@@ -44,6 +44,9 @@ interface RuntimeConfig {
kubeApiProxyEnabled: boolean;
kubeconfigPath: string;
kubeApiConnectHost: string;
nativeServiceProxyEnabled: boolean;
nativeServiceProbeTimeoutMs: number;
nativeServiceFailureCooldownMs: number;
requestTimeoutMs: number;
healthTimeoutMs: number;
services: ManagedService[];
@@ -69,6 +72,7 @@ const logWriter = config.logFile
})
: null;
const kubeClient = loadKubeApiClient();
const nativeServiceFailures = new Map<string, number>();
logWriter?.prune();
function envString(name: string, fallback: string): string {
@@ -92,6 +96,11 @@ function envBool(name: string, fallback: boolean): boolean {
return fallback;
}
function envOptionalString(name: string): string | null {
const value = process.env[name];
return value === undefined || value.trim().length === 0 ? null : value.trim();
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
@@ -259,6 +268,9 @@ function readConfig(): RuntimeConfig {
kubeApiProxyEnabled: envBool("K3SCTL_KUBE_API_PROXY_ENABLED", true),
kubeconfigPath: envString("K3SCTL_KUBECONFIG_PATH", "/var/lib/unidesk/k3s/kubeconfig"),
kubeApiConnectHost: envString("K3SCTL_KUBE_API_CONNECT_HOST", "host.docker.internal"),
nativeServiceProxyEnabled: envBool("K3SCTL_NATIVE_SERVICE_PROXY_ENABLED", true),
nativeServiceProbeTimeoutMs: Math.max(250, Math.min(5000, envNumber("K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS", 1200))),
nativeServiceFailureCooldownMs: Math.max(1000, Math.min(60_000, envNumber("K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS", 10_000))),
requestTimeoutMs: Math.max(1000, Math.min(120_000, envNumber("K3SCTL_REQUEST_TIMEOUT_MS", 30_000))),
healthTimeoutMs: Math.max(500, Math.min(30_000, envNumber("K3SCTL_HEALTH_TIMEOUT_MS", 2500))),
services: mergeServices([...manifestServices, ...inlineServices]),
@@ -399,6 +411,37 @@ function endpointProxyApiPath(service: ManagedService, endpoint: ManagedEndpoint
return `/api/v1/namespaces/${encodeURIComponent(namespace)}/services/${encodeURIComponent(serviceRef)}/proxy${safeTargetPath}`;
}
function nativeServiceFailureKey(service: ManagedService): string {
return `${service.namespace}/${service.id}`;
}
function nativeServiceUrl(service: ManagedService, targetPath: string, query = ""): URL {
const serviceName = routeString(service, "serviceName", service.id);
const servicePort = routeNumber(service, "servicePort", 80);
const safeTargetPath = targetPath.startsWith("/") ? targetPath : `/${targetPath}`;
const override = envOptionalString(`K3SCTL_NATIVE_SERVICE_URL_${service.id.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`);
const base = override ?? `http://${serviceName}.${service.namespace}.svc.cluster.local:${servicePort}`;
const url = new URL(safeTargetPath, base.replace(/\/+$/u, "/"));
url.search = query;
return url;
}
function nativeServiceProxyUsable(service: ManagedService): boolean {
if (!config.nativeServiceProxyEnabled) return false;
const failedAt = nativeServiceFailures.get(nativeServiceFailureKey(service));
return failedAt === undefined || Date.now() - failedAt >= config.nativeServiceFailureCooldownMs;
}
function rememberNativeServiceFailure(service: ManagedService, error: unknown): void {
nativeServiceFailures.set(nativeServiceFailureKey(service), Date.now());
log("warn", "native_service_proxy_failed", {
serviceId: service.id,
namespace: service.namespace,
cooldownMs: config.nativeServiceFailureCooldownMs,
error: errorToJson(error),
});
}
function kubernetesEndpointServiceRef(service: ManagedService, endpoint: ManagedEndpoint): { namespace: string; serviceRef: string } {
const base = new URL(endpoint.baseUrl);
if (base.protocol !== "kubernetes:") throw new Error(`endpoint ${endpoint.id} must use kubernetes:// baseUrl`);
@@ -460,6 +503,48 @@ async function kubeApiServiceProxyResponse(
return kubeApiProxyResponse(service, req, serviceProxyApiPath(service, targetPath), query, timeoutMs);
}
async function nativeServiceProxyResponse(
service: ManagedService,
req: Request,
targetPath: string,
query: string,
timeoutMs: number,
): Promise<Response | null> {
if (!nativeServiceProxyUsable(service)) return null;
const upstreamUrl = nativeServiceUrl(service, targetPath, query);
const headers = forwardHeaders(req);
const bodyText = req.method === "GET" || req.method === "HEAD" ? "" : await req.text();
const controller = new AbortController();
const nativeTimeoutMs = Math.min(timeoutMs, config.nativeServiceProbeTimeoutMs);
const timer = setTimeout(() => controller.abort(), nativeTimeoutMs);
const startedAt = Date.now();
try {
const upstream = await fetch(upstreamUrl, {
method: req.method,
headers,
body: bodyText.length > 0 ? bodyText : undefined,
signal: controller.signal,
});
const body = await boundedText(upstream, 8 * 1024 * 1024);
return new Response(body.text, {
status: upstream.status,
headers: {
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
"x-unidesk-proxy-mode": "kubernetes-native-service",
"x-unidesk-k3s-service": service.id,
"x-unidesk-k3s-service-url": upstreamUrl.origin,
"x-unidesk-upstream-duration-ms": String(Date.now() - startedAt),
"x-unidesk-response-truncated": body.truncated ? "true" : "false",
},
});
} catch (error) {
rememberNativeServiceFailure(service, error);
return null;
} finally {
clearTimeout(timer);
}
}
async function kubeApiEndpointProxyResponse(
service: ManagedService,
endpoint: ManagedEndpoint,
@@ -574,15 +659,13 @@ async function probeKubernetesServiceActive(service: ManagedService): Promise<Js
async function probeKubernetesEndpoint(service: ManagedService, endpoint: ManagedEndpoint, active = false): Promise<JsonRecord> {
if (!active && endpoint.healthMode === "pod-ready") return await probeKubernetesPodReady(service, endpoint);
const checkedAt = new Date().toISOString();
const response = active
? await kubeApiServiceProxyResponse(
service,
new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
endpoint.healthPath,
"",
config.healthTimeoutMs,
)
: await kubeApiEndpointProxyResponse(
let response: Response;
if (active) {
const request = new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } });
response = await nativeServiceProxyResponse(service, request.clone(), endpoint.healthPath, "", config.healthTimeoutMs)
?? await kubeApiServiceProxyResponse(service, request, endpoint.healthPath, "", config.healthTimeoutMs);
} else {
response = await kubeApiEndpointProxyResponse(
service,
endpoint,
new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
@@ -590,6 +673,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage
"",
config.healthTimeoutMs,
);
}
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
const bodyText = await response.text();
let body: JsonValue = bodyText.slice(0, 2000);
@@ -605,7 +689,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage
baseUrl: endpoint.baseUrl,
healthPath: endpoint.healthPath,
healthMode: endpoint.healthMode,
proxyMode: "kubernetes-api-service-proxy",
proxyMode: response.headers.get("x-unidesk-proxy-mode") ?? "kubernetes-api-service-proxy",
route: service.route,
healthy: response.ok,
status: response.ok ? "healthy" : "unhealthy",
@@ -761,7 +845,19 @@ async function controlPlaneSnapshot(): Promise<JsonRecord> {
manifestPaths: config.manifestPaths,
managedServicesHealthy,
noFallback: true,
runtimePath: "frontend -> backend-core -> k3sctl-adapter -> kubernetes api service proxy -> k3s service",
runtimePath: config.nativeServiceProxyEnabled
? "frontend -> backend-core -> provider websocket HTTP tunnel -> k3sctl-adapter -> kubernetes native service/DNS -> k3s service"
: "frontend -> backend-core -> k3sctl-adapter -> kubernetes api service proxy -> k3s service",
nativeServiceProxy: {
enabled: config.nativeServiceProxyEnabled,
mode: "kubernetes-native-service",
failureCooldownMs: config.nativeServiceFailureCooldownMs,
failedServices: Array.from(nativeServiceFailures.entries()).map(([key, failedAt]) => ({
key,
failedAt: new Date(failedAt).toISOString(),
retryAfterMs: Math.max(0, config.nativeServiceFailureCooldownMs - (Date.now() - failedAt)),
})),
},
kubeApiProxy: {
enabled: config.kubeApiProxyEnabled,
configured: kubeClient !== null,
@@ -786,6 +882,8 @@ function forwardHeaders(request: Request): Headers {
async function proxyToService(service: ManagedService, req: Request, targetPath: string, query: string): Promise<Response> {
if (isKubernetesServiceRoute(service)) {
const native = await nativeServiceProxyResponse(service, req.clone(), targetPath, query, config.requestTimeoutMs);
if (native !== null) return native;
return kubeApiServiceProxyResponse(service, req, targetPath, query, config.requestTimeoutMs);
}
log("error", "k3sctl_route_not_kubernetes_service", { serviceId: service.id, route: service.route, noFallback: true });
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@unidesk/provider-gateway",
"version": "0.2.19",
"version": "0.2.20",
"private": true,
"type": "module",
"scripts": {
+39 -1
View File
@@ -5,6 +5,7 @@ import {
type CoreEgressTcpDataMessage,
type CoreEgressTcpOpenedMessage,
type CoreDispatchMessage,
type CoreHttpTunnelRequestMessage,
type CoreHostSshCloseMessage,
type CoreHostSshEofMessage,
type CoreHostSshInputMessage,
@@ -561,7 +562,7 @@ function sendJsonOk(value: unknown): boolean {
}
function sendRegister(): void {
const capabilities = ["heartbeat", "system.status", "docker.status", "docker.ps", "provider.upgrade", "microservice.http", "microservice.http.cache", "echo"];
const capabilities = ["heartbeat", "system.status", "docker.status", "docker.ps", "provider.upgrade", "microservice.http", "microservice.http.cache", "microservice.http.tunnel", "echo"];
if (isHostSshConfigured()) capabilities.push("host.ssh");
if (config.egressProxyEnabled) capabilities.push("network.egress-proxy");
sendJson({
@@ -1989,6 +1990,7 @@ async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<
truncated: bounded.truncated,
transform: transformed.transform,
upstreamDurationMs: Date.now() - requestStartedAt,
proxyMode: "provider-gateway-http-fetch",
};
})();
if (cacheable) microserviceHttpInFlight.set(cacheKey, requestPromise);
@@ -2053,6 +2055,38 @@ async function handleDispatch(message: CoreDispatchMessage): Promise<void> {
}
}
async function handleHttpTunnelRequest(message: CoreHttpTunnelRequestMessage): Promise<void> {
const startedAt = Date.now();
try {
const result = await runMicroserviceHttp(message.payload);
sendJson({
type: "http_tunnel_response",
providerId: config.providerId,
requestId: message.requestId,
ok: (result as { ok?: unknown }).ok === true,
result,
at: new Date().toISOString(),
});
logger("debug", "http_tunnel_completed", {
requestId: message.requestId,
serviceId: typeof message.payload.serviceId === "string" ? message.payload.serviceId : "",
durationMs: Date.now() - startedAt,
ok: (result as { ok?: unknown }).ok === true,
});
} catch (error) {
const text = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
logger("error", "http_tunnel_failed", { requestId: message.requestId, error: text });
sendJson({
type: "http_tunnel_response",
providerId: config.providerId,
requestId: message.requestId,
ok: false,
result: { ok: false, error: text },
at: new Date().toISOString(),
});
}
}
function handleMessage(raw: MessageEvent<string>): void {
try {
const parsed = JSON.parse(raw.data) as { type?: unknown };
@@ -2060,6 +2094,10 @@ function handleMessage(raw: MessageEvent<string>): void {
handleDispatch(parsed as CoreDispatchMessage).catch((error) => logger("error", "dispatch_handler_failed", { error: String(error) }));
return;
}
if (parsed.type === "http_tunnel_request") {
handleHttpTunnelRequest(parsed as CoreHttpTunnelRequestMessage).catch((error) => logger("error", "http_tunnel_handler_failed", { error: String(error) }));
return;
}
if (parsed.type === "host_ssh_open") {
startHostSshSession(parsed as CoreHostSshOpenMessage);
return;
+20 -2
View File
@@ -244,6 +244,21 @@ export interface ProviderEgressTcpCloseMessage {
at: string;
}
export interface CoreHttpTunnelRequestMessage {
type: "http_tunnel_request";
requestId: string;
payload: Record<string, JsonValue>;
}
export interface ProviderHttpTunnelResponseMessage {
type: "http_tunnel_response";
providerId: string;
requestId: string;
ok: boolean;
result: JsonValue;
at: string;
}
export interface CoreEgressTcpOpenedMessage {
type: "egress_tcp_opened";
connectionId: string;
@@ -274,10 +289,12 @@ export type ProviderToCoreMessage =
| ProviderHostSshErrorMessage
| ProviderEgressTcpOpenMessage
| ProviderEgressTcpDataMessage
| ProviderEgressTcpCloseMessage;
| ProviderEgressTcpCloseMessage
| ProviderHttpTunnelResponseMessage;
export type CoreToProviderMessage =
| CoreDispatchMessage
| CoreHttpTunnelRequestMessage
| CoreHostSshOpenMessage
| CoreHostSshInputMessage
| CoreHostSshResizeMessage
@@ -369,7 +386,8 @@ export function isProviderToCoreMessage(value: unknown): value is ProviderToCore
msg.type === "host_ssh_error" ||
msg.type === "egress_tcp_open" ||
msg.type === "egress_tcp_data" ||
msg.type === "egress_tcp_close"
msg.type === "egress_tcp_close" ||
msg.type === "http_tunnel_response"
) &&
typeof msg.providerId === "string" &&
msg.providerId.length > 0