@linxin666/dsh-remote-web-ui 0.1.19 → 0.2.0
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 +41 -19
- package/README.zh.md +17 -12
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +64 -0
- package/lib/client.js +57 -13
- package/lib/client.js.map +1 -1
- package/lib/index.js +408 -130
- package/lib/mobile.js +296 -22
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +7 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +5 -3
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +43 -13
- package/lib/types/client/RemotePanel.d.ts +1 -0
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -3
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +37 -4
- package/lib/types/client/UpdatePanel.js +1 -1
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.js +7 -7
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/update-api.d.ts.map +1 -1
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +16 -5
- package/lib/types/http.d.ts +14 -0
- package/lib/types/http.d.ts.map +1 -0
- package/lib/types/http.js +27 -0
- package/lib/types/index.d.ts +28 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +41 -22
- package/lib/types/invariant.d.ts.map +1 -1
- package/lib/types/mobile/PairRequiredView.d.ts +7 -0
- package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.js +28 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.test.js +24 -0
- package/lib/types/mobile/api.js +1 -1
- package/lib/types/mobile/index.js +17 -4
- package/lib/types/mobile/markdown.d.ts +6 -4
- package/lib/types/mobile/markdown.d.ts.map +1 -1
- package/lib/types/mobile/markdown.js +8 -4
- package/lib/types/mobile/markdown.test.js +5 -1
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +14 -1
- package/lib/types/mobile/messages.test.js +1 -1
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +48 -0
- package/lib/types/mobile/mobile-theme.test.js +2 -2
- package/lib/types/mobile/mux.d.ts.map +1 -1
- package/lib/types/mobile/mux.js +1 -1
- package/lib/types/mobile/mux.test.js +1 -1
- package/lib/types/mobile/pairing.d.ts +31 -0
- package/lib/types/mobile/pairing.d.ts.map +1 -0
- package/lib/types/mobile/pairing.js +62 -0
- package/lib/types/mobile/rpc.d.ts.map +1 -1
- package/lib/types/mobile/theme-toggle.js +1 -1
- package/lib/types/mobile/views/App.d.ts +12 -16
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +39 -12
- package/lib/types/mobile/views/App.test.d.ts +2 -0
- package/lib/types/mobile/views/App.test.d.ts.map +1 -0
- package/lib/types/mobile/views/App.test.js +52 -0
- package/lib/types/mobile/views/ChatView.js +6 -6
- package/lib/types/mobile/views/ChatView.test.js +3 -3
- package/lib/types/mobile/views/SessionListView.js +3 -3
- package/lib/types/mobile/views/SessionListView.test.js +2 -2
- package/lib/types/mobile/views/WorkspaceView.js +3 -3
- package/lib/types/mobile/views/WorkspaceView.test.js +3 -3
- package/lib/types/mobile-api.d.ts +1 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +11 -18
- package/lib/types/mobile-pwa.d.ts +6 -0
- package/lib/types/mobile-pwa.d.ts.map +1 -0
- package/lib/types/mobile-pwa.js +17 -0
- package/lib/types/mobile-routes.d.ts +5 -8
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +130 -39
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/lib/types/mount-once.js +42 -0
- package/lib/types/pairing-access.d.ts +28 -0
- package/lib/types/pairing-access.d.ts.map +1 -0
- package/lib/types/pairing-access.js +26 -0
- package/lib/types/pairing.d.ts +27 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +68 -0
- package/lib/types/pairing.persist.test.d.ts +2 -0
- package/lib/types/pairing.persist.test.d.ts.map +1 -0
- package/lib/types/pairing.persist.test.js +128 -0
- package/lib/types/routes.d.ts +6 -6
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +15 -20
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/update-routes.d.ts +2 -2
- package/lib/types/update-routes.d.ts.map +1 -1
- package/lib/types/update-routes.js +1 -6
- package/lib/types/update.d.ts +3 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +40 -14
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +10 -0
- package/src/client/RemoteEntry.tsx +40 -8
- package/src/client/RemotePanel.tsx +1 -1
- package/src/client/RemoteSettingsCard.tsx +1 -0
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +2 -0
- package/src/client/remote.module.css +26 -0
- package/src/gate.ts +15 -3
- package/src/http.ts +30 -0
- package/src/index.ts +50 -16
- package/src/mobile/PairRequiredView.test.tsx +28 -0
- package/src/mobile/PairRequiredView.tsx +58 -0
- package/src/mobile/index.tsx +15 -1
- package/src/mobile/markdown.test.ts +5 -0
- package/src/mobile/markdown.ts +7 -4
- package/src/mobile/messages.ts +14 -1
- package/src/mobile/mobile-styles.ts +48 -0
- package/src/mobile/pairing.ts +77 -0
- package/src/mobile/views/App.test.tsx +62 -0
- package/src/mobile/views/App.tsx +53 -18
- package/src/mobile-api.ts +9 -17
- package/src/mobile-pwa.ts +19 -0
- package/src/mobile-routes.ts +137 -38
- package/src/mount-once.ts +48 -0
- package/src/pairing-access.ts +36 -0
- package/src/pairing.persist.test.ts +145 -0
- package/src/pairing.ts +70 -1
- package/src/routes.ts +12 -18
- package/src/update-routes.ts +1 -7
- package/src/update.ts +46 -15
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PairingService device-session persistence: with `devicesFile` set, sessions
|
|
3
|
+
* written on accept/stop survive a process restart (a new PairingService
|
|
4
|
+
* instance), so a previously paired phone — whose cookie already lives 365
|
|
5
|
+
* days — never needs to re-scan the QR after `dsh web` restarts.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { pairingConfigOf } from './index.js';
|
|
12
|
+
import { PairingService } from './pairing.js';
|
|
13
|
+
const BASE_CONFIG = {
|
|
14
|
+
tokenTtlMs: 60_000,
|
|
15
|
+
offlineAfterMs: 25_000,
|
|
16
|
+
maxDevices: 4,
|
|
17
|
+
cookieName: 'dsh_pair',
|
|
18
|
+
};
|
|
19
|
+
/** Deterministic clock: tokens are issued in a fixed, readable sequence. */
|
|
20
|
+
function makeClock() {
|
|
21
|
+
let n = 0;
|
|
22
|
+
return {
|
|
23
|
+
now: () => 1_000_000 + n,
|
|
24
|
+
randomToken: () => `tok${(n++).toString().padStart(6, '0')}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Pair a device on a service: issue a token and immediately accept it. */
|
|
28
|
+
function pairDevice(service) {
|
|
29
|
+
service.setPublicBaseUrl('https://pairing.example.trycloudflare.com');
|
|
30
|
+
const { token } = service.issue();
|
|
31
|
+
const result = service.accept(token);
|
|
32
|
+
if (!result.ok)
|
|
33
|
+
throw new Error(`pair failed: ${result.code}`);
|
|
34
|
+
return result.deviceId;
|
|
35
|
+
}
|
|
36
|
+
describe('PairingService device persistence', () => {
|
|
37
|
+
let dir;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
dir = mkdtempSync(join(tmpdir(), 'remote-web-ui-persist-'));
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
rmSync(dir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
it('restores paired sessions across a process restart', () => {
|
|
45
|
+
const file = join(dir, 'devices.json');
|
|
46
|
+
const first = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock());
|
|
47
|
+
const deviceId = pairDevice(first);
|
|
48
|
+
expect(readFileSync(file, 'utf8')).toContain(deviceId);
|
|
49
|
+
// Device ids are session credentials: the persisted file is 0600.
|
|
50
|
+
expect(statSync(file).mode & 0o777).toBe(0o600);
|
|
51
|
+
// Simulated restart: a brand-new service instance reading the same file.
|
|
52
|
+
const second = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock());
|
|
53
|
+
expect(second.hasDevice(deviceId)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('carries devicesFile through the config mapping used by sync() (regression)', () => {
|
|
56
|
+
const file = join(dir, 'devices.json');
|
|
57
|
+
const mapped = pairingConfigOf({
|
|
58
|
+
tokenTtlMs: 60_000,
|
|
59
|
+
offlineAfterMs: 25_000,
|
|
60
|
+
maxDevices: 4,
|
|
61
|
+
cookieName: 'dsh_pair',
|
|
62
|
+
devicesFile: file,
|
|
63
|
+
});
|
|
64
|
+
expect(mapped.devicesFile).toBe(file);
|
|
65
|
+
});
|
|
66
|
+
it('keeps persisting after a sync-style service.config rebuild', () => {
|
|
67
|
+
const file = join(dir, 'devices.json');
|
|
68
|
+
const service = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock());
|
|
69
|
+
// applyImpl/sync() reassigns service.config from the resolved config on
|
|
70
|
+
// mount and on every settings change; the rebuild must not drop devicesFile.
|
|
71
|
+
service.config = pairingConfigOf({
|
|
72
|
+
tokenTtlMs: 60_000,
|
|
73
|
+
offlineAfterMs: 25_000,
|
|
74
|
+
maxDevices: 4,
|
|
75
|
+
cookieName: 'dsh_pair',
|
|
76
|
+
devicesFile: file,
|
|
77
|
+
});
|
|
78
|
+
const deviceId = pairDevice(service);
|
|
79
|
+
const restarted = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock());
|
|
80
|
+
expect(restarted.hasDevice(deviceId)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('clamps a persisted table to maxDevices when the file holds more sessions', () => {
|
|
83
|
+
const file = join(dir, 'devices.json');
|
|
84
|
+
writeFileSync(file, JSON.stringify({
|
|
85
|
+
dev1: { createdAt: 1_000, lastSeenAt: 1_000 },
|
|
86
|
+
dev2: { createdAt: 2_000, lastSeenAt: 2_000 },
|
|
87
|
+
dev3: { createdAt: 3_000, lastSeenAt: 3_000 },
|
|
88
|
+
}));
|
|
89
|
+
const service = new PairingService({ ...BASE_CONFIG, maxDevices: 2, devicesFile: file }, makeClock());
|
|
90
|
+
expect(service.hasDevice('dev3')).toBe(true);
|
|
91
|
+
expect(service.hasDevice('dev2')).toBe(true);
|
|
92
|
+
expect(service.hasDevice('dev1')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it('keeps sessions memory-only when devicesFile is unset', () => {
|
|
95
|
+
const first = new PairingService({ ...BASE_CONFIG }, makeClock());
|
|
96
|
+
const deviceId = pairDevice(first);
|
|
97
|
+
// No file was written; a fresh instance without the option knows nothing.
|
|
98
|
+
const second = new PairingService({ ...BASE_CONFIG }, makeClock());
|
|
99
|
+
expect(second.hasDevice(deviceId)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
it('persists revocation by stop() across a restart', () => {
|
|
102
|
+
const file = join(dir, 'devices.json');
|
|
103
|
+
const first = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock());
|
|
104
|
+
const deviceId = pairDevice(first);
|
|
105
|
+
first.stop();
|
|
106
|
+
const second = new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock());
|
|
107
|
+
expect(second.hasDevice(deviceId)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
it('persists FIFO eviction when the device cap is reached', () => {
|
|
110
|
+
const file = join(dir, 'devices.json');
|
|
111
|
+
const first = new PairingService({ ...BASE_CONFIG, maxDevices: 2, devicesFile: file }, makeClock());
|
|
112
|
+
const firstDevice = pairDevice(first);
|
|
113
|
+
const secondDevice = pairDevice(first);
|
|
114
|
+
const thirdDevice = pairDevice(first); // evicts firstDevice
|
|
115
|
+
expect(first.hasDevice(firstDevice)).toBe(false);
|
|
116
|
+
const second = new PairingService({ ...BASE_CONFIG, maxDevices: 2, devicesFile: file }, makeClock());
|
|
117
|
+
expect(second.hasDevice(secondDevice)).toBe(true);
|
|
118
|
+
expect(second.hasDevice(thirdDevice)).toBe(true);
|
|
119
|
+
expect(second.hasDevice(firstDevice)).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
it('tolerates a corrupt or missing file instead of refusing to boot', () => {
|
|
122
|
+
const file = join(dir, 'devices.json');
|
|
123
|
+
writeFileSync(file, '{ this is not json !!!');
|
|
124
|
+
expect(() => new PairingService({ ...BASE_CONFIG, devicesFile: file }, makeClock())).not.toThrow();
|
|
125
|
+
const ghost = join(dir, 'does-not-exist.json');
|
|
126
|
+
expect(() => new PairingService({ ...BASE_CONFIG, devicesFile: ghost }, makeClock())).not.toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -37,12 +37,12 @@ export declare function isTrustedApiRequest(request: IncomingMessage, trustedHos
|
|
|
37
37
|
export declare function publicHostOf(url: string | undefined): string | undefined;
|
|
38
38
|
/** Route paths (exact matches under /api). */
|
|
39
39
|
export declare const PAIR_PATHS: {
|
|
40
|
-
readonly issue:
|
|
41
|
-
readonly accept:
|
|
42
|
-
readonly stop:
|
|
43
|
-
readonly heartbeat:
|
|
44
|
-
readonly status:
|
|
45
|
-
readonly events:
|
|
40
|
+
readonly issue: '/api/pair/issue';
|
|
41
|
+
readonly accept: '/api/pair/accept';
|
|
42
|
+
readonly stop: '/api/pair/stop';
|
|
43
|
+
readonly heartbeat: '/api/pair/heartbeat';
|
|
44
|
+
readonly status: '/api/pair/status';
|
|
45
|
+
readonly events: '/api/pair/events';
|
|
46
46
|
};
|
|
47
47
|
/**
|
|
48
48
|
* /api/pair request payload contracts. Each POST endpoint validates its body
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;aACrB,KAAK,EAAE,iBAAiB;aACxB,MAAM,EAAE,kBAAkB;aAC1B,IAAI,EAAE,gBAAgB;aACtB,SAAS,EAAE,qBAAqB;aAChC,MAAM,EAAE,kBAAkB;aAC1B,MAAM,EAAE,kBAAkB;CAClB,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;IACH,YAAY,OAAO,EAAE,cAAc,EAElC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAepD;IAED,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAUpC;IAED,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CAoL3D"}
|
package/lib/types/routes.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* on every other /api request.
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
import { UnknownLanAddressError } from
|
|
12
|
-
import { isLoopbackClient, readCookie } from
|
|
11
|
+
import { UnknownLanAddressError } from './pairing.js';
|
|
12
|
+
import { isLoopbackClient, readCookie } from './gate.js';
|
|
13
|
+
import { readBoundedJson, writeJson } from './http.js';
|
|
13
14
|
/**
|
|
14
15
|
* Browser-trust fence for the /api/pair routes, mirroring the connection
|
|
15
16
|
* package's internal fence semantics (Host/Origin based, DNS-rebinding and
|
|
@@ -117,25 +118,10 @@ function parsePairPayload(schema, body) {
|
|
|
117
118
|
const result = schema.safeParse(body ?? {});
|
|
118
119
|
return result.success ? result.data : undefined;
|
|
119
120
|
}
|
|
120
|
-
/**
|
|
121
|
-
function writeJson(res, status, body) {
|
|
122
|
-
const payload = JSON.stringify(body);
|
|
123
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' });
|
|
124
|
-
res.end(payload);
|
|
125
|
-
}
|
|
126
|
-
/** Read a request body up to MAX_BODY_BYTES and parse it as JSON. */
|
|
121
|
+
/** Read a request body up to MAX_BODY_BYTES and parse it as JSON (undefined on failure). */
|
|
127
122
|
async function readJsonBody(req) {
|
|
128
|
-
const chunks = [];
|
|
129
|
-
let size = 0;
|
|
130
|
-
for await (const chunk of req) {
|
|
131
|
-
const buffer = chunk;
|
|
132
|
-
size += buffer.length;
|
|
133
|
-
if (size > MAX_BODY_BYTES)
|
|
134
|
-
return undefined;
|
|
135
|
-
chunks.push(buffer);
|
|
136
|
-
}
|
|
137
123
|
try {
|
|
138
|
-
const parsed =
|
|
124
|
+
const parsed = await readBoundedJson(req, MAX_BODY_BYTES);
|
|
139
125
|
return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
|
|
140
126
|
}
|
|
141
127
|
catch {
|
|
@@ -216,6 +202,15 @@ export function makeRoutes(deps) {
|
|
|
216
202
|
const rateLimitAccept = (req) => {
|
|
217
203
|
const ip = req.socket?.remoteAddress ?? 'unknown';
|
|
218
204
|
const nowMs = Date.now();
|
|
205
|
+
// The map lives as long as the plugin: prune expired windows once the
|
|
206
|
+
// table grows past a modest size so distinct source IPs (LAN clients,
|
|
207
|
+
// brute-force scans) cannot accumulate forever.
|
|
208
|
+
if (acceptAttempts.size > 256) {
|
|
209
|
+
for (const [key, attempt] of acceptAttempts) {
|
|
210
|
+
if (nowMs - attempt.windowStart > ACCEPT_WINDOW_MS)
|
|
211
|
+
acceptAttempts.delete(key);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
219
214
|
const entry = acceptAttempts.get(ip);
|
|
220
215
|
if (entry === undefined || nowMs - entry.windowStart > ACCEPT_WINDOW_MS) {
|
|
221
216
|
acceptAttempts.set(ip, { count: 1, windowStart: nowMs });
|
|
@@ -249,7 +244,7 @@ export function makeRoutes(deps) {
|
|
|
249
244
|
const workspaceQuery = workspaceId === undefined ? '' : `&workspace=${encodeURIComponent(workspaceId)}`;
|
|
250
245
|
writeJson(res, 200, {
|
|
251
246
|
ok: true,
|
|
252
|
-
url: `${base}/?pair=${token}${workspaceQuery}`,
|
|
247
|
+
url: `${base}/m/?pair=${token}${workspaceQuery}`,
|
|
253
248
|
token,
|
|
254
249
|
expiresAt,
|
|
255
250
|
// Every constructible base, so a multi-homed panel can switch the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IAC7C,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC5F;AAkBD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqF;IAE3G,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IAEvE;;OAEG;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,8 +9,8 @@ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
|
9
9
|
import type { UpdateRunResult, UpdateStatus } from './update.ts';
|
|
10
10
|
/** Route paths (exact matches under /api). */
|
|
11
11
|
export declare const UPDATE_PATHS: {
|
|
12
|
-
readonly status:
|
|
13
|
-
readonly run:
|
|
12
|
+
readonly status: '/api/update/status';
|
|
13
|
+
readonly run: '/api/update/run';
|
|
14
14
|
};
|
|
15
15
|
/** Route-family dependencies (test seam). */
|
|
16
16
|
export interface UpdateRoutesDeps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-routes.d.ts","sourceRoot":"","sources":["../../src/update-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"update-routes.d.ts","sourceRoot":"","sources":["../../src/update-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAGhE,8CAA8C;AAC9C,eAAO,MAAM,YAAY;aACvB,MAAM,EAAE,oBAAoB;aAC5B,GAAG,EAAE,iBAAiB;CACd,CAAA;AAEV,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAA;IACxC,yDAAyD;IACzD,KAAK,IAAI,OAAO,CAAC,YAAY,CAAC,CAAA;IAC9B,sDAAsD;IACtD,GAAG,IAAI,OAAO,CAAC,eAAe,CAAC,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,QAAQ,EAAE,CA+BnE"}
|
|
@@ -4,17 +4,12 @@
|
|
|
4
4
|
* pnpm install inside the owning profile, so it must never be reachable
|
|
5
5
|
* from a LAN/phone origin.
|
|
6
6
|
*/
|
|
7
|
+
import { writeJson } from './http.js';
|
|
7
8
|
/** Route paths (exact matches under /api). */
|
|
8
9
|
export const UPDATE_PATHS = {
|
|
9
10
|
status: '/api/update/status',
|
|
10
11
|
run: '/api/update/run',
|
|
11
12
|
};
|
|
12
|
-
/** One JSON response. */
|
|
13
|
-
function writeJson(res, status, body) {
|
|
14
|
-
const payload = JSON.stringify(body);
|
|
15
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' });
|
|
16
|
-
res.end(payload);
|
|
17
|
-
}
|
|
18
13
|
/**
|
|
19
14
|
* Build the /api/update route family.
|
|
20
15
|
* @param deps - fence + check/run seams.
|
package/lib/types/update.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Remote update support for the dsh-web-ui family — host half. Detects the
|
|
3
|
-
* installed aggregate package (@linxin666/dsh-web-ui-all)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* installed aggregate package (@linxin666/dsh-web-ui-all), or the directly
|
|
4
|
+
* installed family packages when the aggregate is absent, probes npm for newer
|
|
5
|
+
* releases, and runs `pnpm update --latest` inside the owning dsh profile.
|
|
6
6
|
*
|
|
7
7
|
* Pure logic with injected seams (manifest reading, registry fetches, process
|
|
8
8
|
* spawning) so the whole surface is unit-testable without touching disk,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
package/lib/types/update.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Remote update support for the dsh-web-ui family — host half. Detects the
|
|
3
|
-
* installed aggregate package (@linxin666/dsh-web-ui-all)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* installed aggregate package (@linxin666/dsh-web-ui-all), or the directly
|
|
4
|
+
* installed family packages when the aggregate is absent, probes npm for newer
|
|
5
|
+
* releases, and runs `pnpm update --latest` inside the owning dsh profile.
|
|
6
6
|
*
|
|
7
7
|
* Pure logic with injected seams (manifest reading, registry fetches, process
|
|
8
8
|
* spawning) so the whole surface is unit-testable without touching disk,
|
|
@@ -173,10 +173,13 @@ export function resolveUpdateTarget(deps) {
|
|
|
173
173
|
const spec = profileManifest?.dependencies?.[anchor];
|
|
174
174
|
if (isLinkedSpec(spec))
|
|
175
175
|
return { error: 'link' };
|
|
176
|
+
// Standalone installs carry no aggregate: the anchor's own dependency
|
|
177
|
+
// list misses every sibling @linxin666/* plugin installed directly into
|
|
178
|
+
// the profile, so union the profile's direct family deps (#377).
|
|
176
179
|
return {
|
|
177
180
|
profileName: profile.name,
|
|
178
181
|
profileDir: profile.dir,
|
|
179
|
-
packages:
|
|
182
|
+
packages: familyUpdatePackages(anchor, manifest, profileManifest),
|
|
180
183
|
};
|
|
181
184
|
}
|
|
182
185
|
/** Family children of the anchor: its dependencies under the family scope. */
|
|
@@ -191,6 +194,18 @@ export function familyChildren(anchorManifest) {
|
|
|
191
194
|
}
|
|
192
195
|
return names;
|
|
193
196
|
}
|
|
197
|
+
/** Registry-managed family packages covered by one update operation. */
|
|
198
|
+
function familyUpdatePackages(anchor, anchorManifest, profileManifest) {
|
|
199
|
+
const names = new Set([anchor, ...familyChildren(anchorManifest)]);
|
|
200
|
+
const dependencies = profileManifest?.dependencies;
|
|
201
|
+
if (typeof dependencies !== 'object' || dependencies === null)
|
|
202
|
+
return [...names];
|
|
203
|
+
for (const [name, spec] of Object.entries(dependencies)) {
|
|
204
|
+
if (name.startsWith(FAMILY_SCOPE) && typeof spec === 'string' && !isLinkedSpec(spec))
|
|
205
|
+
names.add(name);
|
|
206
|
+
}
|
|
207
|
+
return [...names];
|
|
208
|
+
}
|
|
194
209
|
/**
|
|
195
210
|
* Probe the npm registry for one package's latest release.
|
|
196
211
|
* @param name - the package name (scope slash URL-encoded).
|
|
@@ -228,15 +243,20 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
|
228
243
|
*/
|
|
229
244
|
const VERSION_UNKNOWN = '0.0.0';
|
|
230
245
|
/** The resolved current version of one family package (probe failure tolerated). */
|
|
231
|
-
function readInstalledVersion(resolve, name) {
|
|
246
|
+
function readInstalledVersion(resolve, name, profileDir) {
|
|
232
247
|
try {
|
|
233
248
|
const path = resolve(name + '/package.json');
|
|
234
249
|
const version = path === undefined ? undefined : readManifest(path)?.version;
|
|
235
|
-
|
|
250
|
+
if (typeof version === 'string')
|
|
251
|
+
return version;
|
|
236
252
|
}
|
|
237
|
-
catch {
|
|
253
|
+
catch { /* fall through to the profile's direct dependency path */ }
|
|
254
|
+
// Names originate in the profile dependency map, but still validate the
|
|
255
|
+
// npm package shape before using one as path segments.
|
|
256
|
+
if (profileDir === undefined || !/^@linxin666\/[a-z0-9][a-z0-9._-]*$/.test(name))
|
|
238
257
|
return VERSION_UNKNOWN;
|
|
239
|
-
|
|
258
|
+
const version = readManifest(join(profileDir, 'node_modules', ...name.split('/'), 'package.json'))?.version;
|
|
259
|
+
return typeof version === 'string' ? version : VERSION_UNKNOWN;
|
|
240
260
|
}
|
|
241
261
|
/**
|
|
242
262
|
* Build the update status: locate the anchor, detect the install mode, and
|
|
@@ -267,21 +287,27 @@ export async function checkUpdates(deps) {
|
|
|
267
287
|
if (profile === undefined) {
|
|
268
288
|
return { mode: 'link', packages: [], outdated: false };
|
|
269
289
|
}
|
|
270
|
-
|
|
290
|
+
// Union the profile's direct family deps so standalone installs (no
|
|
291
|
+
// aggregate) still check every installed @linxin666/* plugin (#377);
|
|
292
|
+
// familyUpdatePackages skips link:/file: development dependencies.
|
|
293
|
+
const names = familyUpdatePackages(anchor, manifest, profileManifest);
|
|
294
|
+
// The registry probes are independent: run them together instead of
|
|
295
|
+
// serializing up to N x 10s of registry latency behind one status call.
|
|
296
|
+
const latestList = await Promise.all(names.map(name => deps.fetchLatest(name)));
|
|
271
297
|
const packages = [];
|
|
272
298
|
let probeFailures = 0;
|
|
273
|
-
|
|
274
|
-
const latest =
|
|
299
|
+
names.forEach((name, index) => {
|
|
300
|
+
const latest = latestList[index];
|
|
275
301
|
if (latest === undefined)
|
|
276
302
|
probeFailures++;
|
|
277
|
-
const current = readInstalledVersion(deps.resolve, name);
|
|
303
|
+
const current = readInstalledVersion(deps.resolve, name, profile.dir);
|
|
278
304
|
packages.push({
|
|
279
305
|
name,
|
|
280
306
|
current,
|
|
281
307
|
latest,
|
|
282
308
|
outdated: latest !== undefined && latest !== current && compareVersions(latest, current) > 0,
|
|
283
309
|
});
|
|
284
|
-
}
|
|
310
|
+
});
|
|
285
311
|
// Registry unreachable: every probe failed — report the outage distinctly
|
|
286
312
|
// instead of a misleading "all up to date" (the panel needs the reason).
|
|
287
313
|
const error = probeFailures === names.length && names.length > 0 ? 'registry-unreachable' : undefined;
|
|
@@ -491,7 +517,7 @@ export async function runUpdateVerified(deps) {
|
|
|
491
517
|
// green exit that left everything in place could be mistaken for success.
|
|
492
518
|
const before = new Map();
|
|
493
519
|
for (const name of deps.run.packages) {
|
|
494
|
-
const version = readInstalledVersion(deps.check.resolve, name);
|
|
520
|
+
const version = readInstalledVersion(deps.check.resolve, name, deps.run.profileDir);
|
|
495
521
|
if (version !== VERSION_UNKNOWN)
|
|
496
522
|
before.set(name, version);
|
|
497
523
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -61,17 +61,17 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.
|
|
70
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.
|
|
71
|
-
"@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.
|
|
72
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.
|
|
73
|
-
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.
|
|
74
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.
|
|
64
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
|
|
66
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
|
|
71
|
+
"@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.7",
|
|
72
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
|
|
73
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
|
|
74
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
75
75
|
"@testing-library/dom": "^10.4.1",
|
|
76
76
|
"@testing-library/react": "^16.3.2",
|
|
77
77
|
"@types/node": "^22.20.0",
|
|
@@ -56,6 +56,12 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
56
56
|
* already provides the selection.
|
|
57
57
|
*/
|
|
58
58
|
alwaysOpen?: boolean
|
|
59
|
+
/**
|
|
60
|
+
* Hide the save/discard footer: cards whose body applies its own changes
|
|
61
|
+
* immediately (an embedded external settings section) have no staged
|
|
62
|
+
* edits, so the footer would sit there permanently disabled.
|
|
63
|
+
*/
|
|
64
|
+
hideFooter?: boolean
|
|
59
65
|
/** The plugin's controls. */
|
|
60
66
|
children: ReactNode
|
|
61
67
|
}
|
|
@@ -140,6 +146,9 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
140
146
|
<div className={css.body}>
|
|
141
147
|
{!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}
|
|
142
148
|
{props.children}
|
|
149
|
+
{props.hideFooter === true
|
|
150
|
+
? null
|
|
151
|
+
: (
|
|
143
152
|
<div className={css.footer}>
|
|
144
153
|
{state.failed
|
|
145
154
|
? (
|
|
@@ -165,6 +174,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
165
174
|
{props.t(!state.saving ? 'settings.save' : 'settings.saving')}
|
|
166
175
|
</button>
|
|
167
176
|
</div>
|
|
177
|
+
)}
|
|
168
178
|
</div>
|
|
169
179
|
)
|
|
170
180
|
: null}
|
|
@@ -20,8 +20,18 @@ import css from './remote.module.css'
|
|
|
20
20
|
/** Entry props: the sidebar column state + the standard locale seat. */
|
|
21
21
|
export type RemoteEntryProps = PropsRuntime<'sidebar.remote'> & PropsLocale<'remote'>
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Apply one status frame onto the current state: the ready state mirrors
|
|
25
|
+
* the full phase/device picture, while the lan-required banner only keeps
|
|
26
|
+
* the auto-tunnel frame (the signal for the running re-issue).
|
|
27
|
+
*/
|
|
24
28
|
function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
|
|
29
|
+
if (state.kind === 'lan-required') {
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
...(frame.tunnel !== undefined ? { tunnel: frame.tunnel } : {}),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
25
35
|
if (state.kind !== 'ready') return state
|
|
26
36
|
return {
|
|
27
37
|
...state,
|
|
@@ -40,6 +50,11 @@ function mergeFrame(state: PanelState, frame: PairStateFrame): PanelState {
|
|
|
40
50
|
export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
41
51
|
const [open, setOpen] = useState(false)
|
|
42
52
|
const [state, setState] = useState<PanelState>({ kind: 'lan-required' })
|
|
53
|
+
// Latest-state mirror for the EventSource callback: transition detection
|
|
54
|
+
// must live outside setState updaters (updaters may run twice and must be
|
|
55
|
+
// pure), so mint decisions read this ref instead.
|
|
56
|
+
const stateRef = useRef(state)
|
|
57
|
+
useEffect(() => { stateRef.current = state }, [state])
|
|
43
58
|
const [copied, setCopied] = useState(false)
|
|
44
59
|
const eventSource = useRef<EventSource | undefined>(undefined)
|
|
45
60
|
|
|
@@ -93,17 +108,34 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
93
108
|
const next = await mint()
|
|
94
109
|
setState(next)
|
|
95
110
|
// Live status: the desktop panel mirrors the pairing service state. The
|
|
96
|
-
// stream
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
111
|
+
// stream makes sense in the ready state and on the lan-required banner —
|
|
112
|
+
// there the auto-tunnel may still be starting, and following its frames
|
|
113
|
+
// lets the panel re-issue once it runs. The loopback-required and
|
|
114
|
+
// unreachable origins are fenced out of the events endpoint, so opening
|
|
115
|
+
// it there would just start a doomed reconnect loop.
|
|
116
|
+
if (next.kind !== 'ready' && next.kind !== 'lan-required') return
|
|
100
117
|
const source = new EventSource('/api/pair/events')
|
|
101
118
|
eventSource.current = source
|
|
102
119
|
source.onmessage = (event) => {
|
|
103
120
|
try {
|
|
104
121
|
const frame = JSON.parse(event.data as string) as PairStateFrame
|
|
105
122
|
if (frame.type !== 'state') return
|
|
106
|
-
|
|
123
|
+
// The auto-tunnel crossed into running while the panel sat on the
|
|
124
|
+
// lan-required banner: re-issue so the server hands back a ready QR
|
|
125
|
+
// built on the public base (only on the transition into running, to
|
|
126
|
+
// avoid mint storms). Detected on the ref, outside the updater: an
|
|
127
|
+
// updater may be invoked twice and must stay pure, so mint() cannot
|
|
128
|
+
// run inside it.
|
|
129
|
+
const previous = stateRef.current
|
|
130
|
+
if (
|
|
131
|
+
previous.kind === 'lan-required'
|
|
132
|
+
&& frame.tunnel?.state === 'running'
|
|
133
|
+
&& previous.tunnel?.state !== 'running'
|
|
134
|
+
) {
|
|
135
|
+
void mint().then(setState)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
setState(current => mergeFrame(current, frame))
|
|
107
139
|
} catch {
|
|
108
140
|
// Malformed frames are dropped; the snapshot on open is authoritative.
|
|
109
141
|
}
|
|
@@ -190,13 +222,13 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
190
222
|
)
|
|
191
223
|
}
|
|
192
224
|
|
|
193
|
-
/** The trigger: an icon
|
|
225
|
+
/** The trigger: an icon-only control with a persistent accessible label. */
|
|
194
226
|
function TooltipAnchor({ wide, label, onClick }: { wide: boolean; label: string; onClick: () => void }) {
|
|
195
227
|
return (
|
|
196
228
|
<button
|
|
197
229
|
type="button"
|
|
198
230
|
className={css.trigger}
|
|
199
|
-
data-wide={wide ?
|
|
231
|
+
data-wide={wide ? 'wide' : 'rail'}
|
|
200
232
|
aria-label={label}
|
|
201
233
|
title={label}
|
|
202
234
|
onClick={onClick}
|
|
@@ -16,7 +16,7 @@ import css from './remote.module.css'
|
|
|
16
16
|
|
|
17
17
|
/** The panel's view state, owned by the entry component. */
|
|
18
18
|
export type PanelState =
|
|
19
|
-
| { kind: 'lan-required' }
|
|
19
|
+
| { kind: 'lan-required'; tunnel?: TunnelStatusFrame }
|
|
20
20
|
| { kind: 'loopback-required' }
|
|
21
21
|
| { kind: 'unreachable' }
|
|
22
22
|
| {
|