@linxin666/dsh-remote-web-ui 0.1.1 → 0.1.3

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.
Files changed (79) hide show
  1. package/README.md +4 -1
  2. package/lib/client.js +35 -0
  3. package/lib/client.js.map +1 -0
  4. package/lib/mobile.js +15467 -0
  5. package/lib/mobile.js.map +1 -0
  6. package/lib/types/client/FooterRemoteEntry.d.ts +22 -0
  7. package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -0
  8. package/lib/types/client/FooterRemoteEntry.js +10 -0
  9. package/lib/types/client/PairFailedNotice.js +24 -0
  10. package/lib/types/client/PairFailedNotice.js.map +1 -0
  11. package/lib/types/client/PhoneIcon.js +9 -0
  12. package/lib/types/client/PhoneIcon.js.map +1 -0
  13. package/lib/types/client/PluginSettingsCard.js +39 -0
  14. package/lib/types/client/PluginSettingsCard.js.map +1 -0
  15. package/lib/types/client/RemoteEntry.js +162 -0
  16. package/lib/types/client/RemoteEntry.js.map +1 -0
  17. package/lib/types/client/RemotePanel.js +34 -0
  18. package/lib/types/client/RemotePanel.js.map +1 -0
  19. package/lib/types/client/RemoteSettingsCard.js +59 -0
  20. package/lib/types/client/RemoteSettingsCard.js.map +1 -0
  21. package/lib/types/client/deep-link.js +118 -0
  22. package/lib/types/client/deep-link.js.map +1 -0
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +134 -0
  25. package/lib/types/client/index.js.map +1 -0
  26. package/lib/types/client/locales.js +141 -0
  27. package/lib/types/client/locales.js.map +1 -0
  28. package/lib/types/client/pair-api.js +123 -0
  29. package/lib/types/client/pair-api.js.map +1 -0
  30. package/lib/types/client/settings-form.js +211 -0
  31. package/lib/types/client/settings-form.js.map +1 -0
  32. package/lib/types/gate.js +121 -0
  33. package/lib/types/gate.js.map +1 -0
  34. package/lib/types/index.js +227 -0
  35. package/lib/types/index.js.map +1 -0
  36. package/lib/types/invariant.js +23 -0
  37. package/lib/types/invariant.js.map +1 -0
  38. package/lib/types/lan.js +19 -0
  39. package/lib/types/lan.js.map +1 -0
  40. package/lib/types/mobile/api.js +60 -0
  41. package/lib/types/mobile/api.js.map +1 -0
  42. package/lib/types/mobile/index.js +25 -0
  43. package/lib/types/mobile/index.js.map +1 -0
  44. package/lib/types/mobile/messages.js +449 -0
  45. package/lib/types/mobile/messages.js.map +1 -0
  46. package/lib/types/mobile/messages.test.js +189 -0
  47. package/lib/types/mobile/messages.test.js.map +1 -0
  48. package/lib/types/mobile/mobile-styles.js +853 -0
  49. package/lib/types/mobile/mobile-styles.js.map +1 -0
  50. package/lib/types/mobile/mobile-theme.js +70 -0
  51. package/lib/types/mobile/mobile-theme.test.js +71 -0
  52. package/lib/types/mobile/mux.js +110 -0
  53. package/lib/types/mobile/mux.js.map +1 -0
  54. package/lib/types/mobile/rpc.js +83 -0
  55. package/lib/types/mobile/rpc.js.map +1 -0
  56. package/lib/types/mobile/theme-toggle.js +26 -0
  57. package/lib/types/mobile/views/App.js +102 -0
  58. package/lib/types/mobile/views/App.js.map +1 -0
  59. package/lib/types/mobile/views/ChatView.js +348 -0
  60. package/lib/types/mobile/views/ChatView.js.map +1 -0
  61. package/lib/types/mobile/views/ChatView.test.js +215 -0
  62. package/lib/types/mobile/views/SessionListView.js +108 -0
  63. package/lib/types/mobile/views/SessionListView.js.map +1 -0
  64. package/lib/types/mobile/views/SessionListView.test.js +110 -0
  65. package/lib/types/mobile/views/WorkspaceView.js +43 -0
  66. package/lib/types/mobile/views/WorkspaceView.js.map +1 -0
  67. package/lib/types/mobile-api.js +263 -0
  68. package/lib/types/mobile-api.js.map +1 -0
  69. package/lib/types/mobile-routes.js +71 -0
  70. package/lib/types/mobile-routes.js.map +1 -0
  71. package/lib/types/pairing.js +285 -0
  72. package/lib/types/pairing.js.map +1 -0
  73. package/lib/types/routes.js +327 -0
  74. package/lib/types/routes.js.map +1 -0
  75. package/lib/types/tunnel.js +222 -0
  76. package/lib/types/tunnel.js.map +1 -0
  77. package/package.json +9 -6
  78. package/src/client/FooterRemoteEntry.tsx +31 -0
  79. package/src/client/index.ts +23 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Pairing state machine: one active one-time token, a device-session table,
