@hyzyn/dsh-tty 0.21.0 → 0.22.0-rc.2
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/README.en.md +113 -10
- package/README.md +117 -10
- package/client.js +150 -36
- package/lib/index.d.ts +195 -25
- package/lib/index.js +930 -149
- package/lib/index.js.map +1 -1
- package/lib/probe.d.ts +29 -1
- package/lib/probe.js +98 -19
- package/lib/probe.js.map +1 -1
- package/lib/sftp.js +37 -9
- package/lib/sftp.js.map +1 -1
- package/lib/shell-integration.js +25 -1
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh-config.d.ts +34 -2
- package/lib/ssh-config.js +150 -32
- package/lib/ssh-config.js.map +1 -1
- package/lib/ssh.d.ts +256 -2
- package/lib/ssh.js +637 -6
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.js +47 -5
- package/lib/tunnels.js.map +1 -1
- package/package.json +4 -2
- package/scripts/integration.mjs +183 -20
- package/scripts/jump-smoke.mjs +175 -0
- package/scripts/lib/proxy-bridge.mjs +30 -0
- package/scripts/preview/harness.js +74 -26
- package/scripts/probe-route-smoke.mjs +10 -1
- package/scripts/proxycommand-smoke.mjs +267 -0
- package/scripts/windows-smoke.mjs +123 -1
package/lib/index.d.ts
CHANGED
|
@@ -112,6 +112,14 @@ export interface Config {
|
|
|
112
112
|
sftpLimits?: Partial<SftpLimits>;
|
|
113
113
|
/** 服务器状态条(0.17.0):是否采集并推送会话资源指标(CPU/内存/磁盘/uptime/TCP/网速/温度)。默认开。 */
|
|
114
114
|
statsEnabled?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* 允许 ProxyCommand(本机命令执行):**默认关**。
|
|
117
|
+
*
|
|
118
|
+
* 本插件唯一「由设置字段驱动本机任意命令执行」的开关,与跳板机(只连一跳 TCP)不同档:
|
|
119
|
+
* 关着时携带 `proxyCommand` 的连接**明确失败**(不退回直连),`~/.ssh/config` 导入也
|
|
120
|
+
* 永不自动带入该字段——要用的条目必须自己开开关再手填。
|
|
121
|
+
*/
|
|
122
|
+
allowProxyCommand?: boolean;
|
|
115
123
|
/** 内部状态:SSH 持久会话名(远程 tmux 托管,本机 socket 清单看不到,随 settings 留存供新窗口恢复确认)。 */
|
|
116
124
|
persistSessions?: Array<{
|
|
117
125
|
tmuxName: string;
|
|
@@ -135,6 +143,43 @@ export interface SftpLimits {
|
|
|
135
143
|
* `applyPatch` 热应用(见 @hyzyn/dsh-kit 的 settingsEntryScope)。
|
|
136
144
|
*/
|
|
137
145
|
export declare const Config: z;
|
|
146
|
+
/**
|
|
147
|
+
* 会话退出后的**只读保留策略**(D77)。
|
|
148
|
+
*
|
|
149
|
+
* 进程没了之后把会话留在 `sessions` 表里:`tty_list` / `tty_capture` / `tty_screen`
|
|
150
|
+
* 照常能读到它最后那些输出(用户上报的痛点原话:「结果明明就在那里但我看不到」——
|
|
151
|
+
* `tty_open` 跑一条命令,跑完会话就退役,AI 一个字符都取不回来,逼得人先开
|
|
152
|
+
* `/bin/sh` 再往里发命令)。
|
|
153
|
+
*
|
|
154
|
+
* 取 **∞ = 保留到显式关闭**(`tty_close` / 面板关标签 / 宿主重启),不由时间淘汰:
|
|
155
|
+
*
|
|
156
|
+
* - 时间上界对用户是**第二重惊喜**——「命令跑完 → 下一轮读结果」之间隔着人离开、
|
|
157
|
+
* 模型排队,多久都有可能;一个到期就消失的输出比「要主动关」更难理解;
|
|
158
|
+
* - 内存与句柄本来也不由时间决定:条数由 [`MAX_EXITED_SESSIONS`](#) 兜(8 条,
|
|
159
|
+
* 单条几百 KB~一两 MB),而「永久」还有一条天然上界——保留是**内存态**,
|
|
160
|
+
* 宿主 / 插件重启即清空,不会跨天累积;
|
|
161
|
+
* - 连续跑很多短命令时,淘汰节奏变成「超过 8 条按最旧淘汰」(`capExited`),
|
|
162
|
+
* 正是想要的语义:近的才有人读。
|
|
163
|
+
*
|
|
164
|
+
* 需要时间上界的人把这里改成任意毫秒数即可——`reapExited` 那条通路还在
|
|
165
|
+
* (回收器每轮都会调它)。导出仅供单测(test/host-frames.test.ts):到点退役与
|
|
166
|
+
* 条数上限的行为护栏。
|
|
167
|
+
*/
|
|
168
|
+
export declare const EXITED_RETAIN_MS: number;
|
|
169
|
+
/**
|
|
170
|
+
* 只读保留的会话数上限(超出按最旧淘汰,见 SessionManager.capExited)。
|
|
171
|
+
*
|
|
172
|
+
* 与「并发会话上限」(maxSessions,默认 4)是两个口径:那个数**只数活着的会话**
|
|
173
|
+
* (retained 的不占名额,否则跑几条短命令就把面板顶成「会话数已达上限」,
|
|
174
|
+
* 比原缺陷更糟)。这里兜的是内存:每条 retained 约 = 256KB 环形缓冲 + 一块
|
|
175
|
+
* xterm-headless 虚拟屏,16 条仍在 ~20MB 量级。
|
|
176
|
+
*
|
|
177
|
+
* 为什么从 8 抬到 16(D77 补,2026-09-27 真机验收反馈):保留改成「留到显式关闭」
|
|
178
|
+
* 之后,上限就是唯一会**自动**挤掉结果的东西,而 agent 一口气开二十几条一次性
|
|
179
|
+
* 会话是常态——8 条意味着「几分钟前那份结果」被最旧淘汰挤掉,用户只看到「没了」。
|
|
180
|
+
* 淘汰一律 `logger.warn` 留痕(见 finishSession),否则这件事在事后完全不可查。
|
|
181
|
+
*/
|
|
182
|
+
export declare const MAX_EXITED_SESSIONS = 16;
|
|
138
183
|
/**
|
|
139
184
|
* 本地 PTY 顶层 shell 的 best-effort 强杀(D48)。
|
|
140
185
|
*
|
|
@@ -148,6 +193,18 @@ export declare const Config: z;
|
|
|
148
193
|
* 导出仅供单测(test/host-frames.test.ts):平台参数注入,两个分支都能在 macOS/Linux 断言。
|
|
149
194
|
*/
|
|
150
195
|
export declare function killLocalShellTerminal(terminal: unknown, platform?: NodeJS.Platform): void;
|
|
196
|
+
/**
|
|
197
|
+
* Windows 本地 PTY 的**输入归一化**(D74,2026-09-27 真机报告):conhost 把 Enter 当
|
|
198
|
+
* **CR**,裸 LF 只把光标下移一格、**不提交命令行**——于是 `tty_send` 按工具描述发
|
|
199
|
+
* `echo X\n` 时,命令停在输入行上:没有输出、没有新提示符,看起来像「发出去了但没执行」。
|
|
200
|
+
*
|
|
201
|
+
* 修法取报告建议里改动最小的一条:win32 上把行尾补成 CRLF(已有 CR 的不重复补),
|
|
202
|
+
* 让「照描述写 `\n`」这条主路径直接可用。非 win32 原样透传(POSIX 的 Enter 就是 LF),
|
|
203
|
+
* SSH 会话也不归一化(远端是什么系统、什么 shell 插件不知道,乱改会破坏 `cat` 之类的原始输入)。
|
|
204
|
+
*
|
|
205
|
+
* 导出仅供单测:平台参数注入,两个分支都能在 macOS/Linux 断言。
|
|
206
|
+
*/
|
|
207
|
+
export declare function normalizePtyInput(data: string, platform?: NodeJS.Platform): string;
|
|
151
208
|
/** 一次「会话 → 帧」采集器的句柄(本地 = 定时器,SSH = 远端长驻 exec channel)。 */
|
|
152
209
|
interface StatsCollector {
|
|
153
210
|
stop(): void;
|
|
@@ -181,6 +238,18 @@ interface TtySession {
|
|
|
181
238
|
* agent 的 `tty_close`,或用户在面板里接管后照常关标签。
|
|
182
239
|
*/
|
|
183
240
|
owner: 'user' | 'agent';
|
|
241
|
+
/**
|
|
242
|
+
* **只读保留态**(D77):进程已退出,但会话**还留在表里**——读侧工具照常可用,
|
|
243
|
+
* 写侧明确拒写,用户与 agent 都能显式关掉它(`tty_close` / 面板关标签 / TTL 到点)。
|
|
244
|
+
*
|
|
245
|
+
* 与 `closed` 是两件事:`closed` = 真退役(出表 + 释放屏,见 SessionManager.retire),
|
|
246
|
+
* 而「进程退出」**不再**等于退役——否则退出瞬间那些输出就再也取不回来了。
|
|
247
|
+
*/
|
|
248
|
+
exited: {
|
|
249
|
+
code: number | null;
|
|
250
|
+
signal: string | null;
|
|
251
|
+
at: number;
|
|
252
|
+
} | null;
|
|
184
253
|
/** exit 帧只发一次(kill 主动关闭与 shell 自然退出共用同一回调)。 */
|
|
185
254
|
exitSent?: boolean;
|
|
186
255
|
/** agent 工具展示用的元数据。 */
|
|
@@ -192,6 +261,19 @@ interface TtySession {
|
|
|
192
261
|
lastOutputAt: number;
|
|
193
262
|
/** 最近一次 PTY 输入(input 帧 / tty_send)的时间戳:tty_capture{last} 的在途判据之一。 */
|
|
194
263
|
lastInputAt: number;
|
|
264
|
+
/** 累计写入环形缓冲的原始输出字符数(单调,D72):buffer 起点 = `outputSeq - buffer.length`。 */
|
|
265
|
+
outputSeq: number;
|
|
266
|
+
/** agent 已读水位线(绝对字符计数,D72);-1 = 还没被 agent 工具碰过(首次触达时落在当下)。 */
|
|
267
|
+
readSeq: number;
|
|
268
|
+
/** 水位线最近一次推进的时刻(D72):`lastCommand.endedAt > readMarkAt` = 这条命令的输出还没被读过。 */
|
|
269
|
+
readMarkAt: number;
|
|
270
|
+
/**
|
|
271
|
+
* 最近若干条「agent 提交过的命令行」(D75,来自 `tty_send` 且带行尾的那些):
|
|
272
|
+
* 无 shell 集成时 PTY 会把它们**原样回显**进输出流,`tty_expect` 拿回显当命中
|
|
273
|
+
* 就是假阳性(命令还没执行就报 matched)。用这份清单把回显行从匹配窗口里剔掉。
|
|
274
|
+
* 只记 `tty_send` 的写入:面板逐键输入不做行编辑模拟(宁可少抑制,不可错抑制)。
|
|
275
|
+
*/
|
|
276
|
+
recentInputs: string[];
|
|
195
277
|
/** 输出环形缓冲(尾部 256KB,供 tty_capture 与断线重连回放)。 */
|
|
196
278
|
buffer: string;
|
|
197
279
|
/** utf8 分帧兜底:跨 chunk 的多字节序列由 StringDecoder 缓存补齐。 */
|
|
@@ -259,6 +341,8 @@ declare class LiveConfig {
|
|
|
259
341
|
persistSessions: string[];
|
|
260
342
|
/** SFTP 传输限制(客户端浏览器侧执行)。 */
|
|
261
343
|
sftpLimits: Required<SftpLimits>;
|
|
344
|
+
/** 允许 ProxyCommand(本机命令执行):默认关(见 Config.allowProxyCommand)。 */
|
|
345
|
+
allowProxyCommand: boolean;
|
|
262
346
|
constructor(init: {
|
|
263
347
|
shell: string;
|
|
264
348
|
term: string;
|
|
@@ -273,6 +357,7 @@ declare class LiveConfig {
|
|
|
273
357
|
endOnPageClose?: boolean;
|
|
274
358
|
statsEnabled?: boolean;
|
|
275
359
|
sftpLimits?: Partial<SftpLimits>;
|
|
360
|
+
allowProxyCommand?: boolean;
|
|
276
361
|
persistSessions?: string[];
|
|
277
362
|
});
|
|
278
363
|
/** 合并部分更新;空字符串/undefined 保持原值;sshHosts/hostKeys/tunnels 传数组即整体替换。 */
|
|
@@ -290,6 +375,7 @@ declare class LiveConfig {
|
|
|
290
375
|
endOnPageClose: boolean;
|
|
291
376
|
statsEnabled: boolean;
|
|
292
377
|
sftpLimits?: Partial<SftpLimits>;
|
|
378
|
+
allowProxyCommand: boolean;
|
|
293
379
|
persistSessions: string[];
|
|
294
380
|
}>): void;
|
|
295
381
|
findSshHost(name: string): SshHostEntry | undefined;
|
|
@@ -431,6 +517,26 @@ export declare function clearScreenWatchdog(heartbeat: ScreenHeartbeat): void;
|
|
|
431
517
|
export declare function writeToScreen(screen: {
|
|
432
518
|
write(data: string, callback?: () => void): void;
|
|
433
519
|
}, heartbeat: ScreenHeartbeat, text: string, onStall: (reason: string) => void, stallMs?: number): void;
|
|
520
|
+
/** 会话的只读快照形状(tty_list 与 sessions 帧共用;D77 起含只读保留态字段)。 */
|
|
521
|
+
export interface SessionSnapshot {
|
|
522
|
+
sid: string;
|
|
523
|
+
pid?: number;
|
|
524
|
+
cwd: string;
|
|
525
|
+
kind: 'local' | 'ssh';
|
|
526
|
+
target: string;
|
|
527
|
+
startedAt: number;
|
|
528
|
+
lastOutputAt: number;
|
|
529
|
+
persist?: true;
|
|
530
|
+
owner: 'user' | 'agent';
|
|
531
|
+
/** 进程已退出、会话仍在只读保留期内(D77)。 */
|
|
532
|
+
exited?: true;
|
|
533
|
+
/** 退出码(拿不到时省略)。 */
|
|
534
|
+
exitCode?: number;
|
|
535
|
+
/** 退出信号(正常退出时省略)。 */
|
|
536
|
+
signal?: string;
|
|
537
|
+
/** 只读保留的剩余毫秒;**省略 = 不按时间释放**(策略为 ∞,关闭或宿主重启才清)。 */
|
|
538
|
+
retainMs?: number;
|
|
539
|
+
}
|
|
434
540
|
/** 导出仅供单测(test/host-frames.test.ts):上限 / 孤儿回收 / grace 热改的行为护栏。 */
|
|
435
541
|
export declare class SessionManager {
|
|
436
542
|
private readonly sessions;
|
|
@@ -442,40 +548,33 @@ export declare class SessionManager {
|
|
|
442
548
|
/** 配置热生效时调整上限(1~16)。 */
|
|
443
549
|
setLimit(maxSessions: number): void;
|
|
444
550
|
get count(): number;
|
|
551
|
+
/** 活着的会话数(**不含**只读保留的,见 canSpawn)。 */
|
|
552
|
+
get liveCount(): number;
|
|
553
|
+
/** 只读保留的会话数(D77)。 */
|
|
554
|
+
get exitedCount(): number;
|
|
555
|
+
/**
|
|
556
|
+
* 名额判据**只数活着的会话**(D77):只读保留的不占名额。不这样分的话,
|
|
557
|
+
* 「跑几条短命令」就能把面板顶成「会话数已达上限」——用户一条会话都没开,
|
|
558
|
+
* 比原来那个「AI 取不到结果」的缺陷更糟。
|
|
559
|
+
*/
|
|
445
560
|
canSpawn(): boolean;
|
|
446
561
|
add(session: TtySession): void;
|
|
447
562
|
remove(id: string): void;
|
|
448
563
|
get(id: string): TtySession | undefined;
|
|
449
|
-
/** 会话的只读快照(SSH 会话无本地 pid,该字段省略;tmux 持久会话带 persist
|
|
564
|
+
/** 会话的只读快照(SSH 会话无本地 pid,该字段省略;tmux 持久会话带 persist;
|
|
565
|
+
* 只读保留态(D77)额外带 exited/exitCode|signal/retainMs)。 */
|
|
450
566
|
private snapshotOf;
|
|
451
567
|
/** agent 工具用的只读快照。 */
|
|
452
|
-
list():
|
|
453
|
-
sid: string;
|
|
454
|
-
pid?: number;
|
|
455
|
-
cwd: string;
|
|
456
|
-
kind: 'local' | 'ssh';
|
|
457
|
-
target: string;
|
|
458
|
-
startedAt: number;
|
|
459
|
-
lastOutputAt: number;
|
|
460
|
-
persist?: true;
|
|
461
|
-
owner: 'user' | 'agent';
|
|
462
|
-
}>;
|
|
568
|
+
list(): SessionSnapshot[];
|
|
463
569
|
/** sessions 帧用:额外带 attachable(孤儿且未关闭的会话可被新连接 attach)。 */
|
|
464
|
-
listForAttach(): Array<{
|
|
465
|
-
sid: string;
|
|
466
|
-
pid?: number;
|
|
467
|
-
cwd: string;
|
|
468
|
-
kind: 'local' | 'ssh';
|
|
469
|
-
target: string;
|
|
470
|
-
startedAt: number;
|
|
471
|
-
lastOutputAt: number;
|
|
472
|
-
persist?: true;
|
|
473
|
-
owner: 'user' | 'agent';
|
|
570
|
+
listForAttach(): Array<SessionSnapshot & {
|
|
474
571
|
attachable: boolean;
|
|
475
572
|
}>;
|
|
476
573
|
/** 遍历全部会话(状态条采集器的批量收尾等按会话维度的操作)。 */
|
|
477
574
|
forEach(fn: (session: TtySession) => void): void;
|
|
478
|
-
/** 按 tmux
|
|
575
|
+
/** 按 tmux 持久会话名查找**活着**的会话(跨窗口共享用);不存在/已关闭/只读保留态返回 undefined。
|
|
576
|
+
* D77:保留态必须排除——否则「同名 persistName 的新标签」会 rebind 到一具尸体上,
|
|
577
|
+
* 拿到 ready 却永远没有输出。 */
|
|
479
578
|
findByTmuxName(tmuxName: string): TtySession | undefined;
|
|
480
579
|
/** 同步退役:移出全局表 + 释放虚拟屏(幂等,不杀进程)。 */
|
|
481
580
|
retire(session: TtySession): void;
|
|
@@ -492,6 +591,24 @@ export declare class SessionManager {
|
|
|
492
591
|
* 它的关闭入口是 agent 的 tty_close 或用户在面板里接管后关标签。
|
|
493
592
|
*/
|
|
494
593
|
reapOrphans(graceMs: number): Promise<void>;
|
|
594
|
+
/**
|
|
595
|
+
* 只读保留到点退役(D77;回收器每轮调用):超过保留期的会话出表 + 释放屏。
|
|
596
|
+
*
|
|
597
|
+
* 默认策略是 ∞(保留到显式关闭)⇒ 本方法是 no-op,条数由 `capExited` 兜;
|
|
598
|
+
* 把 `EXITED_RETAIN_MS` 改成有限值它就照常工作(策略可调,通路留着)。
|
|
599
|
+
*
|
|
600
|
+
* **不 kill 进程**:这里收的全是已经退出的会话(进程早没了),`retire()` 就够;
|
|
601
|
+
* 真退役(显式 `tty_close` / 面板关标签)走 `killSessionNow`,那条路要处理
|
|
602
|
+
* tmux teardown 与 forceKill 的兜底。
|
|
603
|
+
*/
|
|
604
|
+
reapExited(retainMs: number): void;
|
|
605
|
+
/**
|
|
606
|
+
* 只读保留的数量上限(超出按最旧淘汰,D77):返回被淘汰的会话,便于单测断言。
|
|
607
|
+
*
|
|
608
|
+
* 为什么必须有:`owner:'agent'` 的会话不会走孤儿回收,agent 若不显式 `tty_close`
|
|
609
|
+
* (它常常不会),保留态就是**永久泄漏**——屏与 256KB 缓冲一直挂着。
|
|
610
|
+
*/
|
|
611
|
+
capExited(max: number): TtySession[];
|
|
495
612
|
disposeAll(): Promise<void>;
|
|
496
613
|
}
|
|
497
614
|
/** 导出仅供单测(test/host-frames.test.ts):帧校验 / 绑定 / 孤儿语义的行为护栏。 */
|
|
@@ -549,7 +666,17 @@ export declare class TtyServer {
|
|
|
549
666
|
private sendStats;
|
|
550
667
|
/** registerUpgrade 的 handler(loopback 围栏 + ws 握手)。 */
|
|
551
668
|
handleUpgrade(req: ReqLike, socket: SocketLike, head: Buffer): void;
|
|
669
|
+
/** 围栏放行之后的实际握手(与上面的异步分支共用)。 */
|
|
670
|
+
private finishUpgrade;
|
|
552
671
|
private onConnection;
|
|
672
|
+
/**
|
|
673
|
+
* 摘掉同 sid 上残留的**只读保留**会话(D77):spawn / ssh 新建同名会话前调用。
|
|
674
|
+
*
|
|
675
|
+
* 不摘会真泄漏:`sessions.add()` 用同一个键把旧对象顶出表,而旧对象的虚拟屏与
|
|
676
|
+
* 256KB 环形缓冲再没有任何引用能释放它们(`retire` 是唯一的释放口)。只处理
|
|
677
|
+
* 保留态——活着的同 sid 会话属于「跨连接同名」的既有语义,不在这里动。
|
|
678
|
+
*/
|
|
679
|
+
private retireStaleExited;
|
|
553
680
|
/**
|
|
554
681
|
* 解析帧里的 sid。返回:
|
|
555
682
|
* { sid } 目标会话;
|
|
@@ -650,16 +777,28 @@ export declare class TtyServer {
|
|
|
650
777
|
/** 会话退出事实 → exit 帧(恰好一次;本地 PTY 与 SSH 共用)。 */
|
|
651
778
|
private watchDone;
|
|
652
779
|
/**
|
|
653
|
-
*
|
|
780
|
+
* 会话终局的**唯一出口**:给所有绑定连接发 exit 帧(恰好一次)+ 转只读保留(D77)。
|
|
654
781
|
*
|
|
655
782
|
* `outcome` 正常来自 PTY 句柄的 done;显式 kill 的兜底(KILL_EXIT_FALLBACK_MS)
|
|
656
783
|
* 也走这里,带 code=null / signal=SIGKILL。exit 广播到所有绑定连接(跨窗口共享),
|
|
657
784
|
* 各客户端按自己的 sid 收址。
|
|
785
|
+
*
|
|
786
|
+
* **D77 起「进程退出」不再等于「退役」**:会话留在 `sessions` 表里转只读保留态,
|
|
787
|
+
* 读侧工具(tty_list / tty_capture / tty_screen / tty_expect)照常可用,写侧拒写,
|
|
788
|
+
* 直到显式关闭(tty_close / 面板关标签)或保留期到点(reapExited)。退役只剩
|
|
789
|
+
* `SessionManager.retire` 那一处(出表 + 释放屏)。
|
|
658
790
|
*/
|
|
659
791
|
private finishSession;
|
|
660
792
|
/** 输出下行 + 基于 ws.bufferedAmount 的背压(暂停/恢复 PassThrough)。 */
|
|
661
793
|
private attachOutput;
|
|
662
|
-
/** 立即冲刷待发的合并输出(exit/kill 前调用,保证 exit 帧永远在最后一帧 data 之后)。
|
|
794
|
+
/** 立即冲刷待发的合并输出(exit/kill 前调用,保证 exit 帧永远在最后一帧 data 之后)。
|
|
795
|
+
*
|
|
796
|
+
* D76:`force` 是**终局路径专用**的开关。`finishSession` 必须先置 `closed`(否则终局
|
|
797
|
+
* 之后到达的字节会继续往合并窗口里塞),可它同时又要交出**已经攒在 `pendingOutput`
|
|
798
|
+
* 里的**那批输出——两者共用同一个 `closed` 判据时,尾巴会被下面这行自己的守卫整批
|
|
799
|
+
* 吞掉:进程「打印完就退出」时那正是崩溃堆栈的最后一行 / 命令的结论行。传 `force`
|
|
800
|
+
* 即「我知道它已 closed,这一批仍然要发」。
|
|
801
|
+
*/
|
|
663
802
|
private flushPendingOutput;
|
|
664
803
|
close(): void;
|
|
665
804
|
}
|
|
@@ -691,6 +830,37 @@ export declare class TtyServer {
|
|
|
691
830
|
export declare function handleCredentialRefsRoute(req: ReqLike, res: ResLike): Promise<void>;
|
|
692
831
|
/** 导出仅供单测(test/credential-refs.test.ts):只验键名解析,不取值。 */
|
|
693
832
|
export declare function readCredentialRefNames(): string[];
|
|
833
|
+
/**
|
|
834
|
+
* 数据路由的统一闸门(**导出仅供单测**):回环围栏(加固档)+ 变更端点的同源证明。
|
|
835
|
+
* 十二处路由此前各抄一遍 403 样板,`mutation` 这条判据一加就会各写各的——收敛成一处后
|
|
836
|
+
* 「拒绝分支」只有一份,负例也只测这一份。
|
|
837
|
+
*
|
|
838
|
+
* 哪些是变更端点(`mutation: true`)——**判据是「这次请求会不会改状态」**:
|
|
839
|
+
* - `POST /probe`:真的拨号,且连接簿条目测试会当场 TOFU 记录主机指纹;
|
|
840
|
+
* - `POST /sftp/{mkdir,rename,remove,upload}`、`POST /local-fs/{mkdir,rename,remove,transfer}`:
|
|
841
|
+
* 写远端 / 写本机 / 起传输任务;
|
|
842
|
+
* - `POST /config`**刻意不在其列**(与 docker 同口径):它是插件被禁用后唯一的恢复入口
|
|
843
|
+
* ——卡片靠它渲染、也是重新启用的唯一 UI 入口;跨站 POST 已由上面那条围栏的
|
|
844
|
+
* `sec-fetch-site: cross-site` 与 Origin 比对拦住。
|
|
845
|
+
* 只读端点(GET 全家 + `sftp /list` `/download`、`local-fs /list`)维持 loopback-only:
|
|
846
|
+
* 它们读的是用户自己主动要的东西,读路由加证明只会把旧 Safari / 裸 curl 一起挡在门外。
|
|
847
|
+
*
|
|
848
|
+
* **WS upgrade 不走这里**(那是 socket 握手,不是 req/res 路由):见 `handleUpgrade`
|
|
849
|
+
* 用的 `isLoopbackRequestStrict`——它自带「Origin 有则必须同源」这条判据,等价于给
|
|
850
|
+
* 升级请求也上了证明,但**刻意不放开 Cookie 例外**:长连的生命周期比一次 POST 长得多。
|
|
851
|
+
*/
|
|
852
|
+
export declare function gateRoute(req: ReqLike, res: ResLike, options?: {
|
|
853
|
+
mutation?: boolean;
|
|
854
|
+
}): Promise<boolean>;
|
|
855
|
+
/**
|
|
856
|
+
* 这个前缀路由的子路径是否**会改状态**(导出仅供单测)。
|
|
857
|
+
*
|
|
858
|
+
* 为什么单独抽出来:`/list` 与 `/download` 也是 POST(凭证走 body、不进 URL),但它们只是
|
|
859
|
+
* 读——如果把「POST 就要求证明」一刀切下去,读路由会连带把旧 Safari / 裸 curl 挡在门外,
|
|
860
|
+
* 而它们本来就没有可被跨站利用的副作用。判据是**动作**不是**方法**,所以名单必须显式。
|
|
861
|
+
* 没见过的子路径一律 false(几步之后就是 404,不给它额外的信息量)。
|
|
862
|
+
*/
|
|
863
|
+
export declare function isMutationSubroute(prefix: string, sub: string): boolean;
|
|
694
864
|
interface ResLike {
|
|
695
865
|
writeHead(status: number, headers?: Record<string, string>): void;
|
|
696
866
|
/** 二进制响应(SFTP 下载)也走 end;Node 的 ServerResponse 原生接受 Uint8Array。 */
|