@lemoncat7/dsh-ssh 1.7.0 → 1.9.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.
Files changed (128) hide show
  1. package/ARCHITECTURE.md +15 -0
  2. package/README.md +6 -2
  3. package/docs/dialog-consistency.md +26 -0
  4. package/docs/releases/1.8.0.md +13 -0
  5. package/docs/releases/1.9.0.md +15 -0
  6. package/lib/api.d.ts.map +1 -1
  7. package/lib/api.js +58 -1
  8. package/lib/api.js.map +1 -1
  9. package/lib/browser-transfers.d.ts +26 -0
  10. package/lib/browser-transfers.d.ts.map +1 -0
  11. package/lib/browser-transfers.js +72 -0
  12. package/lib/browser-transfers.js.map +1 -0
  13. package/lib/client-api.d.ts +8 -0
  14. package/lib/client-api.d.ts.map +1 -1
  15. package/lib/client-api.js +45 -8
  16. package/lib/client-api.js.map +1 -1
  17. package/lib/client-download.d.ts +22 -0
  18. package/lib/client-download.d.ts.map +1 -0
  19. package/lib/client-download.js +65 -0
  20. package/lib/client-download.js.map +1 -0
  21. package/lib/client.d.ts.map +1 -1
  22. package/lib/client.js +262 -151
  23. package/lib/client.js.map +4 -4
  24. package/lib/commands-panel.d.ts.map +1 -1
  25. package/lib/commands-panel.js +18 -5
  26. package/lib/commands-panel.js.map +1 -1
  27. package/lib/confirmation-dialog.d.ts +12 -0
  28. package/lib/confirmation-dialog.d.ts.map +1 -0
  29. package/lib/confirmation-dialog.js +41 -0
  30. package/lib/confirmation-dialog.js.map +1 -0
  31. package/lib/connector.d.ts.map +1 -1
  32. package/lib/connector.js +6 -4
  33. package/lib/connector.js.map +1 -1
  34. package/lib/domain.d.ts +10 -0
  35. package/lib/domain.d.ts.map +1 -1
  36. package/lib/domain.js.map +1 -1
  37. package/lib/download-progress.d.ts +17 -0
  38. package/lib/download-progress.d.ts.map +1 -0
  39. package/lib/download-progress.js +68 -0
  40. package/lib/download-progress.js.map +1 -0
  41. package/lib/endpoint-upload.d.ts +10 -0
  42. package/lib/endpoint-upload.d.ts.map +1 -0
  43. package/lib/endpoint-upload.js +63 -0
  44. package/lib/endpoint-upload.js.map +1 -0
  45. package/lib/file-entry-delete-dialog.js +1 -1
  46. package/lib/file-entry-delete-dialog.js.map +1 -1
  47. package/lib/file-entry-sort.d.ts +1 -0
  48. package/lib/file-entry-sort.d.ts.map +1 -1
  49. package/lib/file-entry-sort.js +5 -8
  50. package/lib/file-entry-sort.js.map +1 -1
  51. package/lib/file-transfer-tools.d.ts.map +1 -1
  52. package/lib/file-transfer-tools.js +40 -7
  53. package/lib/file-transfer-tools.js.map +1 -1
  54. package/lib/file-transfer-workspace.d.ts.map +1 -1
  55. package/lib/file-transfer-workspace.js +70 -26
  56. package/lib/file-transfer-workspace.js.map +1 -1
  57. package/lib/ftp-adapter.d.ts.map +1 -1
  58. package/lib/ftp-adapter.js +12 -3
  59. package/lib/ftp-adapter.js.map +1 -1
  60. package/lib/ftp-list-date.d.ts +3 -0
  61. package/lib/ftp-list-date.d.ts.map +1 -0
  62. package/lib/ftp-list-date.js +20 -0
  63. package/lib/ftp-list-date.js.map +1 -0
  64. package/lib/ftp-listing-client.d.ts +7 -0
  65. package/lib/ftp-listing-client.d.ts.map +1 -0
  66. package/lib/ftp-listing-client.js +28 -0
  67. package/lib/ftp-listing-client.js.map +1 -0
  68. package/lib/ftp-profile-editor.js +8 -6
  69. package/lib/ftp-profile-editor.js.map +1 -1
  70. package/lib/gist-sync.d.ts +4 -2
  71. package/lib/gist-sync.d.ts.map +1 -1
  72. package/lib/gist-sync.js +49 -12
  73. package/lib/gist-sync.js.map +1 -1
  74. package/lib/group-proxy-editor.d.ts +10 -0
  75. package/lib/group-proxy-editor.d.ts.map +1 -0
  76. package/lib/group-proxy-editor.js +33 -0
  77. package/lib/group-proxy-editor.js.map +1 -0
  78. package/lib/group-proxy.d.ts +7 -0
  79. package/lib/group-proxy.d.ts.map +1 -0
  80. package/lib/group-proxy.js +29 -0
  81. package/lib/group-proxy.js.map +1 -0
  82. package/lib/locales/messages.d.ts +98 -2
  83. package/lib/locales/messages.d.ts.map +1 -1
  84. package/lib/locales/messages.js +26 -2
  85. package/lib/locales/messages.js.map +1 -1
  86. package/lib/profile-editor.js +3 -2
  87. package/lib/profile-editor.js.map +1 -1
  88. package/lib/remote-files.d.ts +1 -0
  89. package/lib/remote-files.d.ts.map +1 -1
  90. package/lib/remote-files.js.map +1 -1
  91. package/lib/remote-workspace-tree.d.ts +1 -0
  92. package/lib/remote-workspace-tree.d.ts.map +1 -1
  93. package/lib/remote-workspace-tree.js +6 -13
  94. package/lib/remote-workspace-tree.js.map +1 -1
  95. package/lib/session-access.d.ts +1 -0
  96. package/lib/session-access.d.ts.map +1 -1
  97. package/lib/session-access.js +1 -0
  98. package/lib/session-access.js.map +1 -1
  99. package/lib/session-file-upload.d.ts +22 -0
  100. package/lib/session-file-upload.d.ts.map +1 -0
  101. package/lib/session-file-upload.js +56 -0
  102. package/lib/session-file-upload.js.map +1 -0
  103. package/lib/sftp-client.d.ts.map +1 -1
  104. package/lib/sftp-client.js +7 -5
  105. package/lib/sftp-client.js.map +1 -1
  106. package/lib/store.d.ts +4 -0
  107. package/lib/store.d.ts.map +1 -1
  108. package/lib/store.js +15 -0
  109. package/lib/store.js.map +1 -1
  110. package/lib/terminal-workspace.js +1 -1
  111. package/lib/terminal-workspace.js.map +1 -1
  112. package/lib/transfer-task-list.d.ts +16 -0
  113. package/lib/transfer-task-list.d.ts.map +1 -0
  114. package/lib/transfer-task-list.js +35 -0
  115. package/lib/transfer-task-list.js.map +1 -0
  116. package/lib/ui-components.d.ts +3 -1
  117. package/lib/ui-components.d.ts.map +1 -1
  118. package/lib/ui-components.js +6 -3
  119. package/lib/ui-components.js.map +1 -1
  120. package/lib/use-workspace-refresh.d.ts +3 -0
  121. package/lib/use-workspace-refresh.d.ts.map +1 -0
  122. package/lib/use-workspace-refresh.js +44 -0
  123. package/lib/use-workspace-refresh.js.map +1 -0
  124. package/lib/workspace-refresh.d.ts +14 -0
  125. package/lib/workspace-refresh.d.ts.map +1 -0
  126. package/lib/workspace-refresh.js +36 -0
  127. package/lib/workspace-refresh.js.map +1 -0
  128. package/package.json +1 -1
