@zhanguym/chatroom 1.1.0 → 1.2.0-beta.0

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.
Files changed (87) hide show
  1. package/dist/core/runtime/child-environment.js +3 -2
  2. package/dist/infrastructure/http/http-server.js +7 -1
  3. package/dist/native/macos/ChatRoomComputerHelper.app/Contents/MacOS/chatroom-computer-helper +0 -0
  4. package/dist/native/windows/chatroom-computer-helper.exe +0 -0
  5. package/dist/plugins/cloud/tunnel-client.d.ts +2 -84
  6. package/dist/plugins/cloud/tunnel-client.js +2 -426
  7. package/dist/plugins/cloud/v2/stream.d.ts +48 -0
  8. package/dist/plugins/cloud/v2/stream.js +156 -0
  9. package/dist/plugins/cloud/v2/tunnel-client.d.ts +45 -0
  10. package/dist/plugins/cloud/v2/tunnel-client.js +426 -0
  11. package/dist/plugins/process/backend.d.ts +3 -0
  12. package/dist/plugins/process/infrastructure/pty-process-backend.d.ts +2 -0
  13. package/dist/plugins/process/infrastructure/pty-process-backend.js +5 -2
  14. package/dist/plugins/process/infrastructure/terminal-shell.d.ts +4 -0
  15. package/dist/plugins/process/infrastructure/terminal-shell.js +10 -0
  16. package/dist/plugins/process/process-supervisor.d.ts +6 -0
  17. package/dist/plugins/process/process-supervisor.js +26 -0
  18. package/dist/plugins/process/types.d.ts +2 -0
  19. package/dist/plugins/web/http/api-router.d.ts +2 -1
  20. package/dist/plugins/web/http/api-router.js +5 -1
  21. package/dist/plugins/web/http/logs-api-router.js +2 -2
  22. package/dist/plugins/web/http/process-api-router.js +48 -0
  23. package/dist/plugins/web/http/rtc-api-router.d.ts +4 -0
  24. package/dist/plugins/web/http/rtc-api-router.js +316 -0
  25. package/dist/plugins/web/http/runtime-api-router.js +2 -2
  26. package/dist/plugins/web/http/terminal-api-router.d.ts +4 -0
  27. package/dist/plugins/web/http/terminal-api-router.js +132 -0
  28. package/dist/web/assets/{AgentView-BGLIBoxF.css → AgentView-CMxIn3KB.css} +1 -1
  29. package/dist/web/assets/AgentView-DQr-5aLK.js +4 -0
  30. package/dist/web/assets/CloudView-B2VBut3O.js +1 -0
  31. package/dist/web/assets/CloudView-Bgp5G965.css +1 -0
  32. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-DkN8x_6L.js +3 -0
  33. package/dist/web/assets/ComputerView-CIFF1zUb.js +1 -0
  34. package/dist/web/assets/{MarkdownContent-CkjR5fs7.js → MarkdownContent-DRZp88_G.js} +2 -2
  35. package/dist/web/assets/McpToolsView-CSEru3ck.js +1 -0
  36. package/dist/web/assets/OperationsView-B_Nv1Mgi.js +4 -0
  37. package/dist/web/assets/ProcessesView-5K8wNiPC.js +5 -0
  38. package/dist/web/assets/ProcessesView-BmNdR3C0.css +1 -0
  39. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-CQEj-YRM.js +1 -0
  40. package/dist/web/assets/SystemLogsView-CBINILPo.js +1 -0
  41. package/dist/web/assets/SystemLogsView-COvwamo5.css +1 -0
  42. package/dist/web/assets/TerminalView-DNaKG3II.css +1 -0
  43. package/dist/web/assets/TerminalView-Dvyy2GKF.js +16 -0
  44. package/dist/web/assets/VSelect-CBX17QQj.js +1 -0
  45. package/dist/web/assets/VSpacer-9N_Ffoce.js +1 -0
  46. package/dist/web/assets/VSwitch-CvQYU48e.js +1 -0
  47. package/dist/web/assets/VTabs-CftrBFlF.js +1 -0
  48. package/dist/web/assets/VTextarea-Dcui00tn.js +1 -0
  49. package/dist/web/assets/WorkspaceDownloadPreview-DJKSNaZ_.css +1 -0
  50. package/dist/web/assets/WorkspaceDownloadPreview-DzM2p_xl.js +1 -0
  51. package/dist/web/assets/WorkspaceMediaPreview-ctIyG3Ru.js +4 -0
  52. package/dist/web/assets/{WorkspacePdfPreview-CO-ZtDHG.js → WorkspacePdfPreview-CeizxEAH.js} +2 -2
  53. package/dist/web/assets/WorkspacesView-DcmqHGvr.js +2 -0
  54. package/dist/web/assets/index-BRI_re-n.css +1 -0
  55. package/dist/web/assets/index-DTyxBHlJ.js +19 -0
  56. package/dist/web/assets/{useMasterDetailFocus-D1mQ8Qjk.js → useMasterDetailFocus-DR5N95_A.js} +1 -1
  57. package/dist/web/assets/useOperationLabels-Dou4m0ac.js +1 -0
  58. package/dist/web/index.html +2 -2
  59. package/dist/web/transport-sw.js +411 -0
  60. package/package.json +9 -4
  61. package/scripts/ensure-pty-helper.mjs +28 -0
  62. package/dist/web/assets/AgentView-Dd6zCbr-.js +0 -4
  63. package/dist/web/assets/CloudView-DVVvwuWW.css +0 -1
  64. package/dist/web/assets/CloudView-DquY1HDZ.js +0 -1
  65. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-ekOImmBP.js +0 -3
  66. package/dist/web/assets/ComputerView-RD2gMxJZ.js +0 -1
  67. package/dist/web/assets/McpToolsView-Dg2-iaDP.js +0 -1
  68. package/dist/web/assets/OperationsView-C8yCX7WZ.js +0 -4
  69. package/dist/web/assets/ProcessesView-C834LC-f.js +0 -4
  70. package/dist/web/assets/ProcessesView-Ce6iVwjA.css +0 -1
  71. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-By2gilBw.js +0 -1
  72. package/dist/web/assets/SystemLogsView-B6KcZv1J.js +0 -1
  73. package/dist/web/assets/SystemLogsView-CofP4Jvi.css +0 -1
  74. package/dist/web/assets/VSelect-BtAJX-XJ.js +0 -1
  75. package/dist/web/assets/VSpacer-M8bpkhTH.js +0 -1
  76. package/dist/web/assets/VSwitch-C0VTnwLJ.js +0 -1
  77. package/dist/web/assets/VTabs-g61mykvY.js +0 -1
  78. package/dist/web/assets/VTextarea-CRYQYRqg.js +0 -1
  79. package/dist/web/assets/WorkspaceDownloadPreview-BrESF-82.js +0 -1
  80. package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +0 -1
  81. package/dist/web/assets/WorkspaceMediaPreview-HYajqgZ3.js +0 -4
  82. package/dist/web/assets/WorkspacesView-BJCnzBLj.js +0 -2
  83. package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  84. package/dist/web/assets/index-CZ-4tJlF.css +0 -1
  85. package/dist/web/assets/index-u-aADQP-.js +0 -19
  86. package/dist/web/assets/useOperationLabels-BjPIVnJW.js +0 -1
  87. package/dist/web/assets/utils-rtPwWNel.js +0 -1
