@ricsam/r5d-worker 0.0.47 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,3 +37,5 @@ The agent runs commands through the virtual shell command:
37
37
  worker exec macos docker compose up -d
38
38
  worker exec macos bun test
39
39
  ```
40
+
41
+ When the connected `r5d-browser` requests a port forward, the worker opens each relayed connection only to `127.0.0.1` on the requested worker port. Browser-side and worker-side ports may differ. The worker never opens a public listener, and a disconnected worker leaves the browser's long-lived mapping unavailable until the same worker label reconnects.
package/dist/cjs/main.cjs CHANGED
@@ -58,6 +58,7 @@ var import_cli_update = require("./cli-update.cjs");
58
58
  var import_git_identity = require("./git-identity.cjs");
59
59
  var import_heartbeat = require("./heartbeat.cjs");
60
60
  var import_process_tree = require("./process-tree.cjs");
61
+ var import_port_forward_client = require("./port-forward-client.cjs");
61
62
  var import_supervisor = require("./supervisor.cjs");
62
63
  var import_managed_paths = require("./managed-paths.cjs");
63
64
  var import_workspace_sync = require("./workspace-sync.cjs");
@@ -2568,7 +2569,8 @@ async function startWorker(options) {
2568
2569
  r5dctlVersion: (0, import_cli_update.readInstalledCliVersion)("r5dctl"),
2569
2570
  capabilities: {
2570
2571
  updateClis: true,
2571
- canonicalResolverCheckout: true
2572
+ canonicalResolverCheckout: true,
2573
+ browserPortForwarding: true
2572
2574
  },
2573
2575
  projectRoot: projectsRoot,
2574
2576
  artifactRoot,
@@ -2721,6 +2723,17 @@ async function startWorker(options) {
2721
2723
  pendingProcessTerminals.delete(message.runId);
2722
2724
  return;
2723
2725
  }
2726
+ if (message.type === "port_forward_connect") {
2727
+ (0, import_port_forward_client.openWorkerPortForwardRelay)({
2728
+ baseUrl,
2729
+ token,
2730
+ label,
2731
+ forwardId: message.forwardId,
2732
+ relayConnectionId: message.relayConnectionId,
2733
+ workerPort: message.workerPort
2734
+ });
2735
+ return;
2736
+ }
2724
2737
  if (message.type === "update_clis") {
2725
2738
  if (cliUpdateInProgress) {
2726
2739
  sendWorkerMessage(ws, {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var port_forward_client_exports = {};
30
+ __export(port_forward_client_exports, {
31
+ openWorkerPortForwardRelay: () => openWorkerPortForwardRelay
32
+ });
33
+ module.exports = __toCommonJS(port_forward_client_exports);
34
+ var import_node_net = __toESM(require("node:net"), 1);
35
+ var import_ws = __toESM(require("ws"), 1);
36
+ const RELAY_FRAME_BYTES = 64 * 1024;
37
+ const RELAY_PAUSE_BYTES = 1024 * 1024;
38
+ const RELAY_RESUME_BYTES = 256 * 1024;
39
+ function relayUrl(baseUrl, label, forwardId, relayConnectionId) {
40
+ const url = new URL("/worker/port-forward/ws", baseUrl);
41
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
42
+ url.searchParams.set("label", label);
43
+ url.searchParams.set("forward", forwardId);
44
+ url.searchParams.set("connection", relayConnectionId);
45
+ return url.toString();
46
+ }
47
+ function rawDataBuffer(data) {
48
+ if (Buffer.isBuffer(data)) return data;
49
+ if (Array.isArray(data)) return Buffer.concat(data);
50
+ return Buffer.from(data);
51
+ }
52
+ function openWorkerPortForwardRelay(options) {
53
+ const socket = import_node_net.default.createConnection({ host: "127.0.0.1", port: options.workerPort, allowHalfOpen: true });
54
+ socket.pause();
55
+ socket.setNoDelay(true);
56
+ const relay = new import_ws.default(relayUrl(options.baseUrl, options.label, options.forwardId, options.relayConnectionId), {
57
+ headers: { Authorization: `Bearer ${options.token}` },
58
+ perMessageDeflate: false
59
+ });
60
+ let relayReady = false;
61
+ let targetReady = false;
62
+ const pendingChunks = [];
63
+ let pendingBytes = 0;
64
+ let closing = false;
65
+ let resumeTimer;
66
+ const maybeResume = () => {
67
+ if (relayReady && targetReady && !resumeTimer) socket.resume();
68
+ };
69
+ const cleanup = () => {
70
+ if (closing) return;
71
+ closing = true;
72
+ if (resumeTimer) clearInterval(resumeTimer);
73
+ socket.destroy();
74
+ if (relay.readyState === import_ws.default.OPEN || relay.readyState === import_ws.default.CONNECTING) relay.close();
75
+ };
76
+ const sendError = (message) => {
77
+ if (relay.readyState === import_ws.default.OPEN) relay.send(JSON.stringify({ type: "error", error: message }));
78
+ cleanup();
79
+ };
80
+ const waitForRelayDrain = () => {
81
+ if (resumeTimer || relay.bufferedAmount <= RELAY_PAUSE_BYTES) return;
82
+ socket.pause();
83
+ resumeTimer = setInterval(() => {
84
+ if (relay.readyState !== import_ws.default.OPEN) return cleanup();
85
+ if (relay.bufferedAmount > RELAY_RESUME_BYTES) return;
86
+ clearInterval(resumeTimer);
87
+ resumeTimer = void 0;
88
+ maybeResume();
89
+ }, 10);
90
+ };
91
+ const sendChunk = (chunk) => {
92
+ for (let offset = 0; offset < chunk.byteLength; offset += RELAY_FRAME_BYTES) {
93
+ relay.send(chunk.subarray(offset, Math.min(chunk.byteLength, offset + RELAY_FRAME_BYTES)), { binary: true });
94
+ }
95
+ waitForRelayDrain();
96
+ };
97
+ socket.once("connect", () => {
98
+ targetReady = true;
99
+ maybeResume();
100
+ });
101
+ socket.on("data", (chunk) => {
102
+ if (!relayReady || relay.readyState !== import_ws.default.OPEN) {
103
+ pendingBytes += chunk.byteLength;
104
+ if (pendingBytes > RELAY_PAUSE_BYTES) return cleanup();
105
+ pendingChunks.push(Buffer.from(chunk));
106
+ socket.pause();
107
+ return;
108
+ }
109
+ sendChunk(chunk);
110
+ });
111
+ socket.on("end", () => {
112
+ if (relay.readyState === import_ws.default.OPEN) relay.send(JSON.stringify({ type: "end" }));
113
+ });
114
+ socket.on("error", (error) => sendError(`Could not connect to 127.0.0.1:${options.workerPort}: ${error.message}`));
115
+ socket.on("close", cleanup);
116
+ relay.on("message", (data, isBinary) => {
117
+ if (!isBinary) {
118
+ let control;
119
+ try {
120
+ control = JSON.parse(rawDataBuffer(data).toString("utf8"));
121
+ } catch {
122
+ return cleanup();
123
+ }
124
+ if (control.type === "ready") {
125
+ relayReady = true;
126
+ for (const chunk of pendingChunks.splice(0)) sendChunk(chunk);
127
+ pendingBytes = 0;
128
+ maybeResume();
129
+ } else if (control.type === "end") {
130
+ socket.end();
131
+ } else if (control.type === "error") {
132
+ socket.destroy(new Error(control.error || "Port-forward relay failed."));
133
+ }
134
+ return;
135
+ }
136
+ if (!socket.write(rawDataBuffer(data))) {
137
+ relay.pause();
138
+ socket.once("drain", () => relay.resume());
139
+ }
140
+ });
141
+ relay.once("error", cleanup);
142
+ relay.once("close", cleanup);
143
+ socket.pause();
144
+ }
145
+ // Annotate the CommonJS export names for ESM import in node:
146
+ 0 && (module.exports = {
147
+ openWorkerPortForwardRelay
148
+ });
package/dist/mjs/main.mjs CHANGED
@@ -8,6 +8,7 @@ import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
8
8
  import { configureVisibleGitIdentity } from "./git-identity.mjs";
9
9
  import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
10
10
  import { terminateProcessTree } from "./process-tree.mjs";
11
+ import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
11
12
  import {
12
13
  isRetryableWorkerServerStatus,
13
14
  superviseWorkerRuntime,
@@ -2530,7 +2531,8 @@ async function startWorker(options) {
2530
2531
  r5dctlVersion: readInstalledCliVersion("r5dctl"),
2531
2532
  capabilities: {
2532
2533
  updateClis: true,
2533
- canonicalResolverCheckout: true
2534
+ canonicalResolverCheckout: true,
2535
+ browserPortForwarding: true
2534
2536
  },
2535
2537
  projectRoot: projectsRoot,
2536
2538
  artifactRoot,
@@ -2683,6 +2685,17 @@ async function startWorker(options) {
2683
2685
  pendingProcessTerminals.delete(message.runId);
2684
2686
  return;
2685
2687
  }
2688
+ if (message.type === "port_forward_connect") {
2689
+ openWorkerPortForwardRelay({
2690
+ baseUrl,
2691
+ token,
2692
+ label,
2693
+ forwardId: message.forwardId,
2694
+ relayConnectionId: message.relayConnectionId,
2695
+ workerPort: message.workerPort
2696
+ });
2697
+ return;
2698
+ }
2686
2699
  if (message.type === "update_clis") {
2687
2700
  if (cliUpdateInProgress) {
2688
2701
  sendWorkerMessage(ws, {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,114 @@
1
+ import net from "node:net";
2
+ import WebSocket, {} from "ws";
3
+ const RELAY_FRAME_BYTES = 64 * 1024;
4
+ const RELAY_PAUSE_BYTES = 1024 * 1024;
5
+ const RELAY_RESUME_BYTES = 256 * 1024;
6
+ function relayUrl(baseUrl, label, forwardId, relayConnectionId) {
7
+ const url = new URL("/worker/port-forward/ws", baseUrl);
8
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
9
+ url.searchParams.set("label", label);
10
+ url.searchParams.set("forward", forwardId);
11
+ url.searchParams.set("connection", relayConnectionId);
12
+ return url.toString();
13
+ }
14
+ function rawDataBuffer(data) {
15
+ if (Buffer.isBuffer(data)) return data;
16
+ if (Array.isArray(data)) return Buffer.concat(data);
17
+ return Buffer.from(data);
18
+ }
19
+ function openWorkerPortForwardRelay(options) {
20
+ const socket = net.createConnection({ host: "127.0.0.1", port: options.workerPort, allowHalfOpen: true });
21
+ socket.pause();
22
+ socket.setNoDelay(true);
23
+ const relay = new WebSocket(relayUrl(options.baseUrl, options.label, options.forwardId, options.relayConnectionId), {
24
+ headers: { Authorization: `Bearer ${options.token}` },
25
+ perMessageDeflate: false
26
+ });
27
+ let relayReady = false;
28
+ let targetReady = false;
29
+ const pendingChunks = [];
30
+ let pendingBytes = 0;
31
+ let closing = false;
32
+ let resumeTimer;
33
+ const maybeResume = () => {
34
+ if (relayReady && targetReady && !resumeTimer) socket.resume();
35
+ };
36
+ const cleanup = () => {
37
+ if (closing) return;
38
+ closing = true;
39
+ if (resumeTimer) clearInterval(resumeTimer);
40
+ socket.destroy();
41
+ if (relay.readyState === WebSocket.OPEN || relay.readyState === WebSocket.CONNECTING) relay.close();
42
+ };
43
+ const sendError = (message) => {
44
+ if (relay.readyState === WebSocket.OPEN) relay.send(JSON.stringify({ type: "error", error: message }));
45
+ cleanup();
46
+ };
47
+ const waitForRelayDrain = () => {
48
+ if (resumeTimer || relay.bufferedAmount <= RELAY_PAUSE_BYTES) return;
49
+ socket.pause();
50
+ resumeTimer = setInterval(() => {
51
+ if (relay.readyState !== WebSocket.OPEN) return cleanup();
52
+ if (relay.bufferedAmount > RELAY_RESUME_BYTES) return;
53
+ clearInterval(resumeTimer);
54
+ resumeTimer = void 0;
55
+ maybeResume();
56
+ }, 10);
57
+ };
58
+ const sendChunk = (chunk) => {
59
+ for (let offset = 0; offset < chunk.byteLength; offset += RELAY_FRAME_BYTES) {
60
+ relay.send(chunk.subarray(offset, Math.min(chunk.byteLength, offset + RELAY_FRAME_BYTES)), { binary: true });
61
+ }
62
+ waitForRelayDrain();
63
+ };
64
+ socket.once("connect", () => {
65
+ targetReady = true;
66
+ maybeResume();
67
+ });
68
+ socket.on("data", (chunk) => {
69
+ if (!relayReady || relay.readyState !== WebSocket.OPEN) {
70
+ pendingBytes += chunk.byteLength;
71
+ if (pendingBytes > RELAY_PAUSE_BYTES) return cleanup();
72
+ pendingChunks.push(Buffer.from(chunk));
73
+ socket.pause();
74
+ return;
75
+ }
76
+ sendChunk(chunk);
77
+ });
78
+ socket.on("end", () => {
79
+ if (relay.readyState === WebSocket.OPEN) relay.send(JSON.stringify({ type: "end" }));
80
+ });
81
+ socket.on("error", (error) => sendError(`Could not connect to 127.0.0.1:${options.workerPort}: ${error.message}`));
82
+ socket.on("close", cleanup);
83
+ relay.on("message", (data, isBinary) => {
84
+ if (!isBinary) {
85
+ let control;
86
+ try {
87
+ control = JSON.parse(rawDataBuffer(data).toString("utf8"));
88
+ } catch {
89
+ return cleanup();
90
+ }
91
+ if (control.type === "ready") {
92
+ relayReady = true;
93
+ for (const chunk of pendingChunks.splice(0)) sendChunk(chunk);
94
+ pendingBytes = 0;
95
+ maybeResume();
96
+ } else if (control.type === "end") {
97
+ socket.end();
98
+ } else if (control.type === "error") {
99
+ socket.destroy(new Error(control.error || "Port-forward relay failed."));
100
+ }
101
+ return;
102
+ }
103
+ if (!socket.write(rawDataBuffer(data))) {
104
+ relay.pause();
105
+ socket.once("drain", () => relay.resume());
106
+ }
107
+ });
108
+ relay.once("error", cleanup);
109
+ relay.once("close", cleanup);
110
+ socket.pause();
111
+ }
112
+ export {
113
+ openWorkerPortForwardRelay
114
+ };
@@ -0,0 +1,8 @@
1
+ export declare function openWorkerPortForwardRelay(options: {
2
+ baseUrl: string;
3
+ token: string;
4
+ label: string;
5
+ forwardId: string;
6
+ relayConnectionId: string;
7
+ workerPort: number;
8
+ }): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",