@yezack/dsh-remote-mobile 0.0.0-stage → 1.9.1

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 ADDED
@@ -0,0 +1,454 @@
1
+ <div align="center">
2
+
3
+ # dsh-remote-mobile
4
+
5
+ **DeepSeek Harness (DSH) Remote & Mobile Security Guard Plugin**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/dsh-remote-mobile.svg?style=flat-square&color=3b82f6)](https://www.npmjs.com/package/dsh-remote-mobile)
8
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-remote-mobile.svg?style=flat-square&color=22c55e)](https://www.npmjs.com/package/dsh-remote-mobile)
9
+ [![node](https://img.shields.io/badge/node-%3E%3D18.0.0-8b5cf6?style=flat-square)](https://nodejs.org)
10
+ [![license](https://img.shields.io/npm/l/dsh-remote-mobile.svg?style=flat-square&color=10b981)](https://github.com/IceApriler/dsh-remote-mobile/blob/master/LICENSE)
11
+ [![DSH](https://img.shields.io/badge/DSH-Web%20Profile-0ea5e9?style=flat-square)](https://github.com/IceApriler/dsh-remote-mobile)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/IceApriler/dsh-remote-mobile/pulls)
13
+
14
+ <p align="center">
15
+ <b>Zero Core Modifications · Secure LAN / Tailscale Access · QR Pairing · Mobile UI Adaptation · Workspace Parity · Real-Time Push</b>
16
+ </p>
17
+
18
+ <p align="center">
19
+ 📱 <b>Desktop-grade mobile workspace</b>: create / switch workspaces and run terminal commands on your phone<br>
20
+ 🛡️ <b>Multi-tier auth</b>: QR pairing · persistent password · passwordless bypass + RSA encryption + brute-force lockout<br>
21
+ 🎨 <b>Mobile style snippets</b>: three built-in presets, fully customizable with your own CSS
22
+ </p>
23
+
24
+ English Documentation · [简体中文](./README.md)
25
+
26
+ <p align="center">
27
+ <a href="#about">About</a> •
28
+ <a href="#advantages">Key Advantages</a> •
29
+ <a href="#quick-start">Quick Start</a> •
30
+ <a href="#config">Required Config</a> •
31
+ <a href="#preview">UI Preview</a> •
32
+ <a href="#features">Features</a> •
33
+ <a href="#install">Installation & Update</a> •
34
+ <a href="#advanced">Advanced Configuration</a> •
35
+ <a href="#storage">Local File Storage</a> •
36
+ <a href="#faq">FAQ</a>
37
+ </p>
38
+
39
+ ---
40
+
41
+ </div>
42
+
43
+ <span id="about"></span>
44
+ ## 📖 About
45
+
46
+ `dsh-remote-mobile` is a dedicated remote and mobile security governance plugin designed specifically for **DeepSeek Harness (DSH)**.
47
+
48
+ By default, DSH strictly listens only to the local loopback address (`127.0.0.1`), preventing mobile devices, tablets, or external computers from accessing the Web console. This plugin uses **access control middleware** and **request isolation** to securely open up **Tailscale Private Network** and **Local Area Network (LAN / Wi-Fi)** access, complete with RSA transmission encryption, QR code pairing, persistent password auth, and automated brute-force defense.
49
+
50
+ When accessed from a phone, the plugin also adapts the DSH interface for **mobile styles**: a collapsible sidebar drawer with a draggable floating toggle, compatibility with the official 3-column Grid layout & right sidebar, a centered/scaled settings dialog, and dense conversation typography — so the mobile experience feels closer to the desktop.
51
+
52
+ ---
53
+
54
+ <span id="advantages"></span>
55
+ ## ⚡ Key Advantages
56
+
57
+ * 🚀 **Break Network Barriers with Desktop-Grade Workspace**: For accessing DSH Web over LAN / Tailscale. On mobile devices, **common desktop operations such as creating / switching workspaces and running terminal commands are supported**.
58
+ * 📱 **Dedicated Mobile Adaptations & Same-Origin Reuse**: Directly reuses the official DSH Web frontend (the very same UI as desktop) and the wider plugin ecosystem (sidebars, task boards, terminal panels, etc.) without maintaining a separate mobile backend. Includes out-of-the-box mobile adaptations (sidebar drawer toggle, centered settings modal, and compact chat font scaling for higher information density), alongside a built-in **Style Snippet Override system** to toggle presets or inject custom CSS snippets on demand.
59
+ * 🛡️ **Security Gateway & Transport Encryption**: Built-in client-side RSA asymmetric public key encryption, `scrypt` salted slow-hashing password persistence, and automated IP lockout upon consecutive brute-force failures, blocking unauthorized access over public/LAN networks.
60
+ * 📲 **Quick QR Pairing**: Auto-detects Tailscale CGNAT and LAN IP addresses to generate dedicated pairing QR codes. Scan the code to authenticate and obtain persistent credentials.
61
+ * 🔄 **SSE Real-Time Push**: Device connection, reconnection, revocation, and security alert events are pushed in real time via Server-Sent Events without polling.
62
+ * 🤝 **Generic Plugin Coexistence Protection**: Starts safely alongside other remote/Web access plugins by automatically yielding the shared service; the settings panel shows a conflict banner with a one-click copyable diagnosis & fix report.
63
+
64
+ ---
65
+
66
+ <span id="quick-start"></span>
67
+ ## 🚀 Quick Start
68
+
69
+ ### 1. One-Command Installation
70
+
71
+ Run the DSH official plugin management command in your terminal (Recommended):
72
+
73
+ ```bash
74
+ dsh plugin --profile web add dsh-remote-mobile
75
+ ```
76
+
77
+ > [!IMPORTANT]
78
+ > **Ecosystem Plugin Advice**: **Installing `@linxin666/dsh-web-all` (all-in-one package) is not recommended**. We suggest **installing individual standalone plugins on demand**. The all-in-one package bundles multiple frontend modules together, which may result in duplicated mobile floating buttons and conflicting gesture handlers. If you need task boards, pet widgets, or other tools, install their standalone packages directly (see [FAQ Q6](#faq) for the rationale, recommended commands, and coexistence notes).
79
+
80
+ ---
81
+
82
+ <span id="config"></span>
83
+ ### 2. Configuration (Open External Listening)
84
+
85
+ Because DSH defaults to `127.0.0.1`, ensure your `~/.dsh/profiles/web/cordis.patch.yml` (on Windows: `C:\Users\<username>\.dsh\profiles\web\cordis.patch.yml` or enter `%USERPROFILE%\.dsh` in File Explorer) includes the following configuration to allow external connectivity:
86
+
87
+ ```yaml
88
+ # 1. Allow webserver to listen for external connections (Required)
89
+ - id: webserver
90
+ name: '@deepseek-ai/dsh-host-webserver'
91
+ inject: [webStartup]
92
+ config:
93
+ host: '0.0.0.0'
94
+ port: 3080
95
+
96
+ # 2. (Recommended) Keep only ONE remote/Web access plugin to avoid duplicated entries.
97
+ # This plugin starts normally alongside other remote-access plugins: when it
98
+ # detects the shared pairing service is already taken by another plugin, it
99
+ # yields automatically and shows a warning banner at the top of its settings panel.
100
+ # Example: disable the legacy remote plugin bundled in @linxin666/dsh-web-ui-all
101
+ - id: web-ui-remote-web-ui
102
+ disabled: true
103
+ ```
104
+
105
+ > **💡 Note**: Plugin self-registration is automatically handled by the DSH Bundle system. You do **not** need to add `id: remote-mobile` manually. Windows users can press `Win + R` and enter `%USERPROFILE%\.dsh\profiles\web` to quickly jump to the configuration folder.
106
+
107
+ > [!WARNING]
108
+ > **Installing the all-in-one bundle (`@linxin666/dsh-web-all`) is not recommended**: it compiles the frontend logic of multiple sub-plugins into a single `client.js` that self-executes on load, which may duplicate mobile entry points and cause gesture conflicts with this plugin. Install only the standalone plugins you need (task board, pet, etc.). See [FAQ Q6](#faq) for the full rationale, recommended commands, and coexistence notes.
109
+
110
+ ---
111
+
112
+ ### 3. Launch & Pair
113
+
114
+ ```bash
115
+ dsh web --no-open
116
+ ```
117
+
118
+ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remote & Mobile Access**, and scan the QR code with your mobile camera or WeChat to start using DSH on mobile!
119
+
120
+ ---
121
+
122
+ <span id="preview"></span>
123
+ ## 🖼️ UI Preview
124
+
125
+ ### Desktop DSH Plugin Control Panel
126
+
127
+ | Network Access & QR Pairing | Devices & Security |
128
+ | :---: | :---: |
129
+ | ![PC Settings - Network & QR](images/pc-dsh-setting-1.png) | ![PC Settings - Devices & Security](images/pc-dsh-setting-2.png) |
130
+ | **Style Snippets Override** | **Local Data Storage** |
131
+ | ![PC Settings - Style Snippets](images/pc-dsh-setting-3.png) | ![PC Settings - Local Data Storage](images/pc-dsh-setting-4.png) |
132
+
133
+ ---
134
+
135
+ ### Mobile Live Demonstration (Same-Origin Auth & Workspace Control)
136
+
137
+ | Login & Auth | Chat Font Scaling | Mobile Sidebar Styling | Official Trajectory / Trace |
138
+ | :---: | :---: | :---: | :---: |
139
+ | ![Login & Auth](images/mobile-auth.jpg) | ![Chat Font Scaling](images/mobile-dsh-1.jpg) | ![Mobile Sidebar Styling](images/mobile-dsh-2.jpg) | ![Official Trajectory / Trace](images/mobile-dsh-3.jpg) |
140
+
141
+ ---
142
+
143
+ <span id="features"></span>
144
+ ## 🌟 Features
145
+
146
+ ### 1. Network Access
147
+ - **Tailscale Private Network**: Automatically detects local Tailscale IP (`100.64.0.0/10` CGNAT subnet), creates pairing QR codes, and supports direct passwordless bypass (transport encryption guaranteed by Tailscale WireGuard tunnels).
148
+ - **Local Area Network (LAN / Wi-Fi)**: Automatically detects RFC 1918 private IPs (`192.168.x.x`, `10.x.x.x`, `172.16-31.x.x`), generates LAN pairing QR codes with direct link previews and prominent high-risk security warnings.
149
+ - **Instant QR Pairing**: Scan QR codes directly with any mobile camera or scanner app to authenticate.
150
+
151
+ ### 2. Multi-Tier Authentication
152
+ - **Dynamic 6-Digit Pairing Codes**: Generates 6-digit short codes (5-minute validity, single-use) that exchange for 365-day persistent authentication cookies on mobile.
153
+ - **Persistent Access Passwords**: Set customizable persistent passwords (minimum 6 characters with letters and numbers) for convenient long-term logins across multiple devices.
154
+ - **Direct Bypass Mode**: Enable passwordless direct bypass independently for Tailscale or LAN. Disabling bypass immediately cleans up temporary credentials and resets device state.
155
+ - **Device Session Management**: Real-time inspection of connected device types, OS, browser, source IP, and last active timestamp, with single-device revocation and one-click bulk revocation.
156
+
157
+ ### 3. Security & Cryptography
158
+ - **Transport RSA Asymmetric Encryption**: The login endpoint encrypts on the client first, so sensitive passwords and pairing codes travel the network only in ciphertext.
159
+ - **Secure contexts (HTTPS)**: prioritizes native Web Crypto `RSA-OAEP-SHA256`;
160
+ - **DSH default HTTP LAN/Tailscale access (non-secure context)**: falls back to a pure JS shim using `crypto.getRandomValues()` for cryptographically secure random padding.
161
+ - **scrypt Slow Hash Storage**: Server persists passwords with `scrypt` salted slow-hashing (`scrypt:${salt}:${hash}`). Verification uses `crypto.timingSafeEqual` constant-time comparison to prevent timing side-channel attacks.
162
+ - **Brute-Force Defense & Rate Limiting**:
163
+ - Automatically locks out IPs for 15 minutes after reaching consecutive failed attempt thresholds (default: 5), returning HTTP 429;
164
+ - Sliding-window rate limiting (default: 60 visits/min) to prevent high-frequency brute-force scraping;
165
+ - Audit logs and lockout states persist to disk and restore across restarts;
166
+ - Administrators can manually unlock blocked IPs with one click in the panel.
167
+ - **Intelligent Static Asset Whitelisting**: The authentication gate automatically allows verified frontend asset extensions (`.js`, `.css`, `.png`, `.svg`, `.woff2`, etc., 20 types) while blocking extensionless or dynamic API routes (e.g. `/plugins/xxx/admin`), ensuring smooth third-party plugin rendering without manual whitelist configuration. (The DSH core static directory `/assets/` is an official resource and is allowed unconditionally.)
168
+ - **Resilient Atomic Persistence & Debounce**: Millisecond-level debounce write-throttling to safeguard disk I/O (500ms for session data, 300ms for style snippets), coupled with `beforeExit` process flush hooks and atomic temporary file replacement (`renameSync`) for `settings.json` and `style-snippets.json`.
169
+ - **Real Socket IP Extraction**: Relies strictly on underlying Socket connection addresses, preventing spoofed `X-Forwarded-For` header attacks.
170
+ - **Loopback CSRF Defense**: Mutating plugin APIs validate browser same-origin signals (`Origin` / `Sec-Fetch-Site`). Cross-site write requests to `127.0.0.1` driven by malicious webpages are rejected outright; non-browser clients (curl / local scripts) are unaffected.
171
+ - **No Context Rewriting for Loopback Requests**: Context virtualization applies only to external traffic. Loopback requests keep their original Host / Origin (no "laundering"), so DSH's built-in DNS-rebinding and same-origin checks keep working against external domains. The SSE event stream accepts same-origin connections only, with no cross-origin reads.
172
+
173
+ ### 4. Real-Time Sync & Internationalization
174
+ - **SSE Real-Time Push**: Push notifications for new device pairing, reconnections, revocations, and security alerts via Server-Sent Events, with bidirectional close listeners and idempotent cleanup.
175
+ - **Adaptive Bilingual UI**: Automatically switches between English and Simplified Chinese based on DSH settings and browser language preferences.
176
+ - **Non-HTTPS Compatibility Patch**: Injects `crypto.randomUUID` and `navigator.clipboard` Polyfills into HTML templates to fix missing native Web Crypto / Clipboard APIs in HTTP non-secure mobile browser contexts (the clipboard polyfill keeps copy and snippet-copy working in non-secure contexts).
177
+
178
+ ### 5. Mobile Style Snippets (style mini-plugins)
179
+
180
+ The DSH web UI still carries many desktop-oriented styles on phones. This plugin ships a built-in **Mobile Style Snippets** module that splits mobile adaptation into toggleable CSS snippets:
181
+
182
+ - **Three area-based built-in presets** (mobile on / PC off by default):
183
+ - `preset-sidebar` **Sidebar Drawer**: the collapsed sidebar occupies 0 width, with a draggable floating toggle, adaptive drawer width, and touch-friendly 44px tap targets;
184
+ - `preset-settings` **Settings Panel**: vertically stacked layout with horizontally scrollable nav tabs and natural vertical scrolling;
185
+ - `preset-main` **Dense Conversation Typography**: compact 12.5px text, tight line-height and margins for more content per line; built on stable HTML elements and localName suffixes, with no layout scaling and no right-side gaps;
186
+ - **Separate PC / Mobile toggles (viewport-width based, device-independent)**: every preset and custom snippet has independent PC and Mobile switches; "Mobile" = applies at narrow viewports (≤900px) — **a PC browser with a narrow window gets it too**; "PC" = applies at wide viewports (>900px); both on = applies at all widths;
187
+ - **About the `html[data-dsh-mobile]` selector**: that marker is only written for **mobile-UA** requests. If your custom CSS starts with `html[data-dsh-mobile]`, it only matches phone/mobile-UA clients; to also target a narrow desktop window, write the selector directly (without the `html[data-dsh-mobile]` prefix) and the injection layer wraps it in the media query automatically.
188
+ - **Custom snippets (your own mini-plugins)**: paste your CSS in **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets** to add a snippet, with edit/toggle/delete/**one-click copy** support (every snippet has a "📋 Copy CSS" button next to "View CSS"). Persisted to `~/.dsh/remote-mobile/style-snippets.json`; changes apply on the next page load without restart;
189
+ - **UA-based marker + width-based styling**: styles apply by viewport-width band (see above), independent of device UA; mobile UA requests additionally add `data-dsh-mobile="1"` to `<html>` as a scope hook (using it as a selector only matches mobile UA — see the note above), and the draggable toggle script is injected for all clients (it only activates while the sidebar is collapsed).
190
+
191
+ ### 6. Generic Plugin Coexistence Protection
192
+
193
+ - **Conflict-free startup**: when the shared pairing service `remoteWebUiPairing` is already registered by another remote/Web access plugin, this plugin yields automatically (deferred arbitration: it waits until the activation window settles, then checks who owns the service name), avoiding the whole-tree rollback caused by service-name collisions — installing from the plugin market works out of the box with zero manual configuration;
194
+ - **Visible coexistence status**: when a conflict is detected, a dismissible warning banner (Chinese/English) appears at the top of the settings panel, explaining the yield behavior and the "keep only one" recommendation; refresh after resolving and it disappears;
195
+ - **One-click diagnosis report**: the **"📋 Copy diagnosis & fix"** button in the banner copies a full report — the conflicting plugin's package name and loader entry id are auto-detected at runtime, and the report includes line-exact fix configuration and commands (disable the other / remove this one), ready to paste to an AI assistant.
196
+
197
+ ---
198
+
199
+ <span id="install"></span>
200
+ ## 📦 Installation & Update
201
+
202
+ <details>
203
+ <summary><b>Expand to view all 4 installation methods</b></summary>
204
+
205
+ ### Method 1: DSH CLI One-Command Install (Recommended)
206
+ ```bash
207
+ dsh plugin --profile web add dsh-remote-mobile
208
+ ```
209
+
210
+ ### Method 2: Web GUI Plugin Marketplace Install
211
+ 1. Open DSH Web console in your browser;
212
+ 2. Navigate to **Settings ⚙️ -> Plugins**;
213
+ 3. Switch to the **Plugin Management** tab at the top;
214
+ 4. Enter the npm package name **`dsh-remote-mobile`** and click **Install**;
215
+ 5. Restart DSH after installation completes.
216
+
217
+ ### Method 3: Install via Package Manager in Profile Directory
218
+ ```bash
219
+ # 1. Navigate to DSH Web Profile directory
220
+ cd ~/.dsh/profiles/web
221
+
222
+ # 2. Install using pnpm
223
+ pnpm add dsh-remote-mobile
224
+ ```
225
+
226
+ ### Method 4: Local Source Development (Instant Symlink Sync)
227
+
228
+ ```bash
229
+ # 1. Clone source code
230
+ git clone https://github.com/IceApriler/dsh-remote-mobile.git
231
+ cd dsh-remote-mobile
232
+
233
+ # 2. Install dependencies & build
234
+ npm install
235
+ npm run build
236
+ ```
237
+
238
+ **Create symlink to DSH runtime (changes take effect instantly on `npm run build`):**
239
+
240
+ * **macOS / Linux**:
241
+ ```bash
242
+ rm -rf ~/.dsh/profiles/web/node_modules/dsh-remote-mobile
243
+ ln -s $(pwd) ~/.dsh/profiles/web/node_modules/dsh-remote-mobile
244
+ ```
245
+
246
+ * **Windows (PowerShell)**:
247
+ ```powershell
248
+ Remove-Item -Recurse -Force "$HOME\.dsh\profiles\web\node_modules\dsh-remote-mobile"
249
+ New-Item -ItemType Junction -Path "$HOME\.dsh\profiles\web\node_modules\dsh-remote-mobile" -Target (Get-Location)
250
+ ```
251
+
252
+ * **Windows (CMD)**:
253
+ ```cmd
254
+ rmdir /s /q %USERPROFILE%\.dsh\profiles\web\node_modules\dsh-remote-mobile
255
+ mklink /J %USERPROFILE%\.dsh\profiles\web\node_modules\dsh-remote-mobile %CD%
256
+ ```
257
+
258
+ </details>
259
+
260
+ ### 🔄 Updating Plugin
261
+
262
+ Run the update command in your terminal and restart DSH:
263
+
264
+ ```bash
265
+ # Method 1: DSH CLI One-Command Update (Recommended)
266
+ dsh plugin --profile web update dsh-remote-mobile
267
+
268
+ # Method 2: Update via Package Manager in Profile Directory
269
+ cd ~/.dsh/profiles/web && pnpm update dsh-remote-mobile
270
+ ```
271
+
272
+ > **💡 Tip**: You can also click the update button in the Web console under **Settings ⚙️ -> Plugins -> Plugin Management**, or run `dsh plugin --profile web add dsh-remote-mobile@latest`.
273
+
274
+ ### 🗑️ Uninstalling
275
+
276
+ ```bash
277
+ dsh plugin --profile web remove dsh-remote-mobile
278
+ ```
279
+
280
+ ---
281
+
282
+ <span id="advanced"></span>
283
+ ## ⚙️ Advanced Configuration
284
+
285
+ Configurations persist in the plugin-owned file `~/.dsh/remote-mobile/settings.json`; adjust them in the Web UI or edit the file directly:
286
+
287
+ ```jsonc
288
+ {
289
+ "allowTailscale": false, // boolean, default false: allow passwordless access via Tailscale
290
+ "allowLan": false, // boolean, default false: allow passwordless access via LAN (High Risk)
291
+ "secretHash": "", // string, default empty: scrypt salted hash of the persistent password
292
+ "maxVisitsPerMinute": 60, // number, default 60: max login page visits per minute per IP
293
+ "maxFailedAttempts": 5, // number, default 5: max consecutive failed attempts before IP lockout
294
+ "lockDurationMs": 900000 // number, default 900000 (15 mins): IP lockout duration in ms
295
+ }
296
+ ```
297
+
298
+ > **About config storage**: Since DSH **0.1.7** (first noted in the v0.1.7-alpha.1 release notes; continued in 0.2.x), `~/.dsh/settings.yaml` is no longer a live configuration source — on boot it is imported once into the active profile's `cordis.patch.yml` and renamed to `settings.yaml.imported` (see [Q8](#faq)). This plugin therefore no longer writes to settings.yaml and persists to its own `settings.json` above; on first launch it automatically migrates once from the legacy `settings.yaml` / `settings.yaml.imported` (recovering your old persistent password, rate limits, etc.).
299
+
300
+ ### 🎨 Mobile Style Snippets (optional)
301
+
302
+ Custom snippets (style mini-plugins) and their enabled states are persisted in `~/.dsh/remote-mobile/style-snippets.json`. Manage them from the settings panel or edit the file directly:
303
+
304
+ ```json
305
+ {
306
+ "version": 2,
307
+ "presetStates": {
308
+ "preset-sidebar": { "pc": false, "mobile": true },
309
+ "preset-settings": { "pc": false, "mobile": true },
310
+ "preset-main": { "pc": false, "mobile": true }
311
+ },
312
+ "custom": [
313
+ {
314
+ "id": "custom-xxx",
315
+ "name": "My style mini-plugin",
316
+ "css": "html[data-dsh-mobile] .my-selector { ... }",
317
+ "pcEnabled": false,
318
+ "mobileEnabled": true
319
+ }
320
+ ],
321
+ "customOrder": ["custom-xxx"]
322
+ }
323
+ ```
324
+
325
+ > Enabled snippets are merged into a single `<style>` injected into the main page and the `/auth` login page; **effectiveness is decided by viewport width**: snippets with `mobileEnabled` are wrapped in `@media (max-width: 900px)` (apply at ≤900px — a narrow PC window gets them too), `pcEnabled` in `@media (min-width: 901px)`, and both on means all widths (v1 files migrate automatically; `presetStates` always persists all three preset groups with current effective values). Note: enabling the Sidebar Drawer preset on PC changes the desktop layout — turn its PC switch off if undesired.
326
+
327
+ ---
328
+
329
+ <span id="storage"></span>
330
+ ## 📂 Local File Storage Locations
331
+
332
+ > **💡 Path Note**: On macOS / Linux, the base path is `~/.dsh/`; on Windows, it resolves to `C:\Users\<username>\.dsh\` (you can directly paste `%USERPROFILE%\.dsh` into the File Explorer address bar).
333
+
334
+ | Path | Description | Security Level |
335
+ |---|---|---|
336
+ | `~/.dsh/remote-mobile/settings.json` | Plugin config: bypass toggles, persistent-password hash, rate-limit & lockout policy | Local storage, `0o600` (Restricted to current user) |
337
+ | `~/.dsh/remote-mobile/devices.json` | Authorized devices, sessions & IP audit statistics (contains long-lived tokens, `0o600`) | Local storage, restricted to current user |
338
+ | `~/.dsh/remote-mobile/rsa-keys.json` | Server RSA keypair (public & private) | Local storage, `0o600` (Restricted to current user) |
339
+ | `~/.dsh/remote-mobile/style-snippets.json` | Mobile style snippets (built-in toggles + custom CSS mini-plugins) | Local persistent storage |
340
+
341
+ ---
342
+
343
+ <span id="faq"></span>
344
+ ## ❓ FAQ
345
+
346
+ <details>
347
+ <summary><b>Q0: Startup fails with <code>listen EADDRINUSE: address already in use 0.0.0.0:3080</code>?</b></summary>
348
+
349
+ **Answer**: This means **port 3080 is held by another process** (in most cases a previous `dsh web` instance has not fully exited — e.g. a hidden instance launched by a desktop shortcut, or overlapping old/new processes during restart). It is **unrelated to which plugins are installed**. Troubleshooting:
350
+
351
+ 1. **Find the process holding the port**:
352
+ * **macOS / Linux**: `lsof -nP -iTCP:3080`
353
+ * **Windows**: `netstat -ano | findstr 3080` (PID is in the rightmost column)
354
+ 2. **Terminate the old instance**:
355
+ * **macOS / Linux**: `kill <PID>`
356
+ * **Windows**: `taskkill /F /PID <PID>` (or kill the Node process in Task Manager)
357
+ 3. Start `dsh web` again.
358
+ 4. **Note**: An instance launched via the desktop shortcut (dsh-desktop-launcher) and one started manually in a terminal cannot run simultaneously.
359
+
360
+ If your log instead shows `service "remoteWebUiPairing" has been registered`, please upgrade this plugin to the latest version — newer versions ship with generic coexistence protection and can start normally alongside other remote-access plugins by yielding the service automatically, with a banner explaining the details in the settings panel.
361
+ </details>
362
+
363
+ <details>
364
+ <summary><b>Q1: Getting "Connection Refused" or cannot open page when scanning on mobile?</b></summary>
365
+
366
+ **Answer**: Please verify the following items step by step:
367
+ 1. **Configuration**: Ensure `host: '0.0.0.0'` is properly configured in `cordis.patch.yml` and DSH has been restarted;
368
+ 2. **Same Network**: For LAN access, ensure your mobile phone and computer are connected to the exact same Wi-Fi router, and disable standalone VPN/proxy apps on your phone;
369
+ 3. **Windows Defender Firewall (Common for Windows users)**:
370
+ * When prompted by the Windows Firewall popup on startup, ensure both **"Private networks"** and **"Public networks"** are checked before allowing access;
371
+ * If you missed the prompt or still cannot connect, run the following command in **PowerShell as Administrator** to allow inbound traffic on port 3080:
372
+ ```powershell
373
+ New-NetFirewallRule -DisplayName "DSH Web 3080" -Direction Inbound -LocalPort 3080 -Protocol TCP -Action Allow
374
+ ```
375
+ * **Check Wi-Fi Network Profile**: Under Windows "Settings -> Network & Internet -> Wi-Fi", make sure your current Wi-Fi profile is set to **"Private network"** (Windows "Public network" blocks inbound LAN connections by default);
376
+ 4. **Virtual / WSL Network Adapters**: If WSL2 or Hyper-V is installed on Windows, virtual network adapters (e.g. `172.x.x.x`) may be present. Make sure you use the physical WLAN / Wi-Fi LAN IP or the Tailscale QR code shown in the plugin settings panel.
377
+ </details>
378
+
379
+ <details>
380
+ <summary><b>Q2: What is the difference between Tailscale Bypass and LAN Bypass?</b></summary>
381
+
382
+ **Answer**:
383
+ * **Tailscale Bypass**: Highly secure. Tailscale operates over an encrypted WireGuard overlay where only devices logged into your account can connect.
384
+ * **LAN Bypass**: Inherent risks. Anyone connected to your Wi-Fi (including guests or unauthorized devices) can control your workspace. **Never enable this on untrusted networks**.
385
+ </details>
386
+
387
+ <details>
388
+ <summary><b>Q4: Not happy with the mobile styles and want full control?</b></summary>
389
+
390
+ **A**: Open **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets**. First try toggling the three area presets (sidebar / settings / conversation), each with independent PC and Mobile switches; if that is not enough, paste your own CSS into the "Custom Snippets" area (e.g. `.my-selector { ... }`). Refresh the mobile page to see the effect immediately. Everything lives in `~/.dsh/remote-mobile/style-snippets.json` and survives plugin upgrades.
391
+ > Tip: if your selector starts with `html[data-dsh-mobile]`, it only applies to mobile-UA clients and will not match a narrow desktop window; drop the prefix if you want both.
392
+ </details>
393
+
394
+ <details>
395
+ <summary><b>Q5: What does the yellow banner "⚠️ Another remote-access plugin detected" at the top of the settings panel mean?</b></summary>
396
+
397
+ **Answer**: It means another remote/Web access plugin is enabled alongside this one and registered the shared pairing service first. To keep startup safe, this plugin has **automatically yielded** that service (its own security gateway and remote access are unaffected), but running two remote-access features side by side may cause duplicated entries. Keeping only one is recommended:
398
+
399
+ 1. Click **"📋 Copy diagnosis & fix"** in the banner and paste the report to an AI assistant (the report already identifies the conflicting plugin's package name and entry id, with exact fix configuration);
400
+ 2. To keep this plugin: follow the report to append the `disabled: true` lines for the other plugin at the end of `~/.dsh/profiles/web/cordis.patch.yml`, then restart DSH;
401
+ 3. The banner can be temporarily dismissed with ✕; it disappears after a refresh once the conflict is resolved.
402
+ </details>
403
+
404
+ <details>
405
+ <summary><b>Q3: Does mobile support creating and switching workspaces normally?</b></summary>
406
+
407
+ **Answer**: **Supported.** The plugin uses request isolation so that mobile devices get the same workspace management and execution capabilities as the desktop Web interface.
408
+ </details>
409
+
410
+ <details>
411
+ <summary><b>Q6: Why is installing standalone plugins recommended over monolithic all-in-one packages (e.g. @linxin666/dsh-web-all)?</b></summary>
412
+
413
+ **Answer**: This is primarily based on **avoiding functional conflicts** and **keeping the environment lightweight and decoupled**:
414
+ 1. **Avoid duplicated mobile entries and touch gesture conflicts**: All-in-one packages compile the frontend logic of dozens of sub-plugins into a single `client.js`. Its mobile adaptation logic executes upon script load. Even if you set `web-ui-remote-web-ui: disabled: true` in `cordis.patch.yml`, the browser may still render the floating whale button (`#dshRemoteWhale`), causing duplicated mobile floating entry points and touch gesture interference.
415
+ 2. **Lightweight and independently upgradable**: Installing standalone packages on demand keeps a minimal dependency footprint and allows independent upgrading, rollback, and maintenance. Install only the standalone packages you need, for example:
416
+ ```bash
417
+ # Install only what you need — clear responsibilities, no interference
418
+ dsh plugin --profile web add @linxin666/dsh-client-ui-task-board
419
+ dsh plugin --profile web add @linxin666/dsh-client-ui-web-ui-settings
420
+ dsh plugin --profile web add @linxin666/dsh-pet
421
+ dsh plugin --profile web add @linxin666/dsh-ssh
422
+ ```
423
+ *(Note: As of v1.6.0, `dsh-remote-mobile` includes compatibility protection — an injected suppression script writes `dsh-remote-force-desktop=1` into `sessionStorage` and coordinates with CSS to prevent `#dshRemoteWhale` from being **mounted twice**. The whale entry itself may still be rendered by the other plugin, and the root cause of the overlap lies there, so installing standalone packages is still recommended)*.
424
+ </details>
425
+
426
+ <details>
427
+ <summary><b>Q7: What should I keep in mind when deploying behind a NAS / Unified Gateway / Nginx reverse proxy (e.g. fnOS <code>/app/dsh/</code>)?</b></summary>
428
+
429
+ **Answer**:
430
+ 1. **Sub-path & Prefix Adaptive**: As of v1.8.0, the frontend natively adapts to `<base href="...">`, and server-side redirects support the `X-Forwarded-Prefix` header. When accessed through a sub-path gateway (e.g. `http://<ip>:5666/app/dsh/`), requests automatically stay scoped within the current sub-path:
431
+ * **Settings panel & SSE live connection**: API paths are resolved dynamically from `<base>`, so saving config, generating pairing codes, and receiving live events never lose the `/app/dsh` prefix (on earlier versions this showed up as “clicking does nothing”);
432
+ * **Standalone mobile login page & 302 redirects**: login verification, post-login navigation, and the gate / `/auth` redirects all stay within the current sub-path instead of jumping out to the host root;
433
+ * **Note**: server-side 302 redirects depend on the reverse proxy forwarding `X-Forwarded-Prefix`; if it is not forwarded, redirects fall back to the safe default path `/auth`.
434
+ 2. **Gateway Loopback (127.0.0.1) Bypass Mechanism**:
435
+ * For rigorous security (preventing external attackers from forging `X-Forwarded-For` headers to bypass authentication), the plugin security gate strictly evaluates client authenticity based on the physical socket connection.
436
+ * When a reverse proxy / host gateway relays traffic directly from localhost `127.0.0.1` to DSH, the request is identified as a local loopback connection and allowed without pairing code prompts—backed by the NAS/host gateway's own authentication and permission boundary.
437
+ * If you wish to enforce the plugin's pairing code or persistent password even when entering through an external reverse proxy, configure the proxy to transparently pass client IPs or connect directly via the exposed DSH listening port (e.g. default port `3080` or custom mapped ports).
438
+ </details>
439
+
440
+ <details>
441
+ <summary><b>Q8: After upgrading to DSH 0.1.7+ (including 0.2.x), <code>~/.dsh/settings.yaml</code> is gone / my settings were lost?</b></summary>
442
+
443
+ **Answer**: This is an **official DSH behavior change introduced in 0.1.7** (first noted in the v0.1.7-alpha.1 release notes; continued in 0.2.x), unrelated to this plugin:
444
+
445
+ - Since DSH **0.1.7**, `~/.dsh/settings.yaml` is **no longer treated as a live configuration source**. On boot it is imported **once** by the official `dsh-settings` `importLegacyDocument()` into the active profile's `~/.dsh/profiles/<profile>/cordis.patch.yml`, then the original file is **renamed to `settings.yaml.imported`** (it renames first, then imports; a section with no matching entry is skipped, kept only in that file, and never retried). The official v0.1.7-alpha.1 release notes state that settings are now persisted as the current profile's plugin configuration (with declared live-update fields), and the legacy `settings.yaml` is imported only once — custom settings plugins must adapt.
446
+ - So after upgrading, `settings.yaml` disappearing and only `settings.yaml.imported` remaining is **expected**; model providers, locale, etc. should now be viewed/edited in the profile's `cordis.patch.yml`.
447
+ - **As of v1.9.0**, this plugin persists its own config to `~/.dsh/remote-mobile/settings.json` and, on first launch, **migrates once from the legacy `settings.yaml` / `settings.yaml.imported`** (recovering the persistent-password hash, bypass toggles, rate limits, etc.). It no longer depends on DSH's settings.yaml. If you saw a "persistent password stopped working / limits reset" symptom, upgrading to v1.9.0 restores it automatically.
448
+ </details>
449
+
450
+ ---
451
+
452
+ ## 📄 License
453
+
454
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: remote-mobile
3
+ name: 'dsh-remote-mobile'
4
+
@@ -0,0 +1,24 @@
1
+ export declare const RSA_KEY_FILE: string;
2
+ /**
3
+ * 获取服务端的持久化 RSA 公钥 (PEM 格式)
4
+ */
5
+ export declare function getPublicKeyPem(): string;
6
+ /**
7
+ * 使用 RSA 私钥解密客户端发来的 Base64 密文 (多填充算法自适应兼容)
8
+ */
9
+ export declare function decryptWithPrivateKey(encryptedBase64: string): string;
10
+ /**
11
+ * 校验长期密码强度:必须 >= 6 位且包含字母与数字
12
+ */
13
+ export declare function validateSecretStrength(secret: string): {
14
+ valid: boolean;
15
+ reason?: string;
16
+ };
17
+ /**
18
+ * 将明文密码转换为加盐哈希(采用 scrypt 慢哈希算法,杜绝彩虹表与 GPU 暴力破解)
19
+ */
20
+ export declare function hashSecret(plainSecret: string): string;
21
+ /**
22
+ * 比对明文密码与存储的加盐哈希(恒定时间比对,抵御时序侧信道攻击)
23
+ */
24
+ export declare function verifySecretHash(plainSecret: string, storedHashOrPlain: string): boolean;
@@ -0,0 +1,158 @@
1
+ import { generateKeyPairSync, privateDecrypt, constants, randomBytes, createHash, scryptSync, timingSafeEqual } from 'node:crypto';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ export const RSA_KEY_FILE = join(homedir(), '.dsh', 'remote-mobile', 'rsa-keys.json');
6
+ let keyPair = null;
7
+ function loadOrCreateKeyPair() {
8
+ try {
9
+ if (existsSync(RSA_KEY_FILE)) {
10
+ const content = readFileSync(RSA_KEY_FILE, 'utf8');
11
+ const data = JSON.parse(content);
12
+ if (data && data.publicKey && data.privateKey) {
13
+ return data;
14
+ }
15
+ }
16
+ }
17
+ catch { }
18
+ try {
19
+ const pair = generateKeyPairSync('rsa', {
20
+ modulusLength: 2048,
21
+ publicKeyEncoding: {
22
+ type: 'spki',
23
+ format: 'pem',
24
+ },
25
+ privateKeyEncoding: {
26
+ type: 'pkcs8',
27
+ format: 'pem',
28
+ },
29
+ });
30
+ try {
31
+ mkdirSync(dirname(RSA_KEY_FILE), { recursive: true });
32
+ writeFileSync(RSA_KEY_FILE, JSON.stringify(pair, null, 2), { encoding: 'utf8', mode: 0o600 });
33
+ }
34
+ catch { }
35
+ return pair;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function getKeyPair() {
42
+ if (keyPair)
43
+ return keyPair;
44
+ keyPair = loadOrCreateKeyPair();
45
+ return keyPair;
46
+ }
47
+ /**
48
+ * 获取服务端的持久化 RSA 公钥 (PEM 格式)
49
+ */
50
+ export function getPublicKeyPem() {
51
+ const pair = getKeyPair();
52
+ return pair ? pair.publicKey : '';
53
+ }
54
+ /**
55
+ * 使用 RSA 私钥解密客户端发来的 Base64 密文 (多填充算法自适应兼容)
56
+ */
57
+ export function decryptWithPrivateKey(encryptedBase64) {
58
+ if (!encryptedBase64)
59
+ return '';
60
+ const pair = getKeyPair();
61
+ if (!pair || !pair.privateKey)
62
+ return '';
63
+ try {
64
+ const buffer = Buffer.from(encryptedBase64, 'base64');
65
+ // 2048 位 RSA 密文标准长度必须为 256 字节
66
+ if (buffer.length !== 256)
67
+ return '';
68
+ // 1. 优先尝试 RSA-OAEP SHA-256 (Web Crypto 规范标准)
69
+ try {
70
+ const decrypted = privateDecrypt({
71
+ key: pair.privateKey,
72
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
73
+ oaepHash: 'sha256',
74
+ }, buffer);
75
+ return decrypted.toString('utf8');
76
+ }
77
+ catch { }
78
+ // 2. 尝试 RSA-OAEP SHA-1 兼容模式
79
+ try {
80
+ const decrypted = privateDecrypt({
81
+ key: pair.privateKey,
82
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
83
+ oaepHash: 'sha1',
84
+ }, buffer);
85
+ return decrypted.toString('utf8');
86
+ }
87
+ catch { }
88
+ // 3. 尝试 PKCS#1 v1.5 填充兼容模式
89
+ try {
90
+ const decrypted = privateDecrypt({
91
+ key: pair.privateKey,
92
+ padding: constants.RSA_PKCS1_PADDING,
93
+ }, buffer);
94
+ return decrypted.toString('utf8');
95
+ }
96
+ catch { }
97
+ }
98
+ catch { }
99
+ return '';
100
+ }
101
+ /**
102
+ * 校验长期密码强度:必须 >= 6 位且包含字母与数字
103
+ */
104
+ export function validateSecretStrength(secret) {
105
+ const trimmed = (secret || '').trim();
106
+ if (!trimmed)
107
+ return { valid: true }; // 允许留空(不设置长期密码)
108
+ if (trimmed.length < 6) {
109
+ return { valid: false, reason: '密码长度不能少于 6 位' };
110
+ }
111
+ const hasLetter = /[A-Za-z]/.test(trimmed);
112
+ const hasDigit = /\d/.test(trimmed);
113
+ if (!hasLetter || !hasDigit) {
114
+ return { valid: false, reason: '密码必须同时包含字母和数字组合' };
115
+ }
116
+ return { valid: true };
117
+ }
118
+ /**
119
+ * 将明文密码转换为加盐哈希(采用 scrypt 慢哈希算法,杜绝彩虹表与 GPU 暴力破解)
120
+ */
121
+ export function hashSecret(plainSecret) {
122
+ if (!plainSecret)
123
+ return '';
124
+ const salt = randomBytes(16).toString('hex');
125
+ const hash = scryptSync(plainSecret, salt, 64).toString('hex');
126
+ return `scrypt:${salt}:${hash}`;
127
+ }
128
+ /**
129
+ * 比对明文密码与存储的加盐哈希(恒定时间比对,抵御时序侧信道攻击)
130
+ */
131
+ export function verifySecretHash(plainSecret, storedHashOrPlain) {
132
+ if (!storedHashOrPlain || !plainSecret)
133
+ return false;
134
+ if (storedHashOrPlain.startsWith('scrypt:')) {
135
+ const parts = storedHashOrPlain.split(':');
136
+ if (parts.length !== 3)
137
+ return false;
138
+ const salt = parts[1];
139
+ const expectedHash = parts[2];
140
+ const actualHash = scryptSync(plainSecret, salt, 64).toString('hex');
141
+ if (actualHash.length !== expectedHash.length)
142
+ return false;
143
+ return timingSafeEqual(Buffer.from(actualHash, 'hex'), Buffer.from(expectedHash, 'hex'));
144
+ }
145
+ // 兼容老版本 sha256 存储
146
+ if (storedHashOrPlain.startsWith('sha256:')) {
147
+ const parts = storedHashOrPlain.split(':');
148
+ if (parts.length !== 3)
149
+ return false;
150
+ const salt = parts[1];
151
+ const expectedHash = parts[2];
152
+ const actualHash = createHash('sha256').update(plainSecret + ':' + salt).digest('hex');
153
+ if (actualHash.length !== expectedHash.length)
154
+ return false;
155
+ return timingSafeEqual(Buffer.from(actualHash, 'hex'), Buffer.from(expectedHash, 'hex'));
156
+ }
157
+ return false;
158
+ }