@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.
Files changed (144) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +41 -19
  3. package/README.zh.md +17 -12
  4. package/assets/mobile-icon-192.png +0 -0
  5. package/assets/mobile-icon-512.png +0 -0
  6. package/assets/mobile-service-worker.js +64 -0
  7. package/lib/client.js +57 -13
  8. package/lib/client.js.map +1 -1
  9. package/lib/index.js +408 -130
  10. package/lib/mobile.js +296 -22
  11. package/lib/mobile.js.map +1 -1
  12. package/lib/types/client/FooterRemoteEntry.js +1 -1
  13. package/lib/types/client/PluginSettingsCard.d.ts +7 -1
  14. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  15. package/lib/types/client/PluginSettingsCard.js +5 -3
  16. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  17. package/lib/types/client/RemoteEntry.js +43 -13
  18. package/lib/types/client/RemotePanel.d.ts +1 -0
  19. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  20. package/lib/types/client/RemotePanel.js +2 -2
  21. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  22. package/lib/types/client/RemoteSettingsCard.js +3 -3
  23. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  24. package/lib/types/client/UpdateEntry.js +37 -4
  25. package/lib/types/client/UpdatePanel.js +1 -1
  26. package/lib/types/client/deep-link.js +2 -2
  27. package/lib/types/client/index.js +7 -7
  28. package/lib/types/client/locales.d.ts +2 -0
  29. package/lib/types/client/locales.d.ts.map +1 -1
  30. package/lib/types/client/locales.js +2 -0
  31. package/lib/types/client/settings-form.d.ts.map +1 -1
  32. package/lib/types/client/update-api.d.ts.map +1 -1
  33. package/lib/types/gate.d.ts +9 -0
  34. package/lib/types/gate.d.ts.map +1 -1
  35. package/lib/types/gate.js +16 -5
  36. package/lib/types/http.d.ts +14 -0
  37. package/lib/types/http.d.ts.map +1 -0
  38. package/lib/types/http.js +27 -0
  39. package/lib/types/index.d.ts +28 -1
  40. package/lib/types/index.d.ts.map +1 -1
  41. package/lib/types/index.js +41 -22
  42. package/lib/types/invariant.d.ts.map +1 -1
  43. package/lib/types/mobile/PairRequiredView.d.ts +7 -0
  44. package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
  45. package/lib/types/mobile/PairRequiredView.js +28 -0
  46. package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
  47. package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
  48. package/lib/types/mobile/PairRequiredView.test.js +24 -0
  49. package/lib/types/mobile/api.js +1 -1
  50. package/lib/types/mobile/index.js +17 -4
  51. package/lib/types/mobile/markdown.d.ts +6 -4
  52. package/lib/types/mobile/markdown.d.ts.map +1 -1
  53. package/lib/types/mobile/markdown.js +8 -4
  54. package/lib/types/mobile/markdown.test.js +5 -1
  55. package/lib/types/mobile/messages.d.ts.map +1 -1
  56. package/lib/types/mobile/messages.js +14 -1
  57. package/lib/types/mobile/messages.test.js +1 -1
  58. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  59. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  60. package/lib/types/mobile/mobile-styles.js +48 -0
  61. package/lib/types/mobile/mobile-theme.test.js +2 -2
  62. package/lib/types/mobile/mux.d.ts.map +1 -1
  63. package/lib/types/mobile/mux.js +1 -1
  64. package/lib/types/mobile/mux.test.js +1 -1
  65. package/lib/types/mobile/pairing.d.ts +31 -0
  66. package/lib/types/mobile/pairing.d.ts.map +1 -0
  67. package/lib/types/mobile/pairing.js +62 -0
  68. package/lib/types/mobile/rpc.d.ts.map +1 -1
  69. package/lib/types/mobile/theme-toggle.js +1 -1
  70. package/lib/types/mobile/views/App.d.ts +12 -16
  71. package/lib/types/mobile/views/App.d.ts.map +1 -1
  72. package/lib/types/mobile/views/App.js +39 -12
  73. package/lib/types/mobile/views/App.test.d.ts +2 -0
  74. package/lib/types/mobile/views/App.test.d.ts.map +1 -0
  75. package/lib/types/mobile/views/App.test.js +52 -0
  76. package/lib/types/mobile/views/ChatView.js +6 -6
  77. package/lib/types/mobile/views/ChatView.test.js +3 -3
  78. package/lib/types/mobile/views/SessionListView.js +3 -3
  79. package/lib/types/mobile/views/SessionListView.test.js +2 -2
  80. package/lib/types/mobile/views/WorkspaceView.js +3 -3
  81. package/lib/types/mobile/views/WorkspaceView.test.js +3 -3
  82. package/lib/types/mobile-api.d.ts +1 -1
  83. package/lib/types/mobile-api.d.ts.map +1 -1
  84. package/lib/types/mobile-api.js +11 -18
  85. package/lib/types/mobile-pwa.d.ts +6 -0
  86. package/lib/types/mobile-pwa.d.ts.map +1 -0
  87. package/lib/types/mobile-pwa.js +17 -0
  88. package/lib/types/mobile-routes.d.ts +5 -8
  89. package/lib/types/mobile-routes.d.ts.map +1 -1
  90. package/lib/types/mobile-routes.js +130 -39
  91. package/lib/types/mount-once.d.ts +25 -0
  92. package/lib/types/mount-once.d.ts.map +1 -0
  93. package/lib/types/mount-once.js +42 -0
  94. package/lib/types/pairing-access.d.ts +28 -0
  95. package/lib/types/pairing-access.d.ts.map +1 -0
  96. package/lib/types/pairing-access.js +26 -0
  97. package/lib/types/pairing.d.ts +27 -0
  98. package/lib/types/pairing.d.ts.map +1 -1
  99. package/lib/types/pairing.js +68 -0
  100. package/lib/types/pairing.persist.test.d.ts +2 -0
  101. package/lib/types/pairing.persist.test.d.ts.map +1 -0
  102. package/lib/types/pairing.persist.test.js +128 -0
  103. package/lib/types/routes.d.ts +6 -6
  104. package/lib/types/routes.d.ts.map +1 -1
  105. package/lib/types/routes.js +15 -20
  106. package/lib/types/tunnel.d.ts.map +1 -1
  107. package/lib/types/update-routes.d.ts +2 -2
  108. package/lib/types/update-routes.d.ts.map +1 -1
  109. package/lib/types/update-routes.js +1 -6
  110. package/lib/types/update.d.ts +3 -3
  111. package/lib/types/update.d.ts.map +1 -1
  112. package/lib/types/update.js +40 -14
  113. package/package.json +12 -12
  114. package/src/client/PluginSettingsCard.tsx +10 -0
  115. package/src/client/RemoteEntry.tsx +40 -8
  116. package/src/client/RemotePanel.tsx +1 -1
  117. package/src/client/RemoteSettingsCard.tsx +1 -0
  118. package/src/client/UpdateEntry.tsx +36 -4
  119. package/src/client/deep-link.ts +1 -1
  120. package/src/client/locales.ts +2 -0
  121. package/src/client/remote.module.css +26 -0
  122. package/src/gate.ts +15 -3
  123. package/src/http.ts +30 -0
  124. package/src/index.ts +50 -16
  125. package/src/mobile/PairRequiredView.test.tsx +28 -0
  126. package/src/mobile/PairRequiredView.tsx +58 -0
  127. package/src/mobile/index.tsx +15 -1
  128. package/src/mobile/markdown.test.ts +5 -0
  129. package/src/mobile/markdown.ts +7 -4
  130. package/src/mobile/messages.ts +14 -1
  131. package/src/mobile/mobile-styles.ts +48 -0
  132. package/src/mobile/pairing.ts +77 -0
  133. package/src/mobile/views/App.test.tsx +62 -0
  134. package/src/mobile/views/App.tsx +53 -18
  135. package/src/mobile-api.ts +9 -17
  136. package/src/mobile-pwa.ts +19 -0
  137. package/src/mobile-routes.ts +137 -38
  138. package/src/mount-once.ts +48 -0
  139. package/src/pairing-access.ts +36 -0
  140. package/src/pairing.persist.test.ts +145 -0
  141. package/src/pairing.ts +70 -1
  142. package/src/routes.ts +12 -18
  143. package/src/update-routes.ts +1 -7
  144. 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