3
+ * and presence tracking. Pure TypeScript with injected clock/randomness so
4
+ * the whole security semantics are unit-testable without cordis. The
5
+ * cordis-facing surfaces (routes, the api/gate listener) live next door.
6
+ *
7
+ * Security invariants:
8
+ * - One active token at a time; `issue()` replaces it, so a refreshed QR
9
+ * immediately invalidates the previous link.
10
+ * - A token is consumed by the first successful `accept()` — reuse is
11
+ * refused with `'used'`.
12
+ * - Tokens expire; `accept()` on an expired token is refused like an
13
+ * unknown one (no oracle for validity).
14
+ * - `stop()` revokes every device session and clears the token, so paired
15
+ * devices are cut off on their next gated request.
16
+ */
17
+ import { randomBytes } from 'node:crypto';
18
+ /** Thrown by issue() for an address outside the sampled LAN literals. */
19
+ export class UnknownLanAddressError extends Error {
20
+ /**
21
+ * @param address - the offending literal.
22
+ */
23
+ constructor(address) {
24
+ super(`remote-web-ui: unknown LAN address ${JSON.stringify(address)}`);
25
+ this.name = 'UnknownLanAddressError';
26
+ }
27
+ }
28
+ /** Real clock/entropy: 32 random hex chars per token. */
29
+ export const defaultClock = {
30
+ now: () => Date.now(),
31
+ randomToken: () => randomBytes(16).toString('hex'),
32
+ };
33
+ /**
34
+ * The pairing state machine. All mutations notify state listeners after the
35
+ * commit point that makes them true, and notification dedupes against the
36
+ * last emitted snapshot — time-driven transitions (a device aging offline)
37
+ * surface on the next sweep without any mutation.
38
+ */
39
+ export class PairingService {
40
+ config;
41
+ clock;
42
+ tokens = new Map();
43
+ devices = new Map();
44
+ listeners = new Set();
45
+ lastEmitted;
46
+ stopped = false;
47
+ tokenSerial = 0;
48
+ /** LAN base URLs keyed by the advertised IP literal (interface order). */
49
+ lanBases = new Map();
50
+ /** Public (tunneled) base URL, e.g. a Cloudflare Tunnel quick URL. */
51
+ publicBase;
52
+ /** Auto-tunnel status, while the auto-tunnel feature is active. */
53
+ tunnelStatus;
54
+ /**
55
+ * @param config - tunables. The settings surface replaces the object (a
56
+ * fresh literal) when a committed section changes; every operation reads
57
+ * the current one.
58
+ * @param clock - clock/entropy source (injectable for tests).
59
+ */
60
+ constructor(config, clock = defaultClock) {
61
+ this.config = config;
62
+ this.clock = clock;
63
+ }
64
+ /** The default LAN base URL (the first interface; undefined when not LAN-reachable). */
65
+ get lanBaseUrl() {
66
+ return this.lanBases.values().next().value;
67
+ }
68
+ /** The LAN base URL for one specific literal (undefined when not constructible). */
69
+ lanBaseUrlFor(address) {
70
+ return this.lanBases.get(address);
71
+ }
72
+ /** The LAN IP literals QR links can be built from (interface order). */
73
+ get lanAddresses() {
74
+ return [...this.lanBases.keys()];
75
+ }
76
+ /** Set the LAN base URLs once the server bind is known (interface order). */
77
+ setLanBases(entries) {
78
+ this.lanBases = new Map(entries.map(entry => [entry.address, entry.base]));
79
+ this.notify();
80
+ }
81
+ /** The configured public (tunneled) base URL, when present. */
82
+ get publicBaseUrl() {
83
+ return this.publicBase;
84
+ }
85
+ /** Set or clear the public base URL (a tunnel in front of this server). */
86
+ setPublicBaseUrl(url) {
87
+ this.publicBase = url;
88
+ this.notify();
89
+ }
90
+ /** Set or clear the auto-tunnel status frame (undefined when the feature is off). */
91
+ setTunnelStatus(status) {
92
+ this.tunnelStatus = status;
93
+ this.notify();
94
+ }
95
+ /**
96
+ * Issue a fresh token, replacing (invalidating) any previous one. A
97
+ * stopped service re-arms through this call (the panel's refresh button).
98
+ * @param workspaceId - optional workspace the QR link should land in.
99
+ * @param address - optional LAN IP literal the QR must be built from; the
100
+ * default is the public base (when configured) or the first interface.
101
+ * Unknown addresses are refused.
102
+ * @returns the token secret and its expiry.
103
+ * @throws {Error} when no reachable base exists (no all-interfaces bind and
104
+ * no public base) — callers surface this as the lan-required state instead
105
+ * of minting an unusable QR.
106
+ */
107
+ issue(workspaceId, address) {
108
+ if (this.lanBases.size === 0 && this.publicBase === undefined) {
109
+ throw new Error('remote-web-ui: pairing requires a reachable bind (--host 0.0.0.0 or publicBaseUrl)');
110
+ }
111
+ if (address !== undefined && !this.lanBases.has(address)) {
112
+ throw new UnknownLanAddressError(address);
113
+ }
114
+ const now = this.clock.now();
115
+ const token = this.clock.randomToken();
116
+ this.tokens.clear();
117
+ this.stopped = false;
118
+ this.tokenSerial += 1;
119
+ this.tokens.set(token, {
120
+ id: `t${this.tokenSerial}`,
121
+ issuedAt: now,
122
+ expiresAt: now + this.config.tokenTtlMs,
123
+ consumed: false,
124
+ ...(workspaceId !== undefined ? { workspaceId } : {}),
125
+ ...(address !== undefined ? { address } : {}),
126
+ });
127
+ this.notify();
128
+ return { token, expiresAt: now + this.config.tokenTtlMs };
129
+ }
130
+ /**
131
+ * Consume a token and bind a device session. One-time: the second
132
+ * successful call for the same token is impossible because the first
133
+ * consumes it.
134
+ * @param token - the token secret from the QR link.
135
+ * @returns the new device id, or a refusal code.
136
+ */
137
+ accept(token) {
138
+ const record = this.tokens.get(token);
139
+ if (record === undefined || record.consumed || this.stopped || this.clock.now() > record.expiresAt) {
140
+ return { ok: false, code: record?.consumed === true ? 'used' : 'invalid' };
141
+ }
142
+ record.consumed = true;
143
+ const deviceId = this.clock.randomToken();
144
+ const now = this.clock.now();
145
+ if (this.devices.size >= this.config.maxDevices) {
146
+ // Evict the oldest session (FIFO) before binding a new device.
147
+ let oldest;
148
+ for (const [id, session] of this.devices) {
149
+ if (oldest === undefined || session.createdAt < oldest.createdAt)
150
+ oldest = { id, createdAt: session.createdAt };
151
+ }
152
+ if (oldest !== undefined)
153
+ this.devices.delete(oldest.id);
154
+ }
155
+ this.devices.set(deviceId, { createdAt: now, lastSeenAt: now });
156
+ this.notify();
157
+ return { ok: true, deviceId };
158
+ }
159
+ /**
160
+ * Stop remote control: revoke every device session and clear the token.
161
+ * The phone's next gated /api request 403s; the panel falls back to
162
+ * stopped until a fresh QR is issued.
163
+ */
164
+ stop() {
165
+ this.tokens.clear();
166
+ this.devices.clear();
167
+ this.stopped = true;
168
+ this.notify();
169
+ }
170
+ /**
171
+ * The api/gate path: record activity for a device id and report whether
172
+ * the request may proceed. Unknown or revoked ids (including any device
173
+ * after stop()) are refused.
174
+ * @param deviceId - the cookie value of the requesting device.
175
+ * @returns true when the device session is live and was refreshed.
176
+ */
177
+ touchDevice(deviceId) {
178
+ const session = this.devices.get(deviceId);
179
+ if (session === undefined || this.stopped)
180
+ return false;
181
+ session.lastSeenAt = this.clock.now();
182
+ this.notify();
183
+ return true;
184
+ }
185
+ /** Explicit presence heartbeat (the phone's client sends these). */
186
+ heartbeat(deviceId) {
187
+ return this.touchDevice(deviceId);
188
+ }
189
+ /**
190
+ * Periodic sweep: re-evaluate the derived snapshot (a device aging past
191
+ * the offline window flips the phase to disconnected). Emits only when
192
+ * the snapshot actually changed.
193
+ */
194
+ sweep() {
195
+ this.notify();
196
+ }
197
+ /** The current snapshot (fresh object per call — stable between emits). */
198
+ snapshot() {
199
+ const now = this.clock.now();
200
+ const onlineCount = [...this.devices.values()].filter(session => this.isOnlineAt(session, now)).length;
201
+ const token = this.activeToken();
202
+ return {
203
+ phase: this.derivePhase(onlineCount, token !== undefined),
204
+ lanAvailable: this.lanBases.size > 0,
205
+ lanAddresses: [...this.lanBases.keys()],
206
+ ...(this.publicBase !== undefined ? { publicUrl: this.publicBase } : {}),
207
+ ...(this.tunnelStatus !== undefined ? { tunnel: this.tunnelStatus } : {}),
208
+ ...(token !== undefined ? { tokenId: token.record.id, tokenExpiresAt: token.record.expiresAt } : {}),
209
+ deviceCount: this.devices.size,
210
+ onlineCount,
211
+ };
212
+ }
213
+ /** Whether a cookie value names a currently live device session. */
214
+ hasDevice(deviceId) {
215
+ const session = this.devices.get(deviceId);
216
+ return session !== undefined && !this.stopped;
217
+ }
218
+ /** Subscribe to snapshot changes (each emit passes a fresh snapshot). */
219
+ onState(listener) {
220
+ this.listeners.add(listener);
221
+ return () => { this.listeners.delete(listener); };
222
+ }
223
+ activeToken() {
224
+ for (const [token, record] of this.tokens) {
225
+ if (this.stopped)
226
+ return undefined;
227
+ if (this.clock.now() > record.expiresAt)
228
+ continue;
229
+ return { token, record };
230
+ }
231
+ return undefined;
232
+ }
233
+ derivePhase(onlineCount, hasToken) {
234
+ if (this.lanBases.size === 0 && this.publicBase === undefined)
235
+ return 'lan-required';
236
+ if (this.stopped)
237
+ return 'stopped';
238
+ if (onlineCount > 0)
239
+ return 'connected';
240
+ if (this.devices.size > 0)
241
+ return 'disconnected';
242
+ if (hasToken)
243
+ return 'waiting';
244
+ return 'stopped';
245
+ }
246
+ isOnlineAt(session, now) {
247
+ return now - session.lastSeenAt <= this.config.offlineAfterMs;
248
+ }
249
+ notify() {
250
+ const snapshot = this.snapshot();
251
+ if (this.lastEmitted !== undefined && snapshotsEqual(this.lastEmitted, snapshot))
252
+ return;
253
+ this.lastEmitted = snapshot;
254
+ for (const listener of this.listeners) {
255
+ try {
256
+ listener(snapshot);
257
+ }
258
+ catch (error) {
259
+ // A throwing subscriber must not break the emit loop or the caller.
260
+ console.error('remote-web-ui: pairing state listener failed', error);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ /** Structural equality over the snapshot's wire fields. */
266
+ function snapshotsEqual(a, b) {
267
+ return a.phase === b.phase
268
+ && a.lanAvailable === b.lanAvailable
269
+ && sameStrings(a.lanAddresses, b.lanAddresses)
270
+ && a.publicUrl === b.publicUrl
271
+ && tunnelEqual(a.tunnel, b.tunnel)
272
+ && a.tokenId === b.tokenId
273
+ && a.tokenExpiresAt === b.tokenExpiresAt
274
+ && a.deviceCount === b.deviceCount
275
+ && a.onlineCount === b.onlineCount;
276
+ }
277
+ /** Tunnel frame equality (undefined equals undefined; fields compared shallowly). */
278
+ function tunnelEqual(a, b) {
279
+ return a === b || (a !== undefined && b !== undefined
280
+ && a.state === b.state && a.url === b.url && a.error === b.error);
281
+ }
282
+ /** Element-wise string list equality (interface order is meaningful). */
283
+ function sameStrings(a, b) {
284
+ return a.length === b.length && a.every((value, index) => value === b[index]);
285
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,327 @@
1
+ /**
2
+ * The /api/pair route family + the desktop status stream. Exact routes
3
+ * under /api: the webserver matches exact paths before the connection
4
+ * plugin's /api prefix, so these handlers own the full response lifecycle
5
+ * and apply their own trust fence (loopback-only for control endpoints;
6
+ * loopback-or-LAN for the phone-facing accept/heartbeat/status). The
7
+ * cookie set on accept is the device identity the api/gate listener checks
8
+ * on every other /api request.
9
+ */
10
+ import { UnknownLanAddressError } from "./pairing.js";
11
+ import { isLoopbackClient, readCookie } from "./gate.js";
12
+ /**
13
+ * Browser-trust fence for the /api/pair routes, mirroring the connection
14
+ * package's internal fence semantics (Host/Origin based, DNS-rebinding and
15
+ * cross-site defense). The connection package no longer exports its trust
16
+ * predicate — the fence for the /api prefix lives inside the connection
17
+ * plugin — so the pairing routes, which must stay reachable from LAN phones
18
+ * ahead of the connection prefix route (exact routes match first), carry
19
+ * their own copy scoped to the literals the QR links advertise.
20
+ * @param request - the node HTTP request.
21
+ * @param trustedHosts - non-loopback authorities this surface serves: exact
22
+ * `host:port`, or port-less `host` matching any port.
23
+ * @returns true when the Host is ours (loopback or trusted) and any attached
24
+ * browser markers are same-origin.
25
+ */
26
+ function isTrustedApiRequest(request, trustedHosts) {
27
+ const host = request.headers.host;
28
+ if (typeof host !== 'string')
29
+ return false;
30
+ let hostUrl;
31
+ try {
32
+ hostUrl = new URL(`http://${host}`);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ const hostname = hostUrl.hostname;
38
+ const trusted = isLoopbackClient(request) || trustedHosts.some(entry => {
39
+ // A port-less entry matches the hostname on any port; an exact host:port
40
+ // entry matches that authority verbatim (WHATWG normalization both sides).
41
+ const entryUrl = new URL(`http://${entry}`);
42
+ return entryUrl.port === '' ? entryUrl.hostname === hostname : entryUrl.host === hostUrl.host;
43
+ });
44
+ if (!trusted)
45
+ return false;
46
+ // Cross-site fence: an explicit cross-site marker is refused regardless of
47
+ // Origin (modern browsers label the initiator on every fetch).
48
+ if (request.headers['sec-fetch-site'] === 'cross-site')
49
+ return false;
50
+ // Origin fence: when a browser attaches an Origin it must be exactly this
51
+ // authority; absent Origin is fine — the Host fence already bound it.
52
+ const origin = request.headers.origin;
53
+ if (origin === undefined)
54
+ return true;
55
+ try {
56
+ return new URL(origin).host === hostUrl.host;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ /** Cap on pairing request bodies (tokens and workspace ids are tiny). */
63
+ const MAX_BODY_BYTES = 4096;
64
+ /**
65
+ * The host authority of a configured public base URL, e.g. `foo.trycloudflare.com`
66
+ * from `https://foo.trycloudflare.com`. Undefined when the URL does not parse —
67
+ * a malformed config then simply contributes no fence entry (and the panel
68
+ * falls back to LAN-only URLs).
69
+ * @param url - the configured public base URL (or undefined).
70
+ * @returns the `host[:port]` authority the fence should trust.
71
+ */
72
+ export function publicHostOf(url) {
73
+ if (url === undefined)
74
+ return undefined;
75
+ try {
76
+ return new URL(url).host;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ /** Cookie lifetime: one year; revoked sessions die at the gate regardless. */
83
+ const COOKIE_MAX_AGE_SEC = 365 * 24 * 60 * 60;
84
+ /** Route paths (exact matches under /api). */
85
+ export const PAIR_PATHS = {
86
+ issue: '/api/pair/issue',
87
+ accept: '/api/pair/accept',
88
+ stop: '/api/pair/stop',
89
+ heartbeat: '/api/pair/heartbeat',
90
+ status: '/api/pair/status',
91
+ events: '/api/pair/events',
92
+ };
93
+ /** One JSON response. */
94
+ function writeJson(res, status, body) {
95
+ const payload = JSON.stringify(body);
96
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' });
97
+ res.end(payload);
98
+ }
99
+ /** Read a request body up to MAX_BODY_BYTES and parse it as JSON. */
100
+ async function readJsonBody(req) {
101
+ const chunks = [];
102
+ let size = 0;
103
+ for await (const chunk of req) {
104
+ const buffer = chunk;
105
+ size += buffer.length;
106
+ if (size > MAX_BODY_BYTES)
107
+ return undefined;
108
+ chunks.push(buffer);
109
+ }
110
+ try {
111
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
112
+ return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ /** The SSE fan-out for desktop panel status. */
119
+ export class PairingEventsStream {
120
+ streams = new Set();
121
+ /**
122
+ * @param service - the pairing service whose snapshots are fanned out.
123
+ */
124
+ constructor(service) {
125
+ service.onState((snapshot) => { this.push(snapshot); });
126
+ }
127
+ /** Open one stream; the response is owned to completion. */
128
+ open(req, res) {
129
+ res.writeHead(200, {
130
+ 'content-type': 'text/event-stream; charset=utf-8',
131
+ 'cache-control': 'no-cache',
132
+ connection: 'keep-alive',
133
+ });
134
+ const stream = { res, closed: false };
135
+ this.streams.add(stream);
136
+ const close = () => {
137
+ if (stream.closed)
138
+ return;
139
+ stream.closed = true;
140
+ this.streams.delete(stream);
141
+ };
142
+ res.on('close', close);
143
+ req.on('close', close);
144
+ }
145
+ /** Push one frame to every open stream (contained per stream). */
146
+ push(snapshot) {
147
+ const frame = `data: ${JSON.stringify({ type: 'state', ...snapshot })}\n\n`;
148
+ for (const stream of this.streams) {
149
+ try {
150
+ stream.res.write(frame);
151
+ }
152
+ catch {
153
+ stream.closed = true;
154
+ this.streams.delete(stream);
155
+ }
156
+ }
157
+ }
158
+ /** Stream count (tests/diagnostics). */
159
+ get size() {
160
+ return this.streams.size;
161
+ }
162
+ }
163
+ /**
164
+ * Build the /api/pair route family.
165
+ * @param deps - service + fence inputs.
166
+ * @returns the exact routes to register on webServer.
167
+ */
168
+ export function makeRoutes(deps) {
169
+ const { service, lanAddresses } = deps;
170
+ const events = new PairingEventsStream(service);
171
+ /** Loopback-only fence: the desktop panel's control endpoints. */
172
+ const loopbackFence = (req) => isTrustedApiRequest(req, []);
173
+ /** Phone-facing fence: loopback, the derived LAN literals, or the configured public host. */
174
+ const lanFence = (req) => {
175
+ const publicHost = publicHostOf(service.publicBaseUrl);
176
+ return isTrustedApiRequest(req, publicHost === undefined ? lanAddresses : [...lanAddresses, publicHost]);
177
+ };
178
+ const requireMethod = (req, res, method) => {
179
+ if (req.method === method)
180
+ return true;
181
+ res.writeHead(405);
182
+ res.end();
183
+ return false;
184
+ };
185
+ /** Per-source-IP accept rate limit (brute-force defense in depth). */
186
+ const acceptAttempts = new Map();
187
+ const ACCEPT_MAX_ATTEMPTS = 10;
188
+ const ACCEPT_WINDOW_MS = 30_000;
189
+ const rateLimitAccept = (req) => {
190
+ const ip = req.socket?.remoteAddress ?? 'unknown';
191
+ const nowMs = Date.now();
192
+ const entry = acceptAttempts.get(ip);
193
+ if (entry === undefined || nowMs - entry.windowStart > ACCEPT_WINDOW_MS) {
194
+ acceptAttempts.set(ip, { count: 1, windowStart: nowMs });
195
+ return false;
196
+ }
197
+ entry.count += 1;
198
+ return entry.count > ACCEPT_MAX_ATTEMPTS;
199
+ };
200
+ const handleIssue = async (req, res) => {
201
+ if (!requireMethod(req, res, 'POST'))
202
+ return;
203
+ if (!loopbackFence(req)) {
204
+ writeJson(res, 403, { ok: false, code: 'forbidden' });
205
+ return;
206
+ }
207
+ const body = await readJsonBody(req);
208
+ const workspaceId = body === undefined || typeof body.workspaceId !== 'string' || body.workspaceId === ''
209
+ ? undefined
210
+ : body.workspaceId;
211
+ const address = body === undefined || typeof body.address !== 'string' || body.address === ''
212
+ ? undefined
213
+ : body.address;
214
+ try {
215
+ const { token, expiresAt } = service.issue(workspaceId, address);
216
+ // The default base is the public (tunneled) URL when configured — a
217
+ // phone anywhere can reach it — and the first LAN interface otherwise.
218
+ // An explicit address always names a LAN literal.
219
+ const base = address === undefined ? (service.publicBaseUrl ?? service.lanBaseUrl) : service.lanBaseUrlFor(address);
220
+ if (base === undefined)
221
+ throw new Error('remote-web-ui: base unavailable');
222
+ const workspaceQuery = workspaceId === undefined ? '' : `&workspace=${encodeURIComponent(workspaceId)}`;
223
+ writeJson(res, 200, {
224
+ ok: true,
225
+ url: `${base}/?pair=${token}${workspaceQuery}`,
226
+ token,
227
+ expiresAt,
228
+ // Every constructible base, so a multi-homed panel can switch the
229
+ // advertised network without a second round trip.
230
+ lanAddresses: service.lanAddresses,
231
+ // The configured public base, when present — the panel uses it to
232
+ // label the QR as a public (tunneled) link.
233
+ ...(service.publicBaseUrl !== undefined ? { publicBaseUrl: service.publicBaseUrl } : {}),
234
+ });
235
+ }
236
+ catch (error) {
237
+ // lan-required (no bind) and unknown-address are both configuration
238
+ // mistakes the panel should surface distinctly.
239
+ const unknownAddress = error instanceof UnknownLanAddressError;
240
+ writeJson(res, unknownAddress ? 400 : 409, {
241
+ ok: false,
242
+ code: unknownAddress ? 'unknown-address' : 'lan-required',
243
+ });
244
+ }
245
+ };
246
+ const handleAccept = async (req, res) => {
247
+ if (!requireMethod(req, res, 'POST'))
248
+ return;
249
+ if (!lanFence(req)) {
250
+ writeJson(res, 403, { ok: false, code: 'forbidden' });
251
+ return;
252
+ }
253
+ if (rateLimitAccept(req)) {
254
+ writeJson(res, 429, { ok: false, code: 'rate-limited' });
255
+ return;
256
+ }
257
+ const body = await readJsonBody(req);
258
+ const token = typeof body?.token === 'string' ? body.token : '';
259
+ const result = service.accept(token);
260
+ if (!result.ok) {
261
+ writeJson(res, result.code === 'used' ? 409 : 404, { ok: false, code: result.code });
262
+ return;
263
+ }
264
+ res.writeHead(200, {
265
+ 'content-type': 'application/json; charset=utf-8',
266
+ 'set-cookie': [
267
+ `${service.config.cookieName}=${result.deviceId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${String(COOKIE_MAX_AGE_SEC)}`,
268
+ ],
269
+ });
270
+ res.end(JSON.stringify({ ok: true, deviceId: result.deviceId }));
271
+ };
272
+ const handleStop = async (req, res) => {
273
+ if (!requireMethod(req, res, 'POST'))
274
+ return;
275
+ if (!loopbackFence(req)) {
276
+ writeJson(res, 403, { ok: false, code: 'forbidden' });
277
+ return;
278
+ }
279
+ await readJsonBody(req);
280
+ service.stop();
281
+ writeJson(res, 200, { ok: true });
282
+ };
283
+ const handleHeartbeat = async (req, res) => {
284
+ if (!requireMethod(req, res, 'POST'))
285
+ return;
286
+ if (!lanFence(req)) {
287
+ writeJson(res, 403, { ok: false, code: 'forbidden' });
288
+ return;
289
+ }
290
+ await readJsonBody(req);
291
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
292
+ if (deviceId === undefined || !service.heartbeat(deviceId)) {
293
+ writeJson(res, 401, { ok: false, code: 'unpaired' });
294
+ return;
295
+ }
296
+ writeJson(res, 200, { ok: true });
297
+ };
298
+ const handleStatus = async (req, res) => {
299
+ if (!requireMethod(req, res, 'GET'))
300
+ return;
301
+ if (!lanFence(req)) {
302
+ writeJson(res, 403, { ok: false, code: 'forbidden' });
303
+ return;
304
+ }
305
+ const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
306
+ writeJson(res, 200, { ok: true, paired: deviceId !== undefined && service.hasDevice(deviceId), ...service.snapshot() });
307
+ };
308
+ const handleEvents = (req, res) => {
309
+ if (!requireMethod(req, res, 'GET'))
310
+ return;
311
+ if (!loopbackFence(req)) {
312
+ writeJson(res, 403, { ok: false, code: 'forbidden' });
313
+ return;
314
+ }
315
+ events.open(req, res);
316
+ // Snapshot on open: a late-opening panel converges without history.
317
+ events.push(service.snapshot());
318
+ };
319
+ return [
320
+ { kind: 'exact', path: PAIR_PATHS.issue, handler: handleIssue },
321
+ { kind: 'exact', path: PAIR_PATHS.accept, handler: handleAccept },
322
+ { kind: 'exact', path: PAIR_PATHS.stop, handler: handleStop },
323
+ { kind: 'exact', path: PAIR_PATHS.heartbeat, handler: handleHeartbeat },
324
+ { kind: 'exact', path: PAIR_PATHS.status, handler: handleStatus },
325
+ { kind: 'exact', path: PAIR_PATHS.events, handler: handleEvents },
326
+ ];
327
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"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"}