@zhanguym/chatroom 1.1.0-beta.1 → 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 (90) 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-AdAmmDn5.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-D-igEnka.css → WorkspaceMediaPreview-CwtP9xWT.css} +1 -1
  52. package/dist/web/assets/WorkspaceMediaPreview-ctIyG3Ru.js +4 -0
  53. package/dist/web/assets/WorkspacePdfPreview-BtZkUHzV.css +1 -0
  54. package/dist/web/assets/WorkspacePdfPreview-CeizxEAH.js +55 -0
  55. package/dist/web/assets/WorkspacesView-DcmqHGvr.js +2 -0
  56. package/dist/web/assets/index-BRI_re-n.css +1 -0
  57. package/dist/web/assets/index-DTyxBHlJ.js +19 -0
  58. package/dist/web/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
  59. package/dist/web/assets/{useMasterDetailFocus-BQAutDO9.js → useMasterDetailFocus-DR5N95_A.js} +1 -1
  60. package/dist/web/assets/useOperationLabels-Dou4m0ac.js +1 -0
  61. package/dist/web/index.html +2 -2
  62. package/dist/web/transport-sw.js +411 -0
  63. package/package.json +10 -4
  64. package/scripts/ensure-pty-helper.mjs +28 -0
  65. package/dist/web/assets/AgentView-BwL3S7sb.js +0 -4
  66. package/dist/web/assets/CloudView-DH4sOXJr.js +0 -1
  67. package/dist/web/assets/CloudView-DVVvwuWW.css +0 -1
  68. package/dist/web/assets/CodeViewer.vue_vue_type_script_setup_true_lang-BaC1eGzG.js +0 -3
  69. package/dist/web/assets/ComputerView-BJSjCxFr.js +0 -1
  70. package/dist/web/assets/McpToolsView-CPaWUG9h.js +0 -1
  71. package/dist/web/assets/OperationsView-D2FtLkHn.js +0 -4
  72. package/dist/web/assets/ProcessesView-BHOsaM2H.js +0 -4
  73. package/dist/web/assets/ProcessesView-Ce6iVwjA.css +0 -1
  74. package/dist/web/assets/StateChip.vue_vue_type_script_setup_true_lang-iW1SYlX_.js +0 -1
  75. package/dist/web/assets/SystemLogsView-By5ijObd.js +0 -1
  76. package/dist/web/assets/SystemLogsView-CofP4Jvi.css +0 -1
  77. package/dist/web/assets/VSelect-CLnwhyWJ.js +0 -1
  78. package/dist/web/assets/VSpacer-dDRRKhUn.js +0 -1
  79. package/dist/web/assets/VSwitch-CGzxwZuj.js +0 -1
  80. package/dist/web/assets/VTabs-7aSwjrKs.js +0 -1
  81. package/dist/web/assets/VTextarea-D1_2FwMe.js +0 -1
  82. package/dist/web/assets/WorkspaceDownloadPreview-BjuqUdB9.js +0 -1
  83. package/dist/web/assets/WorkspaceDownloadPreview-DnxhH5Q7.css +0 -1
  84. package/dist/web/assets/WorkspaceMediaPreview-LwXeVOY2.js +0 -3
  85. package/dist/web/assets/WorkspacesView-DHl28eWf.js +0 -2
  86. package/dist/web/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  87. package/dist/web/assets/index-BzRlodie.js +0 -19
  88. package/dist/web/assets/index-CZ-4tJlF.css +0 -1
  89. package/dist/web/assets/useOperationLabels-C-zgM_ET.js +0 -1
  90. package/dist/web/assets/utils-rtPwWNel.js +0 -1
