@linxin666/dsh-remote-web-ui 0.1.1 → 0.1.2
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.md +4 -1
- package/lib/client.js.map +1 -0
- package/lib/mobile.js +15467 -0
- package/lib/mobile.js.map +1 -0
- package/lib/types/client/PairFailedNotice.js +24 -0
- package/lib/types/client/PairFailedNotice.js.map +1 -0
- package/lib/types/client/PhoneIcon.js +9 -0
- package/lib/types/client/PhoneIcon.js.map +1 -0
- package/lib/types/client/PluginSettingsCard.js +39 -0
- package/lib/types/client/PluginSettingsCard.js.map +1 -0
- package/lib/types/client/RemoteEntry.js +162 -0
- package/lib/types/client/RemoteEntry.js.map +1 -0
- package/lib/types/client/RemotePanel.js +34 -0
- package/lib/types/client/RemotePanel.js.map +1 -0
- package/lib/types/client/RemoteSettingsCard.js +59 -0
- package/lib/types/client/RemoteSettingsCard.js.map +1 -0
- package/lib/types/client/deep-link.js +118 -0
- package/lib/types/client/deep-link.js.map +1 -0
- package/lib/types/client/index.js +111 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/locales.js +141 -0
- package/lib/types/client/locales.js.map +1 -0
- package/lib/types/client/pair-api.js +123 -0
- package/lib/types/client/pair-api.js.map +1 -0
- package/lib/types/client/settings-form.js +211 -0
- package/lib/types/client/settings-form.js.map +1 -0
- package/lib/types/gate.js +121 -0
- package/lib/types/gate.js.map +1 -0
- package/lib/types/index.js +227 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/invariant.js +23 -0
- package/lib/types/invariant.js.map +1 -0
- package/lib/types/lan.js +19 -0
- package/lib/types/lan.js.map +1 -0
- package/lib/types/mobile/api.js +60 -0
- package/lib/types/mobile/api.js.map +1 -0
- package/lib/types/mobile/index.js +25 -0
- package/lib/types/mobile/index.js.map +1 -0
- package/lib/types/mobile/messages.js +449 -0
- package/lib/types/mobile/messages.js.map +1 -0
- package/lib/types/mobile/messages.test.js +189 -0
- package/lib/types/mobile/messages.test.js.map +1 -0
- package/lib/types/mobile/mobile-styles.js +853 -0
- package/lib/types/mobile/mobile-styles.js.map +1 -0
- package/lib/types/mobile/mobile-theme.js +70 -0
- package/lib/types/mobile/mobile-theme.test.js +71 -0
- package/lib/types/mobile/mux.js +110 -0
- package/lib/types/mobile/mux.js.map +1 -0
- package/lib/types/mobile/rpc.js +83 -0
- package/lib/types/mobile/rpc.js.map +1 -0
- package/lib/types/mobile/theme-toggle.js +26 -0
- package/lib/types/mobile/views/App.js +102 -0
- package/lib/types/mobile/views/App.js.map +1 -0
- package/lib/types/mobile/views/ChatView.js +348 -0
- package/lib/types/mobile/views/ChatView.js.map +1 -0
- package/lib/types/mobile/views/ChatView.test.js +215 -0
- package/lib/types/mobile/views/SessionListView.js +108 -0
- package/lib/types/mobile/views/SessionListView.js.map +1 -0
- package/lib/types/mobile/views/SessionListView.test.js +110 -0
- package/lib/types/mobile/views/WorkspaceView.js +43 -0
- package/lib/types/mobile/views/WorkspaceView.js.map +1 -0
- package/lib/types/mobile-api.js +263 -0
- package/lib/types/mobile-api.js.map +1 -0
- package/lib/types/mobile-routes.js +71 -0
- package/lib/types/mobile-routes.js.map +1 -0
- package/lib/types/pairing.js +285 -0
- package/lib/types/pairing.js.map +1 -0
- package/lib/types/routes.js +327 -0
- package/lib/types/routes.js.map +1 -0
- package/lib/types/tunnel.js +222 -0
- package/lib/types/tunnel.js.map +1 -0
- package/package.json +9 -6
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `api/gate` listener: application-level access control layered on top
|
|
3
|
+
* of the transport fence (the fence is Host/Origin based and explicitly not
|
|
4
|
+
* an authentication layer — packages/client/connection documents this
|
|
5
|
+
* event as the sanctioned seam for pairing/revocation).
|
|
6
|
+
*
|
|
7
|
+
* Policy: loopback requests (the desktop) pass without a device identity;
|
|
8
|
+
* every non-loopback /api request must carry a live, non-revoked device
|
|
9
|
+
* cookie. This makes the QR the only way into a LAN-exposed dsh web and
|
|
10
|
+
* gives "停止" real teeth: revoked devices 403 on their next request,
|
|
11
|
+
* including the mux/SSE stream (which then dies on reconnect).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Whether a normalized URL hostname names the local loopback authority.
|
|
15
|
+
* Semantics mirror the connection package's internal predicate (localhost,
|
|
16
|
+
* IPv6 loopback, any IPv4 address in 127/8); it is reimplemented here because
|
|
17
|
+
* the connection package no longer exports it — the fence now lives inside
|
|
18
|
+
* the connection plugin, and external host plugins only need the
|
|
19
|
+
* classification, not the whole trust decision.
|
|
20
|
+
* @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
|
|
21
|
+
* @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
|
|
22
|
+
*/
|
|
23
|
+
export function isLoopbackHostname(hostname) {
|
|
24
|
+
if (hostname === 'localhost' || hostname === '[::1]')
|
|
25
|
+
return true;
|
|
26
|
+
const parts = hostname.split('.');
|
|
27
|
+
return parts.length === 4
|
|
28
|
+
&& parts[0] === '127'
|
|
29
|
+
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
30
|
+
}
|
|
31
|
+
/** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
|
|
32
|
+
export function isLoopbackAddress(address) {
|
|
33
|
+
if (address === undefined)
|
|
34
|
+
return false;
|
|
35
|
+
const normalized = address.toLowerCase();
|
|
36
|
+
if (normalized === '::1')
|
|
37
|
+
return true;
|
|
38
|
+
if (normalized.startsWith('::ffff:'))
|
|
39
|
+
return isIPv4Loopback(normalized.slice('::ffff:'.length));
|
|
40
|
+
return isIPv4Loopback(normalized);
|
|
41
|
+
}
|
|
42
|
+
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
43
|
+
function isIPv4Loopback(v4) {
|
|
44
|
+
const parts = v4.split('.');
|
|
45
|
+
return parts.length === 4
|
|
46
|
+
&& parts[0] === '127'
|
|
47
|
+
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read one cookie value from a Cookie header.
|
|
51
|
+
* @param header - the raw Cookie header value (or undefined).
|
|
52
|
+
* @param name - the cookie name.
|
|
53
|
+
* @returns the value, or undefined when absent.
|
|
54
|
+
*/
|
|
55
|
+
export function readCookie(header, name) {
|
|
56
|
+
if (header === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
for (const part of header.split(';')) {
|
|
59
|
+
const eq = part.indexOf('=');
|
|
60
|
+
if (eq < 0)
|
|
61
|
+
continue;
|
|
62
|
+
const key = part.slice(0, eq).trim();
|
|
63
|
+
if (key === name)
|
|
64
|
+
return part.slice(eq + 1).trim();
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The effective Host hostname of a request.
|
|
70
|
+
* @param request - node HTTP request.
|
|
71
|
+
* @returns the normalized hostname, or undefined when unparsable.
|
|
72
|
+
*/
|
|
73
|
+
export function hostnameOf(request) {
|
|
74
|
+
const host = request.headers.host;
|
|
75
|
+
if (typeof host !== 'string')
|
|
76
|
+
return undefined;
|
|
77
|
+
try {
|
|
78
|
+
return new URL(`http://${host}`).hostname;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Whether a request comes from the desktop loopback client (loopback socket AND loopback Host). */
|
|
85
|
+
export function isLoopbackClient(request) {
|
|
86
|
+
const hostname = hostnameOf(request);
|
|
87
|
+
if (hostname === undefined || !isLoopbackHostname(hostname))
|
|
88
|
+
return false;
|
|
89
|
+
const socket = request.socket;
|
|
90
|
+
return isLoopbackAddress(socket?.remoteAddress);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Build the api/gate listener for one pairing service.
|
|
94
|
+
* @param service - the pairing service.
|
|
95
|
+
* @param requirePairingForLan - when false, non-loopback requests pass
|
|
96
|
+
* without a device cookie (the feature then only manages tokens/status;
|
|
97
|
+
* revocation of paired devices still holds). A function is re-read per
|
|
98
|
+
* request, so a settings edit takes effect without a restart. Defaults to true.
|
|
99
|
+
* @param enabled - when false, every non-loopback request is vetoed while
|
|
100
|
+
* loopback stays available. A function is re-read per request so the fence
|
|
101
|
+
* stays mounted for the plugin lifetime and disabling the plugin cannot open
|
|
102
|
+
* a LAN-exposed /api. Defaults to true.
|
|
103
|
+
* @returns the cordis waterfall listener: call `next()` to delegate,
|
|
104
|
+
* return false (without calling it) to veto with 403.
|
|
105
|
+
*/
|
|
106
|
+
export function makeGateListener(service, requirePairingForLan = true, enabled = true) {
|
|
107
|
+
return (request, _method, next) => {
|
|
108
|
+
if (isLoopbackClient(request))
|
|
109
|
+
return next();
|
|
110
|
+
const active = typeof enabled === 'function' ? enabled() : enabled;
|
|
111
|
+
if (!active)
|
|
112
|
+
return false;
|
|
113
|
+
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan;
|
|
114
|
+
if (!require)
|
|
115
|
+
return next();
|
|
116
|
+
const deviceId = readCookie(request.headers.cookie, service.config.cookieName);
|
|
117
|
+
if (deviceId === undefined)
|
|
118
|
+
return false;
|
|
119
|
+
return service.touchDevice(deviceId) ? next() : false;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,IAAI,CAAA;IACjE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACjC,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;WACpB,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK;WAClB,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAA;AACzE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,MAA0B,EAAE,IAAY;IACjE,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC1C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC5B,IAAI,EAAE,GAAG,CAAC;YAAE,SAAQ;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACpC,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACpD,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAwB;IACjD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAA;IACjC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC9C,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAA;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAuB,EACvB,uBAAkD,IAAI,EACtD,UAAqC,IAAI;IAEzC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAChC,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;QACpC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACxC,IAAI,kBAAkB,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,EAAE,CAAA;QAC/C,MAAM,MAAM,GAAG,OAAO,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAA;QAClE,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAA;QACzB,MAAM,OAAO,GAAG,OAAO,oBAAoB,KAAK,UAAU,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,oBAAoB,CAAA;QAC1G,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,EAAE,CAAA;QAC3B,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;QAC9E,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACxC,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IACvD,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile remote control for the dsh web GUI — host half. Mounts the pairing
|
|
3
|
+
* service (one-time tokens, device sessions, revocation), the /api/pair
|
|
4
|
+
* route family (issue/accept/stop/heartbeat/status/events), the api/gate
|
|
5
|
+
* listener that enforces pairing on every other /api request from
|
|
6
|
+
* non-loopback hosts, and the presence sweep. The browser half (the
|
|
7
|
+
* `./client` entry) renders the sidebar entry, the pairing panel, and the
|
|
8
|
+
* phone-side pair/accept + deep-link flow.
|
|
9
|
+
*/
|
|
10
|
+
import { setInterval as nodeSetInterval } from 'node:timers';
|
|
11
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
12
|
+
import z from 'schemastery';
|
|
13
|
+
import { PairingService } from "./pairing.js";
|
|
14
|
+
import { makeGateListener } from "./gate.js";
|
|
15
|
+
import { makeRoutes } from "./routes.js";
|
|
16
|
+
import { makeMobileRoutes } from "./mobile-routes.js";
|
|
17
|
+
import { makeMobileApiRoutes } from "./mobile-api.js";
|
|
18
|
+
import { lanIPv4Addresses } from "./lan.js";
|
|
19
|
+
import { TunnelManager } from "./tunnel.js";
|
|
20
|
+
/** Stable cordis plugin name. */
|
|
21
|
+
export const name = 'remote-web-ui';
|
|
22
|
+
/** Services required before the pairing surfaces can mount. */
|
|
23
|
+
export const inject = ['webServer', 'apiProxy'];
|
|
24
|
+
/**
|
|
25
|
+
* Settings namespace of the remote-control capability — the section the web
|
|
26
|
+
* settings surface edits. Spelled here rather than imported: the browser
|
|
27
|
+
* half spells the same value and must not depend on a Host package.
|
|
28
|
+
*/
|
|
29
|
+
export const REMOTE_WEB_UI_SETTINGS_NAMESPACE = settingsNamespace('remote-web-ui');
|
|
30
|
+
export const Config = z.object({
|
|
31
|
+
tokenTtlMs: z.number().step(1).min(60_000).default(10 * 60_000),
|
|
32
|
+
offlineAfterMs: z.number().step(1).min(5_000).default(25_000),
|
|
33
|
+
maxDevices: z.number().step(1).min(1).max(64).default(4),
|
|
34
|
+
cookieName: z.string().min(1).default('dsh_pair'),
|
|
35
|
+
requirePairingForLan: z.boolean().default(true),
|
|
36
|
+
publicBaseUrl: z.string(),
|
|
37
|
+
autoTunnel: z.boolean().default(false),
|
|
38
|
+
enabled: z.boolean().default(true),
|
|
39
|
+
});
|
|
40
|
+
/** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
|
|
41
|
+
const SWEEP_INTERVAL_MS = 10_000;
|
|
42
|
+
/** Schema defaults, re-read for hand-built test contexts (the loader applies them normally). */
|
|
43
|
+
const DEFAULTS = {
|
|
44
|
+
tokenTtlMs: 10 * 60_000,
|
|
45
|
+
offlineAfterMs: 25_000,
|
|
46
|
+
maxDevices: 4,
|
|
47
|
+
cookieName: 'dsh_pair',
|
|
48
|
+
requirePairingForLan: true,
|
|
49
|
+
publicBaseUrl: undefined,
|
|
50
|
+
autoTunnel: false,
|
|
51
|
+
enabled: true,
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Mount the pairing service, routes, gate listener, and presence sweep.
|
|
55
|
+
* @param ctx - host plugin context carrying webServer.
|
|
56
|
+
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
57
|
+
*/
|
|
58
|
+
export function apply(ctx, config) {
|
|
59
|
+
const resolved = {
|
|
60
|
+
tokenTtlMs: config?.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
61
|
+
offlineAfterMs: config?.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
|
62
|
+
maxDevices: config?.maxDevices ?? DEFAULTS.maxDevices,
|
|
63
|
+
cookieName: config?.cookieName ?? DEFAULTS.cookieName,
|
|
64
|
+
requirePairingForLan: config?.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
65
|
+
publicBaseUrl: config?.publicBaseUrl,
|
|
66
|
+
autoTunnel: config?.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
67
|
+
enabled: config?.enabled ?? DEFAULTS.enabled,
|
|
68
|
+
};
|
|
69
|
+
// The live source the pairing service and the gate read: the settings
|
|
70
|
+
// section once the web settings surface is served, the composition entry
|
|
71
|
+
// otherwise (installSettingsSection swaps it when the namespace registers).
|
|
72
|
+
let current = () => config ?? {};
|
|
73
|
+
const resolve = () => {
|
|
74
|
+
const value = current();
|
|
75
|
+
return {
|
|
76
|
+
tokenTtlMs: value.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
77
|
+
offlineAfterMs: value.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
|
78
|
+
maxDevices: value.maxDevices ?? DEFAULTS.maxDevices,
|
|
79
|
+
cookieName: value.cookieName ?? DEFAULTS.cookieName,
|
|
80
|
+
requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
81
|
+
publicBaseUrl: value.publicBaseUrl,
|
|
82
|
+
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
83
|
+
enabled: value.enabled ?? DEFAULTS.enabled,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const service = new PairingService({
|
|
87
|
+
tokenTtlMs: resolved.tokenTtlMs,
|
|
88
|
+
offlineAfterMs: resolved.offlineAfterMs,
|
|
89
|
+
maxDevices: resolved.maxDevices,
|
|
90
|
+
cookieName: resolved.cookieName,
|
|
91
|
+
});
|
|
92
|
+
// ── auto tunnel ─────────────────────────────────────────────────────────
|
|
93
|
+
// The minted public URL becomes the QR base (and the pairing fence's
|
|
94
|
+
// trusted host). Phone /api traffic rides the plugin's own /m/api channel,
|
|
95
|
+
// which is NOT subject to the connection trust fence — so no fence
|
|
96
|
+
// mutation is needed here (a distributable plugin must not change the
|
|
97
|
+
// harness's connection plugin).
|
|
98
|
+
const tunnel = new TunnelManager();
|
|
99
|
+
let autoTunnel = resolved.autoTunnel;
|
|
100
|
+
tunnel.onPhase((info) => {
|
|
101
|
+
if (!autoTunnel)
|
|
102
|
+
return;
|
|
103
|
+
if (info.phase === 'running' && info.url !== undefined) {
|
|
104
|
+
service.setPublicBaseUrl(info.url);
|
|
105
|
+
service.setTunnelStatus({ state: 'running', url: info.url });
|
|
106
|
+
}
|
|
107
|
+
else if (info.phase === 'starting') {
|
|
108
|
+
// A restart mints a NEW hostname: the previous URL dies with the old
|
|
109
|
+
// process, so clear it now rather than advertising a dead link.
|
|
110
|
+
service.setPublicBaseUrl(undefined);
|
|
111
|
+
service.setTunnelStatus({ state: 'starting' });
|
|
112
|
+
}
|
|
113
|
+
else if (info.phase === 'failed') {
|
|
114
|
+
service.setPublicBaseUrl(undefined);
|
|
115
|
+
service.setTunnelStatus(info.error === undefined ? { state: 'failed' } : { state: 'failed', error: info.error });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
ctx.effect(() => () => {
|
|
119
|
+
tunnel.dispose();
|
|
120
|
+
}, 'remote-web-ui: auto tunnel');
|
|
121
|
+
// The bind facts are known by now (webServer is an inject edge): the LAN
|
|
122
|
+
// bases are frozen per process, matching the CLI's once-per-invocation
|
|
123
|
+
// sampling stance. The QR can only advertise addresses the fence accepts;
|
|
124
|
+
// every interface gets its own base URL so a multi-homed machine can pick
|
|
125
|
+
// the network the phone can actually reach.
|
|
126
|
+
const lanBases = ctx.webServer.host === '0.0.0.0'
|
|
127
|
+
? lanIPv4Addresses().map(address => ({ address, base: `http://${address}:${String(ctx.webServer.port)}` }))
|
|
128
|
+
: [];
|
|
129
|
+
service.setLanBases(lanBases);
|
|
130
|
+
const lanAddresses = lanBases.map(entry => entry.address);
|
|
131
|
+
// Push a committed settings section into the service and gate. The service
|
|
132
|
+
// config object is read per operation (token mint, touch, sweep), and the
|
|
133
|
+
// gate re-reads its fence flag per request, so a live edit takes effect
|
|
134
|
+
// without a restart. When `enabled` turns off, the pairing routes and
|
|
135
|
+
// sweep timer are dropped and all device/token state is revoked, but the
|
|
136
|
+
// gate listener stays mounted so a LAN-exposed /api stays behind pairing
|
|
137
|
+
// (now vetoing every non-loopback request) instead of opening the fence.
|
|
138
|
+
let disposeRoutes;
|
|
139
|
+
let disposeSweep;
|
|
140
|
+
// The phone's data channel: pairing routes + the /m page + the /m/api
|
|
141
|
+
// proxy (which needs the host ApiProxy service; the plugin injects it).
|
|
142
|
+
const apiProxy = ctx.get('apiProxy');
|
|
143
|
+
if (apiProxy === undefined) {
|
|
144
|
+
console.warn('remote-web-ui: apiProxy service unavailable — the mobile data channel is disabled');
|
|
145
|
+
}
|
|
146
|
+
const routes = [
|
|
147
|
+
...makeRoutes({ service, lanAddresses }),
|
|
148
|
+
...makeMobileRoutes(),
|
|
149
|
+
...(apiProxy !== undefined ? makeMobileApiRoutes({ service, apiProxy }) : []),
|
|
150
|
+
];
|
|
151
|
+
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
|
|
152
|
+
ctx.effect(() => ctx.on('api/gate', gate), 'remote-web-ui: api gate');
|
|
153
|
+
const sync = () => {
|
|
154
|
+
const value = resolve();
|
|
155
|
+
service.config = {
|
|
156
|
+
tokenTtlMs: value.tokenTtlMs,
|
|
157
|
+
offlineAfterMs: value.offlineAfterMs,
|
|
158
|
+
maxDevices: value.maxDevices,
|
|
159
|
+
cookieName: value.cookieName,
|
|
160
|
+
};
|
|
161
|
+
// The auto tunnel owns the public base while enabled: the minted URL
|
|
162
|
+
// lands in the service through the tunnel's phase listener. The manual
|
|
163
|
+
// publicBaseUrl applies only when the auto tunnel is off.
|
|
164
|
+
autoTunnel = value.autoTunnel === true;
|
|
165
|
+
if (autoTunnel) {
|
|
166
|
+
if (value.publicBaseUrl !== undefined) {
|
|
167
|
+
console.warn('remote-web-ui: autoTunnel is on — ignoring the manually configured publicBaseUrl');
|
|
168
|
+
}
|
|
169
|
+
tunnel.start(`http://127.0.0.1:${String(ctx.webServer.port)}`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
tunnel.stop();
|
|
173
|
+
// A malformed public base is ignored with a warning — LAN-only behavior
|
|
174
|
+
// stays intact rather than silently minting unusable QR links.
|
|
175
|
+
if (value.publicBaseUrl !== undefined && !isHttpUrl(value.publicBaseUrl)) {
|
|
176
|
+
console.warn(`remote-web-ui: ignoring malformed publicBaseUrl ${JSON.stringify(value.publicBaseUrl)} (expected https://host[:port])`);
|
|
177
|
+
service.setPublicBaseUrl(undefined);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
service.setPublicBaseUrl(value.publicBaseUrl);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const enabled = value.enabled;
|
|
184
|
+
if (!enabled)
|
|
185
|
+
service.stop();
|
|
186
|
+
if (disposeRoutes === undefined && enabled) {
|
|
187
|
+
disposeRoutes = ctx.effect(() => {
|
|
188
|
+
const disposers = routes.map(route => ctx.webServer.register(route));
|
|
189
|
+
return () => { for (const dispose of disposers)
|
|
190
|
+
dispose(); };
|
|
191
|
+
}, 'remote-web-ui: pairing routes');
|
|
192
|
+
}
|
|
193
|
+
else if (disposeRoutes !== undefined && !enabled) {
|
|
194
|
+
disposeRoutes();
|
|
195
|
+
disposeRoutes = undefined;
|
|
196
|
+
}
|
|
197
|
+
if (disposeSweep === undefined && enabled) {
|
|
198
|
+
disposeSweep = ctx.effect(() => {
|
|
199
|
+
const timer = nodeSetInterval(() => { service.sweep(); }, SWEEP_INTERVAL_MS);
|
|
200
|
+
timer.unref();
|
|
201
|
+
return () => { clearInterval(timer); };
|
|
202
|
+
}, 'remote-web-ui: presence sweep');
|
|
203
|
+
}
|
|
204
|
+
else if (disposeSweep !== undefined && !enabled) {
|
|
205
|
+
disposeSweep();
|
|
206
|
+
disposeSweep = undefined;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
installSettingsSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
210
|
+
setSource: (source) => {
|
|
211
|
+
current = source;
|
|
212
|
+
sync();
|
|
213
|
+
},
|
|
214
|
+
onChange: sync,
|
|
215
|
+
});
|
|
216
|
+
sync();
|
|
217
|
+
}
|
|
218
|
+
/** Whether a configured public base is a parseable http(s) URL with a host. */
|
|
219
|
+
function isHttpUrl(value) {
|
|
220
|
+
try {
|
|
221
|
+
const url = new URL(value);
|
|
222
|
+
return (url.protocol === 'http:' || url.protocol === 'https:') && url.hostname !== '';
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,aAAa,CAAA;AAG5D,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AACrF,OAAO,CAAC,MAAM,aAAa,CAAA;AAE3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAmB,MAAM,aAAa,CAAA;AAmB5D,iCAAiC;AACjC,MAAM,CAAC,MAAM,IAAI,GAAG,eAAe,CAAA;AAEnC,+DAA+D;AAC/D,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,iBAAiB,CAAC,eAAe,CAAC,CAAA;AAwClF,MAAM,CAAC,MAAM,MAAM,GAAc,CAAC,CAAC,MAAM,CAAC;IACxC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC;IAC/D,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IAC7D,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;IACjD,oBAAoB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC/C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE;IACzB,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACtC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CACnC,CAAC,CAAA;AAEF,uFAAuF;AACvF,MAAM,iBAAiB,GAAG,MAAM,CAAA;AAShC,gGAAgG;AAChG,MAAM,QAAQ,GAAmB;IAC/B,UAAU,EAAE,EAAE,GAAG,MAAM;IACvB,cAAc,EAAE,MAAM;IACtB,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,UAAU;IACtB,oBAAoB,EAAE,IAAI;IAC1B,aAAa,EAAE,SAAS;IACxB,UAAU,EAAE,KAAK;IACjB,OAAO,EAAE,IAAI;CACd,CAAA;AAED;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,MAAe;IACjD,MAAM,QAAQ,GAAmB;QAC/B,UAAU,EAAE,MAAM,EAAE,UAAU,IAAI,QAAQ,CAAC,UAAU;QACrD,cAAc,EAAE,MAAM,EAAE,cAAc,IAAI,QAAQ,CAAC,cAAc;QACjE,UAAU,EAAE,MAAM,EAAE,UAAU,IAAI,QAAQ,CAAC,UAAU;QACrD,UAAU,EAAE,MAAM,EAAE,UAAU,IAAI,QAAQ,CAAC,UAAU;QACrD,oBAAoB,EAAE,MAAM,EAAE,oBAAoB,IAAI,QAAQ,CAAC,oBAAoB;QACnF,aAAa,EAAE,MAAM,EAAE,aAAa;QACpC,UAAU,EAAE,MAAM,EAAE,UAAU,IAAI,QAAQ,CAAC,UAAU;QACrD,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,QAAQ,CAAC,OAAO;KAC7C,CAAA;IACD,sEAAsE;IACtE,yEAAyE;IACzE,4EAA4E;IAC5E,IAAI,OAAO,GAAiB,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAA;IAC9C,MAAM,OAAO,GAAG,GAAmB,EAAE;QACnC,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;QACvB,OAAO;YACL,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;YACnD,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,QAAQ,CAAC,cAAc;YAC/D,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;YACnD,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;YACnD,oBAAoB,EAAE,KAAK,CAAC,oBAAoB,IAAI,QAAQ,CAAC,oBAAoB;YACjF,aAAa,EAAE,KAAK,CAAC,aAAa;YAClC,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU;YACnD,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO;SAC3C,CAAA;IACH,CAAC,CAAA;IACD,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC;QACjC,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,cAAc,EAAE,QAAQ,CAAC,cAAc;QACvC,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,UAAU,EAAE,QAAQ,CAAC,UAAU;KAChC,CAAC,CAAA;IAEF,2EAA2E;IAC3E,qEAAqE;IACrE,2EAA2E;IAC3E,mEAAmE;IACnE,sEAAsE;IACtE,gCAAgC;IAChC,MAAM,MAAM,GAAG,IAAI,aAAa,EAAE,CAAA;IAClC,IAAI,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAA;IACpC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAgB,EAAE,EAAE;QAClC,IAAI,CAAC,UAAU;YAAE,OAAM;QACvB,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;YACvD,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAClC,OAAO,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;QAC9D,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACrC,qEAAqE;YACrE,gEAAgE;YAChE,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAA;YACnC,OAAO,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAA;QAChD,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACnC,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAA;YACnC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAA;QAClH,CAAC;IACH,CAAC,CAAC,CAAA;IACF,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE;QACpB,MAAM,CAAC,OAAO,EAAE,CAAA;IAClB,CAAC,EAAE,4BAA4B,CAAC,CAAA;IAChC,0EAA0E;IAC1E,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,4CAA4C;IAC5C,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,SAAS;QAChD,CAAC,CAAC,gBAAgB,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5G,CAAC,CAAC,EAAE,CAAA;IACN,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAA;IAC7B,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAEzD,2EAA2E;IAC3E,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,IAAI,aAAuC,CAAA;IAC3C,IAAI,YAAsC,CAAA;IAC1C,sEAAsE;IACtE,wEAAwE;IACxE,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IACpC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,mFAAmF,CAAC,CAAA;IACnG,CAAC;IACD,MAAM,MAAM,GAAG;QACb,GAAG,UAAU,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;QACxC,GAAG,gBAAgB,EAAE;QACrB,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9E,CAAA;IACD,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAA;IACrG,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,yBAAyB,CAAC,CAAA;IACrE,MAAM,IAAI,GAAG,GAAS,EAAE;QACtB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAA;QACvB,OAAO,CAAC,MAAM,GAAG;YACf,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,cAAc,EAAE,KAAK,CAAC,cAAc;YACpC,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;SAC7B,CAAA;QACD,qEAAqE;QACrE,uEAAuE;QACvE,0DAA0D;QAC1D,UAAU,GAAG,KAAK,CAAC,UAAU,KAAK,IAAI,CAAA;QACtC,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;gBACtC,OAAO,CAAC,IAAI,CAAC,kFAAkF,CAAC,CAAA;YAClG,CAAC;YACD,MAAM,CAAC,KAAK,CAAC,oBAAoB,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACjE,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,EAAE,CAAA;YACb,wEAAwE;YACxE,+DAA+D;YAC/D,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;gBACzE,OAAO,CAAC,IAAI,CAAC,mDAAmD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,iCAAiC,CAAC,CAAA;gBACrI,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAA;YACrC,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;YAC/C,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;QAC7B,IAAI,CAAC,OAAO;YAAE,OAAO,CAAC,IAAI,EAAE,CAAA;QAC5B,IAAI,aAAa,KAAK,SAAS,IAAI,OAAO,EAAE,CAAC;YAC3C,aAAa,GAAG,GAAG,CAAC,MAAM,CACxB,GAAG,EAAE;gBACH,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;gBACrE,OAAO,GAAG,EAAE,GAAG,KAAK,MAAM,OAAO,IAAI,SAAS;oBAAE,OAAO,EAAE,CAAA,CAAC,CAAC,CAAA;YAC7D,CAAC,EACD,+BAA+B,CAChC,CAAA;QACH,CAAC;aAAM,IAAI,aAAa,KAAK,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC;YACnD,aAAa,EAAE,CAAA;YACf,aAAa,GAAG,SAAS,CAAA;QAC3B,CAAC;QACD,IAAI,YAAY,KAAK,SAAS,IAAI,OAAO,EAAE,CAAC;YAC1C,YAAY,GAAG,GAAG,CAAC,MAAM,CACvB,GAAG,EAAE;gBACH,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,CAAA,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAA;gBAC3E,KAAK,CAAC,KAAK,EAAE,CAAA;gBACb,OAAO,GAAG,EAAE,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC,CAAA;YACvC,CAAC,EACD,+BAA+B,CAChC,CAAA;QACH,CAAC;aAAM,IAAI,YAAY,KAAK,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC;YAClD,YAAY,EAAE,CAAA;YACd,YAAY,GAAG,SAAS,CAAA;QAC1B,CAAC;IACH,CAAC,CAAA;IACD,sBAAsB,CAAC,GAAG,EAAE,gCAAgC,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE;QAClF,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE;YACpB,OAAO,GAAG,MAAM,CAAA;YAChB,IAAI,EAAE,CAAA;QACR,CAAC;QACD,QAAQ,EAAE,IAAI;KACf,CAAC,CAAA;IACF,IAAI,EAAE,CAAA;AACR,CAAC;AAED,+EAA+E;AAC/E,SAAS,SAAS,CAAC,KAAa;IAC9B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAA;QAC1B,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,KAAK,EAAE,CAAA;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@linxin666/dsh-remote-web-ui`.
|
|
3
|
+
* @module @linxin666/dsh-remote-web-ui/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@linxin666/dsh-remote-web-ui';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'remote-web-ui-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: this package owns no durable package-local event
|
|
12
|
+
* stream; its route-table and device-session relationships are asserted by
|
|
13
|
+
* the package's own specs (route register/dispose symmetry via the
|
|
14
|
+
* webServer disposer contract, token/revocation semantics on the service).
|
|
15
|
+
*/
|
|
16
|
+
const install = () => { };
|
|
17
|
+
/**
|
|
18
|
+
* Register this package's invariant companion.
|
|
19
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
20
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
21
|
+
*/
|
|
22
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
23
|
+
/* jscpd:ignore-end */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariant.js","sourceRoot":"","sources":["../../src/invariant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,YAAY,GAAG,gCAAgC,CAAA;AAErD,oCAAoC;AACpC,MAAM,CAAC,MAAM,IAAI,GAAG,yBAAyB,CAAA;AAC7C,2EAA2E;AAC3E,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,YAAY,CAAC,CAAA;AAEpC;;;;;GAKG;AACH,MAAM,OAAO,GAAuB,GAAG,EAAE,GAAE,CAAC,CAAA;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,GAAY,EAAuB,EAAE,CACzD,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AACjE,sBAAsB"}
|
package/lib/types/lan.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAN address derivation for the pairing URLs. Mirrors the dsh CLI's
|
|
3
|
+
* boot-time sampling (apps/cli/src/app-cli-entry.ts `resolveLanTrust`): the
|
|
4
|
+
* pairing links may only name addresses the /api trust fence was configured
|
|
5
|
+
* with, so the same non-internal IPv4 derivation applies here — an external
|
|
6
|
+
* plugin cannot read the CLI's sampled snapshot, but the fence accepts
|
|
7
|
+
* exactly these literals, which is the property that matters.
|
|
8
|
+
*/
|
|
9
|
+
import { networkInterfaces } from 'node:os';
|
|
10
|
+
/**
|
|
11
|
+
* Non-internal IPv4 interface addresses of this machine — the IP-literal
|
|
12
|
+
* authorities an all-interfaces bind is reachable by on the LAN.
|
|
13
|
+
* @returns the addresses in interface order (possibly empty).
|
|
14
|
+
*/
|
|
15
|
+
export function lanIPv4Addresses() {
|
|
16
|
+
return Object.values(networkInterfaces()).flat()
|
|
17
|
+
.filter((iface) => { return iface !== undefined && iface.family === 'IPv4' && !iface.internal; })
|
|
18
|
+
.map(iface => iface.address);
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lan.js","sourceRoot":"","sources":["../../src/lan.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAE3C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,IAAI,EAAE;SAC7C,MAAM,CAAC,CAAC,KAAK,EAAsC,EAAE,GAAG,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAA,CAAC,CAAC,CAAC;SACnI,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile-surface business API: the handful of host RPC methods the
|
|
3
|
+
* simplified surface needs. Types come from the harness apiproxy contract
|
|
4
|
+
* (type-only imports; the wire schemas stay in the bundle only through the
|
|
5
|
+
* rpc/mux layers).
|
|
6
|
+
*/
|
|
7
|
+
import { callUnary } from "./rpc.js";
|
|
8
|
+
/** The workspace roster (session ids come back per workspace). */
|
|
9
|
+
export async function listWorkspaces() {
|
|
10
|
+
const { items } = await callUnary('workspace.list', {});
|
|
11
|
+
return items;
|
|
12
|
+
}
|
|
13
|
+
/** One session.list page; omit the cursor for the first page. */
|
|
14
|
+
export async function listSessions(cursor) {
|
|
15
|
+
return await callUnary('session.list', cursor === undefined ? {} : { cursor });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a blank session (entity birth precedes the first message). Name a
|
|
19
|
+
* workspace to attach it there, or a cwd; omitting both uses the host cwd.
|
|
20
|
+
*/
|
|
21
|
+
export async function createSession(options = {}) {
|
|
22
|
+
return await callUnary('session.create', options);
|
|
23
|
+
}
|
|
24
|
+
/** One history window; omit beforeSeq for the tail page. */
|
|
25
|
+
export async function history(sessionId, beforeSeq, maxMessages = 30) {
|
|
26
|
+
return await callUnary('session.history', {
|
|
27
|
+
sessionId,
|
|
28
|
+
maxMessages,
|
|
29
|
+
...(beforeSeq !== undefined ? { beforeSeq } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Send one text prompt (queued: the agent picks it up in order). */
|
|
33
|
+
export async function prompt(sessionId, text) {
|
|
34
|
+
await callUnary('session.prompt', {
|
|
35
|
+
sessionId,
|
|
36
|
+
mode: 'queue',
|
|
37
|
+
content: [{ type: 'text', text }],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Send one slash command line (e.g. `/permission workspace-write`). */
|
|
41
|
+
export async function sendCommand(sessionId, line) {
|
|
42
|
+
return await callUnary('session.prompt', {
|
|
43
|
+
sessionId,
|
|
44
|
+
mode: 'queue',
|
|
45
|
+
content: [{ type: 'text', text: line }],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Fresh advisory model directory for one session (current + groups + failures). */
|
|
49
|
+
export async function models(sessionId) {
|
|
50
|
+
return await callUnary('session.models', { sessionId });
|
|
51
|
+
}
|
|
52
|
+
/** Select the complete model selection (provider/model/reasoning effort) for a session. */
|
|
53
|
+
export async function selectModel(sessionId, selection) {
|
|
54
|
+
return await callUnary('session.selectModel', {
|
|
55
|
+
sessionId,
|
|
56
|
+
provider: selection.provider,
|
|
57
|
+
model: selection.model,
|
|
58
|
+
...(selection.reasoningEffort !== undefined ? { reasoningEffort: selection.reasoningEffort } : {}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../../src/mobile/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAgBpC,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,SAAS,CAA6B,gBAAgB,EAAE,EAAE,CAAC,CAAA;IACnF,OAAO,KAAK,CAAA;AACd,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAe;IAChD,OAAO,MAAM,SAAS,CAAc,cAAc,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;AAC7F,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,SAAiB,EACjB,SAAkB,EAClB,WAAW,GAAG,EAAE;IAEhB,OAAO,MAAM,SAAS,CAAc,iBAAiB,EAAE;QACrD,SAAS;QACT,WAAW;QACX,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClD,CAAC,CAAA;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,SAAiB,EAAE,IAAY;IAC1D,MAAM,SAAS,CAAqB,gBAAgB,EAAE;QACpD,SAAS;QACT,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;KAClC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Mobile surface entry: the standalone phone UI served at /m. Boots its own
|
|
4
|
+
* React tree (no main-UI module loader), talks to the host over the shared
|
|
5
|
+
* /api transport with the paired-device cookie, and renders a deliberately
|
|
6
|
+
* thin three-level surface:
|
|
7
|
+
* workspaces (landing, no new-session homepage) → sessions (paged) →
|
|
8
|
+
* chat (history paged on demand + live mux stream + prompt input).
|
|
9
|
+
*/
|
|
10
|
+
import { createRoot } from 'react-dom/client';
|
|
11
|
+
import { App } from "./views/App.js";
|
|
12
|
+
import { mobileCss } from "./mobile-styles.js";
|
|
13
|
+
import { initMobileTheme } from "./mobile-theme.js";
|
|
14
|
+
// Apply the persisted (or default light) theme before first paint, so the
|
|
15
|
+
// page never flashes the wrong palette.
|
|
16
|
+
initMobileTheme();
|
|
17
|
+
// Inject the standalone stylesheet (the page has no shell to load it for us).
|
|
18
|
+
const style = document.createElement('style');
|
|
19
|
+
style.dataset.plugin = 'remote-web-ui/mobile';
|
|
20
|
+
style.textContent = mobileCss;
|
|
21
|
+
document.head.appendChild(style);
|
|
22
|
+
const root = document.getElementById('root');
|
|
23
|
+
if (root === null)
|
|
24
|
+
throw new Error('mobile: #root missing');
|
|
25
|
+
createRoot(root).render(_jsx(App, {}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mobile/index.tsx"],"names":[],"mappings":";AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAiB,CAAA;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAE9C,8EAA8E;AAC9E,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;AAC7C,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,sBAAsB,CAAA;AAC7C,KAAK,CAAC,WAAW,GAAG,SAAS,CAAA;AAC7B,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;AAEhC,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;AAC5C,IAAI,IAAI,KAAK,IAAI;IAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;AAC3D,UAAU,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAC,GAAG,KAAG,CAAC,CAAA"}
|