@jingyi0605/codingns4dsh 0.1.2 → 0.1.7-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +14 -5
  3. package/data/build/dist/client/account-bar.js +57 -3
  4. package/data/build/dist/client/bundle.js +36246 -268
  5. package/data/build/dist/client/cli-slots.js +60 -18
  6. package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
  7. package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
  8. package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
  9. package/data/build/dist/client/features/file-management-panel.js +48 -0
  10. package/data/build/dist/client/features/file-management.d.ts +4 -0
  11. package/data/build/dist/client/features/file-management.js +50 -0
  12. package/data/build/dist/client/features/index.d.ts +10 -1
  13. package/data/build/dist/client/features/index.js +12 -1
  14. package/data/build/dist/client/features/login-protection-panel.js +1 -1
  15. package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
  16. package/data/build/dist/client/features/login-protection-session.js +18 -0
  17. package/data/build/dist/client/features/peer-host.d.ts +4 -0
  18. package/data/build/dist/client/features/peer-host.js +89 -0
  19. package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
  20. package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
  21. package/data/build/dist/client/features/reverse-proxy.js +75 -5
  22. package/data/build/dist/client/features/types.d.ts +9 -0
  23. package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
  24. package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
  25. package/data/build/dist/client/file-management-dom.d.ts +12 -0
  26. package/data/build/dist/client/file-management-dom.js +527 -0
  27. package/data/build/dist/client/git-management.js +24 -5
  28. package/data/build/dist/client/host-navigation.d.ts +29 -0
  29. package/data/build/dist/client/host-navigation.js +29 -0
  30. package/data/build/dist/client/host-router.d.ts +3 -0
  31. package/data/build/dist/client/host-router.js +2 -0
  32. package/data/build/dist/client/index.d.ts +6 -1
  33. package/data/build/dist/client/index.js +30 -10
  34. package/data/build/dist/client/locale.js +36 -7
  35. package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
  36. package/data/build/dist/client/peer-host-connection-button.js +69 -0
  37. package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
  38. package/data/build/dist/client/peer-host-management-api.js +33 -0
  39. package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
  40. package/data/build/dist/client/peer-host-management-panel.js +181 -0
  41. package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
  42. package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
  43. package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
  44. package/data/build/dist/client/peer-host-scoped-client.js +316 -0
  45. package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
  46. package/data/build/dist/client/peer-host-session-controller.js +95 -0
  47. package/data/build/dist/client/quick-phrase-slot.js +14 -0
  48. package/data/build/dist/client/remote-web-context.js +26 -4
  49. package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
  50. package/data/build/dist/client/session-changed-files-view.js +337 -0
  51. package/data/build/dist/client/settings-bridge.d.ts +14 -4
  52. package/data/build/dist/client/settings-bridge.js +59 -12
  53. package/data/build/dist/client/settings-section.js +17 -4
  54. package/data/build/dist/client/subscription-slot.js +21 -8
  55. package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
  56. package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
  57. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
  58. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
  59. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
  60. package/data/build/dist/dsh-capabilities/matrix.js +9 -0
  61. package/data/build/dist/dsh-capabilities/routes.js +35 -0
  62. package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
  63. package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
  64. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  65. package/data/build/dist/features/host-router.d.ts +26 -0
  66. package/data/build/dist/features/host-router.js +110 -0
  67. package/data/build/dist/features/registry.js +3 -0
  68. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
  69. package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
  70. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
  71. package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
  72. package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
  73. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
  74. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
  75. package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
  76. package/data/build/dist/host/cli-adapters/feature.js +94 -10
  77. package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
  78. package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
  79. package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
  80. package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
  81. package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
  82. package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
  83. package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
  84. package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
  85. package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
  86. package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
  87. package/data/build/dist/host/cli-adapters/registry.js +41 -7
  88. package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
  89. package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
  90. package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
  91. package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
  92. package/data/build/dist/host/dsh-device-runtime.js +55 -29
  93. package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
  94. package/data/build/dist/host/dsh-runtime-version.js +71 -2
  95. package/data/build/dist/host/features/auth.js +10 -2
  96. package/data/build/dist/host/features/file-management.d.ts +5 -0
  97. package/data/build/dist/host/features/file-management.js +267 -0
  98. package/data/build/dist/host/features/git-management.js +24 -2
  99. package/data/build/dist/host/features/index.d.ts +2 -0
  100. package/data/build/dist/host/features/index.js +6 -0
  101. package/data/build/dist/host/features/peer-host.d.ts +22 -0
  102. package/data/build/dist/host/features/peer-host.js +322 -0
  103. package/data/build/dist/host/features/types.d.ts +4 -0
  104. package/data/build/dist/host/host-router.d.ts +3 -0
  105. package/data/build/dist/host/host-router.js +2 -0
  106. package/data/build/dist/host/index-injection.d.ts +17 -0
  107. package/data/build/dist/host/index-injection.js +24 -0
  108. package/data/build/dist/host/index.d.ts +4 -1
  109. package/data/build/dist/host/index.js +41 -7
  110. package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
  111. package/data/build/dist/host/lan-access-dsh.js +145 -26
  112. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
  113. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
  114. package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
  115. package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
  116. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
  117. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
  118. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
  119. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
  120. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
  121. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
  122. package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
  123. package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
  124. package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
  125. package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
  126. package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
  127. package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
  128. package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
  129. package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
  130. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
  131. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
  132. package/data/build/dist/host/native-session-bridge.d.ts +18 -2
  133. package/data/build/dist/host/native-session-bridge.js +236 -8
  134. package/data/build/dist/host/remote-web-runtime.js +26 -1
  135. package/data/build/dist/host/rpc.js +8 -3
  136. package/data/build/dist/host/session-changes.d.ts +5 -0
  137. package/data/build/dist/host/session-changes.js +163 -0
  138. package/data/build/dist/host/settings.js +6 -0
  139. package/data/build/dist/index.d.ts +1 -1
  140. package/data/build/dist/index.js +1 -1
  141. package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
  142. package/data/build/dist/shared/contracts/config.d.ts +15 -0
  143. package/data/build/dist/shared/contracts/config.js +10 -0
  144. package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
  145. package/data/build/dist/shared/contracts/errors.d.ts +14 -0
  146. package/data/build/dist/shared/contracts/errors.js +15 -0
  147. package/data/build/dist/shared/contracts/feature.d.ts +2 -0
  148. package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
  149. package/data/build/dist/shared/contracts/file-management.js +3 -0
  150. package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
  151. package/data/build/dist/shared/contracts/peer-host.js +19 -0
  152. package/data/build/dist/shared/contracts/version.d.ts +1 -1
  153. package/data/build/dist/shared/contracts/version.js +1 -1
  154. package/data/build/dist/shared/index.d.ts +4 -3
  155. package/data/build/dist/shared/index.js +2 -2
  156. package/data/build/dist/transport/debug.d.ts +5 -0
  157. package/data/build/dist/transport/debug.js +23 -1
  158. package/data/build/dist/transport/dsh-transport.d.ts +2 -0
  159. package/data/build/dist/transport/dsh-transport.js +11 -0
  160. package/data/build/dist/transport/webrtc-client.d.ts +2 -0
  161. package/data/build/dist/transport/webrtc-client.js +2 -1
  162. package/data/build/dist/transport/webrtc-host.d.ts +2 -0
  163. package/data/build/dist/transport/webrtc-host.js +6 -3
  164. package/data/build/dist/typert.host.d.ts +1 -1
  165. package/data/build/dist/typert.host.js +2 -1
  166. package/dsh.bundle.patch +10 -0
  167. package/package.json +19 -11
  168. package/profile/README.md +2 -2
  169. package/profile/package.json +2 -2
  170. package/profile/version.json +1 -1
  171. package/version.json +1 -1
