rdsh-gateway 0.8.5 → 0.8.6
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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/join.js +5 -1
- package/dist/serve.js +5 -1
- package/dist/spawn-dsh.d.ts +37 -0
- package/dist/spawn-dsh.js +87 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type { SessionPayload } from "./session.ts";
|
|
|
10
10
|
export { PairManager } from "./pair.ts";
|
|
11
11
|
export { startGateway } from "./server.ts";
|
|
12
12
|
export type { GatewayOptions, RunningGateway } from "./server.ts";
|
|
13
|
-
export { findDsh, spawnDsh, exchangeDshSessionCookie, detectDshVersion, compareDshVersions, dshVersionWarning, DSH_COMPAT_MIN, DSH_COMPAT_MAX } from "./spawn-dsh.ts";
|
|
13
|
+
export { findDsh, spawnDsh, dshSpawnEnv, RDSH_REMOTE_TTY, checkRemotePickerGraph, remotePickerWarning, exchangeDshSessionCookie, detectDshVersion, compareDshVersions, dshVersionWarning, DSH_COMPAT_MIN, DSH_COMPAT_MAX } from "./spawn-dsh.ts";
|
|
14
14
|
export type { SpawnedDsh } from "./spawn-dsh.ts";
|
|
15
15
|
export { forwardHttp, createUpgradeProxy, rewriteHeadersForDsh } from "./proxy.ts";
|
|
16
16
|
export type { ProxyTarget } from "./proxy.ts";
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { serve } from "./serve.js";
|
|
|
7
7
|
export { SessionManager, SESSION_COOKIE, sessionTokenFromCookie } from "./session.js";
|
|
8
8
|
export { PairManager } from "./pair.js";
|
|
9
9
|
export { startGateway } from "./server.js";
|
|
10
|
-
export { findDsh, spawnDsh, exchangeDshSessionCookie, detectDshVersion, compareDshVersions, dshVersionWarning, DSH_COMPAT_MIN, DSH_COMPAT_MAX } from "./spawn-dsh.js";
|
|
10
|
+
export { findDsh, spawnDsh, dshSpawnEnv, RDSH_REMOTE_TTY, checkRemotePickerGraph, remotePickerWarning, exchangeDshSessionCookie, detectDshVersion, compareDshVersions, dshVersionWarning, DSH_COMPAT_MIN, DSH_COMPAT_MAX } from "./spawn-dsh.js";
|
|
11
11
|
export { forwardHttp, createUpgradeProxy, rewriteHeadersForDsh } from "./proxy.js";
|
|
12
12
|
export { loadConfig, normalizeConfig, resolveConfigPath, saveConfig, DEFAULT_HOST_CONFIG_PATH } from "./config.js";
|
|
13
13
|
export { hashPassword, verifyPassword, UserManager } from "./auth.js";
|
package/dist/join.js
CHANGED
|
@@ -14,7 +14,7 @@ import { request as httpsRequest } from "node:https";
|
|
|
14
14
|
import { hostname as osHostname } from "node:os";
|
|
15
15
|
import { WebSocket } from "ws";
|
|
16
16
|
import { FrameParser, FRAME_TYPE, encodeFrame, jsonPayload, parseJsonPayload, FLAG_E2E, MAX_PAYLOAD_LENGTH } from "rdsh-tunnel";
|
|
17
|
-
import { findDsh, spawnDsh, exchangeDshSessionCookie, detectDshVersion, dshVersionWarning } from "./spawn-dsh.js";
|
|
17
|
+
import { findDsh, spawnDsh, exchangeDshSessionCookie, detectDshVersion, dshVersionWarning, checkRemotePickerGraph, remotePickerWarning } from "./spawn-dsh.js";
|
|
18
18
|
import { rewriteHeadersForDsh } from "./proxy.js";
|
|
19
19
|
import { clearPersistedToken, persistToken, readPersistedToken } from "./token-store.js";
|
|
20
20
|
import { decodeBody, encodeBody, firstEncoding } from "./http-encoding.js";
|
|
@@ -905,6 +905,10 @@ export async function join(opts) {
|
|
|
905
905
|
console.warn("rdsh join: dsh 0.1.2+ 会话 cookie 换发失败——远程访问将返回 401。");
|
|
906
906
|
}
|
|
907
907
|
}
|
|
908
|
+
// 自检:目录选择器必须是浏览器内形态(否则远端浏览器只会看到弹在宿主屏幕上的原生对话框)
|
|
909
|
+
if ((await checkRemotePickerGraph(dsh.port, dshAuthCookieHeader)) === false) {
|
|
910
|
+
console.warn(`\n⚠ ${remotePickerWarning()}\n`);
|
|
911
|
+
}
|
|
908
912
|
// 解析 host token(含证书自动检测 + 持久化);进程重启后复用,避免重复配对。
|
|
909
913
|
const { token, insecure, name } = await registerJoin(opts);
|
|
910
914
|
console.log(`rdsh join: dsh web on 127.0.0.1:${dsh.port}`);
|
package/dist/serve.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { networkInterfaces } from "node:os";
|
|
5
5
|
import { startGateway } from "./server.js";
|
|
6
|
-
import { findDsh, spawnDsh, exchangeDshSessionCookie, detectDshVersion, dshVersionWarning } from "./spawn-dsh.js";
|
|
6
|
+
import { findDsh, spawnDsh, exchangeDshSessionCookie, detectDshVersion, dshVersionWarning, checkRemotePickerGraph, remotePickerWarning } from "./spawn-dsh.js";
|
|
7
7
|
import { loadConfig, resolveConfigPath } from "./config.js";
|
|
8
8
|
import { UserManager } from "./auth.js";
|
|
9
9
|
import { loadTls } from "./tls.js";
|
|
@@ -39,6 +39,10 @@ export async function serve(opts) {
|
|
|
39
39
|
console.warn("rdsh: dsh 0.1.2+ 会话 cookie 换发失败——远程访问将返回 401(请确认 dsh web 已就绪或暂用 dsh@0.1.1-rc.2)。");
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
// 自检:目录选择器必须是浏览器内形态(否则远端浏览器只会看到弹在宿主屏幕上的原生对话框)
|
|
43
|
+
if ((await checkRemotePickerGraph(dsh.port, dshAuthCookieHeader)) === false) {
|
|
44
|
+
console.warn(`\n⚠ ${remotePickerWarning()}\n`);
|
|
45
|
+
}
|
|
42
46
|
// TLS 决策:有 tls.cert/key → https;无 → http(behindProxy 或 pair/none)。
|
|
43
47
|
// password + http + 非反代 → server.ts 安全约束拒绝启动(需自行提供证书)。
|
|
44
48
|
const tlsMaterial = await loadTls(config.tls, config.behindProxy);
|
package/dist/spawn-dsh.d.ts
CHANGED
|
@@ -16,6 +16,27 @@ export interface SpawnedDsh {
|
|
|
16
16
|
* 找不到返回 null。
|
|
17
17
|
*/
|
|
18
18
|
export declare function findDsh(override?: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* 注入给 dsh 的「远端操作者」信号值。用可读字面量而非 `/dev/...`:
|
|
21
|
+
* 让 `env` 里一眼看出这是 rdsh 注入的,而不是真实终端路径。
|
|
22
|
+
*/
|
|
23
|
+
export declare const RDSH_REMOTE_TTY = "rdsh-remote";
|
|
24
|
+
/**
|
|
25
|
+
* 构造 spawn dsh 时的环境:保证 dsh 把目录选择器解析为 **browse**(浏览器内目录浏览器)。
|
|
26
|
+
*
|
|
27
|
+
* 事实依据(doc/fix/20260917-remote-workspace-picker/discussion.md F2/F4/F8):
|
|
28
|
+
* `@deepseek-ai/dsh-host-directory-picker-auto` 在 boot 时读 `SSH_CONNECTION`/`SSH_TTY`
|
|
29
|
+
* (仅继承进程层,非空即可)——「操作者看不到宿主显示」正是 rdsh 的场景(等价于
|
|
30
|
+
* SSH 端口转发下的无人值守宿主)。不注入它,宿主是 macOS/Windows/带 DISPLAY 的 Linux
|
|
31
|
+
* 时会解析成 `native`,原生对话框弹在宿主屏幕上、远端浏览器无法操作。
|
|
32
|
+
*
|
|
33
|
+
* 只设 `SSH_TTY`:`SSH_CONNECTION` 有固定 `"ip port ip port"` 格式,写假值会误导解析它的工具。
|
|
34
|
+
* 用户真的在 SSH 会话里启动时(已有非空信号)保持原值不动。
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ 这是上游启发式的输入:上游若改变判定信号,本注入会静默失效(spawn 后自检见
|
|
37
|
+
* `checkRemotePickerGraph` 与 doc/fix/20260917-remote-workspace-picker/solution.md §6)。
|
|
38
|
+
*/
|
|
39
|
+
export declare function dshSpawnEnv(base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
19
40
|
/**
|
|
20
41
|
* spawn `dsh web --port 0 --no-open` 并等待其报告监听端口。
|
|
21
42
|
* 就绪后把 dsh 的 stdout/stderr 透传到本进程。
|
|
@@ -29,6 +50,22 @@ export declare function spawnDsh(dshPath: string): Promise<SpawnedDsh>;
|
|
|
29
50
|
* 返回 cookie 的 `name=value` 段(不含属性);换发失败/超时/无 cookie → null(调用方降级)。
|
|
30
51
|
*/
|
|
31
52
|
export declare function exchangeDshSessionCookie(port: number, token: string, timeoutMs?: number): Promise<string | null>;
|
|
53
|
+
/**
|
|
54
|
+
* 自检:spawn 出的 dsh 是否真的把目录选择器解析成了 **browse**。
|
|
55
|
+
*
|
|
56
|
+
* 方法(与 doc/fix/20260917-remote-workspace-picker 的验证手段一致):取一次首页,
|
|
57
|
+
* 在注入的 boot graph 里找浏览器内选择器的客户端模块 —— 在 → 远端浏览器可用;
|
|
58
|
+
* 不在 → 选择器是 `native`(原生对话框弹在宿主屏幕上)。
|
|
59
|
+
*
|
|
60
|
+
* @returns `true`(已挂载 browse)/ `false`(确认未挂载)/ `null`(无法判定:网络、鉴权、超限)
|
|
61
|
+
* 只有 `false` 才值得告警;`null` 静默,避免噪声。
|
|
62
|
+
*/
|
|
63
|
+
export declare function checkRemotePickerGraph(port: number, cookieHeader?: string | null, timeoutMs?: number): Promise<boolean | null>;
|
|
64
|
+
/**
|
|
65
|
+
* 自检失败(确认未挂载浏览器内选择器)时的告警文案。
|
|
66
|
+
* 零文档导向:说清后果 + 直接给动作。
|
|
67
|
+
*/
|
|
68
|
+
export declare function remotePickerWarning(): string;
|
|
32
69
|
/** 探测 dsh 版本号(`dsh --version` 输出如 `0.1.2-rc.1`);失败/不可解析 → null。 */
|
|
33
70
|
export declare function detectDshVersion(dshPath: string, timeoutMs?: number): Promise<string | null>;
|
|
34
71
|
/**
|
package/dist/spawn-dsh.js
CHANGED
|
@@ -42,6 +42,32 @@ export function findDsh(override) {
|
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* 注入给 dsh 的「远端操作者」信号值。用可读字面量而非 `/dev/...`:
|
|
47
|
+
* 让 `env` 里一眼看出这是 rdsh 注入的,而不是真实终端路径。
|
|
48
|
+
*/
|
|
49
|
+
export const RDSH_REMOTE_TTY = "rdsh-remote";
|
|
50
|
+
/**
|
|
51
|
+
* 构造 spawn dsh 时的环境:保证 dsh 把目录选择器解析为 **browse**(浏览器内目录浏览器)。
|
|
52
|
+
*
|
|
53
|
+
* 事实依据(doc/fix/20260917-remote-workspace-picker/discussion.md F2/F4/F8):
|
|
54
|
+
* `@deepseek-ai/dsh-host-directory-picker-auto` 在 boot 时读 `SSH_CONNECTION`/`SSH_TTY`
|
|
55
|
+
* (仅继承进程层,非空即可)——「操作者看不到宿主显示」正是 rdsh 的场景(等价于
|
|
56
|
+
* SSH 端口转发下的无人值守宿主)。不注入它,宿主是 macOS/Windows/带 DISPLAY 的 Linux
|
|
57
|
+
* 时会解析成 `native`,原生对话框弹在宿主屏幕上、远端浏览器无法操作。
|
|
58
|
+
*
|
|
59
|
+
* 只设 `SSH_TTY`:`SSH_CONNECTION` 有固定 `"ip port ip port"` 格式,写假值会误导解析它的工具。
|
|
60
|
+
* 用户真的在 SSH 会话里启动时(已有非空信号)保持原值不动。
|
|
61
|
+
*
|
|
62
|
+
* ⚠️ 这是上游启发式的输入:上游若改变判定信号,本注入会静默失效(spawn 后自检见
|
|
63
|
+
* `checkRemotePickerGraph` 与 doc/fix/20260917-remote-workspace-picker/solution.md §6)。
|
|
64
|
+
*/
|
|
65
|
+
export function dshSpawnEnv(base = process.env) {
|
|
66
|
+
const hasSignal = (base.SSH_TTY ?? "") !== "" || (base.SSH_CONNECTION ?? "") !== "";
|
|
67
|
+
if (hasSignal)
|
|
68
|
+
return { ...base };
|
|
69
|
+
return { ...base, SSH_TTY: RDSH_REMOTE_TTY };
|
|
70
|
+
}
|
|
45
71
|
/**
|
|
46
72
|
* spawn `dsh web --port 0 --no-open` 并等待其报告监听端口。
|
|
47
73
|
* 就绪后把 dsh 的 stdout/stderr 透传到本进程。
|
|
@@ -51,6 +77,7 @@ export function spawnDsh(dshPath) {
|
|
|
51
77
|
return new Promise((resolve, reject) => {
|
|
52
78
|
const child = spawn(dshPath, ["web", "--port", "0", "--no-open"], {
|
|
53
79
|
stdio: ["ignore", "pipe", "pipe"],
|
|
80
|
+
env: dshSpawnEnv(),
|
|
54
81
|
});
|
|
55
82
|
let stderrBuf = "";
|
|
56
83
|
const timeout = setTimeout(() => {
|
|
@@ -124,6 +151,66 @@ export function exchangeDshSessionCookie(port, token, timeoutMs = EXCHANGE_TIMEO
|
|
|
124
151
|
});
|
|
125
152
|
});
|
|
126
153
|
}
|
|
154
|
+
const GRAPH_CHECK_TIMEOUT_MS = 5_000;
|
|
155
|
+
const MAX_GRAPH_BYTES = 512 * 1024;
|
|
156
|
+
/** boot graph 里浏览器内目录选择器的客户端模块标识(命中即说明 picker = browse)。 */
|
|
157
|
+
const BROWSE_PICKER_MODULE = "@deepseek-ai/dsh-client-ui-directory-picker-browse";
|
|
158
|
+
/**
|
|
159
|
+
* 自检:spawn 出的 dsh 是否真的把目录选择器解析成了 **browse**。
|
|
160
|
+
*
|
|
161
|
+
* 方法(与 doc/fix/20260917-remote-workspace-picker 的验证手段一致):取一次首页,
|
|
162
|
+
* 在注入的 boot graph 里找浏览器内选择器的客户端模块 —— 在 → 远端浏览器可用;
|
|
163
|
+
* 不在 → 选择器是 `native`(原生对话框弹在宿主屏幕上)。
|
|
164
|
+
*
|
|
165
|
+
* @returns `true`(已挂载 browse)/ `false`(确认未挂载)/ `null`(无法判定:网络、鉴权、超限)
|
|
166
|
+
* 只有 `false` 才值得告警;`null` 静默,避免噪声。
|
|
167
|
+
*/
|
|
168
|
+
export function checkRemotePickerGraph(port, cookieHeader = null, timeoutMs = GRAPH_CHECK_TIMEOUT_MS) {
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const headers = { Host: `127.0.0.1:${port}` };
|
|
171
|
+
if (cookieHeader !== null)
|
|
172
|
+
headers.Cookie = cookieHeader;
|
|
173
|
+
let settled = false;
|
|
174
|
+
const finish = (value) => {
|
|
175
|
+
if (settled)
|
|
176
|
+
return;
|
|
177
|
+
settled = true;
|
|
178
|
+
resolve(value);
|
|
179
|
+
};
|
|
180
|
+
const req = get({ host: "127.0.0.1", port, path: "/", headers }, (res) => {
|
|
181
|
+
if (res.statusCode !== 200) {
|
|
182
|
+
res.resume();
|
|
183
|
+
finish(null);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let body = "";
|
|
187
|
+
res.setEncoding("utf8");
|
|
188
|
+
res.on("data", (chunk) => {
|
|
189
|
+
body += chunk;
|
|
190
|
+
if (body.length > MAX_GRAPH_BYTES) {
|
|
191
|
+
req.destroy();
|
|
192
|
+
finish(null);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
res.on("end", () => finish(body.includes(BROWSE_PICKER_MODULE)));
|
|
196
|
+
res.on("error", () => finish(null));
|
|
197
|
+
});
|
|
198
|
+
req.on("error", () => finish(null));
|
|
199
|
+
req.setTimeout(timeoutMs, () => {
|
|
200
|
+
req.destroy();
|
|
201
|
+
finish(null);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 自检失败(确认未挂载浏览器内选择器)时的告警文案。
|
|
207
|
+
* 零文档导向:说清后果 + 直接给动作。
|
|
208
|
+
*/
|
|
209
|
+
export function remotePickerWarning() {
|
|
210
|
+
return ("rdsh: 远端浏览器的目录选择器不是浏览器内目录浏览器(很可能是宿主原生对话框,远端无法操作)。" +
|
|
211
|
+
"若装了 dsh-web-remote 插件,请检查 profile 的 cordis.patch.yml 是否被其它 patch 层覆盖(目录选择器只允许一个 pin 通道)。" +
|
|
212
|
+
"否则可能是 dsh 版本变更导致远端信号失效——升级 remote-dsh:npm i -g remote-dsh@latest。");
|
|
213
|
+
}
|
|
127
214
|
/** 探测 dsh 版本号(`dsh --version` 输出如 `0.1.2-rc.1`);失败/不可解析 → null。 */
|
|
128
215
|
export function detectDshVersion(dshPath, timeoutMs = VERSION_TIMEOUT_MS) {
|
|
129
216
|
return new Promise((resolve) => {
|