dsh-rfiles 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-rfiles contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,296 @@
1
- # Temporary Holding Version
1
+ # dsh-rfiles
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Host-side plugin for the DSH Web GUI that adds two things the paired remote
4
+ page cannot do on its own: **send files to the Agent** and **download files
5
+ from the workspace**.
6
+
7
+ It works on top of [@linxin666/dsh-remote-web-ui](https://www.npmjs.com/package/@linxin666/dsh-remote-web-ui)
8
+ (the plugin that turns a phone or another computer into a paired DSH client)
9
+ and does not modify DSH core or that plugin.
10
+
11
+ - Chunked, resumable uploads with progress (a file can be a photo, a document,
12
+ or any binary the Agent should receive).
13
+ - Uploaded files are delivered to the session as real attachments, through the
14
+ same receipt/prompt path the built-in composer uses.
15
+ - A workspace browser with native downloads, plus one-click session-log ZIP
16
+ export.
17
+ - Everything is served through the plugin's own exact Fetch routes, so the
18
+ remote page's `/remote` pairing gate keeps working unchanged.
19
+
20
+ 中文文档:[README.zh.md](./README.zh.md)
21
+
22
+ ---
23
+
24
+ ## Why this exists
25
+
26
+ A paired remote page reaches the Host through the `/remote` prefix. The remote
27
+ plugin patches **programmatic** requests (`fetch`, WebSocket, EventSource, and
28
+ sub-resource `src` attributes) so they carry the device credential, but it does
29
+ not patch **navigation**: `<a download>`, `window.open`, `location` and form
30
+ submissions leave the page without the prefix. DSH's `/api` browser-trust fence
31
+ then rejects them.
32
+
33
+ That produces three visible problems on a phone or a second computer:
34
+
35
+ 1. **Downloads fail.** The built-in "download session log" entry is an anchor
36
+ navigation whose URL has no `/remote` prefix, so it is refused by the fence.
37
+ 2. **Uploads are opaque.** The built-in uploader prefers an XHR inside a Web
38
+ Worker, which is outside the channel; large files give no reliable progress.
39
+ 3. **No ranges.** The remote proxy forwards only a small header allowlist and
40
+ drops `Range`, `If-Range`, `Content-Range` and `Accept-Ranges`, so HTTP
41
+ resume and media seeking are unavailable over the remote channel.
42
+
43
+ This plugin sidesteps all three without touching either side:
44
+
45
+ - uploads are plain `fetch` requests with `File.slice` and an idempotent
46
+ byte offset, so progress and resume are computed locally and do not depend on
47
+ the built-in uploader;
48
+ - downloads are served by this plugin's own routes; the widget links to them
49
+ with an explicit `/remote` prefix. Anchor navigation is not rewritten by the
50
+ remote plugin, which is exactly what makes a prefixed URL work;
51
+ - `Content-Disposition`, `Content-Length`, `Content-Type`, `ETag` and
52
+ `Last-Modified` are on the proxy's response allowlist, so the browser saves
53
+ the file normally.
54
+
55
+ ## Features
56
+
57
+ | Feature | Detail |
58
+ | --- | --- |
59
+ | Send files to the Agent | Multi-select any file type; chunked upload with progress; optional text; delivered as a real attachment and a queued prompt |
60
+ | Camera / photo library | `accept="image/*" capture="environment"`; images up to 20 MiB use the official image attachment shape |
61
+ | Save to workspace only | Upload without prompting; files land in `<workspace>/.uploads/` (collisions renamed `name (n).ext`) |
62
+ | Download workspace files | Breadcrumb navigation and directory listing (up to 500 entries per level); native browser download |
63
+ | Session-log ZIP | Streams the official `/api/session.export` (with descendant sessions) and adds the download headers |
64
+ | Progress, cancel, resume | Per-chunk progress, cancel, and resume within 24 h after a disconnect or page reload |
65
+ | Session picker | Lists active sessions, selects the newest by default; a "remote" badge shows whether the `/remote` channel is in use |
66
+ | Desktop and remote | Desktop connections use `/api` (HTTP Range works); remote pages use the `/remote` prefix automatically |
67
+
68
+ Defaults, all configurable: 8 MiB per chunk, 2 GiB per file, 10 GiB of staging
69
+ space. Staging lives in `$DSH_HOME/rfile-transfers/<uploadId>/` and survives a
70
+ Host restart, so interrupted uploads can continue.
71
+
72
+ ## Requirements
73
+
74
+ - DSH `>=0.2.1-rc.3` (the plugin declares this in `dsh.engines.dsh`).
75
+ - Node.js 22.19 or newer; DSH ships a compatible runtime, so no extra install
76
+ is needed.
77
+ - Optional: [@linxin666/dsh-remote-web-ui](https://www.npmjs.com/package/@linxin666/dsh-remote-web-ui)
78
+ for the paired-remote use case. The plugin also works on a plain desktop
79
+ connection.
80
+
81
+ ## Install
82
+
83
+ The package is a DSH **bundle**: its `package.json` declares
84
+ `dsh.bundle.patch` and it ships `cordis.patch.yml`, which inserts the single
85
+ plugin row. Installing it therefore also registers the profile layer.
86
+
87
+ ### From the DSH Web GUI (recommended)
88
+
89
+ Open **Settings → Plugins** (the Plugins page) and install `dsh-rfiles` from
90
+ npm. Without npm, install from the repository URL, the release tarball URL, or
91
+ the local directory when working from a source checkout. Make sure the new
92
+ bundle is enabled; installing a bundle enables it by default. Restart DSH if the
93
+ page asks you to.
94
+
95
+ ### With the CLI
96
+
97
+ From npm (recommended):
98
+
99
+ ```bash
100
+ dsh plugin --profile web add dsh-rfiles
101
+ ```
102
+
103
+ Without npm, install the prebuilt GitHub Release tarball (there is no
104
+ install-time build either way):
105
+
106
+ ```bash
107
+ dsh plugin --profile web add \
108
+ https://github.com/Aperops/dsh-rfiles/releases/download/v0.1.0/dsh-rfiles-0.1.0.tgz
109
+ ```
110
+
111
+ From source, or from a local tarball:
112
+
113
+ ```bash
114
+ dsh plugin --profile web add git+https://github.com/Aperops/dsh-rfiles.git
115
+ dsh plugin --profile web add /path/to/dsh-rfiles-0.1.0.tgz
116
+ ```
117
+
118
+ Installing a bundle also registers its layer, so the profile's
119
+ `dsh.profile.bundles` list gains `dsh-rfiles` automatically; no manual patch
120
+ edit is needed. If you maintain the profile by hand, add `"dsh-rfiles"` to
121
+ `dsh.profile.bundles` in `$DSH_HOME/profiles/web/package.json` and restart DSH.
122
+
123
+ Confirm the composed tree contains the `rfiles` row:
124
+
125
+ ```bash
126
+ dsh --profile web --dump-config | grep -A2 rfiles
127
+ ```
128
+
129
+ ### Confirm it loaded
130
+
131
+ Open any session; a **Files** button appears in the lower-right corner of the
132
+ page. Host-side check (needs a browser-auth cookie for the `/api` fence):
133
+
134
+ ```bash
135
+ curl -s http://127.0.0.1:61298/api/rfiles.health
136
+ # {"ok":true,"version":"1","workspaceRoot":"..."}
137
+ ```
138
+
139
+ If the button is missing, see [Troubleshooting](#troubleshooting) below.
140
+
141
+ ## Usage
142
+
143
+ Open the **Files** button. The panel has two tabs.
144
+
145
+ ### Upload / send to the Agent
146
+
147
+ 1. Pick **Send to Agent** (default) or **Save to workspace**.
148
+ 2. Choose files, or use **Camera / library** to take a photo.
149
+ 3. Upload starts automatically, in 8 MiB chunks, with a progress bar. Failed or
150
+ interrupted uploads can be retried; choosing the same file again within 24 h
151
+ resumes from the last acknowledged offset.
152
+ 4. In **Send to Agent** mode, add optional text and press **Send to Agent**. The
153
+ file arrives as a real attachment (images up to 20 MiB as image content) and
154
+ a prompt is queued on the selected session.
155
+ 5. In **Save to workspace** mode the file is written to
156
+ `<workspace>/.uploads/` and no prompt is sent.
157
+
158
+ ### Download
159
+
160
+ 1. The **Download** tab starts at the workspace root; folders come first.
161
+ 2. Click a file to save it with the browser's native download. Links on a remote
162
+ page already carry the `/remote` prefix.
163
+ 3. If a browser blocks the navigation or the device is paired without cookies
164
+ (header-only credential), use the **⤓** button on the row: it fetches the
165
+ bytes and saves a Blob instead (fetch is rewritten by the remote channel and
166
+ carries the device credential).
167
+ 4. **Download session log ZIP** exports the current session, including
168
+ descendant sessions and attachment objects.
169
+
170
+ Only paths inside the session workspace are listed or served. There is no
171
+ delete, rename, mkdir, or directory zip.
172
+
173
+ ## Configuration
174
+
175
+ All limits have defaults; override them in the profile's
176
+ `cordis.patch.yml` (this is the profile's user layer, applied after the bundle
177
+ layer):
178
+
179
+ ```yaml
180
+ - id: rfiles
181
+ config:
182
+ chunkBytes: 8388608 # bytes per chunk (default 8 MiB)
183
+ maxFileBytes: 2147483648 # per-file limit (default 2 GiB)
184
+ stagingBytes: 10737418240 # total staging limit (default 10 GiB)
185
+ uploadDir: .uploads # workspace-mode destination
186
+ transferRoot: '' # staging root; empty = $DSH_HOME/rfile-transfers
187
+ ```
188
+
189
+ ## Security model
190
+
191
+ - **No new authentication.** Every route is a Connection exact Fetch route.
192
+ A desktop owner browser is authorized by the normal browser credential; a
193
+ remote request must first pass the `/remote` pairing gate (device cookie or
194
+ `x-dsh-remote-device`), after which the proxy calls `/api` with its own
195
+ process credential. Non-owner peers still receive 403.
196
+ - **Workspace confinement.** Listing, downloading and upload destinations are
197
+ resolved inside the session workspace; absolute paths, `..`, NUL bytes and
198
+ symlink escapes are rejected (realpath check).
199
+ - **Staging only.** `$DSH_HOME/rfile-transfers/<uploadId>/` holds the bytes and
200
+ metadata of in-flight uploads only.
201
+ - **Minimal surface.** List, download, upload, send. No delete, rename, mkdir,
202
+ terminal, or code execution.
203
+ - Scripts and documentation never print device ids or credentials.
204
+
205
+ ## Known limitations
206
+
207
+ - **HTTP Range does not work through the remote proxy** (it drops `Range` and
208
+ related headers), so remote downloads are full-body transfers and video
209
+ seeking is unavailable. Desktop `/api` connections support single-range 206
210
+ responses. Upload resume is unaffected because it uses chunk offsets, not
211
+ HTTP ranges.
212
+ - Only the workspace tree is browseable; there is no directory archive
213
+ (the session-log ZIP is the exception).
214
+ - The session picker lists active sessions only; cold sessions need an
215
+ explicit id through the API.
216
+ - Through Cloudflare tunnels, uploads are 8 MiB requests (which avoids a
217
+ single-request body cap) but total time and traffic are still limited by the
218
+ tunnel; Quick Tunnel availability is not guaranteed.
219
+ - `present`-card "open on host" actions are meaningless on a phone; browse the
220
+ workspace in the **Download** tab instead.
221
+ - If @linxin666/dsh-remote-web-ui changes its channel behavior (boot global
222
+ name, header allowlist, `/remote` semantics), this plugin needs a regression
223
+ run. Re-run `scripts/verify.sh` after upgrading it.
224
+ - iOS Safari and Android browsers have not been tested as a full device matrix;
225
+ see the verification notes in the source checkout.
226
+
227
+ ## Troubleshooting
228
+
229
+ | Symptom | Check |
230
+ | --- | --- |
231
+ | No **Files** button | Fully restart DSH (patches are read at boot); hard-refresh the page (the widget is injected into the index head); check that the profile manifest lists the `dsh-rfiles` bundle and that `node_modules/dsh-rfiles` exists; search the console for `__DSH_RFILES__` / `[dsh-rfiles]`; a missing required service in `inject` prevents the plugin from mounting. |
232
+ | Panel says "open a session first" | The picker lists active sessions only. Open a session in the GUI, then press refresh in the panel. |
233
+ | Upload 401/403 | Pairing expired or the device credential is missing: re-pair. Confirm requests go to `/remote/api/rfiles.*`; if they still go to `/api/...` the remote boot patch did not run. |
234
+ | Download 401/403 (including session-log ZIP) | An anchor navigation can only authenticate with the pairing cookie. Devices paired without cookies must use the **⤓** button. |
235
+ | Clicking a file does nothing | The browser blocked the download; use **⤓**; on iOS a preview may appear first ("Save to Files"). |
236
+ | Upload stalls | Inspect the chunk requests: `409 offset-mismatch` is the normal resume signal (the client continues from `received`); `413` means a quota; a dropped tunnel can be resumed by reopening the panel. |
237
+ | `/api/rfiles.health` returns 404 | The plugin is not mounted or DSH was not restarted. |
238
+ | Port is not 61298 | The DSH Web URL in the startup log shows the format/port you should use. |
239
+
240
+ ## Verification
241
+
242
+ From a source checkout (scripts are not part of the npm tarball):
243
+
244
+ ```bash
245
+ cd dsh-rfiles
246
+
247
+ # unit tests: path confinement, ranges, chunk store, routes
248
+ node --test test/
249
+
250
+ # syntax
251
+ node --check lib/index.js && node --check lib/core.js && node --check lib/client.js
252
+
253
+ # live connectivity check against the running Host through the real remote channel
254
+ bash scripts/verify.sh # port defaults to 61298
255
+ bash scripts/verify.sh --port 61299 --require-range
256
+ ```
257
+
258
+ `verify.sh` uses a paired device credential (never printed) and checks health,
259
+ a chunked upload, listing, a download with sha256 comparison, a Range request,
260
+ and session-log export. Remote Range is reported as SKIP because the proxy
261
+ drops the header; pass `--require-range` to turn that into a failure.
262
+
263
+ ## Uninstall
264
+
265
+ 1. In **Settings → Plugins**, disable and remove `dsh-rfiles`, or run
266
+ `dsh plugin --profile web remove dsh-rfiles`.
267
+ 2. Remove `"dsh-rfiles"` from `dsh.profile.bundles` if it is still listed.
268
+ 3. Restart DSH.
269
+ 4. Optional cleanup: `rm -rf "$DSH_HOME/rfile-transfers"` (stale staging) and
270
+ the workspace `.uploads/` directory (not removed automatically).
271
+
272
+ ## Development
273
+
274
+ The package is plain ESM with no build step and no runtime dependencies.
275
+
276
+ ```bash
277
+ cd dsh-rfiles
278
+ node --test test/ # 33 tests
279
+ node scripts/mock-server.mjs --port 4399
280
+ # open http://127.0.0.1:4399/mock.html
281
+ # remote-prefix mode: http://127.0.0.1:4399/mock.html?remote=1
282
+ ```
283
+
284
+ - `lib/index.js` — Cordis plugin: routes, config, widget injection.
285
+ - `lib/core.js` — pure logic: path confinement, Range parsing, download
286
+ headers, chunk store.
287
+ - `lib/client.js` — the browser widget (classic IIFE, Shadow DOM, mobile
288
+ drawer); injected by the Host into the index head.
289
+ - `CONTRACT.md` — the HTTP contract (repo only; keep it in sync with tests).
290
+ - `scripts/install.sh` — profile-local install from a source checkout
291
+ (symlink + patch row), with `--dry-run`.
292
+ - `scripts/verify.sh`, `scripts/mock-server.mjs` — verification tools.
293
+
294
+ ## License
295
+
296
+ MIT