@@ -6,6 +6,19 @@ import { homedir } from 'node:os';
6
6
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
7
7
  import { dirname, join } from 'node:path';
8
8
  import { CodingNsRpcError } from './rpc-table.js';
9
+ /**
10
+ * Cookie 不包含端口信息;多个 DSH 实例若共用名称,浏览器会在实例之间覆盖会话。
11
+ * 正式环境保留旧名称,开发环境由启动包装命令显式指定独立名称。
12
+ */
13
+ export const DEFAULT_LOGIN_PROTECTION_COOKIE_NAME = 'dsh_codingns_session';
14
+ const LOGIN_PROTECTION_COOKIE_NAME_ENV = 'CODINGNS4DSH_LOGIN_COOKIE_NAME';
15
+ const COOKIE_NAME_PATTERN = /^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/u;
16
+ export function resolveLoginProtectionCookieName(configured = process.env[LOGIN_PROTECTION_COOKIE_NAME_ENV]) {
17
+ const value = configured?.trim();
18
+ return value !== undefined && value !== '' && value.length <= 128 && COOKIE_NAME_PATTERN.test(value)
19
+ ? value
20
+ : DEFAULT_LOGIN_PROTECTION_COOKIE_NAME;
21
+ }
9
22
  /** 浏览器中继只携带短期签名票据,密码哈希和签名密钥始终留在 Host。 */
