@linxin666/dsh-remote-web-ui 0.3.11 → 0.3.13
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.i18n.yaml +2 -2
- package/README.md +24 -4
- package/README.zh.md +24 -4
- package/lib/client.js +77 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +409 -44
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +1 -0
- package/lib/types/client/RemotePanel.d.ts +3 -1
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +1 -1
- package/lib/types/client/RemoteSettingsCard.d.ts +16 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +6 -2
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +16 -4
- package/lib/types/client/pair-api.d.ts +10 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +28 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +138 -23
- package/lib/types/pairing.d.ts +17 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +13 -0
- package/lib/types/relay-registry.d.ts +97 -0
- package/lib/types/relay-registry.d.ts.map +1 -0
- package/lib/types/relay-registry.js +191 -0
- package/lib/types/tunnel-plan.d.ts +36 -0
- package/lib/types/tunnel-plan.d.ts.map +1 -0
- package/lib/types/tunnel-plan.js +43 -0
- package/lib/types/tunnel.d.ts +66 -13
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +93 -21
- package/package.json +18 -18
- package/src/client/RemoteEntry.tsx +2 -1
- package/src/client/RemotePanel.tsx +11 -0
- package/src/client/RemoteSettingsCard.tsx +43 -1
- package/src/client/locales.ts +16 -4
- package/src/client/pair-api.ts +11 -0
- package/src/client/settings-card.module.css +14 -8
- package/src/index.ts +153 -25
- package/src/pairing.ts +30 -0
- package/src/relay-registry.ts +232 -0
- package/src/tunnel-plan.ts +51 -0
- package/src/tunnel.ts +120 -24
package/lib/types/tunnel.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-tunnel manager: spawns a Cloudflare
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Auto-tunnel manager: spawns a Cloudflare tunnel through the `cloudflared`
|
|
3
|
+
* npm package — its postinstall downloads the platform binary, so no
|
|
4
|
+
* user-side tooling is involved — surfaces the public URL, and restarts the
|
|
5
|
+
* process after unexpected exits with exponential backoff. Two modes: the
|
|
6
|
+
* accountless quick tunnel (`https://xxx.trycloudflare.com`, hostname minted
|
|
7
|
+
* per start) and an account named tunnel (`cloudflared tunnel run --token`,
|
|
8
|
+
* fixed public hostname — the mode that lets a paired phone keep its
|
|
9
|
+
* bookmark and pairing cookie across restarts).
|
|
7
10
|
*
|
|
8
11
|
* The cloudflared package's Tunnel is a thin spawn wrapper; this manager
|
|
9
12
|
* owns the lifecycle policy (binary readiness, URL timeout, restart
|
|
@@ -12,15 +15,76 @@
|
|
|
12
15
|
* a real binary or network.
|
|
13
16
|
*/
|
|
14
17
|
import { existsSync } from 'node:fs';
|
|
18
|
+
import { EventEmitter } from 'node:events';
|
|
15
19
|
import { bin, install, Tunnel } from 'cloudflared';
|
|
20
|
+
/** Compare two targets for the start idempotence check. */
|
|
21
|
+
function sameTarget(left, right) {
|
|
22
|
+
if (left === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Wrap a named-tunnel process so it fits the quick-tunnel handle shape: the
|
|
28
|
+
* fixed public URL is reported through the same `url` event once the first
|
|
29
|
+
* edge connection registers (cloudflared fires `connected` per connection,
|
|
30
|
+
* so only the first is taken), and exit/error pass through. The manager's
|
|
31
|
+
* URL timeout, crash-restart backoff, and stop semantics then stay fully
|
|
32
|
+
* mode-agnostic.
|
|
33
|
+
* @param inner - the running named-tunnel process.
|
|
34
|
+
* @param publicUrl - the fixed public hostname ingress maps to this server.
|
|
35
|
+
* @returns a handle emitting `url` once the tunnel is reachable.
|
|
36
|
+
*/
|
|
37
|
+
export function namedTunnelHandle(inner, publicUrl) {
|
|
38
|
+
const handle = new EventEmitter();
|
|
39
|
+
let registered = false;
|
|
40
|
+
const onConnected = () => {
|
|
41
|
+
// cloudflared fires `connected` per edge connection (four on a healthy
|
|
42
|
+
// registration); the fixed URL only needs reporting once.
|
|
43
|
+
if (registered)
|
|
44
|
+
return;
|
|
45
|
+
registered = true;
|
|
46
|
+
handle.emit('url', publicUrl);
|
|
47
|
+
};
|
|
48
|
+
const onExit = (code, signal) => {
|
|
49
|
+
handle.emit('exit', code, signal);
|
|
50
|
+
};
|
|
51
|
+
const onError = (value) => {
|
|
52
|
+
handle.emit('error', value);
|
|
53
|
+
};
|
|
54
|
+
inner.on('connected', onConnected);
|
|
55
|
+
inner.on('exit', onExit);
|
|
56
|
+
inner.on('error', onError);
|
|
57
|
+
handle.stop = () => {
|
|
58
|
+
inner.off('connected', onConnected);
|
|
59
|
+
inner.off('exit', onExit);
|
|
60
|
+
inner.off('error', onError);
|
|
61
|
+
return inner.stop();
|
|
62
|
+
};
|
|
63
|
+
return handle;
|
|
64
|
+
}
|
|
16
65
|
/** Default binary readiness: download the platform binary on first use. */
|
|
17
66
|
async function defaultEnsureBinary() {
|
|
18
67
|
if (existsSync(bin))
|
|
19
68
|
return;
|
|
20
69
|
await install(bin);
|
|
21
70
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
71
|
+
/** The flags every tunnel mode shares (see the quick factory comment). */
|
|
72
|
+
const SHARED_TUNNEL_FLAGS = { '--no-autoupdate': true, '--protocol': 'http2' };
|
|
73
|
+
/**
|
|
74
|
+
* Spawn flags for one quick target. `originHostHeader` stamps the Host the
|
|
75
|
+
* local webserver sees: the plugin's phone-facing fence trusts the configured
|
|
76
|
+
* public host (the stable relay origin, Host-bound) and the harness
|
|
77
|
+
* browser-auth binds cookies to the Host authority, while the Workers relay
|
|
78
|
+
* cannot control the origin-side Host at all (fetch forces it to the URL
|
|
79
|
+
* authority) — so the connector must restamp it with the stable origin.
|
|
80
|
+
*/
|
|
81
|
+
export function quickTunnelFlags(originHostHeader) {
|
|
82
|
+
return originHostHeader === undefined
|
|
83
|
+
? { ...SHARED_TUNNEL_FLAGS }
|
|
84
|
+
: { ...SHARED_TUNNEL_FLAGS, '--http-host-header': originHostHeader };
|
|
85
|
+
}
|
|
86
|
+
/** Default factory: the cloudflared package's quick and named tunnels. */
|
|
87
|
+
function defaultFactory(target) {
|
|
24
88
|
// `--no-autoupdate`: the binary must never upgrade itself out from under
|
|
25
89
|
// the manager (a self-updated binary would break the pinned lifecycle).
|
|
26
90
|
// `--protocol http2`: the default `auto` prefers QUIC (UDP 7844), which
|
|
@@ -29,7 +93,10 @@ function defaultFactory(targetUrl) {
|
|
|
29
93
|
// and the auto fallback never fires on a half-open QUIC handshake. http2
|
|
30
94
|
// rides TCP through the same paths reliably (verified live: auto stuck at
|
|
31
95
|
// ready 0 on two fresh registrations; http2 ready immediately).
|
|
32
|
-
|
|
96
|
+
if (target.kind === 'quick') {
|
|
97
|
+
return Tunnel.quick(target.targetUrl, quickTunnelFlags(target.originHostHeader));
|
|
98
|
+
}
|
|
99
|
+
return namedTunnelHandle(Tunnel.withToken(target.token, { ...SHARED_TUNNEL_FLAGS }), target.publicUrl);
|
|
33
100
|
}
|
|
34
101
|
/** Node timers. */
|
|
35
102
|
const nodeTimer = { setTimeout, clearTimeout };
|
|
@@ -47,7 +114,7 @@ export class TunnelManager {
|
|
|
47
114
|
phase = 'stopped';
|
|
48
115
|
url;
|
|
49
116
|
error;
|
|
50
|
-
|
|
117
|
+
target;
|
|
51
118
|
handle;
|
|
52
119
|
urlTimer;
|
|
53
120
|
restartTimer;
|
|
@@ -78,17 +145,22 @@ export class TunnelManager {
|
|
|
78
145
|
};
|
|
79
146
|
}
|
|
80
147
|
/**
|
|
81
|
-
* Start (or keep) a
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
148
|
+
* Start (or keep) a tunnel toward `target`. Restarting with a different
|
|
149
|
+
* target tears the old tunnel down first; restarting with the same target
|
|
150
|
+
* while running is a no-op. A string is the quick mode's local target URL.
|
|
151
|
+
*
|
|
152
|
+
* In quick mode the URL surfaces when cloudflared mints the ephemeral
|
|
153
|
+
* trycloudflare hostname; in named mode the fixed `publicUrl` surfaces
|
|
154
|
+
* once the first edge connection registers (see {@link namedTunnelHandle}).
|
|
155
|
+
* @param target - what to run (a string means quick toward that local URL).
|
|
85
156
|
*/
|
|
86
|
-
start(
|
|
87
|
-
|
|
157
|
+
start(target) {
|
|
158
|
+
const resolved = typeof target === 'string' ? { kind: 'quick', targetUrl: target } : target;
|
|
159
|
+
if (sameTarget(this.target, resolved) && (this.phase === 'starting' || this.phase === 'running'))
|
|
88
160
|
return;
|
|
89
161
|
this.teardown();
|
|
90
162
|
this.stopping = false;
|
|
91
|
-
this.
|
|
163
|
+
this.target = resolved;
|
|
92
164
|
this.attempts = 0;
|
|
93
165
|
this.generation += 1;
|
|
94
166
|
this.attempt();
|
|
@@ -97,7 +169,7 @@ export class TunnelManager {
|
|
|
97
169
|
stop() {
|
|
98
170
|
this.teardown();
|
|
99
171
|
this.stopping = false;
|
|
100
|
-
this.
|
|
172
|
+
this.target = undefined;
|
|
101
173
|
this.setPhase('stopped');
|
|
102
174
|
}
|
|
103
175
|
/** Alias of {@link stop} for plugin-effect disposal. */
|
|
@@ -115,7 +187,7 @@ export class TunnelManager {
|
|
|
115
187
|
return () => { this.phaseListeners.delete(listener); };
|
|
116
188
|
}
|
|
117
189
|
attempt() {
|
|
118
|
-
if (this.stopping || this.
|
|
190
|
+
if (this.stopping || this.target === undefined)
|
|
119
191
|
return;
|
|
120
192
|
const gen = this.generation;
|
|
121
193
|
this.setPhase('starting');
|
|
@@ -123,9 +195,9 @@ export class TunnelManager {
|
|
|
123
195
|
this.url = undefined;
|
|
124
196
|
this.error = undefined;
|
|
125
197
|
void this.ensureBinary().then(() => {
|
|
126
|
-
if (this.stopping || this.
|
|
198
|
+
if (this.stopping || this.target === undefined || gen !== this.generation)
|
|
127
199
|
return;
|
|
128
|
-
const handle = this.factory(this.
|
|
200
|
+
const handle = this.factory(this.target);
|
|
129
201
|
this.handle = handle;
|
|
130
202
|
this.urlTimer = this.timer.setTimeout(() => {
|
|
131
203
|
// The tunnel never reported a URL: kill it and retry with backoff.
|
|
@@ -150,7 +222,7 @@ export class TunnelManager {
|
|
|
150
222
|
});
|
|
151
223
|
}).catch((value) => {
|
|
152
224
|
// Binary install failed (no network, no platform build): report it.
|
|
153
|
-
if (this.stopping || this.
|
|
225
|
+
if (this.stopping || this.target === undefined || gen !== this.generation)
|
|
154
226
|
return;
|
|
155
227
|
const message = value instanceof Error ? value.message : String(value);
|
|
156
228
|
this.fail(`could not obtain the cloudflared binary: ${message}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and revocable device sessions, with a LAN bind toggle, optional Cloudflare tunnel, and one-click family self-update",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.13",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "Apache-2.0",
|
|
37
37
|
"dsh": {
|
|
38
38
|
"engines": {
|
|
39
|
-
"dsh": ">=0.1.2-alpha.
|
|
39
|
+
"dsh": ">=0.1.2-alpha.4"
|
|
40
40
|
},
|
|
41
41
|
"bundle": {
|
|
42
42
|
"patch": "./cordis.patch.yml"
|
|
@@ -63,22 +63,22 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
66
|
-
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.
|
|
67
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.
|
|
68
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.
|
|
69
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-alpha.
|
|
70
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.
|
|
71
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.
|
|
72
|
-
"@deepseek-ai/dsh-client-store": "^0.1.2-alpha.
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.2-alpha.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.
|
|
78
|
-
"@deepseek-ai/dsh-commands": "^0.1.2-alpha.
|
|
79
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.
|
|
80
|
-
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.
|
|
81
|
-
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.
|
|
66
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.4",
|
|
67
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.4",
|
|
68
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.4",
|
|
69
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-alpha.4",
|
|
70
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.4",
|
|
71
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.4",
|
|
72
|
+
"@deepseek-ai/dsh-client-store": "^0.1.2-alpha.4",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.4",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.4",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.4",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.2-alpha.4",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.4",
|
|
78
|
+
"@deepseek-ai/dsh-commands": "^0.1.2-alpha.4",
|
|
79
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.4",
|
|
80
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.4",
|
|
81
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.4",
|
|
82
82
|
"@testing-library/dom": "^10.4.1",
|
|
83
83
|
"@testing-library/react": "^16.3.2",
|
|
84
84
|
"@types/node": "^22.20.0",
|
|
@@ -12,7 +12,7 @@ import { createPortal } from 'react-dom'
|
|
|
12
12
|
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
|
13
13
|
import type { PairingPhase } from '../pairing.ts'
|
|
14
14
|
import { RemotePanel, type PanelState } from './RemotePanel.tsx'
|
|
15
|
-
import { copyText, issuePair, revokePair, stopPair, type DeviceFrame, type IssueResponse, type PairStateFrame, type TunnelStatusFrame } from './pair-api.ts'
|
|
15
|
+
import { copyText, issuePair, revokePair, stopPair, type DeviceFrame, type IssueResponse, type PairStateFrame, type RelayStatusFrame, type TunnelStatusFrame } from './pair-api.ts'
|
|
16
16
|
import { PhoneIcon } from './PhoneIcon.tsx'
|
|
17
17
|
import { UpdateEntry } from './UpdateEntry.tsx'
|
|
18
18
|
import css from './remote.module.css'
|
|
@@ -43,6 +43,7 @@ function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
|
|
|
43
43
|
onlineCount: frame.onlineCount,
|
|
44
44
|
devices: frame.devices ?? [],
|
|
45
45
|
...(frame.tunnel !== undefined ? { tunnel: frame.tunnel as TunnelStatusFrame } : {}),
|
|
46
|
+
...(frame.relay !== undefined ? { relay: frame.relay } : {}),
|
|
46
47
|
...(frame.posture !== undefined ? { posture: frame.posture } : {}),
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
formatLastSeen,
|
|
17
17
|
type DeviceFrame,
|
|
18
18
|
type PostureFrame,
|
|
19
|
+
type RelayStatusFrame,
|
|
19
20
|
type TunnelStatusFrame,
|
|
20
21
|
} from './pair-api.ts'
|
|
21
22
|
import { deviceNameFromUserAgent } from './device-name.ts'
|
|
@@ -46,6 +47,8 @@ export type PanelState =
|
|
|
46
47
|
publicBaseUrl?: string
|
|
47
48
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
48
49
|
tunnel?: TunnelStatusFrame
|
|
50
|
+
/** Relay-registry status, while the stable-origin relay is in play. */
|
|
51
|
+
relay?: RelayStatusFrame
|
|
49
52
|
/** Latest /api posture probe, once a round has completed. */
|
|
50
53
|
posture?: PostureFrame
|
|
51
54
|
}
|
|
@@ -164,6 +167,14 @@ export function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCo
|
|
|
164
167
|
: t('tunnel.starting')}
|
|
165
168
|
</p>
|
|
166
169
|
)}
|
|
170
|
+
{state.relay?.state === 'registering' && (
|
|
171
|
+
<p className={css.tunnelNote} role="status">{t('relay.registering')}</p>
|
|
172
|
+
)}
|
|
173
|
+
{state.relay?.state === 'failed' && (
|
|
174
|
+
<p className={css.tunnelFailed} role="status">
|
|
175
|
+
{t('relay.failed', { error: state.relay.error ?? t('tunnel.unknownError') })}
|
|
176
|
+
</p>
|
|
177
|
+
)}
|
|
167
178
|
|
|
168
179
|
{(state.publicBaseUrl !== undefined || state.lanAddresses.length > 1) && (
|
|
169
180
|
<fieldset className={css.addresses}>
|
|
@@ -9,7 +9,7 @@ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-cli
|
|
|
9
9
|
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
10
10
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
11
11
|
import { PluginSettingsCard, ValueField, BooleanField } from './PluginSettingsCard.tsx'
|
|
12
|
-
import { CardForm, booleanField, numberField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
12
|
+
import { CardForm, booleanField, numberField, secretField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
13
13
|
import { readLanBindStatus, type LanBindFrame } from './pair-api.ts'
|
|
14
14
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
15
15
|
|
|
@@ -33,6 +33,18 @@ export interface RemoteSettings {
|
|
|
33
33
|
publicBaseUrl?: string
|
|
34
34
|
/** When on, the plugin runs its own Cloudflare quick tunnel automatically. */
|
|
35
35
|
autoTunnel?: boolean
|
|
36
|
+
/**
|
|
37
|
+
* Cloudflare named-tunnel token: when set (and autoTunnel is off), the
|
|
38
|
+
* plugin runs the named tunnel toward the fixed public hostname — a phone
|
|
39
|
+
* paired once keeps its session across restarts.
|
|
40
|
+
*/
|
|
41
|
+
tunnelToken?: string
|
|
42
|
+
/**
|
|
43
|
+
* Stable-origin relay: when on (default), the quick tunnel is fronted by a
|
|
44
|
+
* fixed `<id>.dsh-market.com` subdomain so the phone's bookmark and
|
|
45
|
+
* pairing cookie survive restarts without any setup.
|
|
46
|
+
*/
|
|
47
|
+
relay?: boolean
|
|
36
48
|
/**
|
|
37
49
|
* LAN bind toggle: once flipped, the plugin manages the profile patch's
|
|
38
50
|
* webserver block (0.0.0.0 / 127.0.0.1) and the host firewall rule.
|
|
@@ -60,6 +72,10 @@ export interface RemoteSettingsCardState extends CardShell {
|
|
|
60
72
|
publicBaseUrl: CardFieldState
|
|
61
73
|
/** Auto public tunnel switch. */
|
|
62
74
|
autoTunnel: CardFieldState
|
|
75
|
+
/** Named-tunnel token (stored redacted by the Host). */
|
|
76
|
+
tunnelToken: CardFieldState
|
|
77
|
+
/** Stable-origin relay switch. */
|
|
78
|
+
relay: CardFieldState
|
|
63
79
|
/** LAN bind toggle. */
|
|
64
80
|
lanBind: CardFieldState
|
|
65
81
|
}
|
|
@@ -89,6 +105,8 @@ export class RemoteSettingsCardController {
|
|
|
89
105
|
booleanField('requirePairingForLan'),
|
|
90
106
|
textField('publicBaseUrl'),
|
|
91
107
|
booleanField('autoTunnel'),
|
|
108
|
+
secretField('tunnelToken'),
|
|
109
|
+
booleanField('relay'),
|
|
92
110
|
booleanField('lanBind'),
|
|
93
111
|
])
|
|
94
112
|
this.store = this.form.bind(() => this.projection())
|
|
@@ -106,6 +124,8 @@ export class RemoteSettingsCardController {
|
|
|
106
124
|
requirePairingForLan: this.form.field('requirePairingForLan'),
|
|
107
125
|
publicBaseUrl: this.form.field('publicBaseUrl'),
|
|
108
126
|
autoTunnel: this.form.field('autoTunnel'),
|
|
127
|
+
tunnelToken: this.form.field('tunnelToken'),
|
|
128
|
+
relay: this.form.field('relay'),
|
|
109
129
|
lanBind: this.form.field('lanBind'),
|
|
110
130
|
}
|
|
111
131
|
}
|
|
@@ -253,6 +273,28 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
|
|
|
253
273
|
onEdit={(text) => { props.edit('autoTunnel', text) }}
|
|
254
274
|
onReset={() => { props.resetField('autoTunnel') }}
|
|
255
275
|
/>
|
|
276
|
+
<ValueField
|
|
277
|
+
id="settings-remote-tunnel-token"
|
|
278
|
+
label={t('settings.tunnelToken')}
|
|
279
|
+
hint={t('settings.tunnelTokenHint')}
|
|
280
|
+
placeholder="eyJ..."
|
|
281
|
+
{...fieldProps}
|
|
282
|
+
{...state.tunnelToken}
|
|
283
|
+
onEdit={(text) => { props.edit('tunnelToken', text) }}
|
|
284
|
+
onReset={() => { props.resetField('tunnelToken') }}
|
|
285
|
+
/>
|
|
286
|
+
<BooleanField
|
|
287
|
+
id="settings-remote-relay"
|
|
288
|
+
label={t('settings.relay')}
|
|
289
|
+
hint={t('settings.relayHint')}
|
|
290
|
+
inheritLabel={t('settings.inherit')}
|
|
291
|
+
onLabel={t('settings.on')}
|
|
292
|
+
offLabel={t('settings.off')}
|
|
293
|
+
{...fieldProps}
|
|
294
|
+
{...state.relay}
|
|
295
|
+
onEdit={(text) => { props.edit('relay', text) }}
|
|
296
|
+
onReset={() => { props.resetField('relay') }}
|
|
297
|
+
/>
|
|
256
298
|
<LanBindStatus t={t} />
|
|
257
299
|
<BooleanField
|
|
258
300
|
id="settings-remote-lan-bind"
|
package/src/client/locales.ts
CHANGED
|
@@ -65,6 +65,8 @@ export const zh = {
|
|
|
65
65
|
'tunnel.starting': '公网隧道启动中,二维码将自动变为公网链接…',
|
|
66
66
|
'tunnel.failed': '公网隧道启动失败:{error}',
|
|
67
67
|
'tunnel.unknownError': '未知错误',
|
|
68
|
+
'relay.registering': '正在把临时隧道同步到固定域名(<id>.dsh-market.com)…',
|
|
69
|
+
'relay.failed': '固定域名同步失败:{error}。正在自动重试,期间二维码可能使用临时地址。',
|
|
68
70
|
'close.label': '关闭远程访问面板',
|
|
69
71
|
// 插件设置卡片(settings.plugin.item 席位)。
|
|
70
72
|
'settings.title': '远程访问设置',
|
|
@@ -84,9 +86,13 @@ export const zh = {
|
|
|
84
86
|
'settings.requirePairingForLan': '局域网访问要求配对',
|
|
85
87
|
'settings.requirePairingForLanHint': '开启:非本机回环的桌面 Web GUI 改走门控的 /remote,必须携带有效配对 Cookie;关闭:桌面继续走普通路径(仅在该来源已被 SDK 信任时有意义),配对只管理令牌与状态。',
|
|
86
88
|
'settings.publicBaseUrl': '公网地址(可选)',
|
|
87
|
-
'settings.publicBaseUrlHint': '内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com
|
|
89
|
+
'settings.publicBaseUrlHint': '内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com;固定域名隧道(下方令牌)也填这里。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。',
|
|
88
90
|
'settings.autoTunnel': '自动公网隧道',
|
|
89
|
-
'settings.autoTunnelHint': '开启后插件自动启动 Cloudflare quick tunnel
|
|
91
|
+
'settings.autoTunnelHint': '开启后插件自动启动 Cloudflare quick tunnel(无需安装任何工具),并自动更新公网地址与信任配置,手机随时可用公网配对;注意临时域名每次重启都会变化,手机需重新扫码。开启时忽略下方固定隧道令牌与上方手动公网地址。',
|
|
92
|
+
'settings.tunnelToken': '固定域名隧道令牌(Cloudflare Tunnel)',
|
|
93
|
+
'settings.tunnelTokenHint': '填入 Cloudflare 命名隧道的 Token(即 cloudflared tunnel run --token 的值),插件会自动运行该隧道:公共主机名固定不变,手机配对一次后重启也无需重新配对。需先在 Cloudflare 控制台把公共主机名映射到 http://127.0.0.1:<端口>,并在上方"公网地址"填写同一域名。优先级低于"自动公网隧道"。',
|
|
94
|
+
'settings.relay': '固定域名中继(推荐开启)',
|
|
95
|
+
'settings.relayHint': '开启后自动隧道会额外注册一个固定不变的公网子域名(<id>.dsh-market.com,由 dsh-market 边缘中继到你的临时隧道),手机配对一次后重启无需重新扫码,二维码与书签地址永远不变。流量经由 dsh-market 边缘转发(与临时隧道相同的信任面);关闭则回退为纯临时地址,每次重启需重新配对。仅对"自动公网隧道"生效。',
|
|
90
96
|
'settings.lanBind': '局域网访问(绑定 0.0.0.0)',
|
|
91
97
|
'settings.lanBindHint': '开启后插件把绑定默认改写为 0.0.0.0 并写入 profile 补丁(显式 --host 仍优先),同时维护主机防火墙放行(Windows/Linux;macOS 无需管理);关闭回退 127.0.0.1。绑定变化通常在重启 dsh web 后生效。',
|
|
92
98
|
'lan.cardTitle': '局域网访问',
|
|
@@ -229,6 +235,8 @@ export const en = {
|
|
|
229
235
|
'tunnel.starting': 'The public tunnel is starting; the QR code will switch to a public link shortly…',
|
|
230
236
|
'tunnel.failed': 'The public tunnel failed to start: {error}',
|
|
231
237
|
'tunnel.unknownError': 'unknown error',
|
|
238
|
+
'relay.registering': 'Syncing the ephemeral tunnel to the stable hostname (<id>.dsh-market.com)…',
|
|
239
|
+
'relay.failed': 'Stable-hostname sync failed: {error}. Retrying automatically; the QR may use the ephemeral address meanwhile.',
|
|
232
240
|
'close.label': 'Close remote access panel',
|
|
233
241
|
// Plugin settings card (the `settings.plugin.item` seat).
|
|
234
242
|
'settings.title': 'Remote access settings',
|
|
@@ -248,9 +256,13 @@ export const en = {
|
|
|
248
256
|
'settings.requirePairingForLan': 'Require pairing for LAN access',
|
|
249
257
|
'settings.requirePairingForLanHint': 'On: a desktop Web GUI at a non-loopback origin rides the gated /remote channel and must carry a live paired-device cookie. Off: the desktop stays on its original paths (only useful when that origin is already trusted) and pairing only manages tokens/status.',
|
|
250
258
|
'settings.publicBaseUrl': 'Public address (optional)',
|
|
251
|
-
'settings.publicBaseUrlHint': 'The public URL of a tunnel in front of this server, e.g. a Cloudflare Tunnel: https://xxx.trycloudflare.com. When set, the QR link is built from it so a phone anywhere can pair; leave blank for LAN-only usage.',
|
|
259
|
+
'settings.publicBaseUrlHint': 'The public URL of a tunnel in front of this server, e.g. a Cloudflare Tunnel: https://xxx.trycloudflare.com; a fixed-hostname tunnel (token below) also goes here. When set, the QR link is built from it so a phone anywhere can pair; leave blank for LAN-only usage.',
|
|
252
260
|
'settings.autoTunnel': 'Auto public tunnel',
|
|
253
|
-
'settings.autoTunnelHint': 'When on, the plugin runs its own Cloudflare quick tunnel (no tool installation needed) and keeps the public address and trust config in sync automatically, so a phone anywhere can pair at any time;
|
|
261
|
+
'settings.autoTunnelHint': 'When on, the plugin runs its own Cloudflare quick tunnel (no tool installation needed) and keeps the public address and trust config in sync automatically, so a phone anywhere can pair at any time; note the ephemeral hostname changes on every restart, so phones must scan again. The named-tunnel token below and the manual public address above are ignored while this is on.',
|
|
262
|
+
'settings.tunnelToken': 'Fixed-hostname tunnel token (Cloudflare Tunnel)',
|
|
263
|
+
'settings.tunnelTokenHint': 'Paste a Cloudflare named-tunnel token (the value of cloudflared tunnel run --token) and the plugin runs the tunnel itself: the public hostname stays fixed, so a phone pairs once and never again across restarts. First map the public hostname to http://127.0.0.1:<port> in the Cloudflare dashboard and enter the same hostname as the public address above. Lower precedence than the auto quick tunnel.',
|
|
264
|
+
'settings.relay': 'Stable-hostname relay (recommended)',
|
|
265
|
+
'settings.relayHint': 'When on, the auto tunnel additionally registers a never-changing public subdomain (<id>.dsh-market.com, relayed by the dsh-market edge to your ephemeral tunnel), so a phone pairs once and the QR and bookmark stay valid across restarts. Traffic transits the dsh-market edge (the same trust point as the ephemeral tunnel); turn off to fall back to the raw ephemeral address, which needs re-pairing after every restart. Applies to the auto public tunnel only.',
|
|
254
266
|
'settings.lanBind': 'LAN access (bind 0.0.0.0)',
|
|
255
267
|
'settings.lanBindHint': 'When on, the plugin writes a managed block into the profile patch defaulting the bind to 0.0.0.0 (an explicit --host flag still wins), and maintains the matching host firewall rule (Windows/Linux; other platforms need none). When off, the block pins 127.0.0.1. The bind change usually takes effect after dsh web restarts.',
|
|
256
268
|
'lan.cardTitle': 'LAN access',
|
package/src/client/pair-api.ts
CHANGED
|
@@ -62,6 +62,15 @@ export interface TunnelStatusFrame {
|
|
|
62
62
|
error?: string
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** One relay-registry status frame (absent while the relay is not in play). */
|
|
66
|
+
export interface RelayStatusFrame {
|
|
67
|
+
state: 'off' | 'registering' | 'running' | 'failed'
|
|
68
|
+
/** The stable relay origin, once registered. */
|
|
69
|
+
url?: string
|
|
70
|
+
/** Human-readable failure detail of the last sync attempt. */
|
|
71
|
+
error?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
/** One /api posture frame (host half probe; see src/posture.ts). */
|
|
66
75
|
export interface PostureFrame {
|
|
67
76
|
checkedAt: number
|
|
@@ -81,6 +90,8 @@ export interface PairStateFrame {
|
|
|
81
90
|
devices?: DeviceFrame[]
|
|
82
91
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
83
92
|
tunnel?: TunnelStatusFrame
|
|
93
|
+
/** Relay-registry status, while the stable-origin relay is in play. */
|
|
94
|
+
relay?: RelayStatusFrame
|
|
84
95
|
/** Latest /api fence posture probe, once a round has completed. */
|
|
85
96
|
posture?: PostureFrame
|
|
86
97
|
}
|
|
@@ -72,7 +72,10 @@
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
.description {
|
|
75
|
-
|
|
75
|
+
/* Secondary, not tertiary: skins whose tertiary token is tuned for large
|
|
76
|
+
print leave 12-13px hint text below readable contrast (orca-link #778399
|
|
77
|
+
on cream measures ~3.4:1). */
|
|
78
|
+
color: var(--dsw-alias-label-secondary);
|
|
76
79
|
font-size: 13px;
|
|
77
80
|
line-height: 1.5;
|
|
78
81
|
}
|
|
@@ -106,7 +109,7 @@
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
.readOnly {
|
|
109
|
-
color: var(--dsw-alias-label-
|
|
112
|
+
color: var(--dsw-alias-label-secondary);
|
|
110
113
|
margin: 12px 0 0;
|
|
111
114
|
font-size: 12px;
|
|
112
115
|
line-height: 1.5;
|
|
@@ -130,7 +133,10 @@
|
|
|
130
133
|
|
|
131
134
|
.failed {
|
|
132
135
|
min-width: 0;
|
|
133
|
-
|
|
136
|
+
/* The suite's error alias is state-error-primary; a label-error token does
|
|
137
|
+
not exist, so the var() would resolve to nothing and the message would
|
|
138
|
+
inherit the ink color, losing the failure signal. */
|
|
139
|
+
color: var(--dsw-alias-state-error-primary, #b42318);
|
|
134
140
|
flex: 1;
|
|
135
141
|
margin: 0;
|
|
136
142
|
font-size: 12px;
|
|
@@ -278,7 +284,7 @@
|
|
|
278
284
|
}
|
|
279
285
|
|
|
280
286
|
.inputInvalid {
|
|
281
|
-
border: 1px solid var(--dsw-alias-
|
|
287
|
+
border: 1px solid var(--dsw-alias-state-error-primary, #b42318);
|
|
282
288
|
background: var(--dsw-alias-bg-layer-3);
|
|
283
289
|
height: 34px;
|
|
284
290
|
font: inherit;
|
|
@@ -290,9 +296,9 @@
|
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
.inputInvalid:focus-visible {
|
|
293
|
-
outline: 2px solid var(--dsw-alias-
|
|
299
|
+
outline: 2px solid var(--dsw-alias-state-error-primary, #b42318);
|
|
294
300
|
outline-offset: 1px;
|
|
295
|
-
border-color: var(--dsw-alias-
|
|
301
|
+
border-color: var(--dsw-alias-state-error-primary, #b42318);
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
.selectWrap {
|
|
@@ -384,14 +390,14 @@
|
|
|
384
390
|
}
|
|
385
391
|
|
|
386
392
|
.invalid {
|
|
387
|
-
color: var(--dsw-alias-
|
|
393
|
+
color: var(--dsw-alias-state-error-primary, #b42318);
|
|
388
394
|
margin: 0;
|
|
389
395
|
font-size: 12px;
|
|
390
396
|
line-height: 1.5;
|
|
391
397
|
}
|
|
392
398
|
|
|
393
399
|
.hint {
|
|
394
|
-
color: var(--dsw-alias-label-
|
|
400
|
+
color: var(--dsw-alias-label-secondary);
|
|
395
401
|
margin: 0;
|
|
396
402
|
font-size: 12px;
|
|
397
403
|
line-height: 1.5;
|