package/ARCHITECTURE.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  This plugin is split into four boundaries. UI code never reaches SSH or file transports directly, and server code never depends on the DSH client runtime.
4
4
 
5
+ Browser uploads use XHR byte progress and wait for the successful server response before marking the remote save complete. Downloads use `client-download.ts` when the client exposes File System Access in a secure context: the picker runs within user activation, the response is streamed to the selected writable file, progress counts acknowledged writes, and completion waits for `close()`. Unknown or encoded lengths do not produce guessed percentages. Cancellation, HTTP/auth failures, length mismatches, disk-write errors and close failures never report completion; partial writes are aborted. No full-file Blob buffering is used. Without a client save API (including Desktop hosts lacking that capability), native link downloads remain available but their result is explicitly unknown, with no progress bar or false completion. The legacy `download-progress.ts` server endpoint remains for older loaded clients only; current clients never poll it. These transient client transfers are not resumable jobs after page/server restart.
6
+
7
+ FTP LIST timestamps are retained as `modifiedAtText` when MLSD timestamps are unavailable; no timezone/year is guessed and no per-entry MDTM/CWD probes are added to directory listing. Regular file sizes remain the listing-provided byte counts; directory sizes are not recursively computed.
8
+
5
9
  ## Runtime boundaries
6
10
 