@@ -1,3 +1,4 @@
1
+ // Restrict environment inheritance for MCP-controlled and other supervised processes.
1
2
  const CHILD_ENV_KEYS = new Set([
2
3
  "PATH",
3
4
  "HOME",
@@ -25,12 +26,12 @@ const CHILD_ENV_KEYS = new Set([
25
26
  "USERPROFILE",
26
27
  "LOCALAPPDATA",
27
28
  "APPDATA",
28
- ]);
29
+ ].map((key) => key.toUpperCase()));
29
30
  export function childEnvironment(overrides) {
30
31
  const env = {};
31
32
  for (const [key, value] of Object.entries(process.env)) {
32
33
  if (value !== undefined &&
33
- (CHILD_ENV_KEYS.has(key) || key.startsWith("LC_")))
34
+ (CHILD_ENV_KEYS.has(key.toUpperCase()) || key.startsWith("LC_")))
34
35
  env[key] = value;
35
36
  }
36
37
  for (const [key, value] of Object.entries(overrides ?? {}))
@@ -53,7 +53,7 @@ export class HttpServer {
53
53
  app.use(express.urlencoded({ extended: false, limit: "64kb" }));
54
54
  app.use(createOAuthRouter(this.auth, this.ingress, this.logs));
55
55
  app.use("/api", webMutationOrigin(this.ingress));
56
- app.use("/api", createApiRouter(this.application, this.eventBus, this.auth, this.passkeys, this.ingress, this.cloud, this.logs, () => ({
56
+ app.use("/api", createApiRouter(this.application, this.eventBus, this.auth, this.passkeys, this.ingress, this.cloud, this.logs, this.config, () => ({
57
57
  version: CHATROOM_VERSION,
58
58
  mcpRequests: this.mcpRequestCount,
59
59
  uptimeMinutes: Math.floor((Date.now() - this.startedAt) / 60000),
@@ -74,6 +74,12 @@ export class HttpServer {
74
74
  const webIndexPath = path.join(webRoot, "index.html");
75
75
  if (existsSync(webIndexPath)) {
76
76
  const webIndex = readFileSync(webIndexPath, "utf8");
77
+ app.get("/transport-sw.js", (_req, res) => {
78
+ res.setHeader("Cache-Control", "no-store");
79
+ res.type("application/javascript").sendFile("transport-sw.js", {
80
+ root: webRoot,
81
+ });
82
+ });
77
83
  app.use("/assets", express.static(path.join(webRoot, "assets"), {
78
84
  index: false,
79
85
  maxAge: "1y",
@@ -1,84 +1,2 @@
1
- import { z } from "zod";
2
- import { type CloudLeaseState } from "./types.js";
3
- interface TunnelTimings {
4
- readyTimeoutMs: number;
5
- heartbeatIntervalMs: number;
6
- }
7
- declare const controlMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
8
- type: z.ZodLiteral<"challenge">;
9
- nonce: z.ZodString;
10
- }, z.core.$strict>, z.ZodObject<{
11
- type: z.ZodLiteral<"ready">;
12
- }, z.core.$strict>, z.ZodObject<{
13
- type: z.ZodLiteral<"open">;
14
- streamId: z.ZodNumber;
15
- service: z.ZodEnum<{
16
- mcp: "mcp";
17
- web: "web";
18
- }>;
19
- method: z.ZodString;
20
- path: z.ZodString;
21
- headers: z.ZodRecord<z.ZodString, z.ZodString>;
22
- }, z.core.$strict>, z.ZodObject<{
23
- type: z.ZodLiteral<"end">;
24
- streamId: z.ZodNumber;
25
- }, z.core.$strict>, z.ZodObject<{
26
- type: z.ZodLiteral<"abort">;
27
- streamId: z.ZodNumber;
28
- }, z.core.$strict>, z.ZodObject<{
29
- type: z.ZodLiteral<"pause">;
30
- streamId: z.ZodNumber;
31
- }, z.core.$strict>, z.ZodObject<{
32
- type: z.ZodLiteral<"resume">;
33
- streamId: z.ZodNumber;
34
- }, z.core.$strict>], "type">;
35
- type ControlMessage = z.infer<typeof controlMessageSchema>;
36
- export declare function parseTunnelControlMessage(input: string): ControlMessage;
37
- export declare class CloudTunnelClient {
38
- private lease;
39
- private readonly identity;
40
- private readonly local;
41
- private readonly callbacks;
42
- private socket;
43
- private reconnectTimer;
44
- private readyTimer;
45
- private heartbeatTimer;
46
- private awaitingPong;
47
- private stopped;
48
- private attempts;
49
- private closeWhenIdle;
50
- private acceptingServices;
51
- private readonly streams;
52
- private readonly timings;
53
- constructor(lease: CloudLeaseState, identity: {
54
- devicePrivateKey: string;
55
- }, local: {
56
- host: string;
57
- port: number;
58
- }, callbacks: {
59
- onConnected(): void;
60
- onDisconnected(): void;
61
- onLeaseRejected(): void;
62
- onError(error: Error): void;
63
- }, timings?: Partial<TunnelTimings>);
64
- start(): void;
65
- stop(): void;
66
- drainAndStop(): void;
67
- updateLease(lease: CloudLeaseState): void;
68
- private connect;
69
- private handleControl;
70
- private openStream;
71
- private finishStream;
72
- private pauseForBackpressure;
73
- private destroyAllStreams;
74
- private handleBinary;
75
- private send;
76
- private sendBinary;
77
- private startReadyTimeout;
78
- private clearReadyTimeout;
79
- private startHeartbeat;
80
- private clearSocketTimers;
81
- private failSocket;
82
- private scheduleReconnect;
83
- }
84
- export {};
1
+ /** The device speaks V2; only Cloud Server carries the removable V1 adapter. */
2
+ export { V2TunnelClient as CloudTunnelClient } from "./v2/tunnel-client.js";
@@ -1,426 +1,2 @@
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 { cloudServiceForPublicService, } from "./types.js";
6
- const PROTOCOL = "chatroom-tunnel-v1";
7
- const HIGH_WATER = 8 * 1024 * 1024;
8
- const LOW_WATER = 2 * 1024 * 1024;
9
- const READY_TIMEOUT_MS = 10_000;
10
- const HEARTBEAT_INTERVAL_MS = 20_000;
11
- const streamIdSchema = z.number().int().min(0).max(0xffff_ffff);
12
- const controlMessageSchema = z.discriminatedUnion("type", [
13
- z
14
- .object({
15
- type: z.literal("challenge"),
16
- nonce: z.string().min(1),
17
- })
18
- .strict(),
19
- z.object({ type: z.literal("ready") }).strict(),
20
- z
21
- .object({
22
- type: z.literal("open"),
23
- streamId: streamIdSchema,
24
- service: z.enum(["mcp", "web"]),
25
- method: z.string().min(1),
26
- path: z.string().startsWith("/"),
27
- headers: z.record(z.string(), z.string()),
28
- })
29
- .strict(),
30
- z.object({ type: z.literal("end"), streamId: streamIdSchema }).strict(),
31
- z.object({ type: z.literal("abort"), streamId: streamIdSchema }).strict(),
32
- z.object({ type: z.literal("pause"), streamId: streamIdSchema }).strict(),
33
- z.object({ type: z.literal("resume"), streamId: streamIdSchema }).strict(),
34
- ]);
35
- export function parseTunnelControlMessage(input) {
36
- return controlMessageSchema.parse(JSON.parse(input));
37
- }
38
- export class CloudTunnelClient {
39
- lease;
40
- identity;
41
- local;
42
- callbacks;
43
- socket = null;
44
- reconnectTimer = null;
45
- readyTimer = null;
46
- heartbeatTimer = null;
47
- awaitingPong = false;
48
- stopped = true;
49
- attempts = 0;
50
- closeWhenIdle = null;
51
- acceptingServices;
52
- streams = new Map();
53
- timings;
54
- constructor(lease, identity, local, callbacks, timings = {}) {
55
- this.lease = lease;
56
- this.identity = identity;
57
- this.local = local;
58
- this.callbacks = callbacks;
59
- this.acceptingServices = new Set(lease.services);
60
- this.timings = {
61
- readyTimeoutMs: timings.readyTimeoutMs ?? READY_TIMEOUT_MS,
62
- heartbeatIntervalMs: timings.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS,
63
- };
64
- }
65
- start() {
66
- if (!this.stopped)
67
- return;
68
- this.stopped = false;
69
- this.closeWhenIdle = null;
70
- this.acceptingServices = new Set(this.lease.services);
71
- this.connect();
72
- }
73
- stop() {
74
- this.stopped = true;
75
- this.closeWhenIdle = null;
76
- this.acceptingServices.clear();
77
- if (this.reconnectTimer)
78
- clearTimeout(this.reconnectTimer);
79
- this.reconnectTimer = null;
80
- this.clearSocketTimers();
81
- this.socket?.close();
82
- this.socket = null;
83
- this.destroyAllStreams();
84
- }
85
- drainAndStop() {
86
- this.stopped = true;
87
- this.acceptingServices.clear();
88
- if (this.reconnectTimer)
89
- clearTimeout(this.reconnectTimer);
90
- this.reconnectTimer = null;
91
- this.clearSocketTimers();
92
- if (this.streams.size === 0) {
93
- this.closeWhenIdle = null;
94
- this.socket?.close();
95
- return;
96
- }
97
- this.closeWhenIdle = "stop";
98
- }
99
- updateLease(lease) {
100
- const tunnelChanged = lease.tunnelUrl !== this.lease.tunnelUrl;
101
- const removesService = this.lease.services.some((service) => !lease.services.includes(service));
102
- this.lease = lease;
103
- this.acceptingServices = new Set(lease.services);
104
- if (this.stopped)
105
- return;
106
- if (!tunnelChanged) {
107
- if (this.socket?.readyState === WebSocket.OPEN)
108
- this.send({ type: "update-lease", lease: lease.token });
109
- return;
110
- }
111
- if (!removesService || this.streams.size === 0) {
112
- this.socket?.close();
113
- return;
114
- }
115
- this.closeWhenIdle = "reconnect";
116
- }
117
- connect() {
118
- if (this.stopped || this.socket)
119
- return;
120
- const socket = new WebSocket(this.lease.tunnelUrl, PROTOCOL);
121
- this.socket = socket;
122
- socket.binaryType = "nodebuffer";
123
- this.startReadyTimeout(socket);
124
- socket.on("message", (data, isBinary) => {
125
- try {
126
- if (isBinary)
127
- this.handleBinary(Buffer.from(data));
128
- else
129
- this.handleControl(parseTunnelControlMessage(data.toString()));
130
- }
131
- catch (error) {
132
- this.callbacks.onError(error instanceof Error ? error : new Error(String(error)));
133
- socket.close();
134
- }
135
- });
136
- socket.on("pong", () => {
137
- if (this.socket === socket)
138
- this.awaitingPong = false;
139
- });
140
- socket.on("close", (code) => {
141
- if (this.socket === socket) {
142
- this.clearSocketTimers();
143
- this.socket = null;
144
- }
145
- this.closeWhenIdle = null;
146
- this.destroyAllStreams();
147
- this.callbacks.onDisconnected();
148
- if (this.stopped)
149
- return;
150
- if (code === 4003) {
151
- this.stopped = true;
152
- this.callbacks.onLeaseRejected();
153
- return;
154
- }
155
- this.scheduleReconnect();
156
- });
157
- socket.on("error", (error) => this.failSocket(socket, error));
158
- }
159
- handleControl(message) {
160
- if (message.type === "challenge") {
161
- this.send({
162
- type: "authenticate",
163
- lease: this.lease.token,
164
- signature: signChallenge(message.nonce, this.lease.token, this.identity.devicePrivateKey),
165
- });
166
- return;
167
- }
168
- if (message.type === "ready") {
169
- this.attempts = 0;
170
- this.clearReadyTimeout();
171
- if (this.socket)
172
- this.startHeartbeat(this.socket);
173
- this.callbacks.onConnected();
174
- return;
175
- }
176
- if (message.type === "open") {
177
- this.openStream(message);
178
- return;
179
- }
180
- const stream = this.streams.get(message.streamId);
181
- if (!stream)
182
- return;
183
- if (message.type === "end")
184
- stream.request.end();
185
- else if (message.type === "abort") {
186
- stream.request.destroy();
187
- stream.response?.destroy();
188
- this.finishStream(message.streamId);
189
- }
190
- else if (message.type === "pause")
191
- stream.response?.pause();
192
- else if (message.type === "resume")
193
- stream.response?.resume();
194
- }
195
- openStream(message) {
196
- if (this.streams.has(message.streamId))
197
- throw new Error(`Duplicate tunnel stream id: ${message.streamId}`);
198
- const service = cloudServiceForPublicService(message.service);
199
- if (!this.acceptingServices.has(service) ||
200
- !allowedPath(message.service, message.path)) {
201
- this.send({
202
- type: "response",
203
- streamId: message.streamId,
204
- status: 404,
205
- headers: { "content-type": "text/plain" },
206
- });
207
- this.send({ type: "response-end", streamId: message.streamId });
208
- return;
209
- }
210
- const headers = sanitizeHeaders(message.headers);
211
- headers.host = publicHost(this.lease, message.service);
212
- const request = http.request({
213
- host: this.local.host,
214
- port: this.local.port,
215
- method: message.method,
216
- path: message.path,
217
- headers,
218
- }, (response) => {
219
- const stream = this.streams.get(message.streamId);
220
- if (stream)
221
- stream.response = response;
222
- this.send({
223
- type: "response",
224
- streamId: message.streamId,
225
- status: response.statusCode ?? 502,
226
- headers: responseHeaders(response.headers),
227
- });
228
- response.on("data", (chunk) => {
229
- if (!this.sendBinary(message.streamId, Buffer.from(chunk)))
230
- this.pauseForBackpressure(message.streamId, response);
231
- });
232
- response.on("end", () => {
233
- this.send({ type: "response-end", streamId: message.streamId });
234
- this.finishStream(message.streamId);
235
- });
236
- response.on("error", () => {
237
- this.send({ type: "abort", streamId: message.streamId });
238
- this.finishStream(message.streamId);
239
- });
240
- });
241
- request.on("drain", () => this.send({ type: "resume", streamId: message.streamId }));
242
- request.on("error", () => {
243
- this.send({
244
- type: "response",
245
- streamId: message.streamId,
246
- status: 502,
247
- headers: {},
248
- });
249
- this.send({ type: "response-end", streamId: message.streamId });
250
- this.finishStream(message.streamId);
251
- });
252
- this.streams.set(message.streamId, {
253
- request,
254
- response: null,
255
- backpressureTimer: null,
256
- });
257
- }
258
- finishStream(streamId) {
259
- const stream = this.streams.get(streamId);
260
- if (stream?.backpressureTimer)
261
- clearInterval(stream.backpressureTimer);
262
- this.streams.delete(streamId);
263
- if (this.streams.size !== 0 || !this.closeWhenIdle)
264
- return;
265
- const action = this.closeWhenIdle;
266
- this.closeWhenIdle = null;
267
- if (action === "stop") {
268
- this.socket?.close();
269
- return;
270
- }
271
- this.socket?.close();
272
- }
273
- pauseForBackpressure(streamId, response) {
274
- response.pause();
275
- const stream = this.streams.get(streamId);
276
- if (!stream || stream.backpressureTimer)
277
- return;
278
- stream.backpressureTimer = setInterval(() => {
279
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
280
- if (stream.backpressureTimer)
281
- clearInterval(stream.backpressureTimer);
282
- stream.backpressureTimer = null;
283
- return;
284
- }
285
- if (this.socket.bufferedAmount <= LOW_WATER) {
286
- if (stream.backpressureTimer)
287
- clearInterval(stream.backpressureTimer);
288
- stream.backpressureTimer = null;
289
- response.resume();
290
- }
291
- }, 10);
292
- stream.backpressureTimer.unref();
293
- }
294
- destroyAllStreams() {
295
- for (const stream of this.streams.values()) {
296
- if (stream.backpressureTimer)
297
- clearInterval(stream.backpressureTimer);
298
- stream.request.destroy();
299
- stream.response?.destroy();
300
- }
301
- this.streams.clear();
302
- }
303
- handleBinary(frame) {
304
- if (frame.length < 4)
305
- throw new Error("Invalid tunnel data frame");
306
- const streamId = frame.readUInt32BE(0);
307
- const stream = this.streams.get(streamId);
308
- if (!stream)
309
- return;
310
- if (!stream.request.write(frame.subarray(4)))
311
- this.send({ type: "pause", streamId });
312
- }
313
- send(message) {
314
- if (this.socket?.readyState === WebSocket.OPEN)
315
- this.socket.send(JSON.stringify(message));
316
- }
317
- sendBinary(streamId, chunk) {
318
- const socket = this.socket;
319
- if (!socket || socket.readyState !== WebSocket.OPEN)
320
- return false;
321
- const frame = Buffer.allocUnsafe(4 + chunk.length);
322
- frame.writeUInt32BE(streamId, 0);
323
- chunk.copy(frame, 4);
324
- socket.send(frame, { binary: true });
325
- return socket.bufferedAmount < HIGH_WATER;
326
- }
327
- startReadyTimeout(socket) {
328
- this.clearReadyTimeout();
329
- this.readyTimer = setTimeout(() => {
330
- this.readyTimer = null;
331
- this.failSocket(socket, new Error("Tunnel connection timed out"));
332
- }, this.timings.readyTimeoutMs);
333
- this.readyTimer.unref();
334
- }
335
- clearReadyTimeout() {
336
- if (this.readyTimer)
337
- clearTimeout(this.readyTimer);
338
- this.readyTimer = null;
339
- }
340
- startHeartbeat(socket) {
341
- if (this.heartbeatTimer)
342
- clearInterval(this.heartbeatTimer);
343
- this.awaitingPong = false;
344
- this.heartbeatTimer = setInterval(() => {
345
- if (this.socket !== socket || socket.readyState !== WebSocket.OPEN)
346
- return;
347
- if (this.awaitingPong) {
348
- this.failSocket(socket, new Error("Tunnel heartbeat timed out"));
349
- return;
350
- }
351
- this.awaitingPong = true;
352
- socket.ping();
353
- }, this.timings.heartbeatIntervalMs);
354
- this.heartbeatTimer.unref();
355
- }
356
- clearSocketTimers() {
357
- this.clearReadyTimeout();
358
- if (this.heartbeatTimer)
359
- clearInterval(this.heartbeatTimer);
360
- this.heartbeatTimer = null;
361
- this.awaitingPong = false;
362
- }
363
- failSocket(socket, error) {
364
- if (this.socket !== socket)
365
- return;
366
- this.callbacks.onError(error);
367
- this.clearSocketTimers();
368
- this.socket = null;
369
- socket.terminate();
370
- }
371
- scheduleReconnect() {
372
- if (this.reconnectTimer || this.stopped)
373
- return;
374
- const delay = Math.min(30_000, 500 * 2 ** Math.min(this.attempts++, 6)) *
375
- (0.8 + Math.random() * 0.4);
376
- this.reconnectTimer = setTimeout(() => {
377
- this.reconnectTimer = null;
378
- this.connect();
379
- }, delay);
380
- this.reconnectTimer.unref();
381
- }
382
- }
383
- function signChallenge(nonce, lease, privateKey) {
384
- const key = createPrivateKey({
385
- key: Buffer.from(privateKey, "base64url"),
386
- format: "der",
387
- type: "pkcs8",
388
- });
389
- return sign(null, Buffer.from(`chatroom-tunnel-v1\n${nonce}\n${lease}`), key).toString("base64url");
390
- }
391
- function publicHost(lease, service) {
392
- const baseUrl = service === "mcp" ? lease.mcpBaseUrl : lease.webBaseUrl;
393
- if (!baseUrl)
394
- throw new Error("Tunnel lease is missing " + service + " public base URL");
395
- return new URL(baseUrl).host;
396
- }
397
- function allowedPath(service, value) {
398
- const pathname = new URL(value, "http://localhost").pathname.toLowerCase();
399
- const mcpPath = pathname === "/mcp" || pathname.startsWith("/mcp/");
400
- const oauthPath = pathname.startsWith("/oauth/") ||
401
- pathname.startsWith("/.well-known/oauth-");
402
- if (service === "mcp")
403
- return mcpPath || oauthPath;
404
- return !mcpPath && !oauthPath;
405
- }
406
- function sanitizeHeaders(headers) {
407
- const blocked = new Set([
408
- "connection",
409
- "host",
410
- "proxy-connection",
411
- "keep-alive",
412
- "transfer-encoding",
413
- "upgrade",
414
- ]);
415
- return Object.fromEntries(Object.entries(headers).filter(([key]) => !blocked.has(key.toLowerCase())));
416
- }
417
- function responseHeaders(headers) {
418
- const result = {};
419
- for (const [key, value] of Object.entries(headers)) {
420
- if (value === undefined ||
421
- ["connection", "keep-alive", "transfer-encoding", "upgrade"].includes(key.toLowerCase()))
422
- continue;
423
- result[key] = Array.isArray(value) ? value.join(", ") : value;
424
- }
425
- return result;
426
- }
1
+ /** The device speaks V2; only Cloud Server carries the removable V1 adapter. */
2
+ export { V2TunnelClient as CloudTunnelClient } from "./v2/tunnel-client.js";
@@ -0,0 +1,48 @@
1
+ /** Tunnel V2 framing, independent of Node.js, WebSocket and WebRTC. */
2
+ export declare const V2_PROTOCOL = "chatroom-tunnel-v2";
3
+ export declare const DATA_SIZE: number;
4
+ export declare const INITIAL_CREDIT: number;
5
+ export declare const MAX_META: number;
6
+ export declare const MAX_QUEUED: number;
7
+ export declare const enum Op {
8
+ OPEN = 1,
9
+ META = 2,
10
+ DATA = 3,
11
+ FIN = 4,
12
+ RESET = 5,
13
+ CREDIT = 6
14
+ }
15
+ export interface Frame {
16
+ op: Op;
17
+ id: number;
18
+ payload: Uint8Array;
19
+ }
20
+ export declare function encodeFrame(op: Op, id: number, payload?: Uint8Array): Uint8Array;
21
+ export declare function decodeFrame(raw: Uint8Array): Frame;
22
+ export declare function readCredit(payload: Uint8Array): number;
23
+ export declare function creditFrame(id: number, amount: number): Uint8Array;
24
+ export declare class CreditSender {
25
+ private readonly id;
26
+ private readonly send;
27
+ private readonly pause;
28
+ private readonly resume;
29
+ private readonly onFinished;
30
+ private readonly canSend;
31
+ private credit;
32
+ private readonly chunks;
33
+ private queued;
34
+ private ending;
35
+ private finished;
36
+ private cancelled;
37
+ private wakeup;
38
+ constructor(id: number, send: (frame: Uint8Array) => void, pause: () => void, resume: () => void, onFinished?: () => void, canSend?: () => boolean);
39
+ enqueue(chunk: Uint8Array): void;
40
+ addCredit(amount: number): void;
41
+ end(): void;
42
+ cancel(): void;
43
+ /** Called when the physical transport signals that its send buffer drained. */
44
+ writable(): void;
45
+ private flush;
46
+ }
47
+ export declare function parseMeta<T>(payload: Uint8Array): T;
48
+ export declare function metaFrame(op: Op.OPEN | Op.META | Op.RESET, id: number, value: unknown): Uint8Array;