@dev-anywhere/relay 0.1.8 → 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-ZFCNDSFL.js → chunk-SM2GRUCV.js} +739 -739
- 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-ZFCNDSFL.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("/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("/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,106 +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
|
-
|
|
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;
|
|
942
833
|
}
|
|
943
|
-
|
|
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";
|
|
944
847
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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;
|
|
969
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());
|
|
970
933
|
});
|
|
934
|
+
return router;
|
|
971
935
|
}
|
|
972
936
|
|
|
937
|
+
// src/handlers/proxy.ts
|
|
938
|
+
import { WebSocket as WebSocket3 } from "ws";
|
|
939
|
+
|
|
973
940
|
// src/router.ts
|
|
974
941
|
import { WebSocket as WebSocket2 } from "ws";
|
|
975
942
|
function parseMessage(data) {
|
|
@@ -1000,8 +967,9 @@ function routeProxyMessage(raw, proxyId, registry, logger, chaos) {
|
|
|
1000
967
|
return;
|
|
1001
968
|
}
|
|
1002
969
|
const { message } = result;
|
|
1003
|
-
|
|
1004
|
-
|
|
970
|
+
if ("sessionId" in message) {
|
|
971
|
+
registry.addSessionToProxy(proxyId, message.sessionId);
|
|
972
|
+
}
|
|
1005
973
|
const clients = registry.getClientsForProxy(proxyId);
|
|
1006
974
|
for (const clientWs of clients) {
|
|
1007
975
|
if (clientWs.readyState === WebSocket2.OPEN) {
|
|
@@ -1037,6 +1005,7 @@ function routeClientMessage(raw, proxyId, clientWs, registry, logger, chaos) {
|
|
|
1037
1005
|
|
|
1038
1006
|
// src/handlers/proxy.ts
|
|
1039
1007
|
var MAX_BINARY_FRAME_SIZE = 10 * 1024 * 1024;
|
|
1008
|
+
var MAX_JSON_MESSAGE_SIZE = 1 * 1024 * 1024;
|
|
1040
1009
|
function notifyClientsProxyOffline(proxyId, registry, logger, chaos) {
|
|
1041
1010
|
const clients = registry.getClientsForProxy(proxyId);
|
|
1042
1011
|
const msg = JSON.stringify({ type: "proxy_offline", proxyId });
|
|
@@ -1067,6 +1036,15 @@ function broadcastProxyList(registry, chaos) {
|
|
|
1067
1036
|
else clientWs.send(msg);
|
|
1068
1037
|
}
|
|
1069
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
|
+
}
|
|
1070
1048
|
function handleProxyConnection(ws, registry, logger, chaos) {
|
|
1071
1049
|
const proxyWs = ws;
|
|
1072
1050
|
proxyWs.isAlive = true;
|
|
@@ -1099,6 +1077,13 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1099
1077
|
}
|
|
1100
1078
|
return;
|
|
1101
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
|
+
}
|
|
1102
1087
|
const raw = data.toString();
|
|
1103
1088
|
const result = parseMessage(raw);
|
|
1104
1089
|
if (result.kind === "control" && result.message.type === "proxy_register") {
|
|
@@ -1107,7 +1092,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1107
1092
|
proxyWs.proxyId = proxyId;
|
|
1108
1093
|
logger.info({ proxyId, status }, "Proxy registered");
|
|
1109
1094
|
proxyWs.send(
|
|
1110
|
-
|
|
1095
|
+
serializeControl({
|
|
1111
1096
|
type: "proxy_register_response",
|
|
1112
1097
|
status
|
|
1113
1098
|
})
|
|
@@ -1133,25 +1118,17 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1133
1118
|
}
|
|
1134
1119
|
if (result.kind === "control" && result.message.type === "session_sync") {
|
|
1135
1120
|
if (!proxyWs.proxyId) return;
|
|
1136
|
-
const sessions = result.message
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
registry.addSessionToProxy(proxyWs.proxyId, s.id);
|
|
1140
|
-
}
|
|
1141
|
-
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);
|
|
1142
1124
|
}
|
|
1125
|
+
logger.info({ proxyId: proxyWs.proxyId, count: sessions.length }, "Session sync received");
|
|
1143
1126
|
return;
|
|
1144
1127
|
}
|
|
1145
1128
|
if (result.kind === "control") {
|
|
1146
1129
|
if (isProxyToClientRelayControlType(result.message.type)) {
|
|
1147
1130
|
if (!proxyWs.proxyId) {
|
|
1148
|
-
proxyWs
|
|
1149
|
-
JSON.stringify({
|
|
1150
|
-
type: "relay_error",
|
|
1151
|
-
code: RelayErrorCode.NOT_REGISTERED,
|
|
1152
|
-
message: "Proxy must register before sending messages"
|
|
1153
|
-
})
|
|
1154
|
-
);
|
|
1131
|
+
rejectNotRegistered(proxyWs);
|
|
1155
1132
|
return;
|
|
1156
1133
|
}
|
|
1157
1134
|
const clients = registry.getClientsForProxy(proxyWs.proxyId);
|
|
@@ -1178,13 +1155,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1178
1155
|
}
|
|
1179
1156
|
if (result.kind === "envelope") {
|
|
1180
1157
|
if (!proxyWs.proxyId) {
|
|
1181
|
-
proxyWs
|
|
1182
|
-
JSON.stringify({
|
|
1183
|
-
type: "relay_error",
|
|
1184
|
-
code: RelayErrorCode.NOT_REGISTERED,
|
|
1185
|
-
message: "Proxy must register before sending messages"
|
|
1186
|
-
})
|
|
1187
|
-
);
|
|
1158
|
+
rejectNotRegistered(proxyWs);
|
|
1188
1159
|
return;
|
|
1189
1160
|
}
|
|
1190
1161
|
routeProxyMessage(raw, proxyWs.proxyId, registry, logger, chaos);
|
|
@@ -1202,19 +1173,31 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1202
1173
|
return;
|
|
1203
1174
|
}
|
|
1204
1175
|
});
|
|
1205
|
-
proxyWs.on("close", () => {
|
|
1206
|
-
if (proxyWs.proxyId)
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
} catch {
|
|
1211
|
-
}
|
|
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) {
|
|
1212
1181
|
logger.info(
|
|
1213
|
-
{ proxyId: proxyWs.proxyId },
|
|
1214
|
-
"
|
|
1182
|
+
{ proxyId: proxyWs.proxyId, ...closeMeta },
|
|
1183
|
+
"Old proxy ws closed after being superseded by reconnect, skipping offline transition"
|
|
1215
1184
|
);
|
|
1216
|
-
|
|
1185
|
+
return;
|
|
1217
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);
|
|
1218
1201
|
});
|
|
1219
1202
|
proxyWs.on("error", (err) => {
|
|
1220
1203
|
logger.error({ err, proxyId: proxyWs.proxyId }, "Proxy WebSocket error");
|
|
@@ -1224,6 +1207,7 @@ function handleProxyConnection(ws, registry, logger, chaos) {
|
|
|
1224
1207
|
// src/handlers/client.ts
|
|
1225
1208
|
import { WebSocket as WebSocket4 } from "ws";
|
|
1226
1209
|
import { nanoid } from "nanoid";
|
|
1210
|
+
var MAX_JSON_MESSAGE_SIZE2 = 1 * 1024 * 1024;
|
|
1227
1211
|
function handleClientRegister(clientId, clientWs, registry, logger) {
|
|
1228
1212
|
clientWs.clientId = clientId;
|
|
1229
1213
|
const binding = registry.getClientBinding(clientId);
|
|
@@ -1260,6 +1244,26 @@ function handleClientRegister(clientId, clientWs, registry, logger) {
|
|
|
1260
1244
|
);
|
|
1261
1245
|
logger.info({ clientId, proxyId, status: "restored" }, "Client registered");
|
|
1262
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
|
+
}
|
|
1263
1267
|
function handleClientConnection(ws, registry, logger, chaos) {
|
|
1264
1268
|
const clientWs = ws;
|
|
1265
1269
|
clientWs.isAlive = true;
|
|
@@ -1271,6 +1275,13 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1271
1275
|
if (isBinary) {
|
|
1272
1276
|
return;
|
|
1273
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
|
+
}
|
|
1274
1285
|
const raw = data.toString();
|
|
1275
1286
|
const result = parseMessage(raw);
|
|
1276
1287
|
if (result.kind === "control") {
|
|
@@ -1304,15 +1315,9 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1304
1315
|
return;
|
|
1305
1316
|
}
|
|
1306
1317
|
if (isClientToProxyRelayControlType(msg.type)) {
|
|
1307
|
-
const targetProxyId =
|
|
1318
|
+
const targetProxyId = clientWs.boundProxyId;
|
|
1308
1319
|
if (!targetProxyId) {
|
|
1309
|
-
clientWs
|
|
1310
|
-
JSON.stringify({
|
|
1311
|
-
type: "relay_error",
|
|
1312
|
-
code: RelayErrorCode.NOT_BOUND,
|
|
1313
|
-
message: "Client is not bound to any proxy"
|
|
1314
|
-
})
|
|
1315
|
-
);
|
|
1320
|
+
rejectNotBound(clientWs);
|
|
1316
1321
|
return;
|
|
1317
1322
|
}
|
|
1318
1323
|
const proxyWs = registry.getProxy(targetProxyId);
|
|
@@ -1332,15 +1337,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1332
1337
|
}
|
|
1333
1338
|
if (msg.type === "proxy_select") {
|
|
1334
1339
|
if (!registry.isProxyOnline(msg.proxyId)) {
|
|
1335
|
-
clientWs.
|
|
1336
|
-
JSON.stringify({
|
|
1337
|
-
type: "proxy_select_response",
|
|
1338
|
-
requestId: msg.requestId,
|
|
1339
|
-
success: false,
|
|
1340
|
-
errorCode: ControlErrorCode.PROXY_OFFLINE,
|
|
1341
|
-
error: `Proxy not online: ${msg.proxyId}`
|
|
1342
|
-
})
|
|
1343
|
-
);
|
|
1340
|
+
rejectProxySelect(clientWs, msg.requestId, msg.proxyId);
|
|
1344
1341
|
return;
|
|
1345
1342
|
}
|
|
1346
1343
|
if (!clientWs.clientId) {
|
|
@@ -1348,15 +1345,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1348
1345
|
}
|
|
1349
1346
|
const bound = registry.bindClientById(clientWs.clientId, msg.proxyId, clientWs);
|
|
1350
1347
|
if (!bound) {
|
|
1351
|
-
clientWs.
|
|
1352
|
-
JSON.stringify({
|
|
1353
|
-
type: "proxy_select_response",
|
|
1354
|
-
requestId: msg.requestId,
|
|
1355
|
-
success: false,
|
|
1356
|
-
errorCode: ControlErrorCode.PROXY_OFFLINE,
|
|
1357
|
-
error: `Proxy not online: ${msg.proxyId}`
|
|
1358
|
-
})
|
|
1359
|
-
);
|
|
1348
|
+
rejectProxySelect(clientWs, msg.requestId, msg.proxyId);
|
|
1360
1349
|
return;
|
|
1361
1350
|
}
|
|
1362
1351
|
clientWs.boundProxyId = msg.proxyId;
|
|
@@ -1388,13 +1377,7 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1388
1377
|
}
|
|
1389
1378
|
if (result.kind === "envelope") {
|
|
1390
1379
|
if (!clientWs.boundProxyId) {
|
|
1391
|
-
clientWs
|
|
1392
|
-
JSON.stringify({
|
|
1393
|
-
type: "relay_error",
|
|
1394
|
-
code: RelayErrorCode.NOT_BOUND,
|
|
1395
|
-
message: "Client is not bound to any proxy"
|
|
1396
|
-
})
|
|
1397
|
-
);
|
|
1380
|
+
rejectNotBound(clientWs);
|
|
1398
1381
|
return;
|
|
1399
1382
|
}
|
|
1400
1383
|
routeClientMessage(raw, clientWs.boundProxyId, clientWs, registry, logger, chaos);
|
|
@@ -1411,6 +1394,9 @@ function handleClientConnection(ws, registry, logger, chaos) {
|
|
|
1411
1394
|
});
|
|
1412
1395
|
clientWs.on("close", () => {
|
|
1413
1396
|
registry.removeClientWs(clientWs);
|
|
1397
|
+
if (clientWs.clientId) {
|
|
1398
|
+
registry.unbindClientById(clientWs.clientId);
|
|
1399
|
+
}
|
|
1414
1400
|
logger.info({ clientId: clientWs.clientId }, "Client disconnected");
|
|
1415
1401
|
});
|
|
1416
1402
|
clientWs.on("error", (err) => {
|
|
@@ -1488,6 +1474,11 @@ function createRelayServer(options) {
|
|
|
1488
1474
|
const { heartbeatInterval = 3e4, logger, dataDir, proxyToken, clientToken, chaos } = options;
|
|
1489
1475
|
const proxyTokenRequired = typeof proxyToken === "string" && proxyToken.length > 0;
|
|
1490
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
|
+
};
|
|
1491
1482
|
if (!proxyTokenRequired) {
|
|
1492
1483
|
logger.warn(
|
|
1493
1484
|
"proxy auth token not set, /proxy endpoint is open \u2014 ok for dev, not for public relay"
|
|
@@ -1512,7 +1503,7 @@ function createRelayServer(options) {
|
|
|
1512
1503
|
);
|
|
1513
1504
|
}
|
|
1514
1505
|
const app = express();
|
|
1515
|
-
const fontsDir = dataDir ? `${dataDir}/fonts` : `${
|
|
1506
|
+
const fontsDir = dataDir ? `${dataDir}/fonts` : `${homedir()}/.dev-anywhere/relay-data/fonts`;
|
|
1516
1507
|
const fontAssetDir = options.fontAssetDir ?? PACKAGED_FONTS_DIR;
|
|
1517
1508
|
app.use(
|
|
1518
1509
|
"/fonts",
|
|
@@ -1549,6 +1540,16 @@ function createRelayServer(options) {
|
|
|
1549
1540
|
httpServer.on("upgrade", (request, socket, head) => {
|
|
1550
1541
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
1551
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
|
+
}
|
|
1552
1553
|
if (pathname === "/proxy") {
|
|
1553
1554
|
if (proxyTokenRequired) {
|
|
1554
1555
|
const token = url.searchParams.get("token");
|
|
@@ -1627,9 +1628,8 @@ function createRelayServer(options) {
|
|
|
1627
1628
|
}
|
|
1628
1629
|
|
|
1629
1630
|
export {
|
|
1630
|
-
createLogger,
|
|
1631
1631
|
RELAY_VERSION,
|
|
1632
1632
|
parseRelayChaosFromEnv,
|
|
1633
1633
|
createRelayServer
|
|
1634
1634
|
};
|
|
1635
|
-
//# sourceMappingURL=chunk-
|
|
1635
|
+
//# sourceMappingURL=chunk-SM2GRUCV.js.map
|