@ricsam/r5d-worker 0.0.69 → 0.0.71

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/dist/cjs/main.cjs CHANGED
@@ -2296,7 +2296,10 @@ function resolveHostShell(command, platform = process.platform) {
2296
2296
  return /(?:^|[\\/])cmd\.exe$/i.test(file2) ? { file: file2, args: ["/d", "/s", "/c", command] } : { file: file2, args: ["-Command", command] };
2297
2297
  }
2298
2298
  const file = process.env.SHELL || "/bin/bash";
2299
- return { file, args: command === void 0 ? ["-l"] : ["-lc", command] };
2299
+ if (command === void 0) {
2300
+ return { file, args: ["-l"] };
2301
+ }
2302
+ return { file, args: ["-lc", 'eval "$R5D_SHELL_COMMAND"'], env: { R5D_SHELL_COMMAND: command } };
2300
2303
  }
2301
2304
  async function openPty(input) {
2302
2305
  if (activePtys.has(input.message.ptyId)) {
@@ -2331,7 +2334,8 @@ async function openPty(input) {
2331
2334
  ...githubProcessEnv(),
2332
2335
  ...input.message.env ?? {},
2333
2336
  ...planProcessEnv,
2334
- ...targetProcessEnv
2337
+ ...targetProcessEnv,
2338
+ ...shell.env ?? {}
2335
2339
  }
2336
2340
  },
2337
2341
  onOpened: () => {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.69",
3
+ "version": "0.0.71",
4
4
  "type": "commonjs"
5
5
  }
@@ -36,6 +36,7 @@ var import_ws = __toESM(require("ws"), 1);
36
36
  const RELAY_FRAME_BYTES = 64 * 1024;
37
37
  const RELAY_PAUSE_BYTES = 1024 * 1024;
38
38
  const RELAY_RESUME_BYTES = 256 * 1024;
39
+ const LOOPBACK_HOSTS = ["127.0.0.1", "::1"];
39
40
  function relayUrl(baseUrl, label, forwardId, relayConnectionId) {
40
41
  const url = new URL("/worker/port-forward/ws", baseUrl);
41
42
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
@@ -44,42 +45,59 @@ function relayUrl(baseUrl, label, forwardId, relayConnectionId) {
44
45
  url.searchParams.set("connection", relayConnectionId);
45
46
  return url.toString();
46
47
  }
48
+ function loopbackAddress(host, port) {
49
+ return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
50
+ }
47
51
  function rawDataBuffer(data) {
48
52
  if (Buffer.isBuffer(data)) return data;
49
53
  if (Array.isArray(data)) return Buffer.concat(data);
50
54
  return Buffer.from(data);
51
55
  }
52
56
  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
57
  const relay = new import_ws.default(relayUrl(options.baseUrl, options.label, options.forwardId, options.relayConnectionId), {
57
58
  headers: { Authorization: `Bearer ${options.token}` },
58
59
  perMessageDeflate: false
59
60
  });
61
+ let socket;
60
62
  let relayReady = false;
61
63
  let targetReady = false;
64
+ let targetEndRequested = false;
65
+ let targetFailure;
62
66
  const pendingChunks = [];
63
67
  let pendingBytes = 0;
68
+ const pendingWrites = [];
69
+ let pendingWriteBytes = 0;
64
70
  let closing = false;
65
71
  let resumeTimer;
66
72
  const maybeResume = () => {
67
- if (relayReady && targetReady && !resumeTimer) socket.resume();
73
+ if (relayReady && targetReady && !resumeTimer) socket?.resume();
68
74
  };
69
75
  const cleanup = () => {
70
76
  if (closing) return;
71
77
  closing = true;
72
78
  if (resumeTimer) clearInterval(resumeTimer);
73
- socket.destroy();
79
+ socket?.destroy();
74
80
  if (relay.readyState === import_ws.default.OPEN || relay.readyState === import_ws.default.CONNECTING) relay.close();
75
81
  };
76
82
  const sendError = (message) => {
77
- if (relay.readyState === import_ws.default.OPEN) relay.send(JSON.stringify({ type: "error", error: message }));
83
+ if (closing) return;
84
+ const payload = JSON.stringify({ type: "error", error: message });
85
+ if (relay.readyState === import_ws.default.CONNECTING) {
86
+ closing = true;
87
+ if (resumeTimer) clearInterval(resumeTimer);
88
+ socket?.destroy();
89
+ relay.once("open", () => {
90
+ relay.send(payload);
91
+ relay.close();
92
+ });
93
+ return;
94
+ }
95
+ if (relay.readyState === import_ws.default.OPEN) relay.send(payload);
78
96
  cleanup();
79
97
  };
80
98
  const waitForRelayDrain = () => {
81
99
  if (resumeTimer || relay.bufferedAmount <= RELAY_PAUSE_BYTES) return;
82
- socket.pause();
100
+ socket?.pause();
83
101
  resumeTimer = setInterval(() => {
84
102
  if (relay.readyState !== import_ws.default.OPEN) return cleanup();
85
103
  if (relay.bufferedAmount > RELAY_RESUME_BYTES) return;
@@ -94,25 +112,53 @@ function openWorkerPortForwardRelay(options) {
94
112
  }
95
113
  waitForRelayDrain();
96
114
  };
97
- socket.once("connect", () => {
115
+ const adoptTarget = (target, host) => {
116
+ if (closing) return target.destroy();
117
+ socket = target;
118
+ target.on("data", (chunk) => {
119
+ if (!relayReady || relay.readyState !== import_ws.default.OPEN) {
120
+ pendingBytes += chunk.byteLength;
121
+ if (pendingBytes > RELAY_PAUSE_BYTES) return cleanup();
122
+ pendingChunks.push(Buffer.from(chunk));
123
+ target.pause();
124
+ return;
125
+ }
126
+ sendChunk(chunk);
127
+ });
128
+ target.on("end", () => {
129
+ if (relay.readyState === import_ws.default.OPEN) relay.send(JSON.stringify({ type: "end" }));
130
+ });
131
+ target.on("error", (error) => sendError(`Connection to ${loopbackAddress(host, options.workerPort)} failed: ${error.message}`));
132
+ target.on("close", cleanup);
133
+ target.pause();
98
134
  targetReady = true;
135
+ for (const chunk of pendingWrites.splice(0)) target.write(chunk);
136
+ pendingWriteBytes = 0;
137
+ if (targetFailure) return void target.destroy(targetFailure);
138
+ if (targetEndRequested) target.end();
99
139
  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);
140
+ };
141
+ const dialFailures = [];
142
+ const dialTarget = (host, fallbackHosts2) => {
143
+ if (closing) return;
144
+ const candidate = import_node_net.default.createConnection({ host, port: options.workerPort, allowHalfOpen: true });
145
+ candidate.pause();
146
+ candidate.setNoDelay(true);
147
+ const onDialError = (error) => {
148
+ candidate.destroy();
149
+ dialFailures.push(`${loopbackAddress(host, options.workerPort)} (${error.code ?? error.message})`);
150
+ const [nextHost, ...rest] = fallbackHosts2;
151
+ if (nextHost) return dialTarget(nextHost, rest);
152
+ sendError(
153
+ `Could not connect to ${dialFailures.join(" or ")} on the worker. Nothing is listening on either loopback address for port ${options.workerPort} \u2014 make sure the server is running and bound to a loopback or wildcard host.`
154
+ );
155
+ };
156
+ candidate.once("error", onDialError);
157
+ candidate.once("connect", () => {
158
+ candidate.off("error", onDialError);
159
+ adoptTarget(candidate, host);
160
+ });
161
+ };
116
162
  relay.on("message", (data, isBinary) => {
117
163
  if (!isBinary) {
118
164
  let control;
@@ -123,24 +169,36 @@ function openWorkerPortForwardRelay(options) {
123
169
  }
124
170
  if (control.type === "ready") {
125
171
  relayReady = true;
126
- for (const chunk of pendingChunks.splice(0)) sendChunk(chunk);
172
+ for (const chunk2 of pendingChunks.splice(0)) sendChunk(chunk2);
127
173
  pendingBytes = 0;
128
174
  maybeResume();
129
175
  } else if (control.type === "end") {
130
- socket.end();
176
+ if (socket) socket.end();
177
+ else targetEndRequested = true;
131
178
  } else if (control.type === "error") {
132
- socket.destroy(new Error(control.error || "Port-forward relay failed."));
179
+ const failure = new Error(control.error || "Port-forward relay failed.");
180
+ if (socket) socket.destroy(failure);
181
+ else targetFailure = failure;
133
182
  }
134
183
  return;
135
184
  }
136
- if (!socket.write(rawDataBuffer(data))) {
185
+ const chunk = rawDataBuffer(data);
186
+ if (!socket) {
187
+ pendingWriteBytes += chunk.byteLength;
188
+ if (pendingWriteBytes > RELAY_PAUSE_BYTES) return cleanup();
189
+ pendingWrites.push(Buffer.from(chunk));
190
+ return;
191
+ }
192
+ const target = socket;
193
+ if (!target.write(chunk)) {
137
194
  relay.pause();
138
- socket.once("drain", () => relay.resume());
195
+ target.once("drain", () => relay.resume());
139
196
  }
140
197
  });
141
198
  relay.on("error", cleanup);
142
199
  relay.once("close", cleanup);
143
- socket.pause();
200
+ const [primaryHost, ...fallbackHosts] = LOOPBACK_HOSTS;
201
+ dialTarget(primaryHost, fallbackHosts);
144
202
  }
145
203
  // Annotate the CommonJS export names for ESM import in node:
146
204
  0 && (module.exports = {
package/dist/mjs/main.mjs CHANGED
@@ -2267,7 +2267,10 @@ function resolveHostShell(command, platform = process.platform) {
2267
2267
  return /(?:^|[\\/])cmd\.exe$/i.test(file2) ? { file: file2, args: ["/d", "/s", "/c", command] } : { file: file2, args: ["-Command", command] };
2268
2268
  }
2269
2269
  const file = process.env.SHELL || "/bin/bash";
2270
- return { file, args: command === void 0 ? ["-l"] : ["-lc", command] };
2270
+ if (command === void 0) {
2271
+ return { file, args: ["-l"] };
2272
+ }
2273
+ return { file, args: ["-lc", 'eval "$R5D_SHELL_COMMAND"'], env: { R5D_SHELL_COMMAND: command } };
2271
2274
  }
2272
2275
  async function openPty(input) {
2273
2276
  if (activePtys.has(input.message.ptyId)) {
@@ -2302,7 +2305,8 @@ async function openPty(input) {
2302
2305
  ...githubProcessEnv(),
2303
2306
  ...input.message.env ?? {},
2304
2307
  ...planProcessEnv,
2305
- ...targetProcessEnv
2308
+ ...targetProcessEnv,
2309
+ ...shell.env ?? {}
2306
2310
  }
2307
2311
  },
2308
2312
  onOpened: () => {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.69",
3
+ "version": "0.0.71",
4
4
  "type": "module"
5
5
  }
@@ -3,6 +3,7 @@ import WebSocket, {} from "ws";
3
3
  const RELAY_FRAME_BYTES = 64 * 1024;
4
4
  const RELAY_PAUSE_BYTES = 1024 * 1024;
5
5
  const RELAY_RESUME_BYTES = 256 * 1024;
6
+ const LOOPBACK_HOSTS = ["127.0.0.1", "::1"];
6
7
  function relayUrl(baseUrl, label, forwardId, relayConnectionId) {
7
8
  const url = new URL("/worker/port-forward/ws", baseUrl);
8
9
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
@@ -11,42 +12,59 @@ function relayUrl(baseUrl, label, forwardId, relayConnectionId) {
11
12
  url.searchParams.set("connection", relayConnectionId);
12
13
  return url.toString();
13
14
  }
15
+ function loopbackAddress(host, port) {
16
+ return host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`;
17
+ }
14
18
  function rawDataBuffer(data) {
15
19
  if (Buffer.isBuffer(data)) return data;
16
20
  if (Array.isArray(data)) return Buffer.concat(data);
17
21
  return Buffer.from(data);
18
22
  }
19
23
  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
24
  const relay = new WebSocket(relayUrl(options.baseUrl, options.label, options.forwardId, options.relayConnectionId), {
24
25
  headers: { Authorization: `Bearer ${options.token}` },
25
26
  perMessageDeflate: false
26
27
  });
28
+ let socket;
27
29
  let relayReady = false;
28
30
  let targetReady = false;
31
+ let targetEndRequested = false;
32
+ let targetFailure;
29
33
  const pendingChunks = [];
30
34
  let pendingBytes = 0;
35
+ const pendingWrites = [];
36
+ let pendingWriteBytes = 0;
31
37
  let closing = false;
32
38
  let resumeTimer;
33
39
  const maybeResume = () => {
34
- if (relayReady && targetReady && !resumeTimer) socket.resume();
40
+ if (relayReady && targetReady && !resumeTimer) socket?.resume();
35
41
  };
36
42
  const cleanup = () => {
37
43
  if (closing) return;
38
44
  closing = true;
39
45
  if (resumeTimer) clearInterval(resumeTimer);
40
- socket.destroy();
46
+ socket?.destroy();
41
47
  if (relay.readyState === WebSocket.OPEN || relay.readyState === WebSocket.CONNECTING) relay.close();
42
48
  };
43
49
  const sendError = (message) => {
44
- if (relay.readyState === WebSocket.OPEN) relay.send(JSON.stringify({ type: "error", error: message }));
50
+ if (closing) return;
51
+ const payload = JSON.stringify({ type: "error", error: message });
52
+ if (relay.readyState === WebSocket.CONNECTING) {
53
+ closing = true;
54
+ if (resumeTimer) clearInterval(resumeTimer);
55
+ socket?.destroy();
56
+ relay.once("open", () => {
57
+ relay.send(payload);
58
+ relay.close();
59
+ });
60
+ return;
61
+ }
62
+ if (relay.readyState === WebSocket.OPEN) relay.send(payload);
45
63
  cleanup();
46
64
  };
47
65
  const waitForRelayDrain = () => {
48
66
  if (resumeTimer || relay.bufferedAmount <= RELAY_PAUSE_BYTES) return;
49
- socket.pause();
67
+ socket?.pause();
50
68
  resumeTimer = setInterval(() => {
51
69
  if (relay.readyState !== WebSocket.OPEN) return cleanup();
52
70
  if (relay.bufferedAmount > RELAY_RESUME_BYTES) return;
@@ -61,25 +79,53 @@ function openWorkerPortForwardRelay(options) {
61
79
  }
62
80
  waitForRelayDrain();
63
81
  };
64
- socket.once("connect", () => {
82
+ const adoptTarget = (target, host) => {
83
+ if (closing) return target.destroy();
84
+ socket = target;
85
+ target.on("data", (chunk) => {
86
+ if (!relayReady || relay.readyState !== WebSocket.OPEN) {
87
+ pendingBytes += chunk.byteLength;
88
+ if (pendingBytes > RELAY_PAUSE_BYTES) return cleanup();
89
+ pendingChunks.push(Buffer.from(chunk));
90
+ target.pause();
91
+ return;
92
+ }
93
+ sendChunk(chunk);
94
+ });
95
+ target.on("end", () => {
96
+ if (relay.readyState === WebSocket.OPEN) relay.send(JSON.stringify({ type: "end" }));
97
+ });
98
+ target.on("error", (error) => sendError(`Connection to ${loopbackAddress(host, options.workerPort)} failed: ${error.message}`));
99
+ target.on("close", cleanup);
100
+ target.pause();
65
101
  targetReady = true;
102
+ for (const chunk of pendingWrites.splice(0)) target.write(chunk);
103
+ pendingWriteBytes = 0;
104
+ if (targetFailure) return void target.destroy(targetFailure);
105
+ if (targetEndRequested) target.end();
66
106
  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);
107
+ };
108
+ const dialFailures = [];
109
+ const dialTarget = (host, fallbackHosts2) => {
110
+ if (closing) return;
111
+ const candidate = net.createConnection({ host, port: options.workerPort, allowHalfOpen: true });
112
+ candidate.pause();
113
+ candidate.setNoDelay(true);
114
+ const onDialError = (error) => {
115
+ candidate.destroy();
116
+ dialFailures.push(`${loopbackAddress(host, options.workerPort)} (${error.code ?? error.message})`);
117
+ const [nextHost, ...rest] = fallbackHosts2;
118
+ if (nextHost) return dialTarget(nextHost, rest);
119
+ sendError(
120
+ `Could not connect to ${dialFailures.join(" or ")} on the worker. Nothing is listening on either loopback address for port ${options.workerPort} \u2014 make sure the server is running and bound to a loopback or wildcard host.`
121
+ );
122
+ };
123
+ candidate.once("error", onDialError);
124
+ candidate.once("connect", () => {
125
+ candidate.off("error", onDialError);
126
+ adoptTarget(candidate, host);
127
+ });
128
+ };
83
129
  relay.on("message", (data, isBinary) => {
84
130
  if (!isBinary) {
85
131
  let control;
@@ -90,24 +136,36 @@ function openWorkerPortForwardRelay(options) {
90
136
  }
91
137
  if (control.type === "ready") {
92
138
  relayReady = true;
93
- for (const chunk of pendingChunks.splice(0)) sendChunk(chunk);
139
+ for (const chunk2 of pendingChunks.splice(0)) sendChunk(chunk2);
94
140
  pendingBytes = 0;
95
141
  maybeResume();
96
142
  } else if (control.type === "end") {
97
- socket.end();
143
+ if (socket) socket.end();
144
+ else targetEndRequested = true;
98
145
  } else if (control.type === "error") {
99
- socket.destroy(new Error(control.error || "Port-forward relay failed."));
146
+ const failure = new Error(control.error || "Port-forward relay failed.");
147
+ if (socket) socket.destroy(failure);
148
+ else targetFailure = failure;
100
149
  }
101
150
  return;
102
151
  }
103
- if (!socket.write(rawDataBuffer(data))) {
152
+ const chunk = rawDataBuffer(data);
153
+ if (!socket) {
154
+ pendingWriteBytes += chunk.byteLength;
155
+ if (pendingWriteBytes > RELAY_PAUSE_BYTES) return cleanup();
156
+ pendingWrites.push(Buffer.from(chunk));
157
+ return;
158
+ }
159
+ const target = socket;
160
+ if (!target.write(chunk)) {
104
161
  relay.pause();
105
- socket.once("drain", () => relay.resume());
162
+ target.once("drain", () => relay.resume());
106
163
  }
107
164
  });
108
165
  relay.on("error", cleanup);
109
166
  relay.once("close", cleanup);
110
- socket.pause();
167
+ const [primaryHost, ...fallbackHosts] = LOOPBACK_HOSTS;
168
+ dialTarget(primaryHost, fallbackHosts);
111
169
  }
112
170
  export {
113
171
  openWorkerPortForwardRelay
@@ -209,5 +209,6 @@ export declare function prepareShellEnvForTarget(input: {
209
209
  export declare function resolveHostShell(command?: string, platform?: NodeJS.Platform): {
210
210
  file: string;
211
211
  args: string[];
212
+ env?: Record<string, string>;
212
213
  };
213
214
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.69",
3
+ "version": "0.0.71",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",