@@ -0,0 +1,156 @@
1
+ /** Tunnel V2 framing, independent of Node.js, WebSocket and WebRTC. */
2
+ export const V2_PROTOCOL = "chatroom-tunnel-v2";
3
+ export const DATA_SIZE = 16 * 1024;
4
+ export const INITIAL_CREDIT = 256 * 1024;
5
+ export const MAX_META = 64 * 1024;
6
+ export const MAX_QUEUED = 512 * 1024;
7
+ export function encodeFrame(op, id, payload = new Uint8Array()) {
8
+ if (!Number.isSafeInteger(id) || id < 0 || id > 0xffff_ffff)
9
+ throw new Error("Invalid stream ID");
10
+ const frame = new Uint8Array(5 + payload.byteLength);
11
+ const view = new DataView(frame.buffer);
12
+ view.setUint8(0, op);
13
+ view.setUint32(1, id);
14
+ frame.set(payload, 5);
15
+ return frame;
16
+ }
17
+ export function decodeFrame(raw) {
18
+ if (raw.byteLength < 5)
19
+ throw new Error("Truncated V2 frame");
20
+ const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
21
+ const op = view.getUint8(0);
22
+ if (op < 1 /* Op.OPEN */ || op > 6 /* Op.CREDIT */)
23
+ throw new Error("Unknown V2 operation");
24
+ const id = view.getUint32(1);
25
+ const payload = raw.subarray(5);
26
+ if (op === 3 /* Op.DATA */ &&
27
+ (payload.byteLength === 0 || payload.byteLength > DATA_SIZE))
28
+ throw new Error("Invalid DATA frame size");
29
+ if ([1 /* Op.OPEN */, 2 /* Op.META */, 5 /* Op.RESET */].includes(op) &&
30
+ payload.byteLength > MAX_META)
31
+ throw new Error("Metadata too large");
32
+ if (op === 6 /* Op.CREDIT */ &&
33
+ (payload.byteLength !== 4 || readCredit(payload) === 0))
34
+ throw new Error("Invalid credit");
35
+ if (op === 4 /* Op.FIN */ && payload.byteLength !== 0)
36
+ throw new Error("FIN must be empty");
37
+ if (id === 0 && op !== 2 /* Op.META */)
38
+ throw new Error("Reserved control stream");
39
+ return { op, id, payload };
40
+ }
41
+ export function readCredit(payload) {
42
+ return new DataView(payload.buffer, payload.byteOffset, payload.byteLength).getUint32(0);
43
+ }
44
+ export function creditFrame(id, amount) {
45
+ if (!Number.isInteger(amount) || amount < 1 || amount > 0xffff_ffff)
46
+ throw new Error("Invalid credit amount");
47
+ const payload = new Uint8Array(4);
48
+ new DataView(payload.buffer).setUint32(0, amount);
49
+ return encodeFrame(6 /* Op.CREDIT */, id, payload);
50
+ }
51
+ export class CreditSender {
52
+ id;
53
+ send;
54
+ pause;
55
+ resume;
56
+ onFinished;
57
+ canSend;
58
+ credit = INITIAL_CREDIT;
59
+ chunks = [];
60
+ queued = 0;
61
+ ending = false;
62
+ finished = false;
63
+ cancelled = false;
64
+ wakeup = null;
65
+ constructor(id, send, pause, resume, onFinished = () => { }, canSend = () => true) {
66
+ this.id = id;
67
+ this.send = send;
68
+ this.pause = pause;
69
+ this.resume = resume;
70
+ this.onFinished = onFinished;
71
+ this.canSend = canSend;
72
+ }
73
+ enqueue(chunk) {
74
+ if (this.cancelled ||
75
+ this.finished ||
76
+ this.ending ||
77
+ this.queued + chunk.byteLength > MAX_QUEUED)
78
+ throw new Error("V2 stream write overflow");
79
+ this.chunks.push(chunk);
80
+ this.queued += chunk.byteLength;
81
+ this.flush();
82
+ }
83
+ addCredit(amount) {
84
+ if (this.finished || this.cancelled)
85
+ return;
86
+ if (this.credit + amount > INITIAL_CREDIT)
87
+ throw new Error("Invalid V2 flow credit");
88
+ this.credit += amount;
89
+ this.flush();
90
+ }
91
+ end() {
92
+ if (this.cancelled)
93
+ return;
94
+ this.ending = true;
95
+ this.flush();
96
+ }
97
+ cancel() {
98
+ this.cancelled = true;
99
+ if (this.wakeup)
100
+ clearTimeout(this.wakeup);
101
+ this.wakeup = null;
102
+ this.chunks.length = 0;
103
+ this.queued = 0;
104
+ }
105
+ /** Called when the physical transport signals that its send buffer drained. */
106
+ writable() {
107
+ if (this.wakeup) {
108
+ clearTimeout(this.wakeup);
109
+ this.wakeup = null;
110
+ }
111
+ this.flush();
112
+ }
113
+ flush() {
114
+ if (this.cancelled)
115
+ return;
116
+ while (this.credit > 0 && this.chunks.length && this.canSend()) {
117
+ const current = this.chunks[0];
118
+ const length = Math.min(DATA_SIZE, this.credit, current.byteLength);
119
+ this.send(encodeFrame(3 /* Op.DATA */, this.id, current.subarray(0, length)));
120
+ this.credit -= length;
121
+ this.queued -= length;
122
+ if (length === current.byteLength)
123
+ this.chunks.shift();
124
+ else
125
+ this.chunks[0] = current.subarray(length);
126
+ }
127
+ if (this.chunks.length) {
128
+ this.pause();
129
+ if (this.credit && !this.canSend() && !this.wakeup) {
130
+ this.wakeup = setTimeout(() => {
131
+ this.wakeup = null;
132
+ this.flush();
133
+ }, 10);
134
+ }
135
+ }
136
+ else {
137
+ this.resume();
138
+ if (this.ending && !this.finished) {
139
+ this.finished = true;
140
+ this.send(encodeFrame(4 /* Op.FIN */, this.id));
141
+ this.onFinished();
142
+ }
143
+ }
144
+ }
145
+ }
146
+ const encoder = new TextEncoder();
147
+ const decoder = new TextDecoder();
148
+ export function parseMeta(payload) {
149
+ return JSON.parse(decoder.decode(payload));
150
+ }
151
+ export function metaFrame(op, id, value) {
152
+ const encoded = encoder.encode(JSON.stringify(value));
153
+ if (encoded.byteLength > MAX_META)
154
+ throw new Error("V2 metadata too large");
155
+ return encodeFrame(op, id, encoded);
156
+ }
@@ -0,0 +1,45 @@
1
+ import { type CloudLeaseState } from "../types.js";
2
+ export declare class V2TunnelClient {
3
+ private lease;
4
+ private readonly identity;
5
+ private readonly local;
6
+ private readonly callbacks;
7
+ private socket;
8
+ private reconnectTimer;
9
+ private readyTimer;
10
+ private heartbeatTimer;
11
+ private awaitingPong;
12
+ private stopped;
13
+ private attempts;
14
+ private closeWhenIdle;
15
+ private acceptingServices;
16
+ private readonly streams;
17
+ constructor(lease: CloudLeaseState, identity: {
18
+ devicePrivateKey: string;
19
+ }, local: {
20
+ host: string;
21
+ port: number;
22
+ }, callbacks: {
23
+ onConnected(): void;
24
+ onDisconnected(): void;
25
+ onLeaseRejected(): void;
26
+ onError(error: Error): void;
27
+ });
28
+ start(): void;
29
+ stop(): void;
30
+ drainAndStop(): void;
31
+ updateLease(lease: CloudLeaseState): void;
32
+ private connect;
33
+ private handleFrame;
34
+ private refund;
35
+ private finishIfIdle;
36
+ private abort;
37
+ private send;
38
+ private checkDrain;
39
+ private destroyStreams;
40
+ private clearReadyTimer;
41
+ private clearTimers;
42
+ private startHeartbeat;
43
+ private fail;
44
+ private scheduleReconnect;
45
+ }
@@ -0,0 +1,426 @@
1
+ import { createPrivateKey, sign } from "node:crypto";
2
+ import http from "node:http";
3
+ import WebSocket from "ws";
4
+ import { z } from "zod";
5
+ import { CreditSender, INITIAL_CREDIT, creditFrame, decodeFrame, metaFrame, parseMeta, readCredit, V2_PROTOCOL, } from "./stream.js";
6
+ import { cloudServiceForPublicService, } from "../types.js";
7
+ const openSchema = z
8
+ .object({
9
+ service: z.enum(["mcp", "web"]),
10
+ method: z.string().min(1),
11
+ path: z.string().startsWith("/"),
12
+ headers: z.record(z.string(), z.string()),
13
+ })
14
+ .strict();
15
+ const controlSchema = z.discriminatedUnion("type", [
16
+ z.object({ type: z.literal("challenge"), nonce: z.string().min(1) }).strict(),
17
+ z.object({ type: z.literal("ready") }).strict(),
18
+ ]);
19
+ export class V2TunnelClient {
20
+ lease;
21
+ identity;
22
+ local;
23
+ callbacks;
24
+ socket = null;
25
+ reconnectTimer = null;
26
+ readyTimer = null;
27
+ heartbeatTimer = null;
28
+ awaitingPong = false;
29
+ stopped = true;
30
+ attempts = 0;
31
+ closeWhenIdle = null;
32
+ acceptingServices;
33
+ streams = new Map();
34
+ constructor(lease, identity, local, callbacks) {
35
+ this.lease = lease;
36
+ this.identity = identity;
37
+ this.local = local;
38
+ this.callbacks = callbacks;
39
+ this.acceptingServices = new Set(lease.services);
40
+ }
41
+ start() {
42
+ if (!this.stopped)
43
+ return;
44
+ this.stopped = false;
45
+ this.closeWhenIdle = null;
46
+ this.acceptingServices = new Set(this.lease.services);
47
+ this.connect();
48
+ }
49
+ stop() {
50
+ this.stopped = true;
51
+ this.closeWhenIdle = null;
52
+ this.acceptingServices.clear();
53
+ if (this.reconnectTimer)
54
+ clearTimeout(this.reconnectTimer);
55
+ this.reconnectTimer = null;
56
+ this.clearTimers();
57
+ this.socket?.close();
58
+ this.socket = null;
59
+ this.destroyStreams();
60
+ }
61
+ drainAndStop() {
62
+ this.stopped = true;
63
+ this.acceptingServices.clear();
64
+ if (this.reconnectTimer)
65
+ clearTimeout(this.reconnectTimer);
66
+ this.reconnectTimer = null;
67
+ this.clearTimers();
68
+ if (!this.streams.size)
69
+ this.socket?.close();
70
+ else
71
+ this.closeWhenIdle = "stop";
72
+ }
73
+ updateLease(lease) {
74
+ const changed = lease.tunnelUrl !== this.lease.tunnelUrl;
75
+ const removes = this.lease.services.some((service) => !lease.services.includes(service));
76
+ this.lease = lease;
77
+ this.acceptingServices = new Set(lease.services);
78
+ if (this.stopped)
79
+ return;
80
+ if (!changed) {
81
+ this.send(metaFrame(2 /* Op.META */, 0, { type: "update-lease", lease: lease.token }));
82
+ return;
83
+ }
84
+ if (!removes || !this.streams.size)
85
+ this.socket?.close();
86
+ else
87
+ this.closeWhenIdle = "reconnect";
88
+ }
89
+ connect() {
90
+ if (this.stopped || this.socket)
91
+ return;
92
+ const socket = new WebSocket(this.lease.tunnelUrl, V2_PROTOCOL);
93
+ this.socket = socket;
94
+ let ready = false;
95
+ this.readyTimer = setTimeout(() => this.fail(socket, new Error("V2 handshake timed out")), 10000);
96
+ this.readyTimer.unref();
97
+ socket.on("unexpected-response", (_request, response) => {
98
+ const error = response.statusCode === 426
99
+ ? new Error("Cloud Server does not support Tunnel V2; upgrade the server")
100
+ : new Error("Tunnel V2 handshake rejected: " + response.statusCode);
101
+ response.resume();
102
+ this.callbacks.onError(error);
103
+ this.stop();
104
+ socket.terminate();
105
+ });
106
+ socket.on("open", () => {
107
+ if (socket.protocol !== V2_PROTOCOL)
108
+ this.fail(socket, new Error("Unexpected V2 subprotocol"));
109
+ });
110
+ socket.on("message", (raw, binary) => {
111
+ try {
112
+ if (socket !== this.socket)
113
+ return;
114
+ if (ready) {
115
+ if (!binary)
116
+ throw new Error("V2 requires binary frames");
117
+ this.handleFrame(toBuffer(raw));
118
+ return;
119
+ }
120
+ if (binary)
121
+ throw new Error("Unexpected binary handshake");
122
+ const message = controlSchema.parse(JSON.parse(raw.toString()));
123
+ if (message.type === "challenge") {
124
+ socket.send(JSON.stringify({
125
+ type: "authenticate",
126
+ lease: this.lease.token,
127
+ signature: signChallenge(message.nonce, this.lease.token, this.identity.devicePrivateKey),
128
+ }));
129
+ }
130
+ else {
131
+ ready = true;
132
+ this.attempts = 0;
133
+ this.clearReadyTimer();
134
+ this.startHeartbeat(socket);
135
+ this.callbacks.onConnected();
136
+ }
137
+ }
138
+ catch (error) {
139
+ this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
140
+ socket.close(4002, "Invalid V2 message");
141
+ }
142
+ });
143
+ socket.on("pong", () => {
144
+ if (this.socket === socket)
145
+ this.awaitingPong = false;
146
+ });
147
+ socket.on("close", (code) => {
148
+ if (this.socket === socket) {
149
+ this.clearTimers();
150
+ this.socket = null;
151
+ }
152
+ this.closeWhenIdle = null;
153
+ this.destroyStreams();
154
+ this.callbacks.onDisconnected();
155
+ if (this.stopped)
156
+ return;
157
+ if (code === 4003) {
158
+ this.stopped = true;
159
+ this.callbacks.onLeaseRejected();
160
+ return;
161
+ }
162
+ this.scheduleReconnect();
163
+ });
164
+ socket.on("error", (error) => {
165
+ this.fail(socket, error);
166
+ });
167
+ }
168
+ handleFrame(raw) {
169
+ const frame = decodeFrame(raw);
170
+ if (!frame.id)
171
+ throw new Error("Invalid server control frame");
172
+ if (frame.op === 1 /* Op.OPEN */) {
173
+ const meta = openSchema.parse(parseMeta(frame.payload));
174
+ if (this.streams.has(frame.id))
175
+ throw new Error("Duplicate stream");
176
+ if (this.streams.size >= 256 ||
177
+ !this.acceptingServices.has(cloudServiceForPublicService(meta.service)) ||
178
+ !allowedPath(meta.service, meta.path)) {
179
+ this.send(metaFrame(5 /* Op.RESET */, frame.id, { reason: "not_allowed" }));
180
+ return;
181
+ }
182
+ const headers = {
183
+ ...meta.headers,
184
+ host: publicHost(this.lease, meta.service),
185
+ };
186
+ const request = http.request({
187
+ host: this.local.host,
188
+ port: this.local.port,
189
+ method: meta.method,
190
+ path: meta.path,
191
+ headers,
192
+ }, (response) => {
193
+ const state = this.streams.get(frame.id);
194
+ if (!state) {
195
+ response.destroy();
196
+ return;
197
+ }
198
+ state.response = response;
199
+ const sender = new CreditSender(frame.id, (buffer) => this.send(buffer), () => response.pause(), () => response.resume(), () => {
200
+ state.responseEnded = true;
201
+ this.finishIfIdle(frame.id);
202
+ });
203
+ state.sender = sender;
204
+ this.send(metaFrame(2 /* Op.META */, frame.id, {
205
+ status: response.statusCode ?? 502,
206
+ headers: responseHeaders(response.headers),
207
+ }));
208
+ response.on("data", (chunk) => {
209
+ state.responseBytes += chunk.length;
210
+ if (state.responseBytes > 128 * 1024 * 1024) {
211
+ this.abort(frame.id);
212
+ return;
213
+ }
214
+ try {
215
+ sender.enqueue(chunk);
216
+ }
217
+ catch {
218
+ this.abort(frame.id);
219
+ }
220
+ });
221
+ response.on("end", () => {
222
+ sender.end();
223
+ this.finishIfIdle(frame.id);
224
+ });
225
+ response.on("error", () => this.abort(frame.id));
226
+ });
227
+ const state = {
228
+ request,
229
+ response: null,
230
+ sender: null,
231
+ receiveCredit: INITIAL_CREDIT,
232
+ pendingCredit: 0,
233
+ drainArmed: false,
234
+ requestBytes: 0,
235
+ responseBytes: 0,
236
+ requestEnded: false,
237
+ responseEnded: false,
238
+ };
239
+ this.streams.set(frame.id, state);
240
+ request.on("error", () => {
241
+ if (this.streams.has(frame.id))
242
+ this.abort(frame.id);
243
+ });
244
+ return;
245
+ }
246
+ const state = this.streams.get(frame.id);
247
+ if (!state)
248
+ return;
249
+ switch (frame.op) {
250
+ case 3 /* Op.DATA */: {
251
+ state.receiveCredit -= frame.payload.length;
252
+ state.requestBytes += frame.payload.length;
253
+ if (state.receiveCredit < 0 || state.requestBytes > 32 * 1024 * 1024) {
254
+ this.abort(frame.id);
255
+ return;
256
+ }
257
+ if (state.request.write(frame.payload))
258
+ this.refund(frame.id, state, frame.payload.length);
259
+ else {
260
+ state.pendingCredit += frame.payload.length;
261
+ if (!state.drainArmed) {
262
+ state.drainArmed = true;
263
+ state.request.once("drain", () => {
264
+ state.drainArmed = false;
265
+ if (!this.streams.has(frame.id))
266
+ return;
267
+ this.refund(frame.id, state, state.pendingCredit);
268
+ state.pendingCredit = 0;
269
+ });
270
+ }
271
+ }
272
+ break;
273
+ }
274
+ case 4 /* Op.FIN */:
275
+ state.request.end();
276
+ state.requestEnded = true;
277
+ this.finishIfIdle(frame.id);
278
+ break;
279
+ case 5 /* Op.RESET */:
280
+ state.sender?.cancel();
281
+ state.request.destroy();
282
+ state.response?.destroy();
283
+ this.streams.delete(frame.id);
284
+ this.checkDrain();
285
+ break;
286
+ case 6 /* Op.CREDIT */:
287
+ state.sender?.addCredit(readCredit(frame.payload));
288
+ break;
289
+ default:
290
+ throw new Error("Unexpected V2 operation");
291
+ }
292
+ }
293
+ refund(id, state, amount) {
294
+ if (!amount)
295
+ return;
296
+ state.receiveCredit += amount;
297
+ this.send(creditFrame(id, amount));
298
+ }
299
+ finishIfIdle(id) {
300
+ const state = this.streams.get(id);
301
+ if (!state || !state.requestEnded || !state.responseEnded)
302
+ return;
303
+ this.streams.delete(id);
304
+ this.checkDrain();
305
+ }
306
+ abort(id) {
307
+ const state = this.streams.get(id);
308
+ if (!state)
309
+ return;
310
+ this.streams.delete(id);
311
+ state.sender?.cancel();
312
+ state.request.destroy();
313
+ state.response?.destroy();
314
+ this.send(metaFrame(5 /* Op.RESET */, id, { reason: "local_failure" }));
315
+ this.checkDrain();
316
+ }
317
+ send(buffer) {
318
+ if (this.socket?.readyState !== WebSocket.OPEN)
319
+ return;
320
+ if (this.socket.bufferedAmount > 8 * 1024 * 1024) {
321
+ this.socket.terminate();
322
+ return;
323
+ }
324
+ this.socket.send(buffer, { binary: true });
325
+ }
326
+ checkDrain() {
327
+ if (this.streams.size || !this.closeWhenIdle)
328
+ return;
329
+ this.socket?.close();
330
+ }
331
+ destroyStreams() {
332
+ for (const state of this.streams.values()) {
333
+ state.sender?.cancel();
334
+ state.request.destroy();
335
+ state.response?.destroy();
336
+ }
337
+ this.streams.clear();
338
+ }
339
+ clearReadyTimer() {
340
+ if (this.readyTimer)
341
+ clearTimeout(this.readyTimer);
342
+ this.readyTimer = null;
343
+ }
344
+ clearTimers() {
345
+ this.clearReadyTimer();
346
+ if (this.heartbeatTimer)
347
+ clearInterval(this.heartbeatTimer);
348
+ this.heartbeatTimer = null;
349
+ this.awaitingPong = false;
350
+ }
351
+ startHeartbeat(socket) {
352
+ if (this.heartbeatTimer)
353
+ clearInterval(this.heartbeatTimer);
354
+ this.heartbeatTimer = setInterval(() => {
355
+ if (this.socket !== socket || socket.readyState !== WebSocket.OPEN)
356
+ return;
357
+ if (this.awaitingPong) {
358
+ this.fail(socket, new Error("V2 heartbeat timed out"));
359
+ return;
360
+ }
361
+ this.awaitingPong = true;
362
+ socket.ping();
363
+ }, 20000);
364
+ this.heartbeatTimer.unref();
365
+ }
366
+ fail(socket, error) {
367
+ if (this.socket !== socket)
368
+ return;
369
+ this.callbacks.onError(error);
370
+ this.clearTimers();
371
+ this.socket = null;
372
+ socket.terminate();
373
+ }
374
+ scheduleReconnect() {
375
+ if (this.stopped || this.reconnectTimer)
376
+ return;
377
+ const delay = Math.min(30000, 500 * 2 ** Math.min(this.attempts++, 6)) *
378
+ (0.8 + Math.random() * 0.4);
379
+ this.reconnectTimer = setTimeout(() => {
380
+ this.reconnectTimer = null;
381
+ this.connect();
382
+ }, delay);
383
+ this.reconnectTimer.unref();
384
+ }
385
+ }
386
+ function toBuffer(raw) {
387
+ return Buffer.isBuffer(raw)
388
+ ? raw
389
+ : Array.isArray(raw)
390
+ ? Buffer.concat(raw)
391
+ : Buffer.from(raw);
392
+ }
393
+ function signChallenge(nonce, lease, privateKey) {
394
+ const key = createPrivateKey({
395
+ key: Buffer.from(privateKey, "base64url"),
396
+ format: "der",
397
+ type: "pkcs8",
398
+ });
399
+ return sign(null, Buffer.from(`${V2_PROTOCOL}\n${nonce}\n${lease}`), key).toString("base64url");
400
+ }
401
+ function publicHost(lease, service) {
402
+ const base = service === "mcp" ? lease.mcpBaseUrl : lease.webBaseUrl;
403
+ if (!base)
404
+ throw new Error("Missing public URL");
405
+ return new URL(base).host;
406
+ }
407
+ function allowedPath(service, path) {
408
+ const p = new URL(path, "http://localhost").pathname.toLowerCase();
409
+ const mcp = p === "/mcp" || p.startsWith("/mcp/");
410
+ const oauth = p.startsWith("/oauth/") || p.startsWith("/.well-known/oauth-");
411
+ return service === "mcp" ? mcp || oauth : !mcp && !oauth;
412
+ }
413
+ function responseHeaders(headers) {
414
+ const blocked = new Set([
415
+ "connection",
416
+ "keep-alive",
417
+ "transfer-encoding",
418
+ "upgrade",
419
+ ]);
420
+ return Object.fromEntries(Object.entries(headers)
421
+ .filter(([key, value]) => value !== undefined && !blocked.has(key.toLowerCase()))
422
+ .map(([key, value]) => [
423
+ key,
424
+ Array.isArray(value) ? value.join(", ") : String(value),
425
+ ]));
426
+ }
@@ -1,6 +1,7 @@
1
1
  export interface BackendProcess {
2
2
  readonly pid: number | null;
3
3
  write(data: string): void;
4
+ resize?(cols: number, rows: number): void;
4
5
  kill(signal?: NodeJS.Signals): void;
5
6
  onStdout(listener: (chunk: Buffer) => void): void;
6
7
  onStderr(listener: (chunk: Buffer) => void): void;
@@ -14,5 +15,7 @@ export interface ProcessBackend {
14
15
  start(command: string, args: string[], options: {
15
16
  cwd: string;
16
17
  env: NodeJS.ProcessEnv;
18
+ cols?: number;
19
+ rows?: number;
17
20
  }): Promise<BackendProcess>;
18
21
  }
@@ -4,5 +4,7 @@ export declare class PtyProcessBackend implements ProcessBackend {
4
4
  start(command: string, args: string[], options: {
5
5
  cwd: string;
6
6
  env: NodeJS.ProcessEnv;
7
+ cols?: number;
8
+ rows?: number;
7
9
  }): Promise<BackendProcess>;
8
10
  }
@@ -12,8 +12,8 @@ export class PtyProcessBackend {
12
12
  const env = Object.fromEntries(Object.entries(options.env).filter((entry) => typeof entry[1] === "string"));
13
13
  const processHandle = pty.spawn(command, args, {
14
14
  name: env.TERM ?? "xterm-256color",
15
- cols: 120,
16
- rows: 30,
15
+ cols: options.cols ?? 120,
16
+ rows: options.rows ?? 30,
17
17
  cwd: options.cwd,
18
18
  env,
19
19
  });
@@ -31,6 +31,9 @@ class PtyBackendProcess {
31
31
  write(data) {
32
32
  this.processHandle.write(data);
33
33
  }
34
+ resize(cols, rows) {
35
+ this.processHandle.resize(cols, rows);
36
+ }
34
37
  kill(signal = "SIGTERM") {
35
38
  if (process.platform === "win32") {
36
39
  this.processHandle.kill();
@@ -0,0 +1,4 @@
1
+ /** Select the OS default interactive shell for a local terminal session. */
2
+ export declare function defaultTerminalShell(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
3
+ /** macOS Terminal starts an interactive login shell; match its PATH initialization. */
4
+ export declare function defaultTerminalArgs(platform?: NodeJS.Platform): string[];
@@ -0,0 +1,10 @@
1
+ /** Select the OS default interactive shell for a local terminal session. */
2
+ export function defaultTerminalShell(platform = process.platform, env = process.env) {
3
+ return platform === "win32"
4
+ ? env.ComSpec || env.COMSPEC || "cmd.exe"
5
+ : env.SHELL || "/bin/sh";
6
+ }
7
+ /** macOS Terminal starts an interactive login shell; match its PATH initialization. */
8
+ export function defaultTerminalArgs(platform = process.platform) {
9
+ return platform === "darwin" ? ["-l"] : [];
10
+ }