7
11
  ### DSH integration
@@ -14,6 +18,8 @@ This plugin is split into four boundaries. UI code never reaches SSH or file tra
14
18
 
15
19
  - `src/api.ts` translates HTTP routes into explicit store, terminal, file-transfer, forwarding, and credential operations.
16
20
  - `src/store.ts` owns persistent SSH configuration.
21
+ - `src/group-proxy.ts` owns group configuration validation and host-over-group route precedence; `src/connector.ts` resolves the chosen shared proxy and its credentials for all new SSH connections. Group proxy references block unsafe deletion, and group records participate in portable sync without rewriting individual host routes.
22
+ - `src/workspace-refresh.ts` coordinates shared, non-overlapping revision checks and retryable list refreshes; `src/use-workspace-refresh.ts` owns browser visibility/focus scheduling. The revision endpoint reads an in-memory token, never credentials or cloud data. Editors retain their local draft state while collections refresh.
17
23
  - `src/gist-sync.ts` owns portable configuration snapshots, encrypted secret export/import, three-way conflict resolution, tombstones, explicit backups, and serialized background synchronization.
18
24
  - `src/github-device-auth.ts` owns the bounded GitHub Device Flow state machine. Device codes remain server-side and completed access tokens are written directly to the credential service.
19
25
  - `src/session-access.ts` and `src/tools.ts` define the session authorization boundary used by AI tools.
@@ -83,8 +89,12 @@ This plugin is split into four boundaries. UI code never reaches SSH or file tra
83
89
  - SSH command permission and remote-file permission are independent; authorizing one never implies the other.
84
90
  - A regular user fork copies the parent session grant once, before prompt assembly. Existing child grants win, live terminals/jobs are never copied, and `origin: subagent` lineage is excluded.
85
91
  - FTP control and passive data sockets use the same route policy. FTPS wraps both socket classes with verified TLS.
92
+ - Passive data sockets are paused after dialling: proxy negotiation may leave them flowing, and listing/download bytes must remain buffered until the FTP preliminary response attaches the receiving pipeline. The regression test sends data before that control reply and checks repeated list/stat navigation.
93
+ - `ftp-listing-client.ts` handles servers returning empty/factless MLSD listings: verify through LIST in the requested working directory and reuse LIST for that connection. Listing never issues per-entry metadata probes. LIST dates retain their server text instead of guessing a timezone/year.
94
+ - File-transfer layout v3 defaults to one pane (including migration from v2). Explicit 1–4 pane choices persist; a single pane has no next-pane transfer action.
86
95
  - Remote-to-remote transfers use backpressured streams and never stage a complete file on local disk.
87
96
  - Browser browsing sessions and transfer job sessions are isolated so a long transfer cannot block pane navigation.
97
+ - `transfer-task-list.tsx` provides the shared transfer task container and browser-I/O rows. The file workspace merges remote and browser tasks into one active-first queue; compact SFTP views reuse the same container/styles. Client file writes are not passed to the remote-to-remote scheduler. Only an acknowledged local file close produces a completed download; native handoffs remain unconfirmed.
88
98
  - Secrets are write-only from the browser and are never returned by profile APIs.
89
99
  - Portable sync never exports session grants, forwarding rules, or local runtime settings. Passwords and private keys are encrypted with AES-256-GCM before network I/O; the token and encryption passphrase never enter the snapshot.
90
100
  - GitHub authorization requests only the `gist` scope. The browser receives a one-time user code and flow identifier, never the OAuth access token or GitHub device code.
@@ -103,3 +113,8 @@ This plugin is split into four boundaries. UI code never reaches SSH or file tra
103
113
  - Established shared motion tokens and reduced-motion fallbacks without adding an animation runtime dependency.
104
114
 
105
115
  Future feature work should extend the closest feature module instead of adding unrelated state or styles to `client.tsx`.
116
+ # Session-local and browser uploads
117
+
118
+ - `endpoint-upload.ts` owns bounded streaming, temporary remote names, size verification and commit for FTP/FTPS/SFTP uploads. Existing target names are rejected by the adapter; FTP rename cannot guarantee exclusive commit against unrelated concurrent writers.
119
+ - `session-file-upload.ts` restricts Agent reads to regular files within the current DSH session directory, checks permission throughout streaming, and cancels on disposal. Local upload and download are separate opt-in session grants, in addition to transfer and endpoint permissions. Existing sessions default to neither grant.
120
+ - Browser file uploads originate from the user's selected files, not the DSH host directory, and use the shared browser transfer task list. Sent bytes and remote-save acknowledgment are distinct states. Files are streamed without whole-file buffering; single files are capped at 512 MiB with a five-minute operation limit.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 兼容性
8
8
 
