@dev-anywhere/relay 0.1.9 → 0.2.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.
- package/dist/chaos.d.ts +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/{chunk-WVFC2CEL.js → chunk-SM2GRUCV.js} +739 -741
- package/dist/chunk-SM2GRUCV.js.map +1 -0
- package/dist/handlers/client.d.ts +1 -1
- package/dist/handlers/client.d.ts.map +1 -1
- package/dist/handlers/proxy.d.ts +1 -1
- package/dist/handlers/proxy.d.ts.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/index.js +142 -4
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +0 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/router.d.ts +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/runtime-env.d.ts +3 -1
- package/dist/runtime-env.d.ts.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-WVFC2CEL.js.map +0 -1
|
@@ -4,373 +4,85 @@
|
|
|
4
4
|
import express from "express";
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import { createServer } from "http";
|
|
7
|
-
import { homedir
|
|
7
|
+
import { homedir } from "os";
|
|
8
8
|
import { dirname as dirname2, resolve } from "path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
11
11
|
|
|
12
12
|
// src/registry.ts
|
|
13
13
|
import { WebSocket } from "ws";
|
|
14
|
-
var RelayRegistry = class {
|
|
15
|
-
proxyStates = /* @__PURE__ */ new Map();
|
|
16
|
-
clientBindings = /* @__PURE__ */ new Map();
|
|
17
|
-
connectedClients = /* @__PURE__ */ new Set();
|
|
18
|
-
registerProxy(proxyId, ws, name) {
|
|
19
|
-
const existing = this.proxyStates.get(proxyId);
|
|
20
|
-
if (existing) {
|
|
21
|
-
if (existing.ws && existing.ws.readyState === WebSocket.OPEN) {
|
|
22
|
-
existing.ws.terminate();
|
|
23
|
-
}
|
|
24
|
-
existing.ws = ws;
|
|
25
|
-
existing.connectionState = "online";
|
|
26
|
-
existing.disconnectedAt = null;
|
|
27
|
-
if (name !== void 0) existing.name = name;
|
|
28
|
-
return "reconnected";
|
|
29
|
-
}
|
|
30
|
-
this.proxyStates.set(proxyId, {
|
|
31
|
-
ws,
|
|
32
|
-
connectionState: "online",
|
|
33
|
-
sessions: /* @__PURE__ */ new Set(),
|
|
34
|
-
disconnectedAt: null,
|
|
35
|
-
name
|
|
36
|
-
});
|
|
37
|
-
return "new";
|
|
38
|
-
}
|
|
39
|
-
// 标记 proxy 离线,保留所有状态等待重连,不设超时
|
|
40
|
-
// 清理只在 proxy 主动退出(proxy_disconnect)或 relay 启动清理废弃数据时发生
|
|
41
|
-
markProxyOffline(proxyId) {
|
|
42
|
-
const state = this.proxyStates.get(proxyId);
|
|
43
|
-
if (!state) return;
|
|
44
|
-
state.ws = null;
|
|
45
|
-
state.connectionState = "offline";
|
|
46
|
-
state.disconnectedAt = Date.now();
|
|
47
|
-
}
|
|
48
|
-
// 显式状态转换,校验 from 状态匹配后更新 connectionState
|
|
49
|
-
transitionProxy(proxyId, from, to) {
|
|
50
|
-
if (from === to) {
|
|
51
|
-
throw new Error(`Invalid proxy transition: ${from} -> ${to} (same state)`);
|
|
52
|
-
}
|
|
53
|
-
const state = this.proxyStates.get(proxyId);
|
|
54
|
-
if (!state) {
|
|
55
|
-
throw new Error(`Proxy not found: ${proxyId}`);
|
|
56
|
-
}
|
|
57
|
-
if (state.connectionState !== from) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Proxy ${proxyId} state mismatch: expected ${from}, actual ${state.connectionState}`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
state.connectionState = to;
|
|
63
|
-
if (to === "offline") {
|
|
64
|
-
state.ws = null;
|
|
65
|
-
state.disconnectedAt = Date.now();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// 显式客户端状态转换,校验 from 状态匹配后更新 connectionState
|
|
69
|
-
transitionClient(clientId, from, to) {
|
|
70
|
-
if (from === to) {
|
|
71
|
-
throw new Error(`Invalid client transition: ${from} -> ${to} (same state)`);
|
|
72
|
-
}
|
|
73
|
-
const binding = this.clientBindings.get(clientId);
|
|
74
|
-
if (!binding) {
|
|
75
|
-
throw new Error(`Client not found: ${clientId}`);
|
|
76
|
-
}
|
|
77
|
-
if (binding.connectionState !== from) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`Client ${clientId} state mismatch: expected ${from}, actual ${binding.connectionState}`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
binding.connectionState = to;
|
|
83
|
-
}
|
|
84
|
-
getProxyConnectionState(proxyId) {
|
|
85
|
-
return this.proxyStates.get(proxyId)?.connectionState;
|
|
86
|
-
}
|
|
87
|
-
getClientConnectionState(clientId) {
|
|
88
|
-
return this.clientBindings.get(clientId)?.connectionState;
|
|
89
|
-
}
|
|
90
|
-
// 彻底清理 proxy 状态和客户端绑定
|
|
91
|
-
cleanupProxy(proxyId) {
|
|
92
|
-
const state = this.proxyStates.get(proxyId);
|
|
93
|
-
if (!state) return;
|
|
94
|
-
for (const [clientId, binding] of this.clientBindings) {
|
|
95
|
-
if (binding.proxyId === proxyId) {
|
|
96
|
-
this.clientBindings.delete(clientId);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
this.proxyStates.delete(proxyId);
|
|
100
|
-
}
|
|
101
|
-
unregisterProxy(proxyId) {
|
|
102
|
-
this.cleanupProxy(proxyId);
|
|
103
|
-
}
|
|
104
|
-
getProxy(proxyId) {
|
|
105
|
-
const state = this.proxyStates.get(proxyId);
|
|
106
|
-
return state?.ws ?? void 0;
|
|
107
|
-
}
|
|
108
|
-
isProxyOnline(proxyId) {
|
|
109
|
-
const state = this.proxyStates.get(proxyId);
|
|
110
|
-
if (!state || state.connectionState !== "online") return false;
|
|
111
|
-
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return false;
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
// proxy 是否存在(含宽限期中的)
|
|
115
|
-
hasProxy(proxyId) {
|
|
116
|
-
return this.proxyStates.has(proxyId);
|
|
117
|
-
}
|
|
118
|
-
listProxies() {
|
|
119
|
-
return Array.from(this.proxyStates.keys());
|
|
120
|
-
}
|
|
121
|
-
// 返回 proxyId、name、online 的列表,用于 proxy_list_response
|
|
122
|
-
listProxiesWithName() {
|
|
123
|
-
return Array.from(this.proxyStates.entries()).map(([proxyId, state]) => ({
|
|
124
|
-
proxyId,
|
|
125
|
-
...state.name !== void 0 ? { name: state.name } : {},
|
|
126
|
-
online: state.connectionState === "online"
|
|
127
|
-
}));
|
|
128
|
-
}
|
|
129
|
-
getProxyName(proxyId) {
|
|
130
|
-
return this.proxyStates.get(proxyId)?.name;
|
|
131
|
-
}
|
|
132
|
-
// 将 sessionId 关联到 proxy
|
|
133
|
-
addSessionToProxy(proxyId, sessionId) {
|
|
134
|
-
const state = this.proxyStates.get(proxyId);
|
|
135
|
-
if (state) {
|
|
136
|
-
state.sessions.add(sessionId);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// 通过 sessionId 反查所属 proxyId
|
|
140
|
-
getProxyForSession(sessionId) {
|
|
141
|
-
for (const [proxyId, state] of this.proxyStates) {
|
|
142
|
-
if (state.sessions.has(sessionId)) {
|
|
143
|
-
return proxyId;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return void 0;
|
|
147
|
-
}
|
|
148
|
-
// 获取 proxy 关联的所有 sessionId
|
|
149
|
-
getSessionsForProxy(proxyId) {
|
|
150
|
-
const state = this.proxyStates.get(proxyId);
|
|
151
|
-
return state ? Array.from(state.sessions) : [];
|
|
152
|
-
}
|
|
153
|
-
// clientId 绑定方式
|
|
154
|
-
bindClientById(clientId, proxyId, ws) {
|
|
155
|
-
if (!this.proxyStates.has(proxyId)) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
this.clientBindings.set(clientId, { proxyId, ws, connectionState: "bound" });
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
updateClientSocket(clientId, ws) {
|
|
162
|
-
const binding = this.clientBindings.get(clientId);
|
|
163
|
-
if (binding) {
|
|
164
|
-
binding.ws = ws;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// 断开客户端 WebSocket 但保留绑定关系,重连时可恢复
|
|
168
|
-
unbindClientById(clientId) {
|
|
169
|
-
const binding = this.clientBindings.get(clientId);
|
|
170
|
-
if (binding) {
|
|
171
|
-
binding.ws = null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
getClientBinding(clientId) {
|
|
175
|
-
return this.clientBindings.get(clientId);
|
|
176
|
-
}
|
|
177
|
-
// 获取绑定到指定 proxy 的所有活跃客户端 WebSocket
|
|
178
|
-
getClientsForProxy(proxyId) {
|
|
179
|
-
const clients = [];
|
|
180
|
-
for (const [, binding] of this.clientBindings) {
|
|
181
|
-
if (binding.proxyId === proxyId && binding.ws && binding.ws.readyState === WebSocket.OPEN) {
|
|
182
|
-
clients.push(binding.ws);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return clients;
|
|
186
|
-
}
|
|
187
|
-
countClients() {
|
|
188
|
-
let count = 0;
|
|
189
|
-
for (const [, binding] of this.clientBindings) {
|
|
190
|
-
if (binding.ws) count++;
|
|
191
|
-
}
|
|
192
|
-
return count;
|
|
193
|
-
}
|
|
194
|
-
addClientWs(ws) {
|
|
195
|
-
this.connectedClients.add(ws);
|
|
196
|
-
}
|
|
197
|
-
removeClientWs(ws) {
|
|
198
|
-
this.connectedClients.delete(ws);
|
|
199
|
-
}
|
|
200
|
-
getAllClientWs() {
|
|
201
|
-
const clients = [];
|
|
202
|
-
for (const ws of this.connectedClients) {
|
|
203
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
204
|
-
clients.push(ws);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return clients;
|
|
208
|
-
}
|
|
209
|
-
// 获取单个 proxy 的详细状态信息
|
|
210
|
-
getProxyDetail(proxyId) {
|
|
211
|
-
const state = this.proxyStates.get(proxyId);
|
|
212
|
-
if (!state) return void 0;
|
|
213
|
-
return {
|
|
214
|
-
proxyId,
|
|
215
|
-
...state.name !== void 0 ? { name: state.name } : {},
|
|
216
|
-
online: state.connectionState === "online",
|
|
217
|
-
connectionState: state.connectionState,
|
|
218
|
-
sessions: Array.from(state.sessions),
|
|
219
|
-
disconnectedAt: state.disconnectedAt
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
// 获取所有客户端绑定的详细信息
|
|
223
|
-
getClientDetails() {
|
|
224
|
-
const details = [];
|
|
225
|
-
for (const [clientId, binding] of this.clientBindings) {
|
|
226
|
-
details.push({
|
|
227
|
-
clientId,
|
|
228
|
-
proxyId: binding.proxyId,
|
|
229
|
-
online: binding.ws !== null && binding.ws !== void 0 && binding.ws.readyState === WebSocket.OPEN,
|
|
230
|
-
connectionState: binding.connectionState
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
return details;
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// src/health.ts
|
|
238
|
-
import { Router } from "express";
|
|
239
|
-
|
|
240
|
-
// src/version.ts
|
|
241
|
-
import { readFileSync } from "fs";
|
|
242
|
-
import { dirname, join } from "path";
|
|
243
|
-
import { fileURLToPath } from "url";
|
|
244
|
-
function readRelayVersion() {
|
|
245
|
-
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
246
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
247
|
-
return pkg.version ?? "unknown";
|
|
248
|
-
}
|
|
249
|
-
var RELAY_VERSION = readRelayVersion();
|
|
250
|
-
|
|
251
|
-
// src/health.ts
|
|
252
|
-
function bearerToken(authHeader) {
|
|
253
|
-
const match = /^Bearer\s+(.+)$/i.exec(authHeader ?? "");
|
|
254
|
-
return match?.[1] ?? null;
|
|
255
|
-
}
|
|
256
|
-
function healthRouter(registry, options = {}) {
|
|
257
|
-
const router = Router();
|
|
258
|
-
const proxyTokenRequired = options.proxyTokenRequired ?? false;
|
|
259
|
-
const clientTokenRequired = options.clientTokenRequired ?? false;
|
|
260
|
-
router.get("/health", (_req, res) => {
|
|
261
|
-
res.json({
|
|
262
|
-
status: "ok",
|
|
263
|
-
version: RELAY_VERSION,
|
|
264
|
-
uptime: process.uptime(),
|
|
265
|
-
auth: {
|
|
266
|
-
proxyTokenRequired,
|
|
267
|
-
clientTokenRequired
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
router.get("/api/auth/client", (req, res) => {
|
|
272
|
-
if (!clientTokenRequired) {
|
|
273
|
-
res.status(204).end();
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const token = bearerToken(req.get("authorization"));
|
|
277
|
-
if (options.validateClientToken?.(token)) {
|
|
278
|
-
res.status(204).end();
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
res.status(401).json({ error: "invalid_client_token" });
|
|
282
|
-
});
|
|
283
|
-
router.get("/api/admin/client-token", (req, res) => {
|
|
284
|
-
if (!proxyTokenRequired) {
|
|
285
|
-
res.status(401).json({ error: "proxy_token_required" });
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
const token = bearerToken(req.get("authorization"));
|
|
289
|
-
if (!options.validateProxyToken?.(token)) {
|
|
290
|
-
res.status(401).json({ error: "invalid_proxy_token" });
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
const clientToken = options.getClientToken?.() ?? null;
|
|
294
|
-
if (!clientToken) {
|
|
295
|
-
res.status(204).end();
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
res.json({ clientToken });
|
|
299
|
-
});
|
|
300
|
-
router.get("/status", (_req, res) => {
|
|
301
|
-
res.json({
|
|
302
|
-
version: RELAY_VERSION,
|
|
303
|
-
proxyCount: registry.listProxies().length,
|
|
304
|
-
clientCount: registry.countClients(),
|
|
305
|
-
uptime: process.uptime()
|
|
306
|
-
});
|
|
307
|
-
});
|
|
308
|
-
router.get("/api/status", (_req, res) => {
|
|
309
|
-
res.json({
|
|
310
|
-
version: RELAY_VERSION,
|
|
311
|
-
proxyCount: registry.listProxies().length,
|
|
312
|
-
clientCount: registry.countClients(),
|
|
313
|
-
uptime: process.uptime(),
|
|
314
|
-
bindings: registry.getClientDetails()
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
router.get("/api/proxies", (_req, res) => {
|
|
318
|
-
const proxyIds = registry.listProxies();
|
|
319
|
-
const details = proxyIds.map((id) => registry.getProxyDetail(id)).filter((d) => d !== void 0);
|
|
320
|
-
res.json(details);
|
|
321
|
-
});
|
|
322
|
-
router.get("/api/clients", (_req, res) => {
|
|
323
|
-
res.json(registry.getClientDetails());
|
|
324
|
-
});
|
|
325
|
-
return router;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// src/handlers/proxy.ts
|
|
329
|
-
import { WebSocket as WebSocket3 } from "ws";
|
|
330
14
|
|
|
331
15
|
// ../../packages/shared/dist/schemas/envelope.js
|
|
332
|
-
import { z as
|
|
16
|
+
import { z as z6 } from "zod";
|
|
333
17
|
|
|
334
|
-
// ../../packages/shared/dist/schemas/
|
|
18
|
+
// ../../packages/shared/dist/schemas/id.js
|
|
335
19
|
import { z } from "zod";
|
|
336
|
-
var
|
|
337
|
-
|
|
338
|
-
|
|
20
|
+
var MAX_ID_LENGTH = 256;
|
|
21
|
+
var IdSchema = z.string().min(1).max(MAX_ID_LENGTH);
|
|
22
|
+
|
|
23
|
+
// ../../packages/shared/dist/schemas/chat.js
|
|
24
|
+
import { z as z2 } from "zod";
|
|
25
|
+
var UserInputPayloadSchema = z2.object({
|
|
26
|
+
text: z2.string().min(1),
|
|
27
|
+
messageId: z2.string().min(1).optional()
|
|
339
28
|
});
|
|
340
|
-
var AssistantMessagePayloadSchema =
|
|
341
|
-
text:
|
|
342
|
-
isPartial:
|
|
29
|
+
var AssistantMessagePayloadSchema = z2.object({
|
|
30
|
+
text: z2.string(),
|
|
31
|
+
isPartial: z2.boolean()
|
|
343
32
|
});
|
|
344
|
-
var ThinkingPayloadSchema =
|
|
345
|
-
text:
|
|
33
|
+
var ThinkingPayloadSchema = z2.object({
|
|
34
|
+
text: z2.string()
|
|
346
35
|
});
|
|
347
36
|
|
|
348
37
|
// ../../packages/shared/dist/schemas/tool.js
|
|
349
|
-
import { z as
|
|
350
|
-
var ToolUseRequestPayloadSchema =
|
|
351
|
-
toolName:
|
|
352
|
-
toolId:
|
|
353
|
-
parameters:
|
|
38
|
+
import { z as z3 } from "zod";
|
|
39
|
+
var ToolUseRequestPayloadSchema = z3.object({
|
|
40
|
+
toolName: z3.string(),
|
|
41
|
+
toolId: IdSchema,
|
|
42
|
+
parameters: z3.record(z3.string(), z3.unknown())
|
|
354
43
|
});
|
|
355
|
-
var ToolApprovePayloadSchema =
|
|
356
|
-
toolId:
|
|
357
|
-
whitelistTool:
|
|
44
|
+
var ToolApprovePayloadSchema = z3.object({
|
|
45
|
+
toolId: IdSchema,
|
|
46
|
+
whitelistTool: z3.boolean().optional()
|
|
358
47
|
});
|
|
359
|
-
var ToolDenyPayloadSchema =
|
|
360
|
-
toolId:
|
|
361
|
-
reason:
|
|
48
|
+
var ToolDenyPayloadSchema = z3.object({
|
|
49
|
+
toolId: IdSchema,
|
|
50
|
+
reason: z3.string().optional()
|
|
362
51
|
});
|
|
363
|
-
var ToolResultPayloadSchema =
|
|
364
|
-
toolId:
|
|
365
|
-
result:
|
|
366
|
-
isError:
|
|
52
|
+
var ToolResultPayloadSchema = z3.object({
|
|
53
|
+
toolId: IdSchema,
|
|
54
|
+
result: z3.unknown(),
|
|
55
|
+
isError: z3.boolean()
|
|
367
56
|
});
|
|
368
57
|
|
|
369
58
|
// ../../packages/shared/dist/schemas/session.js
|
|
370
|
-
import { z as
|
|
371
|
-
|
|
59
|
+
import { z as z4 } from "zod";
|
|
60
|
+
|
|
61
|
+
// ../../packages/shared/dist/constants/enums.js
|
|
372
62
|
var providerValues = ["claude", "codex"];
|
|
373
63
|
var ptyOwnerValues = ["local-terminal", "proxy-hosted"];
|
|
64
|
+
var sessionModeValues = ["pty", "json"];
|
|
65
|
+
|
|
66
|
+
// ../../packages/shared/dist/constants/pty.js
|
|
67
|
+
var PtySemanticState = {
|
|
68
|
+
WORKING: "working",
|
|
69
|
+
TURN_COMPLETE: "turn_complete",
|
|
70
|
+
APPROVAL_WAIT: "approval_wait"
|
|
71
|
+
};
|
|
72
|
+
var ptySemanticStateValues = [
|
|
73
|
+
PtySemanticState.WORKING,
|
|
74
|
+
PtySemanticState.TURN_COMPLETE,
|
|
75
|
+
PtySemanticState.APPROVAL_WAIT
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// ../../packages/shared/dist/schemas/session.js
|
|
79
|
+
var sessionStateValues = [
|
|
80
|
+
"idle",
|
|
81
|
+
"working",
|
|
82
|
+
"waiting_approval",
|
|
83
|
+
"error",
|
|
84
|
+
"terminated"
|
|
85
|
+
];
|
|
374
86
|
var agentStatusPhaseValues = [
|
|
375
87
|
"idle",
|
|
376
88
|
"thinking",
|
|
@@ -379,171 +91,180 @@ var agentStatusPhaseValues = [
|
|
|
379
91
|
"waiting_permission",
|
|
380
92
|
"error"
|
|
381
93
|
];
|
|
382
|
-
var SessionInfoSchema =
|
|
383
|
-
sessionId:
|
|
384
|
-
name:
|
|
385
|
-
state:
|
|
386
|
-
mode:
|
|
387
|
-
provider:
|
|
94
|
+
var SessionInfoSchema = z4.object({
|
|
95
|
+
sessionId: IdSchema,
|
|
96
|
+
name: z4.string().optional(),
|
|
97
|
+
state: z4.enum(sessionStateValues),
|
|
98
|
+
mode: z4.enum(sessionModeValues).optional(),
|
|
99
|
+
provider: z4.enum(providerValues),
|
|
388
100
|
// PTY 尺寸所有权:
|
|
389
101
|
// - local-terminal: 本地 terminal 进程持有真实 PTY,Web 只按原始 cols/rows 展示
|
|
390
102
|
// - proxy-hosted: serve 内托管 PTY,Web 可按视口请求 resize
|
|
391
|
-
ptyOwner:
|
|
392
|
-
lastActive:
|
|
103
|
+
ptyOwner: z4.enum(ptyOwnerValues).optional(),
|
|
104
|
+
lastActive: z4.number().optional()
|
|
393
105
|
});
|
|
394
|
-
var SessionCreatePayloadSchema =
|
|
395
|
-
name:
|
|
396
|
-
cwd:
|
|
397
|
-
streamDelta:
|
|
106
|
+
var SessionCreatePayloadSchema = z4.object({
|
|
107
|
+
name: z4.string().optional(),
|
|
108
|
+
cwd: z4.string().optional(),
|
|
109
|
+
streamDelta: z4.boolean().optional()
|
|
398
110
|
});
|
|
399
|
-
var SessionListPayloadSchema =
|
|
400
|
-
sessions:
|
|
111
|
+
var SessionListPayloadSchema = z4.object({
|
|
112
|
+
sessions: z4.array(SessionInfoSchema)
|
|
401
113
|
});
|
|
402
|
-
var SessionSwitchPayloadSchema =
|
|
403
|
-
sessionId:
|
|
114
|
+
var SessionSwitchPayloadSchema = z4.object({
|
|
115
|
+
sessionId: IdSchema
|
|
404
116
|
});
|
|
405
|
-
var SessionTerminatePayloadSchema =
|
|
406
|
-
sessionId:
|
|
117
|
+
var SessionTerminatePayloadSchema = z4.object({
|
|
118
|
+
sessionId: IdSchema
|
|
407
119
|
});
|
|
408
|
-
var SessionStatusPayloadSchema =
|
|
409
|
-
sessionId:
|
|
410
|
-
state:
|
|
411
|
-
lastActive:
|
|
120
|
+
var SessionStatusPayloadSchema = z4.object({
|
|
121
|
+
sessionId: IdSchema,
|
|
122
|
+
state: z4.enum(sessionStateValues),
|
|
123
|
+
lastActive: z4.number()
|
|
412
124
|
});
|
|
413
|
-
var PtyStatePayloadSchema =
|
|
414
|
-
state:
|
|
415
|
-
title:
|
|
416
|
-
tool:
|
|
125
|
+
var PtyStatePayloadSchema = z4.object({
|
|
126
|
+
state: z4.enum(ptySemanticStateValues),
|
|
127
|
+
title: z4.string().optional(),
|
|
128
|
+
tool: z4.string().optional()
|
|
417
129
|
});
|
|
418
|
-
var AgentStatusPayloadSchema =
|
|
419
|
-
provider:
|
|
420
|
-
phase:
|
|
421
|
-
seq:
|
|
422
|
-
updatedAt:
|
|
423
|
-
toolName:
|
|
424
|
-
toolInput:
|
|
425
|
-
permissionRequest:
|
|
426
|
-
requestId:
|
|
427
|
-
toolName:
|
|
428
|
-
input:
|
|
130
|
+
var AgentStatusPayloadSchema = z4.object({
|
|
131
|
+
provider: z4.enum(providerValues),
|
|
132
|
+
phase: z4.enum(agentStatusPhaseValues),
|
|
133
|
+
seq: z4.number().int().nonnegative(),
|
|
134
|
+
updatedAt: z4.number(),
|
|
135
|
+
toolName: z4.string().optional(),
|
|
136
|
+
toolInput: z4.record(z4.string(), z4.unknown()).optional(),
|
|
137
|
+
permissionRequest: z4.object({
|
|
138
|
+
requestId: IdSchema,
|
|
139
|
+
toolName: z4.string(),
|
|
140
|
+
input: z4.record(z4.string(), z4.unknown())
|
|
429
141
|
}).optional(),
|
|
430
|
-
permissionResolution:
|
|
431
|
-
requestId:
|
|
432
|
-
outcome:
|
|
142
|
+
permissionResolution: z4.object({
|
|
143
|
+
requestId: IdSchema,
|
|
144
|
+
outcome: z4.enum(["allow", "deny"])
|
|
433
145
|
}).optional(),
|
|
434
|
-
summary:
|
|
146
|
+
summary: z4.string().optional()
|
|
435
147
|
});
|
|
436
148
|
|
|
437
149
|
// ../../packages/shared/dist/schemas/system.js
|
|
438
|
-
import { z as
|
|
439
|
-
var HeartbeatPayloadSchema =
|
|
440
|
-
var AuthPayloadSchema =
|
|
441
|
-
pairingCode:
|
|
442
|
-
token:
|
|
150
|
+
import { z as z5 } from "zod";
|
|
151
|
+
var HeartbeatPayloadSchema = z5.object({});
|
|
152
|
+
var AuthPayloadSchema = z5.object({
|
|
153
|
+
pairingCode: z5.string().optional(),
|
|
154
|
+
token: z5.string().optional()
|
|
443
155
|
});
|
|
444
|
-
var SyncRequestPayloadSchema =
|
|
445
|
-
lastSeq:
|
|
156
|
+
var SyncRequestPayloadSchema = z5.object({
|
|
157
|
+
lastSeq: z5.number().int().nonnegative()
|
|
446
158
|
});
|
|
447
|
-
var SyncResponsePayloadSchema =
|
|
448
|
-
messages:
|
|
159
|
+
var SyncResponsePayloadSchema = z5.object({
|
|
160
|
+
messages: z5.array(z5.record(z5.string(), z5.unknown()))
|
|
449
161
|
});
|
|
450
162
|
|
|
451
163
|
// ../../packages/shared/dist/schemas/envelope.js
|
|
452
164
|
var BaseEnvelopeFields = {
|
|
453
|
-
seq:
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
165
|
+
seq: z6.number().int().nonnegative(),
|
|
166
|
+
timestamp: z6.number(),
|
|
167
|
+
source: z6.enum(["proxy", "client"]),
|
|
168
|
+
version: z6.string()
|
|
169
|
+
};
|
|
170
|
+
var SessionedEnvelopeFields = {
|
|
171
|
+
...BaseEnvelopeFields,
|
|
172
|
+
sessionId: IdSchema
|
|
458
173
|
};
|
|
459
|
-
var MessageEnvelopeSchema =
|
|
174
|
+
var MessageEnvelopeSchema = z6.discriminatedUnion("type", [
|
|
460
175
|
// chat (3)
|
|
461
|
-
|
|
462
|
-
...
|
|
463
|
-
type:
|
|
176
|
+
z6.object({
|
|
177
|
+
...SessionedEnvelopeFields,
|
|
178
|
+
type: z6.literal("user_input"),
|
|
464
179
|
payload: UserInputPayloadSchema
|
|
465
180
|
}),
|
|
466
|
-
|
|
467
|
-
...
|
|
468
|
-
type:
|
|
181
|
+
z6.object({
|
|
182
|
+
...SessionedEnvelopeFields,
|
|
183
|
+
type: z6.literal("assistant_message"),
|
|
469
184
|
payload: AssistantMessagePayloadSchema
|
|
470
185
|
}),
|
|
471
|
-
|
|
472
|
-
...
|
|
473
|
-
type:
|
|
186
|
+
z6.object({
|
|
187
|
+
...SessionedEnvelopeFields,
|
|
188
|
+
type: z6.literal("thinking"),
|
|
474
189
|
payload: ThinkingPayloadSchema
|
|
475
190
|
}),
|
|
476
191
|
// tool (4): 工具审批决策属于 relay control,不进入会话消息信封。
|
|
477
192
|
// tool_use_request: 审批流请求(proxy → client),toolId 是 approval requestId
|
|
478
|
-
|
|
479
|
-
...
|
|
480
|
-
type:
|
|
193
|
+
z6.object({
|
|
194
|
+
...SessionedEnvelopeFields,
|
|
195
|
+
type: z6.literal("tool_use_request"),
|
|
481
196
|
payload: ToolUseRequestPayloadSchema
|
|
482
197
|
}),
|
|
483
198
|
// tool_result: 工具执行结果(proxy → client),toolId 对应 assistant_tool_use / tool_use_request 的 toolId
|
|
484
|
-
|
|
485
|
-
...
|
|
486
|
-
type:
|
|
199
|
+
z6.object({
|
|
200
|
+
...SessionedEnvelopeFields,
|
|
201
|
+
type: z6.literal("tool_result"),
|
|
487
202
|
payload: ToolResultPayloadSchema
|
|
488
203
|
}),
|
|
489
204
|
// assistant_tool_use: 纯展示型工具调用(proxy → client),区别于 tool_use_request 无审批语义
|
|
490
205
|
// payload 结构复用 ToolUseRequestPayloadSchema;toolId 是 Claude 分配的 tool_use id
|
|
491
|
-
|
|
492
|
-
...
|
|
493
|
-
type:
|
|
206
|
+
z6.object({
|
|
207
|
+
...SessionedEnvelopeFields,
|
|
208
|
+
type: z6.literal("assistant_tool_use"),
|
|
494
209
|
payload: ToolUseRequestPayloadSchema
|
|
495
210
|
}),
|
|
496
211
|
// session (5)
|
|
497
|
-
|
|
498
|
-
...
|
|
499
|
-
type:
|
|
212
|
+
z6.object({
|
|
213
|
+
...SessionedEnvelopeFields,
|
|
214
|
+
type: z6.literal("session_create"),
|
|
500
215
|
payload: SessionCreatePayloadSchema
|
|
501
216
|
}),
|
|
502
|
-
|
|
217
|
+
// session_list 是全局广播 (列出所有 session), 不绑定具体 sessionId, 不携带该字段。
|
|
218
|
+
z6.object({
|
|
503
219
|
...BaseEnvelopeFields,
|
|
504
|
-
type:
|
|
220
|
+
type: z6.literal("session_list"),
|
|
505
221
|
payload: SessionListPayloadSchema
|
|
506
222
|
}),
|
|
507
|
-
|
|
508
|
-
...
|
|
509
|
-
type:
|
|
223
|
+
z6.object({
|
|
224
|
+
...SessionedEnvelopeFields,
|
|
225
|
+
type: z6.literal("session_switch"),
|
|
510
226
|
payload: SessionSwitchPayloadSchema
|
|
511
227
|
}),
|
|
512
|
-
|
|
513
|
-
...
|
|
514
|
-
type:
|
|
228
|
+
z6.object({
|
|
229
|
+
...SessionedEnvelopeFields,
|
|
230
|
+
type: z6.literal("session_terminate"),
|
|
515
231
|
payload: SessionTerminatePayloadSchema
|
|
516
232
|
}),
|
|
517
|
-
|
|
518
|
-
...
|
|
519
|
-
type:
|
|
233
|
+
z6.object({
|
|
234
|
+
...SessionedEnvelopeFields,
|
|
235
|
+
type: z6.literal("session_status"),
|
|
520
236
|
payload: SessionStatusPayloadSchema
|
|
521
237
|
}),
|
|
522
|
-
// system (5)
|
|
523
|
-
|
|
238
|
+
// system (5): 心跳 / 认证 / 同步——全局, 无 sessionId
|
|
239
|
+
z6.object({
|
|
524
240
|
...BaseEnvelopeFields,
|
|
525
|
-
type:
|
|
241
|
+
type: z6.literal("heartbeat"),
|
|
526
242
|
payload: HeartbeatPayloadSchema
|
|
527
243
|
}),
|
|
528
|
-
|
|
244
|
+
z6.object({
|
|
529
245
|
...BaseEnvelopeFields,
|
|
530
|
-
type:
|
|
246
|
+
type: z6.literal("auth"),
|
|
531
247
|
payload: AuthPayloadSchema
|
|
532
248
|
}),
|
|
533
|
-
|
|
249
|
+
z6.object({
|
|
534
250
|
...BaseEnvelopeFields,
|
|
535
|
-
type:
|
|
251
|
+
type: z6.literal("sync_request"),
|
|
536
252
|
payload: SyncRequestPayloadSchema
|
|
537
253
|
}),
|
|
538
|
-
|
|
254
|
+
z6.object({
|
|
539
255
|
...BaseEnvelopeFields,
|
|
540
|
-
type:
|
|
256
|
+
type: z6.literal("sync_response"),
|
|
541
257
|
payload: SyncResponsePayloadSchema
|
|
542
258
|
})
|
|
543
259
|
]);
|
|
544
260
|
|
|
261
|
+
// ../../packages/shared/dist/builders/index.js
|
|
262
|
+
function serializeControl(msg) {
|
|
263
|
+
return JSON.stringify(msg);
|
|
264
|
+
}
|
|
265
|
+
|
|
545
266
|
// ../../packages/shared/dist/schemas/relay-control.js
|
|
546
|
-
import { z as
|
|
267
|
+
import { z as z7 } from "zod";
|
|
547
268
|
|
|
548
269
|
// ../../packages/shared/dist/constants/relay-errors.js
|
|
549
270
|
var RelayErrorCode = {
|
|
@@ -570,297 +291,301 @@ var ControlErrorCode = {
|
|
|
570
291
|
};
|
|
571
292
|
|
|
572
293
|
// ../../packages/shared/dist/schemas/relay-control.js
|
|
573
|
-
var ProxyInfoSchema =
|
|
574
|
-
proxyId:
|
|
575
|
-
name:
|
|
576
|
-
online:
|
|
577
|
-
sessions:
|
|
294
|
+
var ProxyInfoSchema = z7.object({
|
|
295
|
+
proxyId: IdSchema,
|
|
296
|
+
name: z7.string().optional(),
|
|
297
|
+
online: z7.boolean(),
|
|
298
|
+
sessions: z7.array(z7.string()).optional()
|
|
578
299
|
});
|
|
579
|
-
var AgentCliAvailabilitySchema =
|
|
580
|
-
available:
|
|
581
|
-
command:
|
|
582
|
-
error:
|
|
583
|
-
suggestions:
|
|
300
|
+
var AgentCliAvailabilitySchema = z7.object({
|
|
301
|
+
available: z7.boolean(),
|
|
302
|
+
command: z7.string().optional(),
|
|
303
|
+
error: z7.string().optional(),
|
|
304
|
+
suggestions: z7.array(z7.string()).optional()
|
|
584
305
|
});
|
|
585
|
-
var AgentCliStatusSchema =
|
|
306
|
+
var AgentCliStatusSchema = z7.object({
|
|
586
307
|
claude: AgentCliAvailabilitySchema,
|
|
587
308
|
codex: AgentCliAvailabilitySchema
|
|
588
309
|
});
|
|
589
|
-
var DirEntrySchema =
|
|
590
|
-
var FileTreeGroupSchema =
|
|
591
|
-
path:
|
|
592
|
-
entries:
|
|
310
|
+
var DirEntrySchema = z7.object({ name: z7.string(), isDir: z7.boolean() });
|
|
311
|
+
var FileTreeGroupSchema = z7.object({
|
|
312
|
+
path: z7.string(),
|
|
313
|
+
entries: z7.array(DirEntrySchema)
|
|
593
314
|
});
|
|
594
|
-
var CommandEntrySchema =
|
|
595
|
-
name:
|
|
596
|
-
description:
|
|
597
|
-
argumentHint:
|
|
598
|
-
source:
|
|
315
|
+
var CommandEntrySchema = z7.object({
|
|
316
|
+
name: z7.string(),
|
|
317
|
+
description: z7.string(),
|
|
318
|
+
argumentHint: z7.string().optional(),
|
|
319
|
+
source: z7.string()
|
|
599
320
|
});
|
|
600
|
-
var HistorySessionSchema =
|
|
601
|
-
id:
|
|
602
|
-
title:
|
|
603
|
-
projectDir:
|
|
604
|
-
updatedAt:
|
|
605
|
-
provider:
|
|
321
|
+
var HistorySessionSchema = z7.object({
|
|
322
|
+
id: z7.string(),
|
|
323
|
+
title: z7.string(),
|
|
324
|
+
projectDir: z7.string(),
|
|
325
|
+
updatedAt: z7.number(),
|
|
326
|
+
provider: z7.enum(providerValues).optional()
|
|
606
327
|
});
|
|
607
|
-
var SessionHistoryMessageSchema =
|
|
608
|
-
role:
|
|
609
|
-
text:
|
|
610
|
-
timestamp:
|
|
611
|
-
cursor:
|
|
328
|
+
var SessionHistoryMessageSchema = z7.object({
|
|
329
|
+
role: z7.enum(["user", "assistant"]),
|
|
330
|
+
text: z7.string(),
|
|
331
|
+
timestamp: z7.number().optional(),
|
|
332
|
+
cursor: z7.string().optional()
|
|
612
333
|
});
|
|
613
|
-
var RequestIdShape = { requestId:
|
|
614
|
-
var ControlErrorCodeSchema =
|
|
334
|
+
var RequestIdShape = { requestId: IdSchema.optional() };
|
|
335
|
+
var ControlErrorCodeSchema = z7.enum(Object.values(ControlErrorCode));
|
|
615
336
|
var RequestErrorShape = {
|
|
616
|
-
error:
|
|
337
|
+
error: z7.string().optional(),
|
|
617
338
|
errorCode: ControlErrorCodeSchema.optional()
|
|
618
339
|
};
|
|
619
|
-
var ClipboardImageMimeTypeSchema =
|
|
340
|
+
var ClipboardImageMimeTypeSchema = z7.enum(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
620
341
|
function control(type, shape, directions) {
|
|
621
342
|
return {
|
|
622
343
|
type,
|
|
623
344
|
directions: new Set(Array.isArray(directions) ? directions : directions ? [directions] : []),
|
|
624
|
-
schema:
|
|
625
|
-
type:
|
|
345
|
+
schema: z7.object({
|
|
346
|
+
type: z7.literal(type),
|
|
626
347
|
...shape ?? {}
|
|
627
348
|
})
|
|
628
349
|
};
|
|
629
350
|
}
|
|
630
351
|
var relayControlDefinitions = [
|
|
631
352
|
control("proxy_register", {
|
|
632
|
-
proxyId:
|
|
633
|
-
name:
|
|
353
|
+
proxyId: IdSchema,
|
|
354
|
+
name: z7.string().optional()
|
|
634
355
|
}),
|
|
635
356
|
control("proxy_register_response", {
|
|
636
|
-
status:
|
|
357
|
+
status: z7.enum(["new", "reconnected"])
|
|
637
358
|
}),
|
|
638
359
|
control("proxy_list_request", RequestIdShape),
|
|
639
360
|
control("proxy_list_response", {
|
|
640
361
|
...RequestIdShape,
|
|
641
|
-
proxies:
|
|
362
|
+
proxies: z7.array(ProxyInfoSchema)
|
|
642
363
|
}),
|
|
643
|
-
control("proxy_select", { ...RequestIdShape, proxyId:
|
|
364
|
+
control("proxy_select", { ...RequestIdShape, proxyId: IdSchema }),
|
|
644
365
|
control("proxy_select_response", {
|
|
645
366
|
...RequestIdShape,
|
|
646
|
-
success:
|
|
647
|
-
proxyId:
|
|
367
|
+
success: z7.boolean(),
|
|
368
|
+
proxyId: IdSchema.optional(),
|
|
648
369
|
...RequestErrorShape
|
|
649
370
|
}),
|
|
650
371
|
control("relay_error", {
|
|
651
|
-
code:
|
|
652
|
-
message:
|
|
372
|
+
code: z7.enum(Object.values(RelayErrorCode)),
|
|
373
|
+
message: z7.string()
|
|
653
374
|
}),
|
|
654
375
|
// 客户端注册协议
|
|
655
376
|
control("client_register", {
|
|
656
|
-
clientId:
|
|
377
|
+
clientId: IdSchema
|
|
657
378
|
}),
|
|
658
379
|
control("client_register_response", {
|
|
659
|
-
status:
|
|
660
|
-
proxyId:
|
|
380
|
+
status: z7.enum(["restored", "proxy_offline", "new"]),
|
|
381
|
+
proxyId: IdSchema.optional()
|
|
661
382
|
}),
|
|
662
383
|
// Proxy 离线通知
|
|
663
384
|
control("proxy_offline", {
|
|
664
|
-
proxyId:
|
|
385
|
+
proxyId: IdSchema
|
|
665
386
|
}),
|
|
666
387
|
// Proxy 主动断开,relay 立即清理资源
|
|
667
388
|
control("proxy_disconnect", {
|
|
668
|
-
proxyId:
|
|
389
|
+
proxyId: IdSchema
|
|
669
390
|
}),
|
|
670
391
|
// Proxy 重连后通知 client 恢复
|
|
671
392
|
control("proxy_online", {
|
|
672
|
-
proxyId:
|
|
393
|
+
proxyId: IdSchema
|
|
673
394
|
}),
|
|
674
395
|
// 目录列表请求与响应
|
|
675
396
|
control("dir_list_request", {
|
|
676
|
-
proxyId:
|
|
397
|
+
proxyId: IdSchema.optional(),
|
|
677
398
|
...RequestIdShape,
|
|
678
|
-
path:
|
|
399
|
+
path: z7.string()
|
|
679
400
|
}, "client_to_proxy"),
|
|
680
|
-
control("dir_list_response", { ...RequestIdShape, ...RequestErrorShape, entries:
|
|
401
|
+
control("dir_list_response", { ...RequestIdShape, ...RequestErrorShape, entries: z7.array(DirEntrySchema), path: z7.string() }, "proxy_to_client"),
|
|
681
402
|
// 目录创建请求与响应
|
|
682
|
-
control("dir_create_request", { ...RequestIdShape, path:
|
|
403
|
+
control("dir_create_request", { ...RequestIdShape, path: z7.string() }, "client_to_proxy"),
|
|
683
404
|
control("dir_create_response", {
|
|
684
405
|
...RequestIdShape,
|
|
685
406
|
...RequestErrorShape,
|
|
686
|
-
path:
|
|
687
|
-
success:
|
|
407
|
+
path: z7.string(),
|
|
408
|
+
success: z7.boolean()
|
|
688
409
|
}, "proxy_to_client"),
|
|
689
410
|
// 命令列表推送,proxy 将可用命令列表推给 client
|
|
690
|
-
control("command_list_push", { commands:
|
|
411
|
+
control("command_list_push", { commands: z7.array(CommandEntrySchema) }, "proxy_to_client"),
|
|
691
412
|
// 文件树推送: 按目录分组, 首组 path 即为 session cwd
|
|
692
413
|
// 前端直接把每组写入 tree[path], 与 dir_list_response 共享 cache slot
|
|
693
414
|
control("file_tree_push", {
|
|
694
|
-
groups:
|
|
415
|
+
groups: z7.array(FileTreeGroupSchema)
|
|
695
416
|
}, "proxy_to_client"),
|
|
696
417
|
// 会话列表请求与权限模式变更
|
|
697
418
|
control("session_list", void 0, ["client_to_proxy", "proxy_to_client"]),
|
|
698
419
|
control("permission_mode_change", {
|
|
699
|
-
mode:
|
|
420
|
+
mode: z7.enum(["default", "auto_accept", "plan"]),
|
|
700
421
|
// sessionId 可选:传入时 proxy 按该会话的 mode 分叉(PTY 发 Tab ANSI),未传走全局日志行为
|
|
701
|
-
sessionId:
|
|
422
|
+
sessionId: IdSchema.optional()
|
|
702
423
|
}, "client_to_proxy"),
|
|
703
424
|
// 会话历史浏览
|
|
704
425
|
control("session_history_request", RequestIdShape, "client_to_proxy"),
|
|
705
|
-
control("session_history_response", { ...RequestIdShape, sessions:
|
|
426
|
+
control("session_history_response", { ...RequestIdShape, sessions: z7.array(HistorySessionSchema) }, "proxy_to_client"),
|
|
706
427
|
// PTY 语义状态,从 Envelope 迁移到 Control 层
|
|
707
|
-
control("pty_state", { sessionId:
|
|
428
|
+
control("pty_state", { sessionId: IdSchema, payload: PtyStatePayloadSchema }, "proxy_to_client"),
|
|
708
429
|
// Provider 语义状态,来自 Claude/Codex hook 等结构化事件,不从 PTY 字节推断
|
|
709
|
-
control("agent_status", { sessionId:
|
|
430
|
+
control("agent_status", { sessionId: IdSchema, payload: AgentStatusPayloadSchema }, "proxy_to_client"),
|
|
710
431
|
// 终端标题变化,proxy -> client
|
|
711
|
-
control("terminal_title", { sessionId:
|
|
432
|
+
control("terminal_title", { sessionId: IdSchema, title: z7.string() }, "proxy_to_client"),
|
|
712
433
|
// 终端尺寸变化,proxy -> client
|
|
713
|
-
control("terminal_resize", { sessionId:
|
|
714
|
-
control("terminal_resize_request", { sessionId:
|
|
434
|
+
control("terminal_resize", { sessionId: IdSchema, cols: z7.number().int().positive(), rows: z7.number().int().positive() }, "proxy_to_client"),
|
|
435
|
+
control("terminal_resize_request", { sessionId: IdSchema, cols: z7.number().int().positive(), rows: z7.number().int().positive() }, "client_to_proxy"),
|
|
715
436
|
// 远程终止 JSON 会话,client -> proxy
|
|
716
|
-
control("session_terminate", { sessionId:
|
|
437
|
+
control("session_terminate", { sessionId: IdSchema }, "client_to_proxy"),
|
|
717
438
|
// 中断当前 turn,client -> proxy,SIGINT 到 worker 进程让 claude CLI abort 当前流
|
|
718
|
-
control("session_worker_abort", { sessionId:
|
|
439
|
+
control("session_worker_abort", { sessionId: IdSchema }, "client_to_proxy"),
|
|
719
440
|
// turn 完成信号,proxy -> client,对应 claude stream-json 的 result 事件
|
|
720
441
|
control("turn_result", {
|
|
721
|
-
sessionId:
|
|
722
|
-
success:
|
|
723
|
-
isError:
|
|
442
|
+
sessionId: IdSchema,
|
|
443
|
+
success: z7.boolean(),
|
|
444
|
+
isError: z7.boolean(),
|
|
724
445
|
// stream-json result.result 是本轮最终文本。assistant_message 流丢失或 CLI 未发增量时,
|
|
725
446
|
// Web 用它作为 JSON 模式兜底展示,避免 turn 已结束但界面空白。
|
|
726
|
-
result:
|
|
447
|
+
result: z7.string().optional()
|
|
727
448
|
}, "proxy_to_client"),
|
|
728
449
|
// 客户端发送到 PTY 的原始字节(ANSI 序列),不追加换行
|
|
729
|
-
control("remote_input_raw", { sessionId:
|
|
450
|
+
control("remote_input_raw", { sessionId: IdSchema, data: z7.string() }, "client_to_proxy"),
|
|
730
451
|
control("clipboard_image_upload", {
|
|
731
452
|
...RequestIdShape,
|
|
732
|
-
sessionId:
|
|
453
|
+
sessionId: IdSchema,
|
|
733
454
|
mimeType: ClipboardImageMimeTypeSchema,
|
|
734
|
-
dataBase64:
|
|
735
|
-
fileName:
|
|
455
|
+
dataBase64: z7.string().min(1),
|
|
456
|
+
fileName: z7.string().optional()
|
|
736
457
|
}, "client_to_proxy"),
|
|
737
458
|
control("clipboard_image_upload_response", {
|
|
738
459
|
...RequestIdShape,
|
|
739
460
|
...RequestErrorShape,
|
|
740
|
-
sessionId:
|
|
741
|
-
success:
|
|
742
|
-
path
|
|
461
|
+
sessionId: IdSchema,
|
|
462
|
+
success: z7.boolean(),
|
|
463
|
+
// success=false 时 proxy 没有有效 path 可填;保持 optional 以避免占位空字符串通过校验。
|
|
464
|
+
path: z7.string().optional()
|
|
743
465
|
}, "proxy_to_client"),
|
|
744
466
|
control("image_preview_request", {
|
|
745
467
|
...RequestIdShape,
|
|
746
|
-
sessionId:
|
|
747
|
-
path:
|
|
468
|
+
sessionId: IdSchema,
|
|
469
|
+
path: z7.string().min(1)
|
|
748
470
|
}, "client_to_proxy"),
|
|
749
471
|
control("image_preview_response", {
|
|
750
472
|
...RequestIdShape,
|
|
751
473
|
...RequestErrorShape,
|
|
752
|
-
sessionId:
|
|
753
|
-
success:
|
|
754
|
-
|
|
474
|
+
sessionId: IdSchema,
|
|
475
|
+
success: z7.boolean(),
|
|
476
|
+
// 同 clipboard_image_upload_response:失败时 proxy 不一定有路径。
|
|
477
|
+
path: z7.string().optional(),
|
|
755
478
|
mimeType: ClipboardImageMimeTypeSchema.optional(),
|
|
756
|
-
dataBase64:
|
|
757
|
-
size:
|
|
479
|
+
dataBase64: z7.string().optional(),
|
|
480
|
+
size: z7.number().int().nonnegative().optional()
|
|
758
481
|
}, "proxy_to_client"),
|
|
759
482
|
// 客户端询问 proxy 的环境信息 (home 路径等), client -> proxy -> response
|
|
760
483
|
// FilePathPicker 用 homePath 作为 select 模式下的默认起点, 新建会话时打开即可浏览
|
|
761
484
|
control("proxy_info_request", RequestIdShape, "client_to_proxy"),
|
|
762
|
-
control("proxy_info", { ...RequestIdShape, homePath:
|
|
763
|
-
control("agent_cli_config_update", { ...RequestIdShape, provider:
|
|
485
|
+
control("proxy_info", { ...RequestIdShape, homePath: z7.string(), agentCli: AgentCliStatusSchema }, "proxy_to_client"),
|
|
486
|
+
control("agent_cli_config_update", { ...RequestIdShape, provider: z7.enum(providerValues), path: z7.string().min(1) }, "client_to_proxy"),
|
|
764
487
|
control("agent_cli_config_update_response", {
|
|
765
488
|
...RequestIdShape,
|
|
766
|
-
provider:
|
|
489
|
+
provider: z7.enum(providerValues),
|
|
767
490
|
agentCli: AgentCliStatusSchema.optional(),
|
|
768
491
|
...RequestErrorShape
|
|
769
492
|
}, "proxy_to_client"),
|
|
770
493
|
// 远程创建 JSON 会话,client -> proxy -> response
|
|
771
494
|
control("session_create", {
|
|
772
495
|
...RequestIdShape,
|
|
773
|
-
cwd:
|
|
774
|
-
provider:
|
|
775
|
-
mode:
|
|
776
|
-
resumeSessionId:
|
|
496
|
+
cwd: z7.string(),
|
|
497
|
+
provider: z7.enum(providerValues),
|
|
498
|
+
mode: z7.enum(sessionModeValues).optional(),
|
|
499
|
+
resumeSessionId: z7.string().optional(),
|
|
777
500
|
// 透传给 claude CLI 的 --permission-mode, undefined 时 proxy 兜底为 "default"
|
|
778
|
-
permissionMode:
|
|
501
|
+
permissionMode: z7.enum(["default", "auto", "acceptEdits", "plan", "bypassPermissions", "dontAsk"]).optional()
|
|
779
502
|
}, "client_to_proxy"),
|
|
780
503
|
control("session_create_response", {
|
|
781
504
|
...RequestIdShape,
|
|
782
|
-
sessionId
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
505
|
+
// 失败路径只送 errorCode/error, sessionId 此时无语义。成功路径才有 id。
|
|
506
|
+
sessionId: IdSchema.optional(),
|
|
507
|
+
mode: z7.enum(sessionModeValues).optional(),
|
|
508
|
+
provider: z7.enum(providerValues).optional(),
|
|
509
|
+
ptyOwner: z7.enum(ptyOwnerValues).optional(),
|
|
786
510
|
...RequestErrorShape
|
|
787
511
|
}, "proxy_to_client"),
|
|
788
512
|
// 客户端请求会话历史消息,client -> proxy
|
|
789
513
|
control("session_messages_request", {
|
|
790
514
|
...RequestIdShape,
|
|
791
|
-
sessionId:
|
|
792
|
-
limit:
|
|
793
|
-
before:
|
|
515
|
+
sessionId: IdSchema,
|
|
516
|
+
limit: z7.number().int().min(1).max(200).optional(),
|
|
517
|
+
before: z7.string().optional()
|
|
794
518
|
}, "client_to_proxy"),
|
|
795
519
|
// 客户端请求会话资源(命令列表 + 文件树),client -> proxy
|
|
796
|
-
control("session_resources_request", { ...RequestIdShape, sessionId:
|
|
520
|
+
control("session_resources_request", { ...RequestIdShape, sessionId: IdSchema }, "client_to_proxy"),
|
|
797
521
|
control("session_resources_response", {
|
|
798
522
|
...RequestIdShape,
|
|
799
523
|
...RequestErrorShape,
|
|
800
|
-
sessionId:
|
|
801
|
-
commands:
|
|
802
|
-
groups:
|
|
524
|
+
sessionId: IdSchema,
|
|
525
|
+
commands: z7.array(CommandEntrySchema),
|
|
526
|
+
groups: z7.array(FileTreeGroupSchema)
|
|
803
527
|
}, "proxy_to_client"),
|
|
804
528
|
// 客户端请求当前 provider 语义状态;不经 relay 缓存,由 proxy 返回当前值
|
|
805
|
-
control("agent_status_request", { ...RequestIdShape, sessionId:
|
|
529
|
+
control("agent_status_request", { ...RequestIdShape, sessionId: IdSchema.optional() }, "client_to_proxy"),
|
|
806
530
|
control("agent_status_response", {
|
|
807
531
|
...RequestIdShape,
|
|
808
|
-
statuses:
|
|
532
|
+
statuses: z7.array(z7.object({ sessionId: IdSchema, payload: AgentStatusPayloadSchema }))
|
|
809
533
|
}, "proxy_to_client"),
|
|
810
534
|
// 客户端确认已收到审批请求;proxy 只记录送达状态,不把它当成用户决策
|
|
811
|
-
control("permission_request_delivered", { sessionId:
|
|
812
|
-
control("tool_approve", { sessionId:
|
|
813
|
-
control("tool_deny", { sessionId:
|
|
535
|
+
control("permission_request_delivered", { sessionId: IdSchema, requestId: IdSchema }, "client_to_proxy"),
|
|
536
|
+
control("tool_approve", { sessionId: IdSchema, payload: ToolApprovePayloadSchema }, "client_to_proxy"),
|
|
537
|
+
control("tool_deny", { sessionId: IdSchema, payload: ToolDenyPayloadSchema }, "client_to_proxy"),
|
|
814
538
|
// proxy 确认用户决策已进入 provider/worker 路径;web 用它更新审批卡片状态
|
|
815
539
|
control("permission_decision_result", {
|
|
816
|
-
sessionId:
|
|
817
|
-
requestId:
|
|
818
|
-
outcome:
|
|
819
|
-
delivered:
|
|
820
|
-
message:
|
|
540
|
+
sessionId: IdSchema,
|
|
541
|
+
requestId: IdSchema,
|
|
542
|
+
outcome: z7.enum(["allow", "deny"]),
|
|
543
|
+
delivered: z7.boolean(),
|
|
544
|
+
message: z7.string().optional()
|
|
821
545
|
}, "proxy_to_client"),
|
|
822
546
|
// proxy 推送当前 pending 的工具审批列表,client 据此恢复审批卡片
|
|
823
547
|
control("pending_approvals_push", {
|
|
824
|
-
sessionId:
|
|
825
|
-
approvals:
|
|
826
|
-
requestId:
|
|
827
|
-
toolName:
|
|
828
|
-
input:
|
|
548
|
+
sessionId: IdSchema,
|
|
549
|
+
approvals: z7.array(z7.object({
|
|
550
|
+
requestId: IdSchema,
|
|
551
|
+
toolName: z7.string(),
|
|
552
|
+
input: z7.record(z7.string(), z7.unknown())
|
|
829
553
|
}))
|
|
830
554
|
}, "proxy_to_client"),
|
|
831
555
|
// 恢复会话时推送历史消息,proxy -> client
|
|
832
556
|
control("session_history_messages", {
|
|
833
557
|
...RequestIdShape,
|
|
834
|
-
sessionId:
|
|
835
|
-
before:
|
|
836
|
-
messages:
|
|
837
|
-
hasMore:
|
|
838
|
-
nextBefore:
|
|
558
|
+
sessionId: IdSchema,
|
|
559
|
+
before: z7.string().optional(),
|
|
560
|
+
messages: z7.array(SessionHistoryMessageSchema),
|
|
561
|
+
hasMore: z7.boolean().optional(),
|
|
562
|
+
nextBefore: z7.string().optional()
|
|
839
563
|
}, "proxy_to_client"),
|
|
840
|
-
// proxy 重连后同步活跃 session 列表给 relay
|
|
564
|
+
// proxy 重连后同步活跃 session 列表给 relay。session_sync 由 relay 自消费(更新 proxy-session
|
|
565
|
+
// 关联)不转发给 client,因此**没有** direction 标注——RelayControlDirection 只描述转发流。
|
|
841
566
|
control("session_sync", {
|
|
842
|
-
sessions:
|
|
843
|
-
id:
|
|
844
|
-
mode:
|
|
845
|
-
provider:
|
|
846
|
-
ptyOwner:
|
|
847
|
-
state:
|
|
567
|
+
sessions: z7.array(z7.object({
|
|
568
|
+
id: z7.string(),
|
|
569
|
+
mode: z7.enum(sessionModeValues),
|
|
570
|
+
provider: z7.enum(providerValues),
|
|
571
|
+
ptyOwner: z7.enum(ptyOwnerValues).optional(),
|
|
572
|
+
state: z7.enum(sessionStateValues)
|
|
848
573
|
}))
|
|
849
574
|
}),
|
|
850
575
|
// PTY 会话订阅,client -> proxy,触发 terminal serialize() 返回当前状态
|
|
851
|
-
control("session_subscribe", { sessionId:
|
|
576
|
+
control("session_subscribe", { sessionId: IdSchema, requestId: IdSchema.optional() }, "client_to_proxy"),
|
|
852
577
|
// PTY 会话快照,proxy -> client,serialize() 的全量终端状态
|
|
853
578
|
control("session_snapshot", {
|
|
854
|
-
sessionId:
|
|
855
|
-
cols:
|
|
856
|
-
rows:
|
|
857
|
-
data:
|
|
858
|
-
outputSeq:
|
|
859
|
-
requestId:
|
|
579
|
+
sessionId: IdSchema,
|
|
580
|
+
cols: z7.number().int().positive(),
|
|
581
|
+
rows: z7.number().int().positive(),
|
|
582
|
+
data: z7.string(),
|
|
583
|
+
outputSeq: z7.number().int().nonnegative(),
|
|
584
|
+
requestId: IdSchema.optional()
|
|
860
585
|
}, "proxy_to_client")
|
|
861
586
|
];
|
|
862
587
|
var relayControlSchemas = relayControlDefinitions.map((definition) => definition.schema);
|
|
863
|
-
var RelayControlSchema =
|
|
588
|
+
var RelayControlSchema = z7.discriminatedUnion("type", relayControlSchemas);
|
|
864
589
|
var ProxyToClientRelayControlTypes = new Set(relayControlDefinitions.filter((definition) => definition.directions.has("proxy_to_client")).map((definition) => definition.type));
|
|
865
590
|
function isProxyToClientRelayControlType(type) {
|
|
866
591
|
return ProxyToClientRelayControlTypes.has(type);
|
|
@@ -870,108 +595,348 @@ function isClientToProxyRelayControlType(type) {
|
|
|
870
595
|
return ClientToProxyRelayControlTypes.has(type);
|
|
871
596
|
}
|
|
872
597
|
|
|
873
|
-
// ../../packages/shared/dist/
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
598
|
+
// ../../packages/shared/dist/binary-frame.js
|
|
599
|
+
var SID_LEN_BYTES = 1;
|
|
600
|
+
var SEQ_BYTES = 4;
|
|
601
|
+
var HEADER_FIXED_BYTES = SID_LEN_BYTES + SEQ_BYTES;
|
|
602
|
+
|
|
603
|
+
// ../../packages/shared/dist/state-machine.js
|
|
604
|
+
function computeAbsorbingSet(transitions) {
|
|
605
|
+
const absorbing = /* @__PURE__ */ new Set();
|
|
606
|
+
const entries = Object.entries(transitions);
|
|
607
|
+
for (const [s, outs] of entries) {
|
|
608
|
+
if (outs.length === 0)
|
|
609
|
+
absorbing.add(s);
|
|
610
|
+
}
|
|
611
|
+
let changed = true;
|
|
612
|
+
while (changed) {
|
|
613
|
+
changed = false;
|
|
614
|
+
for (const [s, outs] of entries) {
|
|
615
|
+
if (absorbing.has(s))
|
|
616
|
+
continue;
|
|
617
|
+
if (outs.length > 0 && outs.every((t) => absorbing.has(t))) {
|
|
618
|
+
absorbing.add(s);
|
|
619
|
+
changed = true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return absorbing;
|
|
883
624
|
}
|
|
884
|
-
function
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
625
|
+
function defineFSM(transitions) {
|
|
626
|
+
const absorbing = computeAbsorbingSet(transitions);
|
|
627
|
+
return {
|
|
628
|
+
canTransition: (from, to) => transitions[from]?.includes(to) ?? false,
|
|
629
|
+
isAbsorbing: (state) => absorbing.has(state)
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/registry.ts
|
|
634
|
+
var proxyConnectionFSM = defineFSM({
|
|
635
|
+
online: ["offline"],
|
|
636
|
+
offline: ["online"]
|
|
637
|
+
});
|
|
638
|
+
var RelayRegistry = class {
|
|
639
|
+
proxyStates = /* @__PURE__ */ new Map();
|
|
640
|
+
clientBindings = /* @__PURE__ */ new Map();
|
|
641
|
+
connectedClients = /* @__PURE__ */ new Set();
|
|
642
|
+
registerProxy(proxyId, ws, name) {
|
|
643
|
+
const existing = this.proxyStates.get(proxyId);
|
|
644
|
+
if (existing) {
|
|
645
|
+
if (existing.ws && existing.ws.readyState === WebSocket.OPEN) {
|
|
646
|
+
existing.ws.terminate();
|
|
647
|
+
}
|
|
648
|
+
existing.ws = ws;
|
|
649
|
+
existing.connectionState = "online";
|
|
650
|
+
existing.disconnectedAt = null;
|
|
651
|
+
if (name !== void 0) existing.name = name;
|
|
652
|
+
return "reconnected";
|
|
653
|
+
}
|
|
654
|
+
this.proxyStates.set(proxyId, {
|
|
655
|
+
ws,
|
|
656
|
+
connectionState: "online",
|
|
657
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
658
|
+
disconnectedAt: null,
|
|
659
|
+
name
|
|
660
|
+
});
|
|
661
|
+
return "new";
|
|
662
|
+
}
|
|
663
|
+
// 显式状态转换,校验 from 状态匹配后更新 connectionState
|
|
664
|
+
transitionProxy(proxyId, from, to) {
|
|
665
|
+
if (!proxyConnectionFSM.canTransition(from, to)) {
|
|
666
|
+
throw new Error(`Invalid proxy transition: ${from} -> ${to}`);
|
|
667
|
+
}
|
|
668
|
+
const state = this.proxyStates.get(proxyId);
|
|
669
|
+
if (!state) {
|
|
670
|
+
throw new Error(`Proxy not found: ${proxyId}`);
|
|
671
|
+
}
|
|
672
|
+
if (state.connectionState !== from) {
|
|
673
|
+
throw new Error(
|
|
674
|
+
`Proxy ${proxyId} state mismatch: expected ${from}, actual ${state.connectionState}`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
state.connectionState = to;
|
|
678
|
+
if (to === "offline") {
|
|
679
|
+
state.ws = null;
|
|
680
|
+
state.disconnectedAt = Date.now();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
getProxyConnectionState(proxyId) {
|
|
684
|
+
return this.proxyStates.get(proxyId)?.connectionState;
|
|
685
|
+
}
|
|
686
|
+
getClientConnectionState(clientId) {
|
|
687
|
+
return this.clientBindings.get(clientId)?.connectionState;
|
|
688
|
+
}
|
|
689
|
+
// 彻底清理 proxy 状态和客户端绑定
|
|
690
|
+
cleanupProxy(proxyId) {
|
|
691
|
+
const state = this.proxyStates.get(proxyId);
|
|
692
|
+
if (!state) return;
|
|
693
|
+
for (const [clientId, binding] of this.clientBindings) {
|
|
694
|
+
if (binding.proxyId === proxyId) {
|
|
695
|
+
this.clientBindings.delete(clientId);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
this.proxyStates.delete(proxyId);
|
|
699
|
+
}
|
|
700
|
+
unregisterProxy(proxyId) {
|
|
701
|
+
this.cleanupProxy(proxyId);
|
|
702
|
+
}
|
|
703
|
+
getProxy(proxyId) {
|
|
704
|
+
const state = this.proxyStates.get(proxyId);
|
|
705
|
+
return state?.ws ?? void 0;
|
|
706
|
+
}
|
|
707
|
+
isProxyOnline(proxyId) {
|
|
708
|
+
const state = this.proxyStates.get(proxyId);
|
|
709
|
+
if (!state || state.connectionState !== "online") return false;
|
|
710
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return false;
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
// proxy 是否存在(含宽限期中的)
|
|
714
|
+
hasProxy(proxyId) {
|
|
715
|
+
return this.proxyStates.has(proxyId);
|
|
716
|
+
}
|
|
717
|
+
listProxies() {
|
|
718
|
+
return Array.from(this.proxyStates.keys());
|
|
719
|
+
}
|
|
720
|
+
// 返回 proxyId、name、online 的列表,用于 proxy_list_response
|
|
721
|
+
listProxiesWithName() {
|
|
722
|
+
return Array.from(this.proxyStates.entries()).map(([proxyId, state]) => ({
|
|
723
|
+
proxyId,
|
|
724
|
+
...state.name !== void 0 ? { name: state.name } : {},
|
|
725
|
+
online: state.connectionState === "online"
|
|
726
|
+
}));
|
|
727
|
+
}
|
|
728
|
+
getProxyName(proxyId) {
|
|
729
|
+
return this.proxyStates.get(proxyId)?.name;
|
|
730
|
+
}
|
|
731
|
+
// 将 sessionId 关联到 proxy
|
|
732
|
+
addSessionToProxy(proxyId, sessionId) {
|
|
733
|
+
const state = this.proxyStates.get(proxyId);
|
|
734
|
+
if (state) {
|
|
735
|
+
state.sessions.add(sessionId);
|
|
892
736
|
}
|
|
893
|
-
} catch (err) {
|
|
894
|
-
const code = err.code;
|
|
895
|
-
if (code !== "ENOENT")
|
|
896
|
-
return;
|
|
897
737
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
738
|
+
// 通过 sessionId 反查所属 proxyId
|
|
739
|
+
getProxyForSession(sessionId) {
|
|
740
|
+
for (const [proxyId, state] of this.proxyStates) {
|
|
741
|
+
if (state.sessions.has(sessionId)) {
|
|
742
|
+
return proxyId;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return void 0;
|
|
901
746
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
return
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
return;
|
|
912
|
-
const currentFileName = basename(currentFilePath);
|
|
913
|
-
const prefix = `${name}-`;
|
|
914
|
-
const candidates = readdirSync(logDir).filter((entry) => entry.startsWith(prefix) && entry.endsWith(".log") && entry !== currentFileName).map((entry) => {
|
|
915
|
-
const path = join2(logDir, entry);
|
|
916
|
-
try {
|
|
917
|
-
return { path, mtimeMs: statSync(path).mtimeMs };
|
|
918
|
-
} catch {
|
|
919
|
-
return null;
|
|
747
|
+
// 获取 proxy 关联的所有 sessionId
|
|
748
|
+
getSessionsForProxy(proxyId) {
|
|
749
|
+
const state = this.proxyStates.get(proxyId);
|
|
750
|
+
return state ? Array.from(state.sessions) : [];
|
|
751
|
+
}
|
|
752
|
+
// clientId 绑定方式
|
|
753
|
+
bindClientById(clientId, proxyId, ws) {
|
|
754
|
+
if (!this.proxyStates.has(proxyId)) {
|
|
755
|
+
return false;
|
|
920
756
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
757
|
+
this.clientBindings.set(clientId, { proxyId, ws, connectionState: "bound" });
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
updateClientSocket(clientId, ws) {
|
|
761
|
+
const binding = this.clientBindings.get(clientId);
|
|
762
|
+
if (binding) {
|
|
763
|
+
binding.ws = ws;
|
|
926
764
|
}
|
|
927
765
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
766
|
+
// 断开客户端 WebSocket 但保留绑定关系,重连时可恢复
|
|
767
|
+
unbindClientById(clientId) {
|
|
768
|
+
const binding = this.clientBindings.get(clientId);
|
|
769
|
+
if (binding) {
|
|
770
|
+
binding.ws = null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
getClientBinding(clientId) {
|
|
774
|
+
return this.clientBindings.get(clientId);
|
|
775
|
+
}
|
|
776
|
+
// 获取绑定到指定 proxy 的所有活跃客户端 WebSocket
|
|
777
|
+
getClientsForProxy(proxyId) {
|
|
778
|
+
const clients = [];
|
|
779
|
+
for (const [, binding] of this.clientBindings) {
|
|
780
|
+
if (binding.proxyId === proxyId && binding.ws && binding.ws.readyState === WebSocket.OPEN) {
|
|
781
|
+
clients.push(binding.ws);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return clients;
|
|
785
|
+
}
|
|
786
|
+
countClients() {
|
|
787
|
+
let count = 0;
|
|
788
|
+
for (const [, binding] of this.clientBindings) {
|
|
789
|
+
if (binding.ws && binding.ws.readyState === WebSocket.OPEN) count++;
|
|
790
|
+
}
|
|
791
|
+
return count;
|
|
792
|
+
}
|
|
793
|
+
addClientWs(ws) {
|
|
794
|
+
this.connectedClients.add(ws);
|
|
795
|
+
}
|
|
796
|
+
removeClientWs(ws) {
|
|
797
|
+
this.connectedClients.delete(ws);
|
|
798
|
+
}
|
|
799
|
+
getAllClientWs() {
|
|
800
|
+
const clients = [];
|
|
801
|
+
for (const ws of this.connectedClients) {
|
|
802
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
803
|
+
clients.push(ws);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return clients;
|
|
807
|
+
}
|
|
808
|
+
// 获取单个 proxy 的详细状态信息
|
|
809
|
+
getProxyDetail(proxyId) {
|
|
810
|
+
const state = this.proxyStates.get(proxyId);
|
|
811
|
+
if (!state) return void 0;
|
|
812
|
+
return {
|
|
813
|
+
proxyId,
|
|
814
|
+
...state.name !== void 0 ? { name: state.name } : {},
|
|
815
|
+
online: state.connectionState === "online",
|
|
816
|
+
connectionState: state.connectionState,
|
|
817
|
+
sessions: Array.from(state.sessions),
|
|
818
|
+
disconnectedAt: state.disconnectedAt
|
|
819
|
+
};
|
|
933
820
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
821
|
+
// 获取所有客户端绑定的详细信息
|
|
822
|
+
getClientDetails() {
|
|
823
|
+
const details = [];
|
|
824
|
+
for (const [clientId, binding] of this.clientBindings) {
|
|
825
|
+
details.push({
|
|
826
|
+
clientId,
|
|
827
|
+
proxyId: binding.proxyId,
|
|
828
|
+
online: binding.ws !== null && binding.ws !== void 0 && binding.ws.readyState === WebSocket.OPEN,
|
|
829
|
+
connectionState: binding.connectionState
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
return details;
|
|
944
833
|
}
|
|
945
|
-
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// src/health.ts
|
|
837
|
+
import { Router } from "express";
|
|
838
|
+
|
|
839
|
+
// src/version.ts
|
|
840
|
+
import { readFileSync } from "fs";
|
|
841
|
+
import { dirname, join } from "path";
|
|
842
|
+
import { fileURLToPath } from "url";
|
|
843
|
+
function readRelayVersion() {
|
|
844
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
845
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
846
|
+
return pkg.version ?? "unknown";
|
|
946
847
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
848
|
+
var RELAY_VERSION = readRelayVersion();
|
|
849
|
+
|
|
850
|
+
// src/health.ts
|
|
851
|
+
function bearerToken(authHeader) {
|
|
852
|
+
const match = /^Bearer\s+(.+)$/i.exec(authHeader ?? "");
|
|
853
|
+
return match?.[1] ?? null;
|
|
854
|
+
}
|
|
855
|
+
function healthRouter(registry, options = {}) {
|
|
856
|
+
const router = Router();
|
|
857
|
+
const proxyTokenRequired = options.proxyTokenRequired ?? false;
|
|
858
|
+
const clientTokenRequired = options.clientTokenRequired ?? false;
|
|
859
|
+
router.get("/health", (_req, res) => {
|
|
860
|
+
res.json({
|
|
861
|
+
status: "ok",
|
|
862
|
+
version: RELAY_VERSION,
|
|
863
|
+
uptime: process.uptime(),
|
|
864
|
+
auth: {
|
|
865
|
+
proxyTokenRequired,
|
|
866
|
+
clientTokenRequired
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
router.get("/api/auth/client", (req, res) => {
|
|
871
|
+
if (!clientTokenRequired) {
|
|
872
|
+
res.status(204).end();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const token = bearerToken(req.get("authorization"));
|
|
876
|
+
if (options.validateClientToken?.(token)) {
|
|
877
|
+
res.status(204).end();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
res.status(401).json({ error: "invalid_client_token" });
|
|
881
|
+
});
|
|
882
|
+
router.get("/api/admin/client-token", (req, res) => {
|
|
883
|
+
if (!proxyTokenRequired) {
|
|
884
|
+
res.status(401).json({ error: "proxy_token_required" });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const token = bearerToken(req.get("authorization"));
|
|
888
|
+
if (!options.validateProxyToken?.(token)) {
|
|
889
|
+
res.status(401).json({ error: "invalid_proxy_token" });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const clientToken = options.getClientToken?.() ?? null;
|
|
893
|
+
if (!clientToken) {
|
|
894
|
+
res.status(204).end();
|
|
895
|
+
return;
|
|
971
896
|
}
|
|
897
|
+
res.json({ clientToken });
|
|
898
|
+
});
|
|
899
|
+
function requireProxyTokenIfConfigured(req, res) {
|
|
900
|
+
if (!proxyTokenRequired) return true;
|
|
901
|
+
const token = bearerToken(req.get("authorization"));
|
|
902
|
+
if (options.validateProxyToken?.(token)) return true;
|
|
903
|
+
res.status(401).json({ error: "invalid_proxy_token" });
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
router.get("/status", (_req, res) => {
|
|
907
|
+
res.json({
|
|
908
|
+
version: RELAY_VERSION,
|
|
909
|
+
proxyCount: registry.listProxies().length,
|
|
910
|
+
clientCount: registry.countClients(),
|
|
911
|
+
uptime: process.uptime()
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
router.get("/api/status", (req, res) => {
|
|
915
|
+
if (!requireProxyTokenIfConfigured(req, res)) return;
|
|
916
|
+
res.json({
|
|
917
|
+
version: RELAY_VERSION,
|
|
918
|
+
proxyCount: registry.listProxies().length,
|
|
919
|
+
clientCount: registry.countClients(),
|
|
920
|
+
uptime: process.uptime(),
|
|
921
|
+
bindings: registry.getClientDetails()
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
router.get("/api/proxies", (req, res) => {
|
|
925
|
+
if (!requireProxyTokenIfConfigured(req, res)) return;
|
|
926
|
+
const proxyIds = registry.listProxies();
|
|
927
|
+
const details = proxyIds.map((id) => registry.getProxyDetail(id)).filter((d) => d !== void 0);
|
|
928
|
+
res.json(details);
|
|
929
|
+
});
|
|
930
|
+
router.get("/api/clients", (req, res) => {
|
|
931
|
+
if (!requireProxyTokenIfConfigured(req, res)) return;
|
|
932
|
+
res.json(registry.getClientDetails());
|
|
972
933
|
});
|
|
934
|
+
return router;
|
|
973
935
|
}
|
|
974
936
|
|
|
937
|
+
// src/handlers/proxy.ts
|
|
938
|
+
import { WebSocket as WebSocket3 } from "ws";
|
|
939
|
+
|
|
975
940
|
// src/router.ts
|
|
976
941
|
import { WebSocket as WebSocket2 } from "ws";
|
|
977
942
|
function parseMessage(data) {
|
|
@@ -1002,8 +967,9 @@ function routeProxyMessage(raw, proxyId, registry, logger, chaos) {
|
|
|
1002
967
|
return;
|
|
1003
968
|
}
|
|
1004
969
|
const { message } = result;
|
|
1005
|
-
|
|
1006
|
-
|
|
970
|
+
if ("sessionId" in message) {
|
|
971
|
+
registry.addSessionToProxy(proxyId, message.sessionId);
|
|
972
|
+
}
|
|
1007
973
|
const clients = registry.getClientsForProxy(proxyId);
|
|
1008
974
|
for (const clientWs of clients) {
|
|
1009
975
|
if (clientWs.readyState === WebSocket2.OPEN) {
|
|
@@ -1039,6 +1005,7 @@ function routeClientMessage(raw, proxyId, clientWs, registry, logger, chaos) {
|
|
|
1039
1005
|
|
|
1040
1006
|
// src/handlers/proxy.ts
|
|
1041
1007
|
var MAX_BINARY_FRAME_SIZE = 10 * 1024 * 1024;
|
|
1008
|
+
var MAX_JSON_MESSAGE_SIZE = 1 * 1024 * 1024;
|
|
1042
1009
|
function notifyClientsProxyOffline(proxyId, registry, logger, chaos) {
|
|
1043
1010
|
const clients = registry.getClientsForProxy(proxyId);
|
|
1044
1011
|
const msg = JSON.stringify({ type: "proxy_offline", proxyId });
|
|
@@ -1069,6 +1036,15 @@ function broadcastProxyList(registry, chaos) {
|
|
|
1069
1036
|
else clientWs.send(msg);
|
|
1070
1037
|
}
|
|
1071
1038
|
}
|
|
1039
|
+
function rejectNotRegistered(ws) {
|
|
1040
|
+
ws.send(
|
|
1041
|
+
JSON.stringify({
|
|
1042
|
+
type: "relay_error",
|
|
1043
|
+
code: RelayErrorCode.NOT_REGISTERED,
|
|
1044
|
+
message: "Proxy must register before sending messages"
|
|
1045
|
+
})
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1072
1048
|
function handleProxyConnection(ws, registry, logger, chaos) {
|
|
1073
1049
|
const proxyWs = ws;
|
|
1074
1050
|
proxyWs.isAlive = true;
|
|
@@ -1101,6 +1077,13 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1101
1077
|
}
|
|
1102
1078
|
return;
|
|
1103
1079
|
}
|
|
1080
|
+
if (data.length > MAX_JSON_MESSAGE_SIZE) {
|
|
1081
|
+
logger.warn(
|
|
1082
|
+
{ size: data.length, proxyId: proxyWs.proxyId },
|
|
1083
|
+
"JSON message rejected: exceeds max size"
|
|
1084
|
+
);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1104
1087
|
const raw = data.toString();
|
|
1105
1088
|
const result = parseMessage(raw);
|
|
1106
1089
|
if (result.kind === "control" && result.message.type === "proxy_register") {
|
|
@@ -1109,7 +1092,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1109
1092
|
proxyWs.proxyId = proxyId;
|
|
1110
1093
|
logger.info({ proxyId, status }, "Proxy registered");
|
|
1111
1094
|
proxyWs.send(
|
|
1112
|
-
|
|
1095
|
+
serializeControl({
|
|
1113
1096
|
type: "proxy_register_response",
|
|
1114
1097
|
status
|
|
1115
1098
|
})
|
|
@@ -1135,25 +1118,17 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1135
1118
|
}
|
|
1136
1119
|
if (result.kind === "control" && result.message.type === "session_sync") {
|
|
1137
1120
|
if (!proxyWs.proxyId) return;
|
|
1138
|
-
const sessions = result.message
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
registry.addSessionToProxy(proxyWs.proxyId, s.id);
|
|
1142
|
-
}
|
|
1143
|
-
logger.info({ proxyId: proxyWs.proxyId, count: sessions.length }, "Session sync received");
|
|
1121
|
+
const { sessions } = result.message;
|
|
1122
|
+
for (const s of sessions) {
|
|
1123
|
+
registry.addSessionToProxy(proxyWs.proxyId, s.id);
|
|
1144
1124
|
}
|
|
1125
|
+
logger.info({ proxyId: proxyWs.proxyId, count: sessions.length }, "Session sync received");
|
|
1145
1126
|
return;
|
|
1146
1127
|
}
|
|
1147
1128
|
if (result.kind === "control") {
|
|
1148
1129
|
if (isProxyToClientRelayControlType(result.message.type)) {
|
|
1149
1130
|
if (!proxyWs.proxyId) {
|
|
1150
|
-
proxyWs
|
|
1151
|
-
JSON.stringify({
|
|
1152
|
-
type: "relay_error",
|
|
1153
|
-
code: RelayErrorCode.NOT_REGISTERED,
|
|
1154
|
-
message: "Proxy must register before sending messages"
|
|
1155
|
-
})
|
|
1156
|
-
);
|
|
1131
|
+
rejectNotRegistered(proxyWs);
|
|
1157
1132
|
return;
|
|
1158
1133
|
}
|
|
1159
1134
|
const clients = registry.getClientsForProxy(proxyWs.proxyId);
|
|
@@ -1180,13 +1155,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1180
1155
|
}
|
|
1181
1156
|
if (result.kind === "envelope") {
|
|
1182
1157
|
if (!proxyWs.proxyId) {
|
|
1183
|
-
proxyWs
|
|
1184
|
-
JSON.stringify({
|
|
1185
|
-
type: "relay_error",
|
|
1186
|
-
code: RelayErrorCode.NOT_REGISTERED,
|
|
1187
|
-
message: "Proxy must register before sending messages"
|
|
1188
|
-
})
|
|
1189
|
-
);
|
|
1158
|
+
rejectNotRegistered(proxyWs);
|
|
1190
1159
|
return;
|
|
1191
1160
|
}
|
|
1192
1161
|
routeProxyMessage(raw, proxyWs.proxyId, registry, logger, chaos);
|
|
@@ -1204,19 +1173,31 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1204
1173
|
return;
|
|
1205
1174
|
}
|
|
1206
1175
|
});
|
|
1207
|
-
proxyWs.on("close", () => {
|
|
1208
|
-
if (proxyWs.proxyId)
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
} catch {
|
|
1213
|
-
}
|
|
1176
|
+
proxyWs.on("close", (code, reason) => {
|
|
1177
|
+
if (!proxyWs.proxyId) return;
|
|
1178
|
+
const closeMeta = { code, reason: reason.toString() || void 0 };
|
|
1179
|
+
const current = registry.getProxy(proxyWs.proxyId);
|
|
1180
|
+
if (current && current !== proxyWs) {
|
|
1214
1181
|
logger.info(
|
|
1215
|
-
{ proxyId: proxyWs.proxyId },
|
|
1216
|
-
"
|
|
1182
|
+
{ proxyId: proxyWs.proxyId, ...closeMeta },
|
|
1183
|
+
"Old proxy ws closed after being superseded by reconnect, skipping offline transition"
|
|
1217
1184
|
);
|
|
1218
|
-
|
|
1185
|
+
return;
|
|
1219
1186
|
}
|
|
1187
|
+
notifyClientsProxyOffline(proxyWs.proxyId, registry, logger, chaos);
|
|
1188
|
+
try {
|
|
1189
|
+
registry.transitionProxy(proxyWs.proxyId, "online", "offline");
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
logger.debug(
|
|
1192
|
+
{ proxyId: proxyWs.proxyId, err: String(err) },
|
|
1193
|
+
"transitionProxy on close skipped"
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
logger.info(
|
|
1197
|
+
{ proxyId: proxyWs.proxyId, ...closeMeta },
|
|
1198
|
+
"Proxy disconnected, state preserved for reconnect"
|
|
1199
|
+
);
|
|
1200
|
+
broadcastProxyList(registry, chaos);
|
|
1220
1201
|
});
|
|
1221
1202
|
proxyWs.on("error", (err) => {
|
|
1222
1203
|
logger.error({ err, proxyId: proxyWs.proxyId }, "Proxy WebSocket error");
|
|
@@ -1226,6 +1207,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1226
1207
|
// src/handlers/client.ts
|
|
1227
1208
|
import { WebSocket as WebSocket4 } from "ws";
|
|
1228
1209
|
import { nanoid } from "nanoid";
|
|
1210
|
+
var MAX_JSON_MESSAGE_SIZE2 = 1 * 1024 * 1024;
|
|
1229
1211
|
function handleClientRegister(clientId, clientWs, registry, logger) {
|
|
1230
1212
|
clientWs.clientId = clientId;
|
|
1231
1213
|
const binding = registry.getClientBinding(clientId);
|
|
@@ -1262,6 +1244,26 @@ function handleClientRegister(clientId, clientWs, registry, logger) {
|
|
|
1262
1244
|
);
|
|
1263
1245
|
logger.info({ clientId, proxyId, status: "restored" }, "Client registered");
|
|
1264
1246
|
}
|
|
1247
|
+
function rejectNotBound(ws) {
|
|
1248
|
+
ws.send(
|
|
1249
|
+
JSON.stringify({
|
|
1250
|
+
type: "relay_error",
|
|
1251
|
+
code: RelayErrorCode.NOT_BOUND,
|
|
1252
|
+
message: "Client is not bound to any proxy"
|
|
1253
|
+
})
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
function rejectProxySelect(ws, requestId, proxyId) {
|
|
1257
|
+
ws.send(
|
|
1258
|
+
JSON.stringify({
|
|
1259
|
+
type: "proxy_select_response",
|
|
1260
|
+
requestId,
|
|
1261
|
+
success: false,
|
|
1262
|
+
errorCode: ControlErrorCode.PROXY_OFFLINE,
|
|
1263
|
+
error: `Proxy not online: ${proxyId}`
|
|
1264
|
+
})
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1265
1267
|
function handleClientConnection(ws, registry, logger, chaos) {
|
|
1266
1268
|
const clientWs = ws;
|
|
1267
1269
|
clientWs.isAlive = true;
|
|
@@ -1273,6 +1275,13 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1273
1275
|
if (isBinary) {
|
|
1274
1276
|
return;
|
|
1275
1277
|
}
|
|
1278
|
+
if (data.length > MAX_JSON_MESSAGE_SIZE2) {
|
|
1279
|
+
logger.warn(
|
|
1280
|
+
{ size: data.length, clientId: clientWs.clientId },
|
|
1281
|
+
"JSON message rejected: exceeds max size"
|
|
1282
|
+
);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1276
1285
|
const raw = data.toString();
|
|
1277
1286
|
const result = parseMessage(raw);
|
|
1278
1287
|
if (result.kind === "control") {
|
|
@@ -1306,15 +1315,9 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1306
1315
|
return;
|
|
1307
1316
|
}
|
|
1308
1317
|
if (isClientToProxyRelayControlType(msg.type)) {
|
|
1309
|
-
const targetProxyId =
|
|
1318
|
+
const targetProxyId = clientWs.boundProxyId;
|
|
1310
1319
|
if (!targetProxyId) {
|
|
1311
|
-
clientWs
|
|
1312
|
-
JSON.stringify({
|
|
1313
|
-
type: "relay_error",
|
|
1314
|
-
code: RelayErrorCode.NOT_BOUND,
|
|
1315
|
-
message: "Client is not bound to any proxy"
|
|
1316
|
-
})
|
|
1317
|
-
);
|
|
1320
|
+
rejectNotBound(clientWs);
|
|
1318
1321
|
return;
|
|
1319
1322
|
}
|
|
1320
1323
|
const proxyWs = registry.getProxy(targetProxyId);
|
|
@@ -1334,15 +1337,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1334
1337
|
}
|
|
1335
1338
|
if (msg.type === "proxy_select") {
|
|
1336
1339
|
if (!registry.isProxyOnline(msg.proxyId)) {
|
|
1337
|
-
clientWs.
|
|
1338
|
-
JSON.stringify({
|
|
1339
|
-
type: "proxy_select_response",
|
|
1340
|
-
requestId: msg.requestId,
|
|
1341
|
-
success: false,
|
|
1342
|
-
errorCode: ControlErrorCode.PROXY_OFFLINE,
|
|
1343
|
-
error: `Proxy not online: ${msg.proxyId}`
|
|
1344
|
-
})
|
|
1345
|
-
);
|
|
1340
|
+
rejectProxySelect(clientWs, msg.requestId, msg.proxyId);
|
|
1346
1341
|
return;
|
|
1347
1342
|
}
|
|
1348
1343
|
if (!clientWs.clientId) {
|
|
@@ -1350,15 +1345,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1350
1345
|
}
|
|
1351
1346
|
const bound = registry.bindClientById(clientWs.clientId, msg.proxyId, clientWs);
|
|
1352
1347
|
if (!bound) {
|
|
1353
|
-
clientWs.
|
|
1354
|
-
JSON.stringify({
|
|
1355
|
-
type: "proxy_select_response",
|
|
1356
|
-
requestId: msg.requestId,
|
|
1357
|
-
success: false,
|
|
1358
|
-
errorCode: ControlErrorCode.PROXY_OFFLINE,
|
|
1359
|
-
error: `Proxy not online: ${msg.proxyId}`
|
|
1360
|
-
})
|
|
1361
|
-
);
|
|
1348
|
+
rejectProxySelect(clientWs, msg.requestId, msg.proxyId);
|
|
1362
1349
|
return;
|
|
1363
1350
|
}
|
|
1364
1351
|
clientWs.boundProxyId = msg.proxyId;
|
|
@@ -1390,13 +1377,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1390
1377
|
}
|
|
1391
1378
|
if (result.kind === "envelope") {
|
|
1392
1379
|
if (!clientWs.boundProxyId) {
|
|
1393
|
-
clientWs
|
|
1394
|
-
JSON.stringify({
|
|
1395
|
-
type: "relay_error",
|
|
1396
|
-
code: RelayErrorCode.NOT_BOUND,
|
|
1397
|
-
message: "Client is not bound to any proxy"
|
|
1398
|
-
})
|
|
1399
|
-
);
|
|
1380
|
+
rejectNotBound(clientWs);
|
|
1400
1381
|
return;
|
|
1401
1382
|
}
|
|
1402
1383
|
routeClientMessage(raw, clientWs.boundProxyId, clientWs, registry, logger, chaos);
|
|
@@ -1413,6 +1394,9 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1413
1394
|
});
|
|
1414
1395
|
clientWs.on("close", () => {
|
|
1415
1396
|
registry.removeClientWs(clientWs);
|
|
1397
|
+
if (clientWs.clientId) {
|
|
1398
|
+
registry.unbindClientById(clientWs.clientId);
|
|
1399
|
+
}
|
|
1416
1400
|
logger.info({ clientId: clientWs.clientId }, "Client disconnected");
|
|
1417
1401
|
});
|
|
1418
1402
|
clientWs.on("error", (err) => {
|
|
@@ -1490,6 +1474,11 @@ function createRelayServer(options) {
|
|
|
1490
1474
|
const { heartbeatInterval = 3e4, logger, dataDir, proxyToken, clientToken, chaos } = options;
|
|
1491
1475
|
const proxyTokenRequired = typeof proxyToken === "string" && proxyToken.length > 0;
|
|
1492
1476
|
const clientTokenRequired = typeof clientToken === "string" && clientToken.length > 0;
|
|
1477
|
+
const allowedOriginsSet = options.allowedOrigins && options.allowedOrigins.length > 0 ? new Set(options.allowedOrigins) : null;
|
|
1478
|
+
const checkOrigin = (origin) => {
|
|
1479
|
+
if (!allowedOriginsSet) return true;
|
|
1480
|
+
return typeof origin === "string" && allowedOriginsSet.has(origin);
|
|
1481
|
+
};
|
|
1493
1482
|
if (!proxyTokenRequired) {
|
|
1494
1483
|
logger.warn(
|
|
1495
1484
|
"proxy auth token not set, /proxy endpoint is open \u2014 ok for dev, not for public relay"
|
|
@@ -1514,7 +1503,7 @@ function createRelayServer(options) {
|
|
|
1514
1503
|
);
|
|
1515
1504
|
}
|
|
1516
1505
|
const app = express();
|
|
1517
|
-
const fontsDir = dataDir ? `${dataDir}/fonts` : `${
|
|
1506
|
+
const fontsDir = dataDir ? `${dataDir}/fonts` : `${homedir()}/.dev-anywhere/relay-data/fonts`;
|
|
1518
1507
|
const fontAssetDir = options.fontAssetDir ?? PACKAGED_FONTS_DIR;
|
|
1519
1508
|
app.use(
|
|
1520
1509
|
"/fonts",
|
|
@@ -1551,6 +1540,16 @@ function createRelayServer(options) {
|
|
|
1551
1540
|
httpServer.on("upgrade", (request, socket, head) => {
|
|
1552
1541
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
1553
1542
|
const { pathname } = url;
|
|
1543
|
+
const origin = request.headers.origin;
|
|
1544
|
+
if (!checkOrigin(origin)) {
|
|
1545
|
+
logger.warn(
|
|
1546
|
+
{ ip: request.socket.remoteAddress, origin: origin ?? "(missing)", pathname },
|
|
1547
|
+
"rejected upgrade: origin not in allowedOrigins"
|
|
1548
|
+
);
|
|
1549
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1550
|
+
socket.destroy();
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1554
1553
|
if (pathname === "/proxy") {
|
|
1555
1554
|
if (proxyTokenRequired) {
|
|
1556
1555
|
const token = url.searchParams.get("token");
|
|
@@ -1629,9 +1628,8 @@ function createRelayServer(options) {
|
|
|
1629
1628
|
}
|
|
1630
1629
|
|
|
1631
1630
|
export {
|
|
1632
|
-
createLogger,
|
|
1633
1631
|
RELAY_VERSION,
|
|
1634
1632
|
parseRelayChaosFromEnv,
|
|
1635
1633
|
createRelayServer
|
|
1636
1634
|
};
|
|
1637
|
-
//# sourceMappingURL=chunk-
|
|
1635
|
+
//# sourceMappingURL=chunk-SM2GRUCV.js.map
|