10
23
  export async function openLoginProtectionSession(store, username, password, scope) {
11
24
  const config = await store.read();
@@ -13,9 +26,15 @@ export async function openLoginProtectionSession(store, username, password, scop
13
26
  return { token: '', expiresAt: new Date().toISOString() };
14
27
  if (username !== config.username || !verifyPassword(password, config))
15
28
  throw new CodingNsRpcError('CODINGNS_RPC_UNAUTHENTICATED', '本地用户名或密码错误');
16
- const expiresAt = Date.now() + config.timeoutSeconds * 1000;
17
- const payload = encodeSessionPayload({ username: config.username, scope, expiresAt, nonce: randomBytes(16).toString('base64url') });
18
- return { token: `${payload}.${signSessionPayload(payload, config)}`, expiresAt: new Date(expiresAt).toISOString() };
29
+ return issueLoginProtectionSession(config, scope);
30
+ }
31
+ /** 在旧票据仍有效时滚动续签;过期票据不能借此恢复登录。 */
32
+ export async function refreshLoginProtectionSession(store, token, scope) {
33
+ const config = await store.read();
34
+ if (config === null || !config.enabled || !config.scopes[scope] || typeof token !== 'string' || !verifySignedSessionToken(token, config, scope)) {
35
+ throw new CodingNsRpcError('CODINGNS_RPC_UNAUTHENTICATED', '登录保护会话已失效,请重新登录');
36
+ }
37
+ return issueLoginProtectionSession(config, scope);
19
38
  }
20
39
  /** 校验中继票据;配置关闭或未覆盖该范围时保持向后兼容,直接放行。 */
21
40
  export async function verifyLoginProtectionSession(store, token, scope) {
@@ -121,10 +140,12 @@ export class LanAccessDshProxy {
121
140
  /** 当前进程内主动退出的会话;正常会话由签名令牌承载,可跨代理重建复用。 */
122
141
  revokedSessions = new Set();
123
142
  upstreamCookie = null;
124
- constructor(runtime = createNodeLanAccessDshRuntime(), authenticatedUrl) {
143
+ constructor(runtime = createNodeLanAccessDshRuntime(), authenticatedUrl, sessionCookieName = resolveLoginProtectionCookieName()) {
125
144
  this.runtime = runtime;
126
145
  this.authenticatedUrl = authenticatedUrl;
146
+ this.sessionCookieName = resolveLoginProtectionCookieName(sessionCookieName);
127
147
  }
148
+ sessionCookieName;
128
149
  listenHosts() {
129
150
  return this.runtime.listListenHosts();
130
151
  }
@@ -238,8 +259,9 @@ export class LanAccessDshProxy {
238
259
  dshSocket.once('error', removeDsh);
239
260
  // DSH 的 API 信任校验依据上游看到的 Host/Origin。局域网入口的地址
240
261
  // 与真实 DSH 地址不同,直接透传会导致静态页面能打开但所有 /api 返回 403。
241
- // 只改写请求头,响应和 WebSocket 帧仍然原样双向转发。测试运行时的最小
242
- // FakeStream 没有 Node Writable 接口,保留原始 pipe 以便验证连接关系。
262
+ // 改写请求头,并兼容旧版 Connection RPC 的正文格式;响应和 WebSocket 帧仍然
263
+ // 原样双向转发。测试运行时的最小 FakeStream 没有 Node Writable 接口,保留原始
264
+ // pipe 以便验证连接关系。
243
265
  if (typeof dshSocket.on === 'function') {
244
266
  const localAddress = isLoopbackSocket(localSocket);
245
267
  const authTransform = new LanAccessDshAuthTransform(localSocket, (request) => this.authorize(request, localAddress));
@@ -267,14 +289,26 @@ export class LanAccessDshProxy {
267
289
  authorize(request, localAddress) {
268
290
  const config = this.loginConfig;
269
291
  if (request.path === '/__codingns/session' && request.method === 'GET') {
270
- const token = readCookie(request.headers.cookie, 'dsh_codingns_session');
292
+ const token = readCookie(request.headers.cookie, this.sessionCookieName);
271
293
  const authenticated = !localAddress && config !== null && config.enabled && config.scopes.lan
272
294
  && token !== undefined && !this.revokedSessions.has(token)
273
295
  && verifySignedSessionToken(token, config, 'lan');
296
+ const expiresAt = authenticated && config !== null && token !== undefined ? readLoginProtectionSessionExpiresAt(token, config, 'lan') : null;
274
297
  return loginJsonResponse(200, authenticated
275
- ? { authenticated: true, username: config.username }
298
+ ? { authenticated: true, username: config.username, ...(expiresAt === null ? {} : { expiresAt }) }
276
299
  : { authenticated: false });
277
300
  }
301
+ if (request.path === '/__codingns/session/refresh' && request.method === 'POST') {
302
+ const token = readCookie(request.headers.cookie, this.sessionCookieName);
303
+ if (localAddress || config === null || !config.enabled || !config.scopes.lan || token === undefined || this.revokedSessions.has(token) || !verifySignedSessionToken(token, config, 'lan')) {
304
+ return loginJsonResponse(401, { authenticated: false });
305
+ }
306
+ const session = issueLoginProtectionSession(config, 'lan');
307
+ this.revokedSessions.delete(session.token);
308
+ return loginJsonResponse(200, { authenticated: true, username: config.username, expiresAt: session.expiresAt }, {
309
+ 'Set-Cookie': sessionCookie(this.sessionCookieName, session.token, config.timeoutSeconds),
310
+ });
311
+ }
278
312
  if (localAddress || config === null || !config.enabled || !config.scopes.lan)
279
313
  return 'pass';
280
314
  if (request.path === '/__codingns/login' && request.method === 'POST') {
@@ -286,19 +320,19 @@ export class LanAccessDshProxy {
286
320
  const payload = encodeSessionPayload({ username: config.username, scope: 'lan', expiresAt, nonce: randomBytes(16).toString('base64url') });
287
321
  const token = `${payload}.${signSessionPayload(payload, config)}`;
288
322
  this.revokedSessions.delete(token);
289
- return loginResponse(303, '', { 'Set-Cookie': sessionCookie(token, config.timeoutSeconds), Location: '/' });
323
+ return loginResponse(303, '', { 'Set-Cookie': sessionCookie(this.sessionCookieName, token, config.timeoutSeconds), Location: '/' });
290
324
  }
291
325
  if (request.path === '/__codingns/logout') {
292
- const token = readCookie(request.headers.cookie, 'dsh_codingns_session');
326
+ const token = readCookie(request.headers.cookie, this.sessionCookieName);
293
327
  if (token !== undefined && verifySignedSessionToken(token, config, 'lan'))
294
328
  this.revokedSessions.add(token);
295
- return loginResponse(303, '', { 'Set-Cookie': 'dsh_codingns_session=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0', Location: '/' });
329
+ return loginResponse(303, '', { 'Set-Cookie': clearSessionCookie(this.sessionCookieName), Location: '/' });
296
330
  }
297
331
  // 浏览器会在页面刷新时独立请求 Web App manifest;它只包含静态元数据,
298
332
  // 不应因为登录保护缺少 Cookie 而产生 401 控制台噪声。
299
333
  if (request.path === '/manifest.webmanifest' && request.method === 'GET')
300
334
  return 'pass';
301
- const token = readCookie(request.headers.cookie, 'dsh_codingns_session');
335
+ const token = readCookie(request.headers.cookie, this.sessionCookieName);
302
336
  if (token !== undefined && !this.revokedSessions.has(token) && verifySignedSessionToken(token, config, 'lan')) {
303
337
  return 'pass';
304
338
  }
@@ -397,8 +431,16 @@ function readCookie(value, name) {
397
431
  }
398
432
  return undefined;
399
433
  }
400
- function sessionCookie(token, timeoutSeconds) {
401
- return `dsh_codingns_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${timeoutSeconds}`;
434
+ function sessionCookie(name, token, timeoutSeconds) {
435
+ return `${name}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${timeoutSeconds}`;
436
+ }
437
+ function issueLoginProtectionSession(config, scope) {
438
+ const expiresAt = Date.now() + config.timeoutSeconds * 1000;
439
+ const payload = encodeSessionPayload({ username: config.username, scope, expiresAt, nonce: randomBytes(16).toString('base64url') });
440
+ return { token: `${payload}.${signSessionPayload(payload, config)}`, expiresAt: new Date(expiresAt).toISOString() };
441
+ }
442
+ function clearSessionCookie(name) {
443
+ return `${name}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;
402
444
  }
403
445
  function getSetCookie(headers) {
404
446
  const extended = headers;
@@ -423,8 +465,8 @@ function loginResponse(status, body, extra = {}) {
423
465
  const head = encodeLatin1(`HTTP/1.1 ${status} ${statusText(status)}\r\n${Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\r\n')}\r\n\r\n`);
424
466
  return concatBytes(head, content);
425
467
  }
426
- function loginJsonResponse(status, value) {
427
- return loginResponse(status, JSON.stringify(value), { 'Content-Type': 'application/json; charset=utf-8' });
468
+ function loginJsonResponse(status, value, extra = {}) {
469
+ return loginResponse(status, JSON.stringify(value), { 'Content-Type': 'application/json; charset=utf-8', ...extra });
428
470
  }
429
471
  function loginPage() {
430
472
  // 视觉规则直接复用 codingns4dsh-h5/styles.css 的 Cyber 登录区,只把 Connect
@@ -450,11 +492,11 @@ function escapeHtml(value) { return value.replace(/[&<>"']/g, (char) => ({ '&':
450
492
  * Content-Length/分块边界时误把 keep-alive 上的第二个请求当成正文。WebSocket
451
493
  * 升级请求则保持 Upgrade,升级后的帧不再经过头部解析。
452
494
  */
453
- class LanAccessDshRequestTransform extends Transform {
495
+ export class LanAccessDshRequestTransform extends Transform {
454
496
  targetAuthority;
455
497
  upstreamCookie;
456
498
  pending = new Uint8Array(0);
457
- bodyRemaining = 0;
499
+ pendingRequest = null;
458
500
  finished = false;
459
501
  constructor(targetAuthority, upstreamCookie = null) {
460
502
  super();
@@ -471,6 +513,14 @@ class LanAccessDshRequestTransform extends Transform {
471
513
  callback();
472
514
  }
473
515
  _flush(callback) {
516
+ if (this.pendingRequest !== null) {
517
+ // 请求正文不完整时无法判断是否为 JSON,保留原正文并只改写请求头。
518
+ this.push(rewriteLanAccessDshRequestHeaders(this.pendingRequest.head, this.targetAuthority, this.upstreamCookie ?? undefined));
519
+ if (this.pending.length > 0)
520
+ this.push(this.pending);
521
+ callback();
522
+ return;
523
+ }
474
524
  if (this.pending.length > 0)
475
525
  callback(null, this.pending);
476
526
  else
@@ -478,13 +528,15 @@ class LanAccessDshRequestTransform extends Transform {
478
528
  }
479
529
  drainPending() {
480
530
  while (!this.finished) {
481
- if (this.bodyRemaining > 0) {
482
- const size = Math.min(this.bodyRemaining, this.pending.length);
483
- if (size === 0)
531
+ if (this.pendingRequest !== null) {
532
+ if (this.pending.length < this.pendingRequest.bodyLength)
484
533
  return;
485
- this.push(this.pending.subarray(0, size));
486
- this.pending = this.pending.subarray(size);
487
- this.bodyRemaining -= size;
534
+ const body = this.pending.subarray(0, this.pendingRequest.bodyLength);
535
+ this.pending = this.pending.subarray(this.pendingRequest.bodyLength);
536
+ const normalizedBody = normalizeLanAccessDshRpcBody(this.pendingRequest.path, this.pendingRequest.method, body);
537
+ const head = rewriteLanAccessDshRequestHeaders(this.pendingRequest.head, this.targetAuthority, this.upstreamCookie ?? undefined);
538
+ this.push(concatBytes(rewriteContentLength(head, normalizedBody.length), normalizedBody));
539
+ this.pendingRequest = null;
488
540
  continue;
489
541
  }
490
542
  const end = findHeaderEnd(this.pending);
@@ -500,9 +552,9 @@ class LanAccessDshRequestTransform extends Transform {
500
552
  const head = this.pending.subarray(0, end + 4);
501
553
  this.pending = this.pending.subarray(end + 4);
502
554
  const upgrade = isUpgradeRequest(head);
503
- this.push(rewriteLanAccessDshRequestHeaders(head, this.targetAuthority, this.upstreamCookie ?? undefined));
504
555
  if (upgrade) {
505
556
  // WebSocket 头部之后全部是帧数据,不能再次进入 HTTP 头缓存。
557
+ this.push(rewriteLanAccessDshRequestHeaders(head, this.targetAuthority, this.upstreamCookie ?? undefined));
506
558
  this.finished = true;
507
559
  if (this.pending.length > 0) {
508
560
  this.push(this.pending);
@@ -513,6 +565,7 @@ class LanAccessDshRequestTransform extends Transform {
513
565
  // DSH 的普通请求均为无体 GET 或带 Content-Length 的 JSON POST。
514
566
  // 分块上传无法安全识别下一条请求,遇到它时透传本连接剩余字节。
515
567
  if (/\r\ntransfer-encoding:\s*[^\r\n]*\bchunked\b/iu.test(decodeLatin1(head))) {
568
+ this.push(rewriteLanAccessDshRequestHeaders(head, this.targetAuthority, this.upstreamCookie ?? undefined));
516
569
  this.finished = true;
517
570
  if (this.pending.length > 0) {
518
571
  this.push(this.pending);
@@ -520,10 +573,43 @@ class LanAccessDshRequestTransform extends Transform {
520
573
  }
521
574
  return;
522
575
  }
523
- this.bodyRemaining = contentLengthOf(head);
576
+ const bodyLength = contentLengthOf(head);
577
+ if (bodyLength === 0) {
578
+ this.push(rewriteLanAccessDshRequestHeaders(head, this.targetAuthority, this.upstreamCookie ?? undefined));
579
+ continue;
580
+ }
581
+ const requestLine = decodeLatin1(head).split('\r\n', 1)[0] ?? '';
582
+ const parts = requestLine.split(' ');
583
+ this.pendingRequest = {
584
+ head,
585
+ method: parts[0] ?? 'GET',
586
+ path: (parts[1] ?? '/').split('?', 1)[0] ?? '/',
587
+ bodyLength,
588
+ };
524
589
  }
525
590
  }
526
591
  }
592
+ /** 兼容旧版 DSH Web Client 直接把 RPC 参数放入 payload 的请求格式。 */
593
+ export function normalizeLanAccessDshRpcBody(path, method, body) {
594
+ if (method.toUpperCase() !== 'POST' || !path.startsWith('/api/'))
595
+ return body;
596
+ try {
597
+ const message = JSON.parse(new TextDecoder().decode(body));
598
+ if (!isOfficialConnectionRpcMessage(message) || !isRecord(message.payload) || 'args' in message.payload)
599
+ return body;
600
+ return new TextEncoder().encode(JSON.stringify({ ...message, payload: { args: message.payload } }));
601
+ }
602
+ catch {
603
+ return body;
604
+ }
605
+ }
606
+ /** 只兼容 DSH 官方两段式 Remote endpoint,不能改写 CodingNS 自有 RPC。 */
607
+ function isOfficialConnectionRpcMessage(value) {
608
+ if (!isRecord(value) || value.type !== 'client-request' || typeof value.method !== 'string')
609
+ return false;
610
+ const parts = value.method.split('/');
611
+ return parts.length === 2 && parts.every((part) => part !== '') && parts[0] !== 'codingns';
612
+ }
527
613
  /** 改写请求中的 Host、Origin,并关闭普通 HTTP 上游连接。 */
528
614
  export function rewriteLanAccessDshRequestHeaders(input, targetAuthority, upstreamCookie) {
529
615
  const text = decodeLatin1(input);
@@ -569,6 +655,14 @@ function contentLengthOf(input) {
569
655
  const match = /\r\ncontent-length:\s*(\d+)/iu.exec(decodeLatin1(input));
570
656
  return match?.[1] === undefined ? 0 : Number(match[1]);
571
657
  }
658
+ function rewriteContentLength(input, length) {
659
+ const lines = decodeLatin1(input).split('\r\n');
660
+ for (let index = 1; index < lines.length; index += 1) {
661
+ if (/^content-length\s*:/iu.test(lines[index] ?? ''))
662
+ lines[index] = `Content-Length: ${length}`;
663
+ }
664
+ return encodeLatin1(lines.join('\r\n'));
665
+ }
572
666
  function isUpgradeRequest(input) {
573
667
  const text = decodeLatin1(input);
574
668
  return /\r\nconnection:\s*[^\r\n]*\bupgrade\b/iu.test(text)
@@ -660,6 +754,10 @@ export function createLanAccessDshRpcHandler(proxy, settings, loginStore = new F
660
754
  const input = parseLoginSessionPayload(payload);
661
755
  return openLoginProtectionSession(loginStore, input.username, input.password, input.scope);
662
756
  }
757
+ case 'login/session/refresh': {
758
+ const input = parseLoginSessionRefreshPayload(payload);
759
+ return refreshLoginProtectionSession(loginStore, input.token, input.scope);
760
+ }
663
761
  case 'start':
664
762
  return proxy.start(parseStartPayload(payload));
665
763
  case 'stop':
@@ -682,6 +780,16 @@ function parseLoginSessionPayload(value) {
682
780
  throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录会话应用范围无效');
683
781
  return { username, password: input.password, scope };
684
782
  }
783
+ function parseLoginSessionRefreshPayload(value) {
784
+ if (!value || typeof value !== 'object')
785
+ throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录会话刷新参数必须是对象');
786
+ const input = value;
787
+ const token = requireLoginText(input.token, '会话票据', 32, 4096);
788
+ const scope = input.scope === 'lan' || input.scope === 'relay' ? input.scope : undefined;
789
+ if (scope === undefined)
790
+ throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', '登录会话范围无效');
791
+ return { token, scope };
792
+ }
685
793
  function defaultLanAccessDshSettings() {
686
794
  return { autoStart: false, listenHost: '0.0.0.0', listenPort: 13080, dshPort: 0 };
687
795
  }
@@ -757,6 +865,17 @@ function verifySignedSessionToken(token, config, scope) {
757
865
  return false;
758
866
  }
759
867
  }
868
+ function readLoginProtectionSessionExpiresAt(token, config, scope) {
869
+ if (!verifySignedSessionToken(token, config, scope))
870
+ return null;
871
+ try {
872
+ const value = JSON.parse(Buffer.from(token.slice(0, token.lastIndexOf('.')), 'base64url').toString('utf8'));
873
+ return typeof value.expiresAt === 'number' && Number.isFinite(value.expiresAt) ? value.expiresAt : null;
874
+ }
875
+ catch {
876
+ return null;
877
+ }
878
+ }
760
879
  function requireLoginText(value, field, min, max) {
761
880
  if (typeof value !== 'string' || value.trim().length < min || value.trim().length > max || /[\u0000-\u001F\u007F]/u.test(value))
762
881
  throw new LanAccessDshError('LAN_ACCESS_DSH_INVALID', `${field} 格式无效`);
@@ -0,0 +1,52 @@
1
+ import type { HostScope, PeerHostErrorCode } from '../../../shared/contracts/peer-host.js';
2
+ import { PeerHostSessionService } from './peer-host-session.js';
3
+ import { PeerHostStore } from './peer-host-store.js';
4
+ export declare const PEER_HOST_HTTP_PROXY_RULES: readonly [{
5
+ readonly prefix: "/api/workspaces";
6
+ readonly methods: readonly ["GET"];
7
+ }, {
8
+ readonly prefix: "/api/sessions";
9
+ readonly methods: readonly ["GET", "POST"];
10
+ }, {
11
+ readonly prefix: "/api/file-tree";
12
+ readonly methods: readonly ["GET"];
13
+ }, {
14
+ readonly prefix: "/api/files";
15
+ readonly methods: readonly ["GET", "PUT", "POST"];
16
+ }, {
17
+ readonly prefix: "/api/git";
18
+ readonly methods: readonly ["GET", "POST"];
19
+ }, {
20
+ readonly prefix: "/api/terminal";
21
+ readonly methods: readonly ["GET", "POST"];
22
+ }, {
23
+ readonly prefix: "/api/right-tools";
24
+ readonly methods: readonly ["GET", "POST"];
25
+ }];
26
+ export declare class PeerHostProxyError extends Error {
27
+ readonly code: PeerHostErrorCode;
28
+ constructor(code: PeerHostErrorCode, message: string);
29
+ }
30
+ /** 当前 Host 到目标 Host 的 HTTP 正向代理。 */
31
+ export declare class PeerHostHttpProxyService {
32
+ private readonly store;
33
+ private readonly sessions;
34
+ private readonly fetchImpl;
35
+ constructor(store: PeerHostStore, sessions: PeerHostSessionService, options?: {
36
+ readonly fetchImpl?: typeof fetch;
37
+ });
38
+ handle(peerHostId: string, request: Request): Promise<Response>;
39
+ /** 将 RPC 的结构化请求转换为同一条 HTTP 白名单代理;不接受绝对 URL。 */
40
+ request(peerHostId: string, input: {
41
+ readonly scope: HostScope;
42
+ readonly path: string;
43
+ readonly method?: string;
44
+ readonly body?: string;
45
+ }): Promise<{
46
+ readonly status: number;
47
+ readonly headers: readonly [string, string][];
48
+ readonly body: string;
49
+ }>;
50
+ private requireReady;
51
+ }
52
+ //# sourceMappingURL=host-api-proxy-service.d.ts.map
@@ -0,0 +1,166 @@
1
+ import { PEER_HOST_ERROR_CODES } from '../../../shared/contracts/peer-host.js';
2
+ import { PeerHostSessionService } from './peer-host-session.js';
3
+ import { PeerHostStore } from './peer-host-store.js';
4
+ import { peerHostSafeError } from './peer-host-diagnostics.js';
5
+ const MAX_PROXY_BODY_BYTES = 4 * 1024 * 1024;
6
+ const ALLOWED_QUERY = new Set(['workspaceId', 'sessionId', 'scopeGeneration', 'cursor', 'path', 'toolId']);
7
+ const HOP_BY_HOP_HEADERS = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'host', 'authorization']);
8
+ export const PEER_HOST_HTTP_PROXY_RULES = [
9
+ { prefix: '/api/workspaces', methods: ['GET'] },
10
+ { prefix: '/api/sessions', methods: ['GET', 'POST'] },
11
+ { prefix: '/api/file-tree', methods: ['GET'] },
12
+ { prefix: '/api/files', methods: ['GET', 'PUT', 'POST'] },
13
+ { prefix: '/api/git', methods: ['GET', 'POST'] },
14
+ { prefix: '/api/terminal', methods: ['GET', 'POST'] },
15
+ { prefix: '/api/right-tools', methods: ['GET', 'POST'] },
16
+ ];
17
+ export class PeerHostProxyError extends Error {
18
+ code;
19
+ constructor(code, message) {
20
+ super(message);
21
+ this.code = code;
22
+ this.name = 'PeerHostProxyError';
23
+ }
24
+ }
25
+ /** 当前 Host 到目标 Host 的 HTTP 正向代理。 */
26
+ export class PeerHostHttpProxyService {
27
+ store;
28
+ sessions;
29
+ fetchImpl;
30
+ constructor(store, sessions, options = {}) {
31
+ this.store = store;
32
+ this.sessions = sessions;
33
+ this.fetchImpl = options.fetchImpl ?? fetch;
34
+ }
35
+ async handle(peerHostId, request) {
36
+ try {
37
+ const record = await this.requireReady(peerHostId);
38
+ const scope = readScope(request.headers, peerHostId);
39
+ const targetPath = parseProxyPath(request.url);
40
+ validateQuery(targetPath);
41
+ validateRule(request.method, targetPath.pathname);
42
+ const body = await readBody(request);
43
+ const accessToken = await this.sessions.getAccessToken(peerHostId);
44
+ const targetUrl = buildTargetUrl(record, targetPath);
45
+ const response = await this.fetchImpl(targetUrl, {
46
+ method: request.method,
47
+ headers: buildForwardHeaders(request.headers, accessToken),
48
+ ...(body === undefined ? {} : { body }),
49
+ });
50
+ if (response.status === 401) {
51
+ await this.sessions.invalidate(peerHostId);
52
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.SESSION_REQUIRED, '目标 Host 登录态已失效');
53
+ }
54
+ return await forwardResponse(response, scope);
55
+ }
56
+ catch (error) {
57
+ return errorResponse(error);
58
+ }
59
+ }
60
+ /** 将 RPC 的结构化请求转换为同一条 HTTP 白名单代理;不接受绝对 URL。 */
61
+ async request(peerHostId, input) {
62
+ const path = typeof input.path === 'string' ? input.path : '';
63
+ if (!path.startsWith('/api/') || path.includes('://'))
64
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED, 'PeerHost 代理路径必须是固定 API 路径');
65
+ const method = (input.method ?? 'GET').toUpperCase();
66
+ const body = input.body === undefined ? undefined : String(input.body);
67
+ if (body !== undefined && new TextEncoder().encode(body).byteLength > MAX_PROXY_BODY_BYTES)
68
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED, 'PeerHost 代理请求体过大');
69
+ const headers = new Headers({
70
+ 'x-codingns-host-id': input.scope.hostId,
71
+ 'x-codingns-target-host-id': input.scope.targetHostId ?? '',
72
+ 'x-codingns-workspace-id': input.scope.workspaceId,
73
+ 'x-codingns-scope-generation': String(input.scope.scopeGeneration),
74
+ ...(input.scope.sessionId === null ? {} : { 'x-codingns-session-id': input.scope.sessionId }),
75
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
76
+ });
77
+ const response = await this.handle(peerHostId, new Request(new URL(path, 'http://peer-host.invalid'), { method, headers, ...(body === undefined ? {} : { body }) }));
78
+ const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
79
+ if (response.body !== null && !contentType.includes('json') && !contentType.startsWith('text/'))
80
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.RESPONSE_INVALID, '目标 Host 返回了不支持的响应类型');
81
+ return { status: response.status, headers: [...response.headers.entries()], body: await response.text() };
82
+ }
83
+ async requireReady(peerHostId) {
84
+ const record = await this.store.get(peerHostId);
85
+ if (record === null)
86
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.NOT_FOUND, 'PeerHost 不存在');
87
+ if (record.status !== 'ready')
88
+ throw new PeerHostProxyError(record.status === 'session_required' ? PEER_HOST_ERROR_CODES.SESSION_REQUIRED : PEER_HOST_ERROR_CODES.NOT_READY, 'PeerHost 尚未准备好代理');
89
+ if (record.route.kind !== 'lan')
90
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.RELAY_UNAVAILABLE, '中转 PeerHost 暂不可用');
91
+ return record;
92
+ }
93
+ }
94
+ function readScope(headers, peerHostId) {
95
+ const hostId = headers.get('x-codingns-host-id')?.trim() ?? '';
96
+ const targetHostId = headers.get('x-codingns-target-host-id')?.trim() ?? '';
97
+ const workspaceId = headers.get('x-codingns-workspace-id')?.trim() ?? '';
98
+ const sessionHeader = headers.get('x-codingns-session-id');
99
+ const generation = Number(headers.get('x-codingns-scope-generation'));
100
+ if (!hostId || targetHostId !== peerHostId || !workspaceId || !Number.isSafeInteger(generation) || generation < 0) {
101
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.SCOPE_MISMATCH, 'PeerHost 请求作用域不匹配');
102
+ }
103
+ return { hostId, targetHostId, workspaceId, sessionId: sessionHeader?.trim() || null, scopeGeneration: generation };
104
+ }
105
+ function parseProxyPath(rawUrl) {
106
+ const url = new URL(rawUrl);
107
+ if (!url.pathname.startsWith('/api/'))
108
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED, 'PeerHost 代理路径不在 API 范围内');
109
+ return url;
110
+ }
111
+ function validateQuery(url) {
112
+ for (const key of url.searchParams.keys())
113
+ if (!ALLOWED_QUERY.has(key))
114
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED, 'PeerHost 代理查询参数未加入白名单');
115
+ }
116
+ function validateRule(method, pathname) {
117
+ const rule = PEER_HOST_HTTP_PROXY_RULES.find((candidate) => pathname === candidate.prefix || pathname.startsWith(`${candidate.prefix}/`));
118
+ if (rule === undefined || !rule.methods.includes(method.toUpperCase()))
119
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED, 'PeerHost 代理路径或方法未加入白名单');
120
+ }
121
+ async function readBody(request) {
122
+ if (request.method === 'GET' || request.method === 'HEAD')
123
+ return undefined;
124
+ const contentLength = Number(request.headers.get('content-length') ?? '0');
125
+ if (Number.isFinite(contentLength) && contentLength > MAX_PROXY_BODY_BYTES)
126
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED, 'PeerHost 代理请求体过大');
127
+ const body = await request.text();
128
+ if (new TextEncoder().encode(body).byteLength > MAX_PROXY_BODY_BYTES)
129
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED, 'PeerHost 代理请求体过大');
130
+ return body;
131
+ }
132
+ function buildTargetUrl(record, source) {
133
+ if (record.route.kind !== 'lan')
134
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.RELAY_UNAVAILABLE, '中转 PeerHost 暂不可用');
135
+ return new URL(`${source.pathname}${source.search}`, record.route.normalizedOrigin).toString();
136
+ }
137
+ function buildForwardHeaders(source, accessToken) {
138
+ const headers = new Headers();
139
+ source.forEach((value, name) => {
140
+ const normalized = name.toLowerCase();
141
+ if (HOP_BY_HOP_HEADERS.has(normalized) || normalized.startsWith('x-codingns-'))
142
+ return;
143
+ if (normalized === 'content-length')
144
+ return;
145
+ headers.set(name, value);
146
+ });
147
+ headers.set('authorization', `Bearer ${accessToken}`);
148
+ return headers;
149
+ }
150
+ async function forwardResponse(response, scope) {
151
+ const contentType = response.headers.get('content-type')?.toLowerCase() ?? '';
152
+ if (response.body !== null && !contentType.includes('json') && !contentType.startsWith('text/'))
153
+ throw new PeerHostProxyError(PEER_HOST_ERROR_CODES.RESPONSE_INVALID, '目标 Host 返回了不支持的响应类型');
154
+ const headers = new Headers();
155
+ response.headers.forEach((value, name) => { if (!HOP_BY_HOP_HEADERS.has(name.toLowerCase()))
156
+ headers.set(name, value); });
157
+ headers.set('x-codingns-scope-generation', String(scope.scopeGeneration));
158
+ return new Response(response.body, { status: response.status, headers });
159
+ }
160
+ function errorResponse(error) {
161
+ const code = error instanceof PeerHostProxyError ? error.code : PEER_HOST_ERROR_CODES.PROXY_UNREACHABLE;
162
+ const message = error instanceof PeerHostProxyError ? peerHostSafeError(error.code).message : '目标 Host 代理请求失败';
163
+ const status = code === PEER_HOST_ERROR_CODES.SCOPE_MISMATCH || code === PEER_HOST_ERROR_CODES.PROXY_PATH_NOT_ALLOWED ? 400 : code === PEER_HOST_ERROR_CODES.NOT_FOUND ? 404 : code === PEER_HOST_ERROR_CODES.SESSION_REQUIRED ? 401 : 502;
164
+ return Response.json({ error: { code, message } }, { status });
165
+ }
166
+ //# sourceMappingURL=host-api-proxy-service.js.map
@@ -0,0 +1,31 @@
1
+ import type { PeerHostRemoteConnector } from './host-ws-proxy-service.js';
2
+ /** DSH 原生工作台 WebSocket 的固定入口;不接受客户端传入路径。 */
3
+ export declare const PEER_HOST_REMOTE_WS_PATH = "/ws";
4
+ export interface PeerHostRawWebSocket {
5
+ readonly readyState: number;
6
+ on(event: 'open' | 'message' | 'close' | 'error' | 'unexpected-response', listener: (...args: any[]) => void): unknown;
7
+ once(event: 'open' | 'close' | 'error' | 'unexpected-response', listener: (...args: any[]) => void): unknown;
8
+ off?(event: 'open' | 'close' | 'error' | 'unexpected-response', listener: (...args: any[]) => void): unknown;
9
+ send(data: string): void;
10
+ close(code?: number, reason?: string): void;
11
+ terminate?(): void;
12
+ }
13
+ export declare class PeerHostConnectorError extends Error {
14
+ readonly code: 'PEER_HOST_SESSION_REQUIRED' | 'PEER_HOST_PROXY_UNREACHABLE' | 'PEER_HOST_RELAY_UNAVAILABLE';
15
+ constructor(code: 'PEER_HOST_SESSION_REQUIRED' | 'PEER_HOST_PROXY_UNREACHABLE' | 'PEER_HOST_RELAY_UNAVAILABLE', message: string);
16
+ }
17
+ export interface PeerHostWsConnectorOptions {
18
+ readonly timeoutMs?: number;
19
+ readonly websocketFactory?: (url: string, options: {
20
+ readonly headers: Readonly<Record<string, string>>;
21
+ }) => PeerHostRawWebSocket;
22
+ }
23
+ /**
24
+ * 创建当前 Host 到目标 DSH 的 LAN 工作台 connector。
25
+ *
26
+ * 目标 URL、access token 和 WebSocket 路径均由 Host 侧生成;Client 只能通过
27
+ * 上层代理提交 HostScope。目标 DSH 的消息白名单仍由 `PeerHostWsProxyService`
28
+ * 在双端转发前执行,这里只负责固定端点的认证握手和消息类型的文本适配。
29
+ */
30
+ export declare function createPeerHostRemoteConnector(options?: PeerHostWsConnectorOptions): PeerHostRemoteConnector;
31
+ //# sourceMappingURL=host-ws-connector.d.ts.map