+ });
@@ -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: "/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";
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;AAGhG;;;;;;;;;;;;;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;;;;;;;CAOb,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA4CjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;gBACS,OAAO,EAAE,cAAc;IAInC,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;IAiBrD,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAYrC,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,CA4K3D"}
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"}
@@ -8,8 +8,9 @@
8
8
  * on every other /api request.
9
9
  */
10
10
  import { z } from 'zod';
11
- import { UnknownLanAddressError } from "./pairing.js";
12
- import { isLoopbackClient, readCookie } from "./gate.js";
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
- /** One JSON response. */
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 = JSON.parse(Buffer.concat(chunks).toString('utf8'));
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":"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"}
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: "/api/update/status";
13
- readonly run: "/api/update/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;AAEhE,8CAA8C;AAC9C,eAAO,MAAM,YAAY;;;CAGf,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;AASD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,GAAG,QAAQ,EAAE,CA+BnE"}
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.
@@ -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) and its family
4
- * children, probes the npm registry for newer releases, and runs the actual
5
- * update as `pnpm update --latest` inside the owning dsh profile directory.
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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"}
@@ -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) and its family
4
- * children, probes the npm registry for newer releases, and runs the actual
5
- * update as `pnpm update --latest` inside the owning dsh profile directory.
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: [anchor, ...familyChildren(manifest)],
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
- return typeof version === 'string' ? version : VERSION_UNKNOWN;
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
- const names = [anchor, ...familyChildren(manifest)];
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
- for (const name of names) {
274
- const latest = await deps.fetchLatest(name);
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.1.19",
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.6",
65
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
66
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
67
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
68
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
69
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
70
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
71
- "@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.6",
72
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
73
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
74
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
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
- /** Apply one status frame onto the current ready state. */
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 only makes sense in the ready state — on a failure banner the
97
- // events endpoint is unreachable too (loopback fence), so opening it
98
- // would just start a doomed reconnect loop.
99
- if (next.kind !== 'ready') return
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
- setState(previous => mergeFrame(previous, frame))
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 button matching the settings rail/row geometry. */
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 ? undefined : 'rail'}
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
  | {
@@ -140,6 +140,7 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
140
140
  t={t}
141
141
  titleKey="settings.title"
142
142
  descriptionKey="settings.description"
143
+ defaultOpen={false}
143
144
  state={state}
144
145
  onSave={props.save}
145
146
  onDiscard={props.discard}