9
- 版本 `1.7.0` 适配 DeepSeek Harness `0.1.5` 新版主面板与右侧标签 API,保留旧版回退;已在 `0.1.5-rc.1` 镜像验证。需要 Node.js `^22.19.0` 或 `>=24.0.0`。详见 [1.7.0 更新说明](docs/releases/1.7.0.md)。
9
+ 版本 `1.9.0` 适配 DeepSeek Harness `0.1.5` 新版主面板与右侧标签 API,保留旧版回退;已在 `0.1.5-rc.1` 镜像验证。需要 Node.js `^22.19.0` 或 `>=24.0.0`。详见 [1.9.0 更新说明](docs/releases/1.9.0.md)。
10
10
 
11
11
  ## 1.7.0 界面语言
12
12
 
@@ -49,7 +49,7 @@ GitHub 同步收到 HTTP 401 后会用账号接口复核,不删除凭据。确
49
49
 
50
50
  - **同主机多个目录**:先挂载主机,再点选多个固定目录。目录旁会显示“已挂载”;可单独设定默认工作目录。右侧远端目录提供快捷切换,Agent 的 `ssh_list` 也会返回全部挂载目录。目录挂载不改变原有 SSH 权限范围。
51
51
  - **终端多标签与分屏**:`+` 新建标签;双屏支持左右/上下排列,增加分屏最多 4 个,桌面三/四屏按四宫格排列,窄屏上下排列。点击已显示的标签只切换焦点,不交换位置;其他标签或新标签仅替换当前选中窗格。命令与连接按钮作用于选中的终端。SFTP 可从主机标题栏收起/展开,保留目录视图与宽度。切换工作台内的页面、主机或标签不主动断开连接;关闭标签或退出工作台会释放对应连接。新标签使用当时选中的远端路径,不修改已经运行的终端。每主机最多 8 个标签,服务端最多 32 个运行中/连接中的浏览器终端。
52
- - **常用命令**:新增命令页面,支持搜索、新建、编辑和删除;终端内点击“常用命令”选择后先预览,确认后仅在该终端执行。支持多行,不自动采集终端输入;勿保存密码、Token。记录属于当前 DSH 实例,暂不参与 Gist 同步。
52
+ - **常用命令**:支持搜索、新建、编辑和删除;终端内点击“常用命令”选择后先预览,确认后仅在该终端执行。支持多行,不自动采集终端输入;勿保存密码、Token。开发版已接入 Gist 同步与备份,旧备份缺少命令集合时保留本机命令,显式删除仍参与智能合并。
53
53
  - **页面统一**:沿用文件传输页的标题栏、圆角、透明材质、字体和列表间距;终端仅在首次连接时创建,隐藏标签降低备用轮询频率。
54
54
 
55
55
  ## 1.4.6 更新
@@ -217,6 +217,8 @@ Device Flow 需要一个属于插件发布者的 GitHub OAuth App Client ID。Cl
217
217
 
218
218
  - SSH 主机、FTP/FTPS 连接与固定远端项目目录
219
219
  - 代理库及代理密码
220
+ - 常用命令(名称与正文按配置元数据保存到私有 Gist,不自动识别或加密正文中的密码;不要在命令中嵌入密码、Token)
221
+ - SSH 分组默认代理;从主机分组标题旁的编辑按钮配置,主机自身代理优先,没有设置时才回退分组代理,分组也没有代理时直连。只影响新连接,不修改主机自身配置,不影响 FTP 分组。被分组引用的代理不能直接删除。
220
222
  - 密钥库元数据,以及密码、私钥和私钥口令
221
223
  - 连接自身保存的 SSH/FTP 密码和内联代理密码
222
224
 
@@ -237,6 +239,8 @@ Device Flow 需要一个属于插件发布者的 GitHub OAuth App Client ID。Cl
237
239
 
238
240
  空白新设备第一次连接已有 Gist 时始终先安全拉取云端,不会因为选择“本地优先”而覆盖已有配置。自动同步会在插件启动后、本地可同步配置变化约 3 秒后以及后台每 5 分钟运行;任务串行执行,避免并发覆盖。
239
241
 
242
+ 工作台共享每 5 秒一次的轻量版本检查,后台页面暂停,回到页面立即检查;仅数据改变时刷新主机、FTP、代理库、密钥库、分组配置及命令列表。手动同步完成后立即检查,不重建终端或编辑弹窗。旧版本不认识新增集合,跨设备使用这些功能时建议各端同时升级。
243
+
240
244
  覆盖可能丢失的一侧之前,插件会在同一个 Gist 中创建显式备份文件,并按设置保留 0~50 份。该数量只控制 `dsh-ssh.backup.*.json` 文件;GitHub 自己维护的 Gist revision history 无法由插件裁剪。
241
245
 
242
246
  设置页显示最近一次由 GitHub 返回的云端 Gist revision SHA(短格式展示,完整值保留在提示中)。创建、上传、下载、合并或连接测试都会刷新这个云端版本;它表示真实的 Gist 修订版本,不是固定的数据结构版本号。
@@ -0,0 +1,26 @@
1
+ # Dialog consistency
2
+
3
+ `Dialog` owns the shared accessible title/description and closing behavior.
4
+ `dialog.css`, loaded after feature styles, owns common chrome: surface tokens,
5
+ 14px radius, title typography, header/content insets and action sizes. Feature
6
+ styles retain only content layout and purpose-specific dimensions.
7
+
8
+ - Confirmation (440px): host, FTP connection, credential, proxy, pinned directory,
9
+ saved command, file/directory deletion; terminal close, unsaved-draft decisions,
10
+ transfer conflict and file download.
11
+ - Forms: host, credential, proxy, forwarding, group proxy, pinned directory,
12
+ saved command and GitHub device authorization retain their content requirements.
13
+ - Larger work surfaces: FTP manager, session access, command picker, session
14
+ workspace picker and directory/file preview retain browsing space but share chrome.
15
+
16
+ Do not introduce browser `confirm()` dialogs. `useConfirmationDialog` provides
17
+ target capture, in-flight protection and visible retryable failures for simple
18
+ asynchronous destructive actions. Complex deletion dialogs retain dependency
19
+ checks and use the same confirmation variant. Set `dismissible={false}` while
20
+ busy to prevent close animations from hiding a request that is still running.
21
+
22
+ `scripts/verify-i18n.mjs` checks light/dark confirmations and host forms at phone
23
+ and desktop widths, landscape scrolling, long names, cancellation and retries.
24
+ `scripts/verify-preview-refresh.mjs` covers preview/edit/refresh state retention.
25
+ Browser fixtures stub host primitives; icons and host focus/portal behavior
26
+ must also be checked in the installed DSH application.
@@ -0,0 +1,13 @@
1
+ # 1.8.0
2
+
3
+ 正式版。适配 DSH 0.1.5,已在 0.1.5-rc.1 镜像部署验证。需要 Node.js ^22.19.0 或 >=24.0.0。
4
+
5
+ - GitHub Gist 同步增加常用命令和分组代理配置;兼容旧备份缺少这些集合的情况,避免误删本地数据。
6
+ - 同步后自动更新代理库、主机与命令列表,保留正在编辑的草稿;页面隐藏时暂停检查,返回时检查更新。
7
+ - 主机分组支持默认代理:主机自己的代理或跳板配置优先,未配置时使用分组代理;已建立的连接不受修改影响。
8
+ - 统一 SSH 弹窗的标题、圆角、材质、留白和按钮规范;删除确认使用紧凑布局,适配手机和横屏。
9
+ - 凭据、代理、固定目录删除不再使用浏览器原生确认框;FTP 删除、文件删除、命令删除及终端关闭统一确认样式。异步删除失败可重试,处理中防重复操作。
10
+
11
+ 安全说明:常用命令属于私有 Gist 中的配置元数据,并非加密凭据。请勿在命令中直接保存密码或 Token;密码、私钥仍独立加密。
12
+
13
+ 验证:146 项自动测试通过;浏览器覆盖深浅色、375px/1024px、横屏、长名称、取消与失败重试,以及 Markdown 编辑与预览刷新状态保持。
@@ -0,0 +1,15 @@
1
+ # 1.9.0
2
+
3
+ 正式版。适配 DSH 0.1.5,已在 0.1.5-rc.1 镜像部署验证。需要 Node.js ^22.19.0 或 >=24.0.0。
4
+
5
+ - 修复 FTP 文件识别、目录进入、大小和修改时间解析及排序;文件传输默认单栏,保留多栏选择。
6
+ - 本机上传、下载与远端传输统一显示在传输任务列表,支持任务时间与终态清理。
7
+ - 支持文件保存接口的浏览器按实际写入字节显示下载进度,保存关闭成功后才显示完成。其他浏览器或 Desktop 环境交由原生下载,不伪造进度或保存成功状态。
8
+ - 文件传输页增加本机文件上传,支持 FTP、FTPS、SFTP,多选文件依次上传;发送完成后等待远端保存确认。
9
+ - 会话访问增加 Agent 本地上传、下载独立授权,默认关闭,同时要求端点授权和传输权限;新增 file_upload_from_local 工具。
10
+ - Agent 本地指 DSH 运行机器的当前会话目录,不是浏览器电脑。上传仅支持目录内普通文件;下载默认保存在该目录;单文件最大 512 MiB,限时五分钟。
11
+ - 上传使用临时文件、大小校验及最终改名;同名文件按冲突处理,不主动覆盖。FTP 协议无法保证其他客户端并发改名时的排他提交。
12
+
13
+ 升级提醒:原先允许远端传输的会话不会自动获得本地上传/下载权限,请在“文件传输 → 会话访问”明确开启。
14
+
15
+ 验证:163 项自动测试通过;传输页面覆盖 375/697/1024px、深浅色及一万条目录记录;浏览器测试覆盖实际流式写入、保存确认、取消、失败及上传进度。Desktop 原生文件保存接口未实机验证,不支持时明确显示无法确认进度。
package/lib/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,EAAwB,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAKrD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAErC,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAIzE,OAAO,EAAE,gBAAgB,EAAwB,MAAM,sBAAsB,CAAA;AAG7E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EAAE,mBAAmB,EAA+B,MAAM,4BAA4B,CAAA;AAC7F,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAwB,KAAK,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAG5F,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAWhD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE;QACd,IAAI,EAAE,QAAQ,CAAA;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KACzE,GAAG,MAAM,IAAI,CAAA;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,QAAQ,CAAA;IACf,WAAW,EAAE,kBAAkB,CAAA;IAC/B,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,YAAY,CAAA;IACvB,QAAQ,EAAE,cAAc,CAAA;IACxB,SAAS,EAAE,sBAAsB,CAAA;IACjC,WAAW,EAAE,iBAAiB,CAAA;IAC9B,cAAc,EAAE,gBAAgB,CAAA;IAChC,MAAM,EAAE,aAAa,CAAA;IACrB,KAAK,EAAE,iBAAiB,CAAA;IACxB,SAAS,EAAE,mBAAmB,CAAA;IAC9B,YAAY,EAAE,sBAAsB,CAAA;IACpC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACjD,yBAAyB,CAAC,IAAI,yBAAyB,GAAG,SAAS,CAAA;CACpE;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,IAAI,CAa3G"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,EAAwB,YAAY,EAAE,MAAM,gBAAgB,CAAA;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAKrD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGrC,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAIzE,OAAO,EAAE,gBAAgB,EAAwB,MAAM,sBAAsB,CAAA;AAG7E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,EAAE,mBAAmB,EAA+B,MAAM,4BAA4B,CAAA;AAC7F,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AAGtE,OAAO,EAAwB,KAAK,yBAAyB,EAAE,MAAM,uBAAuB,CAAA;AAG5F,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAWhD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE;QACd,IAAI,EAAE,QAAQ,CAAA;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KACzE,GAAG,MAAM,IAAI,CAAA;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,QAAQ,CAAA;IACf,WAAW,EAAE,kBAAkB,CAAA;IAC/B,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,YAAY,CAAA;IACvB,QAAQ,EAAE,cAAc,CAAA;IACxB,SAAS,EAAE,sBAAsB,CAAA;IACjC,WAAW,EAAE,iBAAiB,CAAA;IAC9B,cAAc,EAAE,gBAAgB,CAAA;IAChC,MAAM,EAAE,aAAa,CAAA;IACrB,KAAK,EAAE,iBAAiB,CAAA;IACxB,SAAS,EAAE,mBAAmB,CAAA;IAC9B,YAAY,EAAE,sBAAsB,CAAA;IACpC,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACjD,yBAAyB,CAAC,IAAI,yBAAyB,GAAG,SAAS,CAAA;CACpE;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,IAAI,CAwB3G"}
package/lib/api.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { uploadEndpointFile } from './endpoint-upload.js';
3
+ import { DownloadProgressStore } from './download-progress.js';
2
4
  import { pipeline } from 'node:stream/promises';
3
5
  import { HostKeyRequiredError } from './connector.js';
4
6
  import { normalizeCredentialEntryDraft, normalizeForwardDraft, normalizeFtpProfileDraft, normalizeProfileDraft, normalizeProxyEntryDraft, normalizeRemoteProjectDraft, normalizeSecrets, } from './domain.js';
7
+ import { saveGroupProxy } from './group-proxy.js';
5
8
  import { normalizeRemoteDirectory, setSessionDirectory } from './directory.js';
6
9
  import { streamTerminalOutput } from './terminal-stream.js';
7
10
  import { normalizeGitHubProxy } from './github-http.js';
@@ -23,15 +26,27 @@ import { saveSftpMarkdown } from './sftp-markdown-file.js';
23
26
  const MAX_BODY_BYTES = 1_048_576;
24
27
  const MAX_SFTP_UPLOAD_BYTES = 512 * 1024 * 1024;
25
28
  export function registerSshApi(webServer, prefix, runtime) {
29
+ const downloads = new DownloadProgressStore();
26
30
  return webServer.register({
27
31
  kind: 'prefix',
28
32
  path: prefix,
29
33
  handler: async (req, res) => {
34
+ let progress;
30
35
  try {
31
36
  assertSameOrigin(req);
37
+ const url = new URL(req.url ?? '/', 'http://localhost');
38
+ if (req.method === 'GET' && url.pathname === `${prefix}/download-progress`) {
39
+ const status = downloads.get(url.searchParams.get('id') ?? '');
40
+ return sendJson(res, status ? 200 : 404, status ?? { error: 'download not found' });
41
+ }
42
+ const id = url.searchParams.get('transferId');
43
+ if (req.method === 'GET' && id && /\/(?:download|local-download)$/.test(url.pathname) && url.searchParams.get('inline') !== '1')
44
+ progress = downloads.track(id, res);
32
45
  await dispatch(req, res, prefix, runtime);
46
+ progress?.complete();
33
47
  }
34
48
  catch (error) {
49
+ progress?.fail(error);
35
50
  sendError(res, error);
36
51
  }
37
52
  },
@@ -42,6 +57,19 @@ async function dispatch(req, res, prefix, runtime) {
42
57
  const relative = url.pathname.slice(prefix.length).replace(/^\/+|\/+$/g, '');
43
58
  const segments = relative ? relative.split('/').map(decodeURIComponent) : [];
44
59
  const method = req.method ?? 'GET';
60
+ if (method === 'GET' && relative === 'workspace-revision') {
61
+ res.setHeader('Cache-Control', 'no-store');
62
+ return sendJson(res, 200, { revision: runtime.store.workspaceRevision() });
63
+ }
64
+ if (relative === 'group-proxies') {
65
+ if (method === 'GET')
66
+ return sendJson(res, 200, runtime.store.groupProxies());
67
+ if (method === 'PUT') {
68
+ requireMutationHeader(req);
69
+ await saveGroupProxy(runtime.store, await readObject(req));
70
+ return sendJson(res, 200, runtime.store.groupProxies());
71
+ }
72
+ }
45
73
  if (segments[0] === 'commands') {
46
74
  if (method === 'GET' && segments.length === 1)
47
75
  return sendJson(res, 200, runtime.store.commands().sort((a, b) => b.updatedAt - a.updatedAt));
@@ -91,6 +119,28 @@ async function dispatch(req, res, prefix, runtime) {
91
119
  }
92
120
  }
93
121
  if (segments[0] === 'file-transfer') {
122
+ if (method === 'PUT' && segments[1] === 'upload' && segments.length === 2) {
123
+ requireMutationHeader(req);
124
+ const endpointId = requireText(url.searchParams.get('endpointId'), 'endpointId', 110);
125
+ const directory = requireRawText(url.searchParams.get('directory'), 'directory', 4096);
126
+ const name = requireRemoteFilename(url.searchParams.get('name'));
127
+ const length = req.headers['content-length'] === undefined ? undefined : Number(req.headers['content-length']);
128
+ if (length !== undefined && (!Number.isSafeInteger(length) || length < 0 || length > MAX_SFTP_UPLOAD_BYTES))
129
+ throw httpError(413, 'upload exceeds the 512 MB limit');
130
+ const controller = new AbortController();
131
+ const abort = () => { if (!res.writableEnded)
132
+ controller.abort(new Error('上传连接已断开')); };
133
+ req.once('aborted', abort);
134
+ res.once('close', abort);
135
+ try {
136
+ const result = await uploadEndpointFile(runtime.files, endpointId, directory, name, req, AbortSignal.any([controller.signal, AbortSignal.timeout(300_000)]), length);
137
+ return sendJson(res, 201, result);
138
+ }
139
+ finally {
140
+ req.off('aborted', abort);
141
+ res.off('close', abort);
142
+ }
143
+ }
94
144
  if (method === 'GET' && segments[1] === 'stat' && segments.length === 2) {
95
145
  const endpointId = requireText(url.searchParams.get('endpointId'), 'endpointId', 110);
96
146
  const paneId = requireText(url.searchParams.get('paneId'), 'paneId', 100);
@@ -328,6 +378,8 @@ async function dispatch(req, res, prefix, runtime) {
328
378
  if (id !== undefined && method === 'DELETE' && segments.length === 2) {
329
379
  requireMutationHeader(req);
330
380
  requiredProxyEntry(runtime.store, id);
381
+ if (runtime.store.groupProxies().some(group => group.proxyId === id))
382
+ throw httpError(409, '代理仍被分组使用,请先修改分组代理');
331
383
  const references = runtime.store.profiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === id).length + runtime.store.ftpProfiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === id).length;
332
384
  if (references > 0)
333
385
  throw httpError(409, `proxy entry is used by ${references} connection(s)`);
@@ -584,6 +636,9 @@ async function dispatch(req, res, prefix, runtime) {
584
636
  requireMutationHeader(req);
585
637
  const body = await readObject(req);
586
638
  const previous = runtime.store.injection(sessionId);
639
+ for (const key of ['allowLocalUpload', 'allowLocalDownload'])
640
+ if (body[key] !== undefined && typeof body[key] !== 'boolean')
641
+ throw httpError(400, `${key} must be boolean`);
587
642
  const profileIds = parseProfileIds(body.profileIds, runtime.store);
588
643
  const fileEndpointIds = body.fileEndpointIds === undefined ? previous?.fileEndpointIds ?? [] : parseFileEndpointIds(body.fileEndpointIds, runtime.files);
589
644
  const filePermission = body.filePermission === undefined ? previous?.filePermission ?? 'browse' : body.filePermission === 'browse' ? 'browse' : body.filePermission === 'transfer' ? 'transfer' : undefined;
@@ -610,6 +665,8 @@ async function dispatch(req, res, prefix, runtime) {
610
665
  }
611
666
  const injection = {
612
667
  sessionId, profileIds, fileEndpointIds, filePermission, requireFileApproval: body.requireFileApproval === undefined ? previous?.requireFileApproval ?? true : body.requireFileApproval !== false,
668
+ allowLocalUpload: body.allowLocalUpload === undefined ? previous?.allowLocalUpload === true : body.allowLocalUpload === true,
669
+ allowLocalDownload: body.allowLocalDownload === undefined ? previous?.allowLocalDownload === true : body.allowLocalDownload === true,
613
670
  permission, requireCommandApproval: body.requireCommandApproval !== false, workingDirectories, workingProjectIds, mountedProjectIds, updatedAt: Date.now(),
614
671
  };
615
672
  await runtime.store.update(state => { state.injections = [...state.injections.filter(item => item.sessionId !== sessionId), injection]; });
@@ -905,7 +962,7 @@ async function proxyEntryViews(runtime) {
905
962
  }
906
963
  async function proxyEntryView(runtime, entry) {
907
964
  const credential = await runtime.credentials.describeProxyEntry(entry.id);
908
- return { ...entry, credential, references: runtime.store.profiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === entry.id).length + runtime.store.ftpProfiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === entry.id).length };
965
+ return { ...entry, credential, references: runtime.store.profiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === entry.id).length + runtime.store.ftpProfiles().filter(profile => profile.proxy.type === 'saved' && profile.proxy.proxyId === entry.id).length + (runtime.store.groupProxies?.() ?? []).filter(group => group.proxyId === entry.id).length };
909
966
  }
910
967
  async function ftpProfileViews(runtime) { return Promise.all(runtime.store.ftpProfiles().map(profile => ftpProfileView(runtime, profile))); }
911
968
  async function ftpProfileView(runtime, profile) {