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 +21 -0
- package/README.md +295 -2
- package/README.zh.md +429 -0
- package/assets/icon.png +0 -0
- package/assets/icon.svg +13 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +1662 -0
- package/lib/core.js +728 -0
- package/lib/index.js +668 -0
- package/package.json +54 -4
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
|
-
#
|
|
1
|
+
# dsh-rfiles
|
|
2
2
|
|
|
3
|
-
|
|
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
|