@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.1
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 +56 -26
- package/README.zh.md +20 -15
- 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 +272 -47
- package/lib/client.js.map +1 -1
- package/lib/index.js +316 -60
- package/lib/mobile.js +487 -56
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +28 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +147 -6
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +13 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +36 -3
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +1 -1
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +17 -4
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +30 -13
- 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.d.ts +10 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +4 -0
- package/lib/types/mobile/index.js +14 -1
- package/lib/types/mobile/messages.d.ts +24 -0
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +60 -23
- package/lib/types/mobile/messages.test.js +49 -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 +86 -0
- 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/views/App.d.ts +12 -5
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +34 -7
- 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.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +34 -11
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +30 -4
- package/lib/types/mobile/views/SessionListView.test.js +58 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +4 -2
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +10 -1
- 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 +115 -30
- 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.map +1 -1
- package/lib/types/routes.js +23 -3
- package/lib/types/tunnel.d.ts +1 -0
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -2
- package/lib/types/update.d.ts +4 -4
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +35 -12
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +229 -18
- package/src/client/RemoteEntry.tsx +12 -2
- package/src/client/RemoteSettingsCard.tsx +3 -2
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/UpdatePanel.tsx +2 -0
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +4 -0
- package/src/client/remote.module.css +26 -0
- package/src/client/settings-card.module.css +90 -1
- package/src/gate.ts +17 -3
- package/src/index.ts +46 -15
- package/src/mobile/PairRequiredView.test.tsx +28 -0
- package/src/mobile/PairRequiredView.tsx +58 -0
- package/src/mobile/api.ts +14 -1
- package/src/mobile/index.tsx +15 -1
- package/src/mobile/messages.test.ts +52 -1
- package/src/mobile/messages.ts +65 -22
- package/src/mobile/mobile-styles.ts +86 -0
- package/src/mobile/pairing.ts +77 -0
- package/src/mobile/views/App.test.tsx +62 -0
- package/src/mobile/views/App.tsx +53 -6
- package/src/mobile/views/ChatView.tsx +33 -10
- package/src/mobile/views/SessionListView.test.tsx +69 -1
- package/src/mobile/views/SessionListView.tsx +58 -5
- package/src/mobile/views/WorkspaceView.tsx +4 -2
- package/src/mobile-api.ts +9 -1
- package/src/mobile-pwa.ts +19 -0
- package/src/mobile-routes.ts +123 -30
- 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 +23 -3
- package/src/tunnel.ts +7 -2
- package/src/update.ts +42 -14
package/lib/types/pairing.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* devices are cut off on their next gated request.
|
|
16
16
|
*/
|
|
17
17
|
import { randomBytes } from 'node:crypto';
|
|
18
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
18
20
|
/** Thrown by issue() for an address outside the sampled LAN literals. */
|
|
19
21
|
export class UnknownLanAddressError extends Error {
|
|
20
22
|
/**
|
|
@@ -60,6 +62,70 @@ export class PairingService {
|
|
|
60
62
|
constructor(config, clock = defaultClock) {
|
|
61
63
|
this.config = config;
|
|
62
64
|
this.clock = clock;
|
|
65
|
+
this.loadPersisted();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Restore device sessions persisted by a previous process run. A corrupt
|
|
69
|
+
* or missing file is tolerated (an empty device table, never a throw) —
|
|
70
|
+
* persistence is an availability convenience, not a security boundary.
|
|
71
|
+
*/
|
|
72
|
+
loadPersisted() {
|
|
73
|
+
const file = this.config.devicesFile;
|
|
74
|
+
if (file === undefined)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
const saved = JSON.parse(readFileSync(file, 'utf8'));
|
|
78
|
+
if (typeof saved !== 'object' || saved === null)
|
|
79
|
+
return;
|
|
80
|
+
for (const [deviceId, session] of Object.entries(saved)) {
|
|
81
|
+
if (typeof deviceId !== 'string')
|
|
82
|
+
continue;
|
|
83
|
+
if (typeof session !== 'object' || session === null)
|
|
84
|
+
continue;
|
|
85
|
+
const { createdAt, lastSeenAt } = session;
|
|
86
|
+
if (typeof createdAt !== 'number' || typeof lastSeenAt !== 'number')
|
|
87
|
+
continue;
|
|
88
|
+
this.devices.set(deviceId, { createdAt, lastSeenAt });
|
|
89
|
+
}
|
|
90
|
+
this.clampToMaxDevices();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Unreadable/corrupt: start empty rather than refusing to boot.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** FIFO-cap the device table (a persisted file may outlive a lowered cap). */
|
|
97
|
+
clampToMaxDevices() {
|
|
98
|
+
if (this.devices.size <= this.config.maxDevices)
|
|
99
|
+
return;
|
|
100
|
+
const overflow = this.devices.size - this.config.maxDevices;
|
|
101
|
+
const ordered = [...this.devices.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
102
|
+
for (const [id] of ordered.slice(0, overflow))
|
|
103
|
+
this.devices.delete(id);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Write the current device table to the configured file. Called on the
|
|
107
|
+
* mutation boundaries that change the set of live sessions (accept and
|
|
108
|
+
* stop); `lastSeenAt` refreshes are deliberately not persisted here so a
|
|
109
|
+
* per-request write storm is avoided — after a restart the phone's first
|
|
110
|
+
* heartbeat re-warms its own session.
|
|
111
|
+
*
|
|
112
|
+
* Device ids are session credentials (the gate authorizes requests by the
|
|
113
|
+
* cookie's device id), so the file is written 0600 via a temp file and
|
|
114
|
+
* atomic rename; a crash mid-write can never leave a half-written store.
|
|
115
|
+
*/
|
|
116
|
+
persist() {
|
|
117
|
+
const file = this.config.devicesFile;
|
|
118
|
+
if (file === undefined)
|
|
119
|
+
return;
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
122
|
+
const temp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
123
|
+
writeFileSync(temp, JSON.stringify(Object.fromEntries(this.devices)), { mode: 0o600 });
|
|
124
|
+
renameSync(temp, file);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('remote-web-ui: failed to persist paired devices', error);
|
|
128
|
+
}
|
|
63
129
|
}
|
|
64
130
|
/** The default LAN base URL (the first interface; undefined when not LAN-reachable). */
|
|
65
131
|
get lanBaseUrl() {
|
|
@@ -153,6 +219,7 @@ export class PairingService {
|
|
|
153
219
|
this.devices.delete(oldest.id);
|
|
154
220
|
}
|
|
155
221
|
this.devices.set(deviceId, { createdAt: now, lastSeenAt: now });
|
|
222
|
+
this.persist();
|
|
156
223
|
this.notify();
|
|
157
224
|
return { ok: true, deviceId };
|
|
158
225
|
}
|
|
@@ -164,6 +231,7 @@ export class PairingService {
|
|
|
164
231
|
stop() {
|
|
165
232
|
this.tokens.clear();
|
|
166
233
|
this.devices.clear();
|
|
234
|
+
this.persist();
|
|
167
235
|
this.stopped = true;
|
|
168
236
|
this.notify();
|
|
169
237
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.persist.test.d.ts","sourceRoot":"","sources":["../../src/pairing.persist.test.ts"],"names":[],"mappings":""}
|
|
@@ -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
|
+
});
|
|
@@ -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;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,
|
|
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,CAwM3D"}
|
package/lib/types/routes.js
CHANGED
|
@@ -200,7 +200,16 @@ export function makeRoutes(deps) {
|
|
|
200
200
|
const ACCEPT_MAX_ATTEMPTS = 10;
|
|
201
201
|
const ACCEPT_WINDOW_MS = 30_000;
|
|
202
202
|
const rateLimitAccept = (req) => {
|
|
203
|
-
const
|
|
203
|
+
const socketIp = req.socket?.remoteAddress ?? 'unknown';
|
|
204
|
+
// Behind the auto-tunnel every internet client arrives from 127.0.0.1,
|
|
205
|
+
// so a single shared bucket would let one attacker keep the legitimate
|
|
206
|
+
// owner rate-limited. Partition the availability bucket by the first
|
|
207
|
+
// client-visible XFF hop (set by the tunnel edge): XFF is untrusted for
|
|
208
|
+
// authentication and only separates buckets, it never grants access.
|
|
209
|
+
const forwarded = typeof req.headers['x-forwarded-for'] === 'string'
|
|
210
|
+
? (req.headers['x-forwarded-for'].split(',')[0] ?? '').trim()
|
|
211
|
+
: undefined;
|
|
212
|
+
const ip = forwarded === undefined || forwarded === '' ? socketIp : socketIp + '|' + forwarded;
|
|
204
213
|
const nowMs = Date.now();
|
|
205
214
|
// The map lives as long as the plugin: prune expired windows once the
|
|
206
215
|
// table grows past a modest size so distinct source IPs (LAN clients,
|
|
@@ -244,7 +253,7 @@ export function makeRoutes(deps) {
|
|
|
244
253
|
const workspaceQuery = workspaceId === undefined ? '' : `&workspace=${encodeURIComponent(workspaceId)}`;
|
|
245
254
|
writeJson(res, 200, {
|
|
246
255
|
ok: true,
|
|
247
|
-
url: `${base}/?pair=${token}${workspaceQuery}`,
|
|
256
|
+
url: `${base}/m/?pair=${token}${workspaceQuery}`,
|
|
248
257
|
token,
|
|
249
258
|
expiresAt,
|
|
250
259
|
// Every constructible base, so a multi-homed panel can switch the
|
|
@@ -287,6 +296,9 @@ export function makeRoutes(deps) {
|
|
|
287
296
|
writeJson(res, result.code === 'used' ? 409 : 404, { ok: false, code: result.code });
|
|
288
297
|
return;
|
|
289
298
|
}
|
|
299
|
+
// No Secure attribute: LAN pairing runs over plain HTTP (the cookie must
|
|
300
|
+
// work there), and the same cookie rides HTTPS on the tunnel. Lax keeps
|
|
301
|
+
// top-level navigations working while blocking cross-site subrequests.
|
|
290
302
|
res.writeHead(200, {
|
|
291
303
|
'content-type': 'application/json; charset=utf-8',
|
|
292
304
|
'set-cookie': [
|
|
@@ -337,7 +349,15 @@ export function makeRoutes(deps) {
|
|
|
337
349
|
return;
|
|
338
350
|
}
|
|
339
351
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
340
|
-
|
|
352
|
+
const paired = deviceId !== undefined && service.hasDevice(deviceId);
|
|
353
|
+
const snapshot = service.snapshot();
|
|
354
|
+
// Unpaired LAN/tunnel clients get only the pairing-relevant fields; the
|
|
355
|
+
// token expiry, device roster, and public tunnel URL are an oracle for
|
|
356
|
+
// targeting and timing and stay behind a live device cookie.
|
|
357
|
+
const visible = paired
|
|
358
|
+
? snapshot
|
|
359
|
+
: { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable, lanAddresses: snapshot.lanAddresses };
|
|
360
|
+
writeJson(res, 200, { ok: true, paired, ...visible });
|
|
341
361
|
};
|
|
342
362
|
const handleEvents = (req, res) => {
|
|
343
363
|
if (!requireMethod(req, res, 'GET'))
|
package/lib/types/tunnel.d.ts
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/lib/types/tunnel.js
CHANGED
|
@@ -46,6 +46,9 @@ export class TunnelManager {
|
|
|
46
46
|
urlTimer;
|
|
47
47
|
restartTimer;
|
|
48
48
|
attempts = 0;
|
|
49
|
+
// Generation counter: a stale ensureBinary resolution from an earlier
|
|
50
|
+
// start() must not spawn a second handle after a stop/start cycle.
|
|
51
|
+
generation = 0;
|
|
49
52
|
stopping = false;
|
|
50
53
|
urlListeners = new Set();
|
|
51
54
|
phaseListeners = new Set();
|
|
@@ -81,6 +84,7 @@ export class TunnelManager {
|
|
|
81
84
|
this.stopping = false;
|
|
82
85
|
this.targetUrl = targetUrl;
|
|
83
86
|
this.attempts = 0;
|
|
87
|
+
this.generation += 1;
|
|
84
88
|
this.attempt();
|
|
85
89
|
}
|
|
86
90
|
/** Stop the tunnel for good: no restarts, no state. */
|
|
@@ -107,12 +111,13 @@ export class TunnelManager {
|
|
|
107
111
|
attempt() {
|
|
108
112
|
if (this.stopping || this.targetUrl === undefined)
|
|
109
113
|
return;
|
|
114
|
+
const gen = this.generation;
|
|
110
115
|
this.setPhase('starting');
|
|
111
116
|
this.handle = undefined;
|
|
112
117
|
this.url = undefined;
|
|
113
118
|
this.error = undefined;
|
|
114
119
|
void this.ensureBinary().then(() => {
|
|
115
|
-
if (this.stopping || this.targetUrl === undefined)
|
|
120
|
+
if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
|
|
116
121
|
return;
|
|
117
122
|
const handle = this.factory(this.targetUrl);
|
|
118
123
|
this.handle = handle;
|
|
@@ -139,7 +144,7 @@ export class TunnelManager {
|
|
|
139
144
|
});
|
|
140
145
|
}).catch((value) => {
|
|
141
146
|
// Binary install failed (no network, no platform build): report it.
|
|
142
|
-
if (this.stopping || this.targetUrl === undefined)
|
|
147
|
+
if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
|
|
143
148
|
return;
|
|
144
149
|
const message = value instanceof Error ? value.message : String(value);
|
|
145
150
|
this.fail(`could not obtain the cloudflared binary: ${message}`);
|
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,
|
|
@@ -137,7 +137,7 @@ export declare function familyChildren(anchorManifest: Record<string, unknown>):
|
|
|
137
137
|
* @param timeoutMs - probe timeout.
|
|
138
138
|
* @returns the latest version string, or undefined on any failure.
|
|
139
139
|
*/
|
|
140
|
-
export declare function fetchLatestVersion(name: string, fetchImpl: (url: string) => Promise<{
|
|
140
|
+
export declare function fetchLatestVersion(name: string, fetchImpl: (url: string, init?: RequestInit) => Promise<{
|
|
141
141
|
ok: boolean;
|
|
142
142
|
json(): Promise<unknown>;
|
|
143
143
|
}>, timeoutMs?: number): Promise<string | undefined>;
|
|
@@ -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).
|
|
@@ -203,7 +218,7 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
|
203
218
|
const controller = new AbortController();
|
|
204
219
|
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
205
220
|
try {
|
|
206
|
-
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest');
|
|
221
|
+
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', { signal: controller.signal });
|
|
207
222
|
if (!response.ok)
|
|
208
223
|
return undefined;
|
|
209
224
|
const body = await response.json();
|
|
@@ -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,7 +287,10 @@ 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);
|
|
271
294
|
// The registry probes are independent: run them together instead of
|
|
272
295
|
// serializing up to N x 10s of registry latency behind one status call.
|
|
273
296
|
const latestList = await Promise.all(names.map(name => deps.fetchLatest(name)));
|
|
@@ -277,7 +300,7 @@ export async function checkUpdates(deps) {
|
|
|
277
300
|
const latest = latestList[index];
|
|
278
301
|
if (latest === undefined)
|
|
279
302
|
probeFailures++;
|
|
280
|
-
const current = readInstalledVersion(deps.resolve, name);
|
|
303
|
+
const current = readInstalledVersion(deps.resolve, name, profile.dir);
|
|
281
304
|
packages.push({
|
|
282
305
|
name,
|
|
283
306
|
current,
|
|
@@ -494,7 +517,7 @@ export async function runUpdateVerified(deps) {
|
|
|
494
517
|
// green exit that left everything in place could be mistaken for success.
|
|
495
518
|
const before = new Map();
|
|
496
519
|
for (const name of deps.run.packages) {
|
|
497
|
-
const version = readInstalledVersion(deps.check.resolve, name);
|
|
520
|
+
const version = readInstalledVersion(deps.check.resolve, name, deps.run.profileDir);
|
|
498
521
|
if (version !== VERSION_UNKNOWN)
|
|
499
522
|
before.set(name, version);
|
|
500
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.1
|
|
4
|
+
"version": "0.2.1",
|
|
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",
|