@linxin666/dsh-remote-web-ui 0.3.12 → 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 +1 -1
- 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
|
@@ -31,5 +31,5 @@ export function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCo
|
|
|
31
31
|
? _jsx("p", { className: css.expired, children: t('pair.expired') })
|
|
32
32
|
: _jsx("p", { className: css.expiry, children: t('pair.expires', { time: formatClock(state.expiresAt) }) })] }), _jsx("p", { className: css.hint, children: state.public ? t('pair.publicHint') : t('pair.hint') }), _jsx("div", { className: css.pairLinks, children: _jsxs("div", { className: css.pairLinkRow, children: [_jsxs("div", { className: css.pairLinkText, children: [_jsx("span", { className: css.pairLinkLabel, children: t('pair.linkLabel') }), _jsx("code", { className: css.link, title: state.url, children: state.url })] }), _jsxs("button", { type: "button", className: css.copyLink, onClick: () => onCopy(state.url), children: [_jsx(IconCopyOutline16, { size: 14 }), copied ? t('action.copied') : t('action.copyLink')] })] }) }), _jsx("p", { className: css.oneTimeHint, children: t('pair.oneTimeHint') }), state.phase === 'stopped' && _jsx("p", { className: css.stoppedHint, children: t('stopped.hint') }), state.tunnel !== undefined && state.tunnel.state !== 'running' && (_jsx("p", { className: state.tunnel.state === 'failed' ? css.tunnelFailed : css.tunnelNote, role: "status", children: state.tunnel.state === 'failed'
|
|
33
33
|
? t('tunnel.failed', { error: state.tunnel.error ?? t('tunnel.unknownError') })
|
|
34
|
-
: t('tunnel.starting') })), (state.publicBaseUrl !== undefined || state.lanAddresses.length > 1) && (_jsxs("fieldset", { className: css.addresses, children: [_jsx("legend", { children: t('address.label') }), state.publicBaseUrl !== undefined && (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": t('address.public'), checked: state.public, onChange: onPickPublic }), _jsx("span", { children: t('address.public') }), _jsx("code", { className: css.addressValue, children: state.publicBaseUrl })] }, "public")), state.lanAddresses.map(address => (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": address, checked: !state.public && address === state.address, onChange: () => onPickAddress(address) }), _jsx("span", { children: t('address.lan') }), _jsx("code", { className: css.addressValue, children: address })] }, address))), _jsx("p", { className: css.addressHint, children: t('address.hint') })] })), _jsxs("div", { className: css.actions, children: [_jsxs("button", { type: "button", className: css.action, onClick: onStop, children: [_jsx(IconStopFill16, { size: 14 }), t('action.stop')] }), _jsxs("button", { type: "button", className: css.action, onClick: onRefresh, children: [_jsx(IconRefreshOutline16, { size: 14 }), t('action.refresh')] })] }), _jsxs("section", { className: css.devices, "data-testid": "remote-devices", "aria-label": t('devices.title'), children: [_jsx("h3", { className: css.devicesTitle, children: t('devices.title') }), state.devices.length === 0 ? (_jsx("p", { className: css.devicesEmpty, children: t('devices.empty') })) : (_jsx("ul", { className: css.deviceList, children: state.devices.map(device => (_jsxs("li", { className: css.deviceRow, children: [_jsxs("div", { className: css.deviceMeta, children: [_jsx("span", { className: css.deviceName, children: deviceNameFromUserAgent(device.userAgent) ?? t('devices.unknown') }), _jsx("span", { className: clsx(css.devicePresence, device.online ? css.deviceOnline : css.deviceOffline), children: device.online ? t('devices.online') : t('devices.offline') }), _jsx("span", { className: css.deviceSeen, children: t('devices.lastSeen', { time: formatLastSeen(device.lastSeenAt) }) })] }), _jsx("button", { type: "button", className: css.deviceRevoke, "aria-label": t('devices.revoke.label'), onClick: () => { onRevoke(device.id); }, children: t('devices.revoke') })] }, device.id))) }))] })] }))] }));
|
|
34
|
+
: t('tunnel.starting') })), state.relay?.state === 'registering' && (_jsx("p", { className: css.tunnelNote, role: "status", children: t('relay.registering') })), state.relay?.state === 'failed' && (_jsx("p", { className: css.tunnelFailed, role: "status", children: t('relay.failed', { error: state.relay.error ?? t('tunnel.unknownError') }) })), (state.publicBaseUrl !== undefined || state.lanAddresses.length > 1) && (_jsxs("fieldset", { className: css.addresses, children: [_jsx("legend", { children: t('address.label') }), state.publicBaseUrl !== undefined && (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": t('address.public'), checked: state.public, onChange: onPickPublic }), _jsx("span", { children: t('address.public') }), _jsx("code", { className: css.addressValue, children: state.publicBaseUrl })] }, "public")), state.lanAddresses.map(address => (_jsxs("label", { className: css.address, children: [_jsx("input", { type: "radio", name: "lan-address", "aria-label": address, checked: !state.public && address === state.address, onChange: () => onPickAddress(address) }), _jsx("span", { children: t('address.lan') }), _jsx("code", { className: css.addressValue, children: address })] }, address))), _jsx("p", { className: css.addressHint, children: t('address.hint') })] })), _jsxs("div", { className: css.actions, children: [_jsxs("button", { type: "button", className: css.action, onClick: onStop, children: [_jsx(IconStopFill16, { size: 14 }), t('action.stop')] }), _jsxs("button", { type: "button", className: css.action, onClick: onRefresh, children: [_jsx(IconRefreshOutline16, { size: 14 }), t('action.refresh')] })] }), _jsxs("section", { className: css.devices, "data-testid": "remote-devices", "aria-label": t('devices.title'), children: [_jsx("h3", { className: css.devicesTitle, children: t('devices.title') }), state.devices.length === 0 ? (_jsx("p", { className: css.devicesEmpty, children: t('devices.empty') })) : (_jsx("ul", { className: css.deviceList, children: state.devices.map(device => (_jsxs("li", { className: css.deviceRow, children: [_jsxs("div", { className: css.deviceMeta, children: [_jsx("span", { className: css.deviceName, children: deviceNameFromUserAgent(device.userAgent) ?? t('devices.unknown') }), _jsx("span", { className: clsx(css.devicePresence, device.online ? css.deviceOnline : css.deviceOffline), children: device.online ? t('devices.online') : t('devices.offline') }), _jsx("span", { className: css.deviceSeen, children: t('devices.lastSeen', { time: formatLastSeen(device.lastSeenAt) }) })] }), _jsx("button", { type: "button", className: css.deviceRevoke, "aria-label": t('devices.revoke.label'), onClick: () => { onRevoke(device.id); }, children: t('devices.revoke') })] }, device.id))) }))] })] }))] }));
|
|
35
35
|
}
|
|
@@ -27,6 +27,18 @@ export interface RemoteSettings {
|
|
|
27
27
|
publicBaseUrl?: string;
|
|
28
28
|
/** When on, the plugin runs its own Cloudflare quick tunnel automatically. */
|
|
29
29
|
autoTunnel?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Cloudflare named-tunnel token: when set (and autoTunnel is off), the
|
|
32
|
+
* plugin runs the named tunnel toward the fixed public hostname — a phone
|
|
33
|
+
* paired once keeps its session across restarts.
|
|
34
|
+
*/
|
|
35
|
+
tunnelToken?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Stable-origin relay: when on (default), the quick tunnel is fronted by a
|
|
38
|
+
* fixed `<id>.dsh-market.com` subdomain so the phone's bookmark and
|
|
39
|
+
* pairing cookie survive restarts without any setup.
|
|
40
|
+
*/
|
|
41
|
+
relay?: boolean;
|
|
30
42
|
/**
|
|
31
43
|
* LAN bind toggle: once flipped, the plugin manages the profile patch's
|
|
32
44
|
* webserver block (0.0.0.0 / 127.0.0.1) and the host firewall rule.
|
|
@@ -53,6 +65,10 @@ export interface RemoteSettingsCardState extends CardShell {
|
|
|
53
65
|
publicBaseUrl: CardFieldState;
|
|
54
66
|
/** Auto public tunnel switch. */
|
|
55
67
|
autoTunnel: CardFieldState;
|
|
68
|
+
/** Named-tunnel token (stored redacted by the Host). */
|
|
69
|
+
tunnelToken: CardFieldState;
|
|
70
|
+
/** Stable-origin relay switch. */
|
|
71
|
+
relay: CardFieldState;
|
|
56
72
|
/** LAN bind toggle. */
|
|
57
73
|
lanBind: CardFieldState;
|
|
58
74
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4CAA4C,CAAA;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAElE,OAAO,
|
|
1
|
+
{"version":3,"file":"RemoteSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/RemoteSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4CAA4C,CAAA;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAA;AAElE,OAAO,EAA+D,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAIrK,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6FAA6F;IAC7F,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,4CAA4C;AAC5C,MAAM,WAAW,uBAAwB,SAAQ,SAAS;IACxD,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,sBAAsB;IACtB,UAAU,EAAE,cAAc,CAAA;IAC1B,gCAAgC;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,yBAAyB;IACzB,UAAU,EAAE,cAAc,CAAA;IAC1B,0BAA0B;IAC1B,YAAY,EAAE,cAAc,CAAA;IAC5B,0BAA0B;IAC1B,UAAU,EAAE,cAAc,CAAA;IAC1B,sBAAsB;IACtB,oBAAoB,EAAE,cAAc,CAAA;IACpC,kCAAkC;IAClC,aAAa,EAAE,cAAc,CAAA;IAC7B,iCAAiC;IACjC,UAAU,EAAE,cAAc,CAAA;IAC1B,wDAAwD;IACxD,WAAW,EAAE,cAAc,CAAA;IAC3B,kCAAkC;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,uBAAuB;IACvB,OAAO,EAAE,cAAc,CAAA;CACxB;AAED,gEAAgE;AAChE,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,KAAK,EAAE;QACL,oEAAoE;QACpE,kBAAkB,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAA;KAC3D,CAAA;CACF;AAED,qEAAqE;AACrE,qBAAa,4BAA4B;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAE9D,iFAAiF;gBACrE,KAAK,EAAE,aAAa,CAAC,cAAc,CAAC;IAkBhD,OAAO,CAAC,UAAU;IAkBlB;;;OAGG;IACH,MAAM,IAAI,sBAAsB;IAIhC;;;OAGG;IACH,OAAO,IAAI,IAAI;CAGhB;AAED,4DAA4D;AAC5D,MAAM,MAAM,uBAAuB,GACjC,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,QAAQ,CAAC,GACrB,UAAU,CAAC,sBAAsB,CAAC,CAAA;AAEtC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,+BAwJhE"}
|
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
*/
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
import { PluginSettingsCard, ValueField, BooleanField } from "./PluginSettingsCard.js";
|
|
9
|
-
import { CardForm, booleanField, numberField, textField } from "./settings-form.js";
|
|
9
|
+
import { CardForm, booleanField, numberField, secretField, textField } from "./settings-form.js";
|
|
10
10
|
import { readLanBindStatus } from "./pair-api.js";
|
|
11
11
|
/** Bridges the `remote-web-ui` scope onto the card's staged form. */
|
|
12
12
|
export class RemoteSettingsCardController {
|
|
@@ -24,6 +24,8 @@ export class RemoteSettingsCardController {
|
|
|
24
24
|
booleanField('requirePairingForLan'),
|
|
25
25
|
textField('publicBaseUrl'),
|
|
26
26
|
booleanField('autoTunnel'),
|
|
27
|
+
secretField('tunnelToken'),
|
|
28
|
+
booleanField('relay'),
|
|
27
29
|
booleanField('lanBind'),
|
|
28
30
|
]);
|
|
29
31
|
this.store = this.form.bind(() => this.projection());
|
|
@@ -40,6 +42,8 @@ export class RemoteSettingsCardController {
|
|
|
40
42
|
requirePairingForLan: this.form.field('requirePairingForLan'),
|
|
41
43
|
publicBaseUrl: this.form.field('publicBaseUrl'),
|
|
42
44
|
autoTunnel: this.form.field('autoTunnel'),
|
|
45
|
+
tunnelToken: this.form.field('tunnelToken'),
|
|
46
|
+
relay: this.form.field('relay'),
|
|
43
47
|
lanBind: this.form.field('lanBind'),
|
|
44
48
|
};
|
|
45
49
|
}
|
|
@@ -73,7 +77,7 @@ export function RemoteSettingsCard(props) {
|
|
|
73
77
|
invalidLabel: t('settings.invalidNumber'),
|
|
74
78
|
disabled,
|
|
75
79
|
};
|
|
76
|
-
return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", defaultOpen: false, state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-remote-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(ValueField, { id: "settings-remote-token-ttl", label: t('settings.tokenTtlMs'), hint: t('settings.tokenTtlMsHint'), numeric: true, ...fieldProps, ...state.tokenTtlMs, onEdit: (text) => { props.edit('tokenTtlMs', text); }, onReset: () => { props.resetField('tokenTtlMs'); } }), _jsx(ValueField, { id: "settings-remote-offline", label: t('settings.offlineAfterMs'), hint: t('settings.offlineAfterMsHint'), numeric: true, ...fieldProps, ...state.offlineAfterMs, onEdit: (text) => { props.edit('offlineAfterMs', text); }, onReset: () => { props.resetField('offlineAfterMs'); } }), _jsx(ValueField, { id: "settings-remote-max-devices", label: t('settings.maxDevices'), hint: t('settings.maxDevicesHint'), numeric: true, ...fieldProps, ...state.maxDevices, onEdit: (text) => { props.edit('maxDevices', text); }, onReset: () => { props.resetField('maxDevices'); } }), _jsx(ValueField, { id: "settings-remote-idle-expire", label: t('settings.idleExpireMs'), hint: t('settings.idleExpireMsHint'), numeric: true, ...fieldProps, ...state.idleExpireMs, onEdit: (text) => { props.edit('idleExpireMs', text); }, onReset: () => { props.resetField('idleExpireMs'); } }), _jsx(ValueField, { id: "settings-remote-cookie", label: t('settings.cookieName'), hint: t('settings.cookieNameHint'), ...fieldProps, ...state.cookieName, onEdit: (text) => { props.edit('cookieName', text); }, onReset: () => { props.resetField('cookieName'); } }), _jsx(BooleanField, { id: "settings-remote-fence", label: t('settings.requirePairingForLan'), hint: t('settings.requirePairingForLanHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.requirePairingForLan, onEdit: (text) => { props.edit('requirePairingForLan', text); }, onReset: () => { props.resetField('requirePairingForLan'); } }), _jsx(ValueField, { id: "settings-remote-public-base", label: t('settings.publicBaseUrl'), hint: t('settings.publicBaseUrlHint'), placeholder: "https://example.trycloudflare.com", ...fieldProps, ...state.publicBaseUrl, onEdit: (text) => { props.edit('publicBaseUrl', text); }, onReset: () => { props.resetField('publicBaseUrl'); } }), _jsx(BooleanField, { id: "settings-remote-auto-tunnel", label: t('settings.autoTunnel'), hint: t('settings.autoTunnelHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.autoTunnel, onEdit: (text) => { props.edit('autoTunnel', text); }, onReset: () => { props.resetField('autoTunnel'); } }), _jsx(LanBindStatus, { t: t }), _jsx(BooleanField, { id: "settings-remote-lan-bind", label: t('settings.lanBind'), hint: t('settings.lanBindHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.lanBind, onEdit: (text) => { props.edit('lanBind', text); }, onReset: () => { props.resetField('lanBind'); } })] }));
|
|
80
|
+
return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", defaultOpen: false, state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-remote-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(ValueField, { id: "settings-remote-token-ttl", label: t('settings.tokenTtlMs'), hint: t('settings.tokenTtlMsHint'), numeric: true, ...fieldProps, ...state.tokenTtlMs, onEdit: (text) => { props.edit('tokenTtlMs', text); }, onReset: () => { props.resetField('tokenTtlMs'); } }), _jsx(ValueField, { id: "settings-remote-offline", label: t('settings.offlineAfterMs'), hint: t('settings.offlineAfterMsHint'), numeric: true, ...fieldProps, ...state.offlineAfterMs, onEdit: (text) => { props.edit('offlineAfterMs', text); }, onReset: () => { props.resetField('offlineAfterMs'); } }), _jsx(ValueField, { id: "settings-remote-max-devices", label: t('settings.maxDevices'), hint: t('settings.maxDevicesHint'), numeric: true, ...fieldProps, ...state.maxDevices, onEdit: (text) => { props.edit('maxDevices', text); }, onReset: () => { props.resetField('maxDevices'); } }), _jsx(ValueField, { id: "settings-remote-idle-expire", label: t('settings.idleExpireMs'), hint: t('settings.idleExpireMsHint'), numeric: true, ...fieldProps, ...state.idleExpireMs, onEdit: (text) => { props.edit('idleExpireMs', text); }, onReset: () => { props.resetField('idleExpireMs'); } }), _jsx(ValueField, { id: "settings-remote-cookie", label: t('settings.cookieName'), hint: t('settings.cookieNameHint'), ...fieldProps, ...state.cookieName, onEdit: (text) => { props.edit('cookieName', text); }, onReset: () => { props.resetField('cookieName'); } }), _jsx(BooleanField, { id: "settings-remote-fence", label: t('settings.requirePairingForLan'), hint: t('settings.requirePairingForLanHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.requirePairingForLan, onEdit: (text) => { props.edit('requirePairingForLan', text); }, onReset: () => { props.resetField('requirePairingForLan'); } }), _jsx(ValueField, { id: "settings-remote-public-base", label: t('settings.publicBaseUrl'), hint: t('settings.publicBaseUrlHint'), placeholder: "https://example.trycloudflare.com", ...fieldProps, ...state.publicBaseUrl, onEdit: (text) => { props.edit('publicBaseUrl', text); }, onReset: () => { props.resetField('publicBaseUrl'); } }), _jsx(BooleanField, { id: "settings-remote-auto-tunnel", label: t('settings.autoTunnel'), hint: t('settings.autoTunnelHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.autoTunnel, onEdit: (text) => { props.edit('autoTunnel', text); }, onReset: () => { props.resetField('autoTunnel'); } }), _jsx(ValueField, { id: "settings-remote-tunnel-token", label: t('settings.tunnelToken'), hint: t('settings.tunnelTokenHint'), placeholder: "eyJ...", ...fieldProps, ...state.tunnelToken, onEdit: (text) => { props.edit('tunnelToken', text); }, onReset: () => { props.resetField('tunnelToken'); } }), _jsx(BooleanField, { id: "settings-remote-relay", label: t('settings.relay'), hint: t('settings.relayHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.relay, onEdit: (text) => { props.edit('relay', text); }, onReset: () => { props.resetField('relay'); } }), _jsx(LanBindStatus, { t: t }), _jsx(BooleanField, { id: "settings-remote-lan-bind", label: t('settings.lanBind'), hint: t('settings.lanBindHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.lanBind, onEdit: (text) => { props.edit('lanBind', text); }, onReset: () => { props.resetField('lanBind'); } })] }));
|
|
77
81
|
}
|
|
78
82
|
/**
|
|
79
83
|
* Live LAN-bind facts above the toggle: the managed block's host, the live
|
|
@@ -64,6 +64,8 @@ export declare const zh: {
|
|
|
64
64
|
'tunnel.starting': string;
|
|
65
65
|
'tunnel.failed': string;
|
|
66
66
|
'tunnel.unknownError': string;
|
|
67
|
+
'relay.registering': string;
|
|
68
|
+
'relay.failed': string;
|
|
67
69
|
'close.label': string;
|
|
68
70
|
'settings.title': string;
|
|
69
71
|
'settings.description': string;
|
|
@@ -85,6 +87,10 @@ export declare const zh: {
|
|
|
85
87
|
'settings.publicBaseUrlHint': string;
|
|
86
88
|
'settings.autoTunnel': string;
|
|
87
89
|
'settings.autoTunnelHint': string;
|
|
90
|
+
'settings.tunnelToken': string;
|
|
91
|
+
'settings.tunnelTokenHint': string;
|
|
92
|
+
'settings.relay': string;
|
|
93
|
+
'settings.relayHint': string;
|
|
88
94
|
'settings.lanBind': string;
|
|
89
95
|
'settings.lanBindHint': string;
|
|
90
96
|
'lan.cardTitle': string;
|
|
@@ -223,6 +229,8 @@ export declare const en: {
|
|
|
223
229
|
'tunnel.starting': string;
|
|
224
230
|
'tunnel.failed': string;
|
|
225
231
|
'tunnel.unknownError': string;
|
|
232
|
+
'relay.registering': string;
|
|
233
|
+
'relay.failed': string;
|
|
226
234
|
'close.label': string;
|
|
227
235
|
'settings.title': string;
|
|
228
236
|
'settings.description': string;
|
|
@@ -244,6 +252,10 @@ export declare const en: {
|
|
|
244
252
|
'settings.publicBaseUrlHint': string;
|
|
245
253
|
'settings.autoTunnel': string;
|
|
246
254
|
'settings.autoTunnelHint': string;
|
|
255
|
+
'settings.tunnelToken': string;
|
|
256
|
+
'settings.tunnelTokenHint': string;
|
|
257
|
+
'settings.relay': string;
|
|
258
|
+
'settings.relayHint': string;
|
|
247
259
|
'settings.lanBind': string;
|
|
248
260
|
'settings.lanBindHint': string;
|
|
249
261
|
'lan.cardTitle': string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiKmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiKsB,CAAA"}
|
|
@@ -64,6 +64,8 @@ export const zh = {
|
|
|
64
64
|
'tunnel.starting': '公网隧道启动中,二维码将自动变为公网链接…',
|
|
65
65
|
'tunnel.failed': '公网隧道启动失败:{error}',
|
|
66
66
|
'tunnel.unknownError': '未知错误',
|
|
67
|
+
'relay.registering': '正在把临时隧道同步到固定域名(<id>.dsh-market.com)…',
|
|
68
|
+
'relay.failed': '固定域名同步失败:{error}。正在自动重试,期间二维码可能使用临时地址。',
|
|
67
69
|
'close.label': '关闭远程访问面板',
|
|
68
70
|
// 插件设置卡片(settings.plugin.item 席位)。
|
|
69
71
|
'settings.title': '远程访问设置',
|
|
@@ -83,9 +85,13 @@ export const zh = {
|
|
|
83
85
|
'settings.requirePairingForLan': '局域网访问要求配对',
|
|
84
86
|
'settings.requirePairingForLanHint': '开启:非本机回环的桌面 Web GUI 改走门控的 /remote,必须携带有效配对 Cookie;关闭:桌面继续走普通路径(仅在该来源已被 SDK 信任时有意义),配对只管理令牌与状态。',
|
|
85
87
|
'settings.publicBaseUrl': '公网地址(可选)',
|
|
86
|
-
'settings.publicBaseUrlHint': '内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com
|
|
88
|
+
'settings.publicBaseUrlHint': '内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com;固定域名隧道(下方令牌)也填这里。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。',
|
|
87
89
|
'settings.autoTunnel': '自动公网隧道',
|
|
88
|
-
'settings.autoTunnelHint': '开启后插件自动启动 Cloudflare quick tunnel
|
|
90
|
+
'settings.autoTunnelHint': '开启后插件自动启动 Cloudflare quick tunnel(无需安装任何工具),并自动更新公网地址与信任配置,手机随时可用公网配对;注意临时域名每次重启都会变化,手机需重新扫码。开启时忽略下方固定隧道令牌与上方手动公网地址。',
|
|
91
|
+
'settings.tunnelToken': '固定域名隧道令牌(Cloudflare Tunnel)',
|
|
92
|
+
'settings.tunnelTokenHint': '填入 Cloudflare 命名隧道的 Token(即 cloudflared tunnel run --token 的值),插件会自动运行该隧道:公共主机名固定不变,手机配对一次后重启也无需重新配对。需先在 Cloudflare 控制台把公共主机名映射到 http://127.0.0.1:<端口>,并在上方"公网地址"填写同一域名。优先级低于"自动公网隧道"。',
|
|
93
|
+
'settings.relay': '固定域名中继(推荐开启)',
|
|
94
|
+
'settings.relayHint': '开启后自动隧道会额外注册一个固定不变的公网子域名(<id>.dsh-market.com,由 dsh-market 边缘中继到你的临时隧道),手机配对一次后重启无需重新扫码,二维码与书签地址永远不变。流量经由 dsh-market 边缘转发(与临时隧道相同的信任面);关闭则回退为纯临时地址,每次重启需重新配对。仅对"自动公网隧道"生效。',
|
|
89
95
|
'settings.lanBind': '局域网访问(绑定 0.0.0.0)',
|
|
90
96
|
'settings.lanBindHint': '开启后插件把绑定默认改写为 0.0.0.0 并写入 profile 补丁(显式 --host 仍优先),同时维护主机防火墙放行(Windows/Linux;macOS 无需管理);关闭回退 127.0.0.1。绑定变化通常在重启 dsh web 后生效。',
|
|
91
97
|
'lan.cardTitle': '局域网访问',
|
|
@@ -221,6 +227,8 @@ export const en = {
|
|
|
221
227
|
'tunnel.starting': 'The public tunnel is starting; the QR code will switch to a public link shortly…',
|
|
222
228
|
'tunnel.failed': 'The public tunnel failed to start: {error}',
|
|
223
229
|
'tunnel.unknownError': 'unknown error',
|
|
230
|
+
'relay.registering': 'Syncing the ephemeral tunnel to the stable hostname (<id>.dsh-market.com)…',
|
|
231
|
+
'relay.failed': 'Stable-hostname sync failed: {error}. Retrying automatically; the QR may use the ephemeral address meanwhile.',
|
|
224
232
|
'close.label': 'Close remote access panel',
|
|
225
233
|
// Plugin settings card (the `settings.plugin.item` seat).
|
|
226
234
|
'settings.title': 'Remote access settings',
|
|
@@ -240,9 +248,13 @@ export const en = {
|
|
|
240
248
|
'settings.requirePairingForLan': 'Require pairing for LAN access',
|
|
241
249
|
'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.',
|
|
242
250
|
'settings.publicBaseUrl': 'Public address (optional)',
|
|
243
|
-
'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.',
|
|
251
|
+
'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.',
|
|
244
252
|
'settings.autoTunnel': 'Auto public tunnel',
|
|
245
|
-
'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;
|
|
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; 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.',
|
|
254
|
+
'settings.tunnelToken': 'Fixed-hostname tunnel token (Cloudflare Tunnel)',
|
|
255
|
+
'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.',
|
|
256
|
+
'settings.relay': 'Stable-hostname relay (recommended)',
|
|
257
|
+
'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.',
|
|
246
258
|
'settings.lanBind': 'LAN access (bind 0.0.0.0)',
|
|
247
259
|
'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.',
|
|
248
260
|
'lan.cardTitle': 'LAN access',
|
|
@@ -47,6 +47,14 @@ export interface TunnelStatusFrame {
|
|
|
47
47
|
url?: string;
|
|
48
48
|
error?: string;
|
|
49
49
|
}
|
|
50
|
+
/** One relay-registry status frame (absent while the relay is not in play). */
|
|
51
|
+
export interface RelayStatusFrame {
|
|
52
|
+
state: 'off' | 'registering' | 'running' | 'failed';
|
|
53
|
+
/** The stable relay origin, once registered. */
|
|
54
|
+
url?: string;
|
|
55
|
+
/** Human-readable failure detail of the last sync attempt. */
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
50
58
|
/** One /api posture frame (host half probe; see src/posture.ts). */
|
|
51
59
|
export interface PostureFrame {
|
|
52
60
|
checkedAt: number;
|
|
@@ -68,6 +76,8 @@ export interface PairStateFrame {
|
|
|
68
76
|
devices?: DeviceFrame[];
|
|
69
77
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
70
78
|
tunnel?: TunnelStatusFrame;
|
|
79
|
+
/** Relay-registry status, while the stable-origin relay is in play. */
|
|
80
|
+
relay?: RelayStatusFrame;
|
|
71
81
|
/** Latest /api fence posture probe, once a round has completed. */
|
|
72
82
|
posture?: PostureFrame;
|
|
73
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -88,9 +88,34 @@ export interface Config {
|
|
|
88
88
|
* cloudflared binary ships with the package — no user-side install) and
|
|
89
89
|
* feeds the minted public URL into the QR base and the phone-facing
|
|
90
90
|
* pairing fence dynamically, so phones anywhere can pair without any manual
|
|
91
|
-
* tunnel setup. The
|
|
91
|
+
* tunnel setup. The minted hostname changes on every start, so a paired
|
|
92
|
+
* phone must re-pair after each restart. The manual `publicBaseUrl` and
|
|
93
|
+
* `tunnelToken` are ignored while this is on.
|
|
92
94
|
*/
|
|
93
95
|
autoTunnel?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Cloudflare named-tunnel token (`cloudflared tunnel run --token <t>`).
|
|
98
|
+
* When set (and `autoTunnel` is off), the plugin runs the named tunnel
|
|
99
|
+
* itself — same binary, same lifecycle management — toward the fixed
|
|
100
|
+
* public hostname configured in the Cloudflare dashboard. Because that
|
|
101
|
+
* hostname never changes, a paired phone keeps its bookmark and its
|
|
102
|
+
* pairing cookie across `dsh web` restarts: pair once, never again.
|
|
103
|
+
* Requires `publicBaseUrl` to name that same hostname (the token does not
|
|
104
|
+
* carry it); without a valid `publicBaseUrl` the tunnel stays off and a
|
|
105
|
+
* warning explains what is missing. Treated as a secret: the settings
|
|
106
|
+
* surface stores it redacted.
|
|
107
|
+
*/
|
|
108
|
+
tunnelToken?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Stable-origin relay: when on (default), the quick tunnel is fronted by a
|
|
111
|
+
* fixed `https://<id>.dsh-market.com` subdomain (the dsh-market worker's
|
|
112
|
+
* registry), so the phone's bookmark and pairing cookie survive `dsh web`
|
|
113
|
+
* restarts without any user setup. Traffic transits the dsh-market edge
|
|
114
|
+
* (the same trust point as the quick tunnel itself); turn off to fall back
|
|
115
|
+
* to the raw ephemeral quick URL. Named tunnels keep their dashboard
|
|
116
|
+
* hostname and never touch the relay.
|
|
117
|
+
*/
|
|
118
|
+
relay?: boolean;
|
|
94
119
|
/**
|
|
95
120
|
* LAN bind toggle. When the user flips it (true or false) the plugin
|
|
96
121
|
* writes the managed webserver block into the profile patch — true pins
|
|
@@ -121,11 +146,12 @@ export declare const Config: z<Config>;
|
|
|
121
146
|
* which legitimately resolves to `undefined` when unset (the schema keeps it
|
|
122
147
|
* optional, so `Required` alone would over-narrow it to `string`).
|
|
123
148
|
*/
|
|
124
|
-
type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'devicesFile' | 'lanBind' | 'profile'>> & {
|
|
149
|
+
type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'devicesFile' | 'lanBind' | 'profile' | 'tunnelToken'>> & {
|
|
125
150
|
publicBaseUrl: string | undefined;
|
|
126
151
|
devicesFile: string;
|
|
127
152
|
/** undefined until the user flips the toggle once; undefined never writes the patch. */
|
|
128
153
|
lanBind: boolean | undefined;
|
|
154
|
+
tunnelToken: string | undefined;
|
|
129
155
|
profile: string;
|
|
130
156
|
};
|
|
131
157
|
/**
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,CAAC,MAAM,aAAa,CAAA;AAO3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.js
CHANGED
|
@@ -22,6 +22,8 @@ import { claimPostureKey, postureTargets, probePosture, releasePostureKey } from
|
|
|
22
22
|
import { lanIPv4Addresses } from "./lan.js";
|
|
23
23
|
import { ensureFirewallRule, firewallSummary, removeFirewallRule } from "./firewall.js";
|
|
24
24
|
import { lanBindState, writeLanBind } from "./lan-bind.js";
|
|
25
|
+
import { isHttpUrl, tunnelPlanOf } from "./tunnel-plan.js";
|
|
26
|
+
import { loadRelayIdentity, RelayRegistrar } from "./relay-registry.js";
|
|
25
27
|
import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
|
|
26
28
|
import { createInnerAuth } from "./inner-auth.js";
|
|
27
29
|
import { TunnelManager } from "./tunnel.js";
|
|
@@ -50,6 +52,8 @@ export const Config = z.object({
|
|
|
50
52
|
publicBaseUrl: z.string(),
|
|
51
53
|
devicesFile: z.string(),
|
|
52
54
|
autoTunnel: z.boolean().default(false),
|
|
55
|
+
tunnelToken: z.string().role('secret'),
|
|
56
|
+
relay: z.boolean().default(true),
|
|
53
57
|
lanBind: z.boolean(),
|
|
54
58
|
profile: z.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/),
|
|
55
59
|
enabled: z.boolean().default(true),
|
|
@@ -87,6 +91,8 @@ const DEFAULTS = {
|
|
|
87
91
|
publicBaseUrl: undefined,
|
|
88
92
|
devicesFile: defaultDevicesFile(),
|
|
89
93
|
autoTunnel: false,
|
|
94
|
+
tunnelToken: undefined,
|
|
95
|
+
relay: true,
|
|
90
96
|
lanBind: undefined,
|
|
91
97
|
profile: process.env.DSH_PROFILE ?? 'web',
|
|
92
98
|
enabled: true,
|
|
@@ -108,6 +114,8 @@ function applyImpl(ctx, config) {
|
|
|
108
114
|
publicBaseUrl: config?.publicBaseUrl,
|
|
109
115
|
devicesFile: config?.devicesFile ?? DEFAULTS.devicesFile,
|
|
110
116
|
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
117
|
+
tunnelToken: config?.tunnelToken,
|
|
118
|
+
relay: config?.relay ?? DEFAULTS.relay,
|
|
111
119
|
lanBind: config?.lanBind,
|
|
112
120
|
profile: config?.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
|
|
113
121
|
enabled: config?.enabled ?? DEFAULTS.enabled,
|
|
@@ -128,6 +136,8 @@ function applyImpl(ctx, config) {
|
|
|
128
136
|
publicBaseUrl: value.publicBaseUrl,
|
|
129
137
|
devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
|
|
130
138
|
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
139
|
+
tunnelToken: value.tunnelToken,
|
|
140
|
+
relay: value.relay ?? DEFAULTS.relay,
|
|
131
141
|
lanBind: value.lanBind,
|
|
132
142
|
profile: value.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
|
|
133
143
|
enabled: value.enabled ?? DEFAULTS.enabled,
|
|
@@ -141,27 +151,100 @@ function applyImpl(ctx, config) {
|
|
|
141
151
|
// mutation is needed here (a distributable plugin must not change the
|
|
142
152
|
// harness's connection plugin).
|
|
143
153
|
const tunnel = new TunnelManager();
|
|
144
|
-
|
|
154
|
+
// Relay registry: one stable `<id>.dsh-market.com` subdomain per profile
|
|
155
|
+
// in front of the quick tunnel. The QR base prefers the relay origin once
|
|
156
|
+
// it is registered; until then (or when the registry is unreachable) the
|
|
157
|
+
// raw quick URL is used, exactly as before. Named tunnels keep their fixed
|
|
158
|
+
// dashboard hostname and never touch the relay.
|
|
159
|
+
let relayRegistrar;
|
|
160
|
+
let relayUrl;
|
|
161
|
+
let rawTunnelUrl;
|
|
162
|
+
/** The tunnel target the registrar last announced (dedupes sync re-runs). */
|
|
163
|
+
let relayAnnouncedFor;
|
|
164
|
+
const setPublicBase = () => {
|
|
165
|
+
service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
|
|
166
|
+
};
|
|
167
|
+
const disposeRelayRegistrar = (unregister = false) => {
|
|
168
|
+
const registrar = relayRegistrar;
|
|
169
|
+
relayRegistrar = undefined;
|
|
170
|
+
relayUrl = undefined;
|
|
171
|
+
relayAnnouncedFor = undefined;
|
|
172
|
+
if (registrar === undefined)
|
|
173
|
+
return;
|
|
174
|
+
// Toggle-off removes the registry row so the stable origin stops
|
|
175
|
+
// proxying; teardown/mode changes keep the row (the phone then sees the
|
|
176
|
+
// relay's offline page instead of a dangling claim, and re-enabling
|
|
177
|
+
// reuses the same identity).
|
|
178
|
+
if (unregister)
|
|
179
|
+
void registrar.unregister().catch(() => undefined);
|
|
180
|
+
registrar.dispose();
|
|
181
|
+
};
|
|
182
|
+
const ensureRelayRegistrar = () => {
|
|
183
|
+
if (resolve().relay === false)
|
|
184
|
+
return undefined;
|
|
185
|
+
if (relayRegistrar === undefined) {
|
|
186
|
+
try {
|
|
187
|
+
const identity = loadRelayIdentity(resolve().profile);
|
|
188
|
+
relayRegistrar = new RelayRegistrar(identity, (state) => {
|
|
189
|
+
service.setRelayStatus(state.state === 'off' ? undefined : state);
|
|
190
|
+
if (state.state === 'running') {
|
|
191
|
+
relayUrl = state.url;
|
|
192
|
+
}
|
|
193
|
+
else if (state.state === 'off') {
|
|
194
|
+
relayUrl = undefined;
|
|
195
|
+
}
|
|
196
|
+
else if (state.state === 'failed') {
|
|
197
|
+
// Keep the last relay URL on failures: the phone origin only
|
|
198
|
+
// breaks when the mapping itself goes stale, not when one
|
|
199
|
+
// refresh call fails. The registrar retries with backoff.
|
|
200
|
+
console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
|
|
201
|
+
}
|
|
202
|
+
setPublicBase();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.warn(`remote-web-ui: relay registry unavailable (${error instanceof Error ? error.message : String(error)}) — the quick URL is the QR base`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return relayRegistrar;
|
|
210
|
+
};
|
|
211
|
+
const announceRelay = (registrar, target) => {
|
|
212
|
+
if (relayAnnouncedFor === target)
|
|
213
|
+
return;
|
|
214
|
+
relayAnnouncedFor = target;
|
|
215
|
+
void registrar.announce(target);
|
|
216
|
+
};
|
|
217
|
+
// 'off' until a sync pass turns a mode on; the phase listener only feeds
|
|
218
|
+
// the public base while a plugin-managed tunnel (quick or named) runs.
|
|
219
|
+
let tunnelMode = resolved.autoTunnel ? 'quick' : 'off';
|
|
145
220
|
tunnel.onPhase((info) => {
|
|
146
|
-
if (
|
|
221
|
+
if (tunnelMode === 'off')
|
|
147
222
|
return;
|
|
148
223
|
if (info.phase === 'running' && info.url !== undefined) {
|
|
149
|
-
|
|
224
|
+
rawTunnelUrl = info.url;
|
|
225
|
+
setPublicBase();
|
|
150
226
|
service.setTunnelStatus({ state: 'running', url: info.url });
|
|
227
|
+
const registrar = tunnelMode === 'quick' ? ensureRelayRegistrar() : undefined;
|
|
228
|
+
if (registrar !== undefined)
|
|
229
|
+
announceRelay(registrar, info.url);
|
|
151
230
|
runPostureProbe();
|
|
152
231
|
}
|
|
153
232
|
else if (info.phase === 'starting') {
|
|
154
233
|
// A restart mints a NEW hostname: the previous URL dies with the old
|
|
155
234
|
// process, so clear it now rather than advertising a dead link.
|
|
156
|
-
|
|
235
|
+
rawTunnelUrl = undefined;
|
|
236
|
+
relayUrl = undefined;
|
|
237
|
+
setPublicBase();
|
|
157
238
|
service.setTunnelStatus({ state: 'starting' });
|
|
158
239
|
}
|
|
159
240
|
else if (info.phase === 'failed') {
|
|
160
|
-
|
|
241
|
+
rawTunnelUrl = undefined;
|
|
242
|
+
setPublicBase();
|
|
161
243
|
service.setTunnelStatus(info.error === undefined ? { state: 'failed' } : { state: 'failed', error: info.error });
|
|
162
244
|
}
|
|
163
245
|
});
|
|
164
246
|
ctx.effect(() => () => {
|
|
247
|
+
disposeRelayRegistrar();
|
|
165
248
|
tunnel.dispose();
|
|
166
249
|
}, 'remote-web-ui: auto tunnel');
|
|
167
250
|
// The bind facts are known by now (webServer is an inject edge): the LAN
|
|
@@ -510,18 +593,60 @@ function applyImpl(ctx, config) {
|
|
|
510
593
|
}
|
|
511
594
|
}
|
|
512
595
|
}
|
|
513
|
-
// The
|
|
514
|
-
// lands in the service through the tunnel's phase listener
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
596
|
+
// The plugin-managed tunnels own the public base while one runs: the URL
|
|
597
|
+
// lands in the service through the tunnel's phase listener (the minted
|
|
598
|
+
// quick URL, or the named tunnel's fixed public hostname). The manual
|
|
599
|
+
// publicBaseUrl applies only when no tunnel runs.
|
|
600
|
+
const plan = tunnelPlanOf(value, ctx.webServer.port);
|
|
601
|
+
tunnelMode = plan.mode;
|
|
602
|
+
if (plan.mode !== 'quick') {
|
|
603
|
+
// The relay only fronts the quick tunnel; named mode owns its fixed
|
|
604
|
+
// dashboard hostname and the off mode has no public base at all.
|
|
605
|
+
disposeRelayRegistrar();
|
|
606
|
+
if (plan.mode !== 'named')
|
|
607
|
+
setPublicBase();
|
|
608
|
+
}
|
|
609
|
+
else if (value.relay === false) {
|
|
610
|
+
// The relay toggle is off: no stable origin, the raw quick URL is the
|
|
611
|
+
// QR base exactly as before the relay existed.
|
|
612
|
+
disposeRelayRegistrar(true);
|
|
613
|
+
setPublicBase();
|
|
614
|
+
}
|
|
615
|
+
else if (rawTunnelUrl !== undefined) {
|
|
616
|
+
// The relay just turned on (or the registrar is new) while the tunnel
|
|
617
|
+
// already runs: announce now — no phase event will fire for an
|
|
618
|
+
// unchanged target.
|
|
619
|
+
const registrar = ensureRelayRegistrar();
|
|
620
|
+
if (registrar !== undefined)
|
|
621
|
+
announceRelay(registrar, rawTunnelUrl);
|
|
622
|
+
}
|
|
623
|
+
if (plan.mode === 'quick') {
|
|
624
|
+
for (const ignored of plan.ignored) {
|
|
625
|
+
console.warn(`remote-web-ui: autoTunnel is on — ignoring the configured ${ignored}`);
|
|
520
626
|
}
|
|
521
|
-
|
|
627
|
+
// With the relay on, the connector stamps the stable origin as the
|
|
628
|
+
// origin-side Host (`--http-host-header`): the plugin fence and the
|
|
629
|
+
// harness browser-auth are Host-bound to the relay origin, and the
|
|
630
|
+
// Workers relay cannot control the origin-side Host (fetch forces it
|
|
631
|
+
// to the URL authority). The target identity change is also what
|
|
632
|
+
// restarts a running tunnel on a relay toggle.
|
|
633
|
+
const registrar = ensureRelayRegistrar();
|
|
634
|
+
const originHostHeader = registrar === undefined ? undefined : new URL(registrar.baseUrl).host;
|
|
635
|
+
tunnel.start(originHostHeader === undefined
|
|
636
|
+
? plan.targetUrl
|
|
637
|
+
: { kind: 'quick', targetUrl: plan.targetUrl, originHostHeader });
|
|
638
|
+
}
|
|
639
|
+
else if (plan.mode === 'named') {
|
|
640
|
+
tunnel.start({ kind: 'named', token: plan.token, publicUrl: plan.publicUrl });
|
|
522
641
|
}
|
|
523
642
|
else {
|
|
524
643
|
tunnel.stop();
|
|
644
|
+
// A named-tunnel token without a usable public hostname cannot serve
|
|
645
|
+
// the QR: stay off and say exactly what is missing instead of running
|
|
646
|
+
// a tunnel nothing points at.
|
|
647
|
+
if (value.tunnelToken !== undefined && value.tunnelToken !== '') {
|
|
648
|
+
console.warn('remote-web-ui: tunnelToken is set but publicBaseUrl is missing or not a valid URL — fill the fixed public hostname of the named tunnel (e.g. https://dsh.example.com) to run it');
|
|
649
|
+
}
|
|
525
650
|
// A malformed public base is ignored with a warning — LAN-only behavior
|
|
526
651
|
// stays intact rather than silently minting unusable QR links.
|
|
527
652
|
if (value.publicBaseUrl !== undefined && !isHttpUrl(value.publicBaseUrl)) {
|
|
@@ -594,13 +719,3 @@ function applyImpl(ctx, config) {
|
|
|
594
719
|
});
|
|
595
720
|
sync();
|
|
596
721
|
}
|
|
597
|
-
/** Whether a configured public base is a parseable http(s) URL with a host. */
|
|
598
|
-
function isHttpUrl(value) {
|
|
599
|
-
try {
|
|
600
|
-
const url = new URL(value);
|
|
601
|
-
return (url.protocol === 'http:' || url.protocol === 'https:') && url.hostname !== '';
|
|
602
|
-
}
|
|
603
|
-
catch {
|
|
604
|
-
return false;
|
|
605
|
-
}
|
|
606
|
-
}
|
package/lib/types/pairing.d.ts
CHANGED
|
@@ -89,6 +89,18 @@ export interface TunnelStatus {
|
|
|
89
89
|
/** Human-readable failure detail. */
|
|
90
90
|
error?: string;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* One relay-registry status frame (undefined while the stable-origin relay
|
|
94
|
+
* is not in play: autoTunnel off, or the relay toggle disabled).
|
|
95
|
+
*/
|
|
96
|
+
export interface RelayStatus {
|
|
97
|
+
/** registering: a sync is in flight; running: the mapping is accepted. */
|
|
98
|
+
state: 'off' | 'registering' | 'running' | 'failed';
|
|
99
|
+
/** The stable relay origin (`https://<id>.dsh-market.com`). */
|
|
100
|
+
url?: string;
|
|
101
|
+
/** Human-readable failure detail of the last sync attempt. */
|
|
102
|
+
error?: string;
|
|
103
|
+
}
|
|
92
104
|
/** One snapshot frame pushed to desktop status streams. */
|
|
93
105
|
export interface PairingSnapshot {
|
|
94
106
|
phase: PairingPhase;
|
|
@@ -100,6 +112,8 @@ export interface PairingSnapshot {
|
|
|
100
112
|
publicUrl?: string;
|
|
101
113
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
102
114
|
tunnel?: TunnelStatus;
|
|
115
|
+
/** Relay-registry status, while the stable-origin relay is in play. */
|
|
116
|
+
relay?: RelayStatus;
|
|
103
117
|
/** Latest /api posture probe (undefined until the first round completes). */
|
|
104
118
|
posture?: PostureSnapshot;
|
|
105
119
|
/** Opaque (non-secret) id of the active token (undefined when stopped/lan-required). */
|
|
@@ -182,6 +196,7 @@ export declare class PairingService {
|
|
|
182
196
|
private publicBase;
|
|
183
197
|
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
184
198
|
private tunnelStatus;
|
|
199
|
+
private relayStatus;
|
|
185
200
|
private posture;
|
|
186
201
|
/** True when lastSeenAt changed since the last persist (flushed on sweep). */
|
|
187
202
|
private dirty;
|
|
@@ -230,6 +245,8 @@ export declare class PairingService {
|
|
|
230
245
|
setPublicBaseUrl(url: string | undefined): void;
|
|
231
246
|
/** Set or clear the auto-tunnel status frame (undefined when the feature is off). */
|
|
232
247
|
setTunnelStatus(status: TunnelStatus | undefined): void;
|
|
248
|
+
/** Set or clear the relay-registry status frame (undefined when not in play). */
|
|
249
|
+
setRelayStatus(status: RelayStatus | undefined): void;
|
|
233
250
|
/** Set the latest /api posture probe result (see posture.ts). */
|
|
234
251
|
setPosture(snapshot: PostureSnapshot | undefined): void;
|
|
235
252
|
/**
|