@linxin666/dsh-remote-web-ui 0.2.4 → 0.2.5

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 (89) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +148 -77
  3. package/README.zh.md +32 -26
  4. package/lib/client.js +602 -67
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +667 -32
  7. package/lib/mobile.js +3 -1
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/FenceNotice.d.ts +21 -0
  10. package/lib/types/client/FenceNotice.d.ts.map +1 -0
  11. package/lib/types/client/FenceNotice.js +10 -0
  12. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  13. package/lib/types/client/RemoteEntry.js +18 -11
  14. package/lib/types/client/RemotePanel.d.ts +10 -4
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +7 -6
  17. package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +3 -1
  20. package/lib/types/client/device-name.d.ts +3 -0
  21. package/lib/types/client/device-name.d.ts.map +1 -0
  22. package/lib/types/client/device-name.js +30 -0
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +48 -0
  25. package/lib/types/client/locales.d.ts +51 -1
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +79 -29
  28. package/lib/types/client/pair-api.d.ts +35 -0
  29. package/lib/types/client/pair-api.d.ts.map +1 -1
  30. package/lib/types/client/pair-api.js +39 -0
  31. package/lib/types/client/remote-channel.d.ts +93 -0
  32. package/lib/types/client/remote-channel.d.ts.map +1 -0
  33. package/lib/types/client/remote-channel.js +246 -0
  34. package/lib/types/dsh-home.d.ts +17 -0
  35. package/lib/types/dsh-home.d.ts.map +1 -0
  36. package/lib/types/dsh-home.js +34 -0
  37. package/lib/types/gate.d.ts.map +1 -1
  38. package/lib/types/gate.js +5 -2
  39. package/lib/types/index.d.ts +19 -12
  40. package/lib/types/index.d.ts.map +1 -1
  41. package/lib/types/index.js +73 -5
  42. package/lib/types/loopback-proxy.d.ts +28 -0
  43. package/lib/types/loopback-proxy.d.ts.map +1 -0
  44. package/lib/types/loopback-proxy.js +112 -0
  45. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  46. package/lib/types/mobile/views/SessionListView.js +6 -1
  47. package/lib/types/mobile-api.d.ts.map +1 -1
  48. package/lib/types/mobile-api.js +6 -2
  49. package/lib/types/mobile-routes.js +1 -1
  50. package/lib/types/pairing.d.ts +63 -11
  51. package/lib/types/pairing.d.ts.map +1 -1
  52. package/lib/types/pairing.js +137 -20
  53. package/lib/types/pairing.persist.test.js +60 -5
  54. package/lib/types/posture.d.ts +75 -0
  55. package/lib/types/posture.d.ts.map +1 -0
  56. package/lib/types/posture.js +116 -0
  57. package/lib/types/remote-api.d.ts +60 -0
  58. package/lib/types/remote-api.d.ts.map +1 -0
  59. package/lib/types/remote-api.js +181 -0
  60. package/lib/types/remote-methods.d.ts +33 -0
  61. package/lib/types/remote-methods.d.ts.map +1 -0
  62. package/lib/types/remote-methods.js +54 -0
  63. package/lib/types/routes.d.ts +4 -0
  64. package/lib/types/routes.d.ts.map +1 -1
  65. package/lib/types/routes.js +33 -4
  66. package/package.json +2 -2
  67. package/src/client/FenceNotice.tsx +43 -0
  68. package/src/client/RemoteEntry.tsx +19 -9
  69. package/src/client/RemotePanel.tsx +83 -10
  70. package/src/client/RemoteSettingsCard.tsx +16 -0
  71. package/src/client/device-name.ts +31 -0
  72. package/src/client/index.ts +46 -0
  73. package/src/client/locales.ts +79 -29
  74. package/src/client/pair-api.ts +60 -0
  75. package/src/client/remote-channel.ts +269 -0
  76. package/src/client/remote.module.css +240 -16
  77. package/src/dsh-home.ts +36 -0
  78. package/src/gate.ts +5 -2
  79. package/src/index.ts +89 -17
  80. package/src/loopback-proxy.ts +126 -0
  81. package/src/mobile/views/SessionListView.tsx +6 -1
  82. package/src/mobile-api.ts +6 -2
  83. package/src/mobile-routes.ts +1 -1
  84. package/src/pairing.persist.test.ts +64 -5
  85. package/src/pairing.ts +167 -20
  86. package/src/posture.ts +154 -0
  87. package/src/remote-api.ts +217 -0
  88. package/src/remote-methods.ts +62 -0
  89. package/src/routes.ts +33 -4
@@ -1,44 +1,67 @@
1
- /** `remote` namespace dictionaries: the mobile remote-control surface copy. */
1
+ /** `remote` namespace dictionaries: the remote-access surface copy. */
2
2
  /** Simplified Chinese dictionary (the key-set source of truth). */
3
3
  export const zh = {
4
- 'entry.label': '移动端远程控制',
5
- 'title': '移动端远程控制',
6
- 'subtitle': '扫码或在手机上打开链接,即可远程控制当前工作区',
7
- 'card.title': '手机扫码连接',
4
+ 'entry.label': '远程访问',
5
+ 'title': '远程访问',
6
+ 'subtitle': '通过手机或另一台电脑配对,远程访问当前工作区',
7
+ 'card.title': '设备配对',
8
8
  'public.badge': '公网',
9
- 'status.waiting': '等待手机连接',
9
+ 'status.waiting': '等待设备连接',
10
10
  'status.connected': '已连接 {n} 台设备',
11
11
  'status.disconnected': '已配对设备离线',
12
- 'status.stopped': '已停止移动端访问',
12
+ 'status.stopped': '已停止远程访问',
13
13
  'status.lanRequired': '此功能需要以 --host 0.0.0.0 启动 dsh web,或配置公网地址才能使用',
14
14
  'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请用 dsh web --host 0.0.0.0 重新启动,或在设置中填写内网穿透的公网地址。',
15
15
  'status.loopbackRequired': '配对面板仅限本机使用',
16
16
  'status.loopbackRequiredHint': '请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。',
17
17
  'status.unreachable': '无法连接配对服务',
18
18
  'status.unreachableHint': '请刷新页面后重试。',
19
- 'pair.hint': '无法扫码?可以在手机上打开链接',
20
- 'pair.publicHint': '公网链接:手机无需与电脑在同一网络',
19
+ 'pair.hint': '无法扫码?可直接打开下方配对链接',
20
+ 'pair.publicHint': '公网链接:设备无需与本机处于同一网络',
21
21
  'address.label': '选择二维码指向的网络',
22
22
  'address.public': '公网地址',
23
23
  'address.lan': '局域网',
24
- 'address.hint': '公网地址适合手机不在同一网络时使用;局域网地址仅在手机与电脑同一网络时可用。',
24
+ 'address.hint': '远程设备不在同一网络时请使用公网地址;局域网地址仅限同一网络内使用。',
25
25
  'pair.expires': '二维码有效至 {time}',
26
26
  'pair.expired': '二维码已过期,请刷新',
27
+ 'pair.phoneLabel': '手机配对链接',
28
+ 'pair.desktopLabel': '电脑配对链接',
29
+ 'pair.oneTimeHint': '两条链接共用同一枚一次性令牌;任意设备配对成功后,另一条链接立即失效。',
27
30
  'pair.failed.title': '配对失败',
28
31
  'pair.failed.detail': '链接无效或已使用,请回到电脑端刷新二维码后重新扫码。',
32
+ 'fence.unpaired.title': '此设备未配对,无法访问工作区数据',
33
+ 'fence.unpaired.eyebrow': '需要设备配对',
34
+ 'fence.unpaired.hint': '为保护工作区、会话与插件数据,远程电脑必须先通过主电脑授权。',
35
+ 'fence.unpaired.stepDesktop': '在主电脑打开 http://127.0.0.1:3080,进入“远程访问”。',
36
+ 'fence.unpaired.stepLink': '在“设备配对”中复制电脑配对链接。',
37
+ 'fence.unpaired.stepOpen': '在当前浏览器打开该链接,完成授权后即可进入。',
38
+ 'fence.unpaired.retry': '重新检测',
39
+ 'fence.unpaired.footnote': '请勿使用他人提供的配对链接;管理员可随时取消此设备的授权。',
40
+ 'posture.exposed': '/api 通道对未配对设备敞开',
41
+ 'posture.exposedHint': '以下来源的请求未经配对即可访问完整桌面 API:{hosts}。请移除对应来源的 --trusted-host(配对机制已覆盖远程访问),或改为仅绑定 127.0.0.1 并使用隧道。',
29
42
  'action.stop': '停止',
30
43
  'action.refresh': '刷新二维码',
31
44
  'action.copy': '复制链接',
45
+ 'action.copyPhone': '复制手机链接',
46
+ 'action.copyDesktop': '复制电脑链接',
32
47
  'action.copied': '已复制',
33
- 'stopped.hint': '已停止移动端访问。点击"刷新二维码"重新开启。',
48
+ 'devices.title': '已授权设备',
49
+ 'devices.empty': '还没有已配对的设备。扫码或打开链接后会出现在这里。',
50
+ 'devices.unknown': '未知设备',
51
+ 'devices.online': '在线',
52
+ 'devices.offline': '离线',
53
+ 'devices.lastSeen': '最近活动 {time}',
54
+ 'devices.revoke': '取消配对',
55
+ 'devices.revoke.label': '取消配对此设备',
56
+ 'stopped.hint': '已停止远程访问。点击"刷新二维码"重新开启。',
34
57
  'tunnel.starting': '公网隧道启动中,二维码将自动变为公网链接…',
35
58
  'tunnel.failed': '公网隧道启动失败:{error}',
36
59
  'tunnel.unknownError': '未知错误',
37
- 'close.label': '关闭移动端远程控制面板',
60
+ 'close.label': '关闭远程访问面板',
38
61
  // 插件设置卡片(settings.plugin.item 席位)。
39
- 'settings.title': '移动端远程控制',
62
+ 'settings.title': '远程访问设置',
40
63
  'settings.description': '配对安全与设备限额。',
41
- 'settings.enabled': '启用移动端远程控制',
64
+ 'settings.enabled': '启用远程访问',
42
65
  'settings.enabledHint': '关闭后移除侧边栏入口并停用配对路由与局域网栅栏。',
43
66
  'settings.tokenTtlMs': '配对令牌有效期(毫秒)',
44
67
  'settings.tokenTtlMsHint': '新生成的二维码链接在此时间后失效。',
@@ -46,10 +69,12 @@ export const zh = {
46
69
  'settings.offlineAfterMsHint': '配对设备超过该时长未上报心跳即视为离线。',
47
70
  'settings.maxDevices': '已配对设备上限',
48
71
  'settings.maxDevicesHint': '超过上限时淘汰最旧的设备会话。',
72
+ 'settings.idleExpireMs': '空闲过期(毫秒)',
73
+ 'settings.idleExpireMsHint': '超过该时长没有任何心跳或请求的已配对设备会被删除,必须重新扫码。默认 7 天。',
49
74
  'settings.cookieName': '设备 Cookie 名',
50
75
  'settings.cookieNameHint': '携带已配对设备标识的 Cookie 名称。',
51
76
  'settings.requirePairingForLan': '局域网访问要求配对',
52
- 'settings.requirePairingForLanHint': '开启:所有非本机回环的 /api 请求必须携带有效配对 Cookie;关闭:局域网栅栏放开,配对仅用于令牌与状态管理。',
77
+ 'settings.requirePairingForLanHint': '开启:非本机回环的桌面 Web GUI 改走门控的 /remote,必须携带有效配对 Cookie;关闭:桌面继续走普通路径(仅在该来源已被 SDK 信任时有意义),配对只管理令牌与状态。',
53
78
  'settings.publicBaseUrl': '公网地址(可选)',
54
79
  'settings.publicBaseUrlHint': '内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。',
55
80
  'settings.autoTunnel': '自动公网隧道',
@@ -110,44 +135,67 @@ export const zh = {
110
135
  };
111
136
  /** English dictionary, checked complete against the zh key set. */
112
137
  export const en = {
113
- 'entry.label': 'Mobile remote control',
114
- 'title': 'Mobile remote control',
115
- 'subtitle': 'Scan the QR code or open the link on your phone to control this workspace remotely',
116
- 'card.title': 'Scan to connect',
138
+ 'entry.label': 'Remote access',
139
+ 'title': 'Remote access',
140
+ 'subtitle': 'Pair a phone or another computer to access this workspace remotely',
141
+ 'card.title': 'Pair a device',
117
142
  'public.badge': 'Public',
118
- 'status.waiting': 'Waiting for a phone',
143
+ 'status.waiting': 'Waiting for a device',
119
144
  'status.connected': '{n} device(s) connected',
120
145
  'status.disconnected': 'Paired devices offline',
121
- 'status.stopped': 'Mobile access stopped',
146
+ 'status.stopped': 'Remote access stopped',
122
147
  'status.lanRequired': 'This feature needs dsh web started with --host 0.0.0.0, or a configured public address',
123
148
  'status.lanRequiredHint': 'The server is bound to 127.0.0.1 and no public address is configured, so a phone cannot reach it. Restart with dsh web --host 0.0.0.0, or set the tunneled public address in settings.',
124
149
  'status.loopbackRequired': 'The pairing panel works on this machine only',
125
150
  'status.loopbackRequiredHint': 'Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.',
126
151
  'status.unreachable': 'Cannot reach the pairing service',
127
152
  'status.unreachableHint': 'Refresh the page and try again.',
128
- 'pair.hint': 'Cannot scan? Open the link on your phone',
129
- 'pair.publicHint': 'Public link: the phone does not need to be on the same network',
153
+ 'pair.hint': 'Cannot scan? Open one of the pairing links below',
154
+ 'pair.publicHint': 'Public links work when the device is not on the same network',
130
155
  'address.label': 'Network the QR code points to',
131
156
  'address.public': 'Public',
132
157
  'address.lan': 'LAN',
133
- 'address.hint': 'Use the public address when the phone is not on the same network; LAN addresses only work on the same network.',
158
+ 'address.hint': 'Use the public address when the remote device is on another network; LAN addresses only work on the same network.',
134
159
  'pair.expires': 'QR code valid until {time}',
135
160
  'pair.expired': 'QR code expired — refresh it',
161
+ 'pair.phoneLabel': 'Phone pairing link',
162
+ 'pair.desktopLabel': 'Computer pairing link',
163
+ 'pair.oneTimeHint': 'Both links share one single-use token. Pairing either device invalidates the other link immediately.',
136
164
  'pair.failed.title': 'Pairing failed',
137
165
  'pair.failed.detail': 'The link is invalid or was already used. Refresh the QR code on your computer and scan again.',
166
+ 'fence.unpaired.title': 'This device is not paired and cannot reach workspace data',
167
+ 'fence.unpaired.eyebrow': 'Device pairing required',
168
+ 'fence.unpaired.hint': 'To protect workspace, session, and plugin data, a remote computer must first be authorized by the primary computer.',
169
+ 'fence.unpaired.stepDesktop': 'On the primary computer, open http://127.0.0.1:3080 and go to Remote access.',
170
+ 'fence.unpaired.stepLink': 'Copy the computer pairing link under Pair a device.',
171
+ 'fence.unpaired.stepOpen': 'Open that link in this browser. After authorization, the workspace will become available.',
172
+ 'fence.unpaired.retry': 'Check again',
173
+ 'fence.unpaired.footnote': 'Do not use pairing links from people you do not trust. An administrator can revoke this device at any time.',
174
+ 'posture.exposed': 'The /api channel is open to unpaired devices',
175
+ 'posture.exposedHint': 'Requests from {hosts} reach the full desktop API without pairing. Remove --trusted-host for them (pairing already covers remote access), or bind loopback only and use the tunnel.',
138
176
  'action.stop': 'Stop',
139
177
  'action.refresh': 'Refresh QR',
140
178
  'action.copy': 'Copy link',
179
+ 'action.copyPhone': 'Copy phone link',
180
+ 'action.copyDesktop': 'Copy computer link',
141
181
  'action.copied': 'Copied',
142
- 'stopped.hint': 'Mobile access is stopped. Click "Refresh QR" to re-enable it.',
182
+ 'devices.title': 'Authorized devices',
183
+ 'devices.empty': 'No paired devices yet. Scan the QR or open the link to add one.',
184
+ 'devices.unknown': 'Unknown device',
185
+ 'devices.online': 'Online',
186
+ 'devices.offline': 'Offline',
187
+ 'devices.lastSeen': 'Last active {time}',
188
+ 'devices.revoke': 'Unpair',
189
+ 'devices.revoke.label': 'Unpair this device',
190
+ 'stopped.hint': 'Remote access is stopped. Click "Refresh QR" to re-enable it.',
143
191
  'tunnel.starting': 'The public tunnel is starting; the QR code will switch to a public link shortly…',
144
192
  'tunnel.failed': 'The public tunnel failed to start: {error}',
145
193
  'tunnel.unknownError': 'unknown error',
146
- 'close.label': 'Close mobile remote control panel',
194
+ 'close.label': 'Close remote access panel',
147
195
  // Plugin settings card (the `settings.plugin.item` seat).
148
- 'settings.title': 'Mobile remote control',
196
+ 'settings.title': 'Remote access settings',
149
197
  'settings.description': 'Pairing security and device limits.',
150
- 'settings.enabled': 'Enable mobile remote control',
198
+ 'settings.enabled': 'Enable remote access',
151
199
  'settings.enabledHint': 'When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.',
152
200
  'settings.tokenTtlMs': 'Pairing token lifetime (ms)',
153
201
  'settings.tokenTtlMsHint': 'How long a minted QR link stays valid before it dies.',
@@ -155,10 +203,12 @@ export const en = {
155
203
  'settings.offlineAfterMsHint': 'A paired device flips to offline when it has not been seen for this long.',
156
204
  'settings.maxDevices': 'Paired device cap',
157
205
  'settings.maxDevicesHint': 'Hard cap on paired device sessions; the oldest is evicted when full.',
206
+ 'settings.idleExpireMs': 'Idle expiry (ms)',
207
+ 'settings.idleExpireMsHint': 'A paired device with no heartbeat or request for this long is deleted and must scan again. Default is 7 days.',
158
208
  'settings.cookieName': 'Device cookie name',
159
209
  'settings.cookieNameHint': 'Cookie that carries the paired device id.',
160
210
  'settings.requirePairingForLan': 'Require pairing for LAN access',
161
- 'settings.requirePairingForLanHint': 'On: every non-loopback /api request must carry a live paired-device cookie. Off: the LAN fence is open and pairing only manages tokens/status.',
211
+ 'settings.requirePairingForLanHint': 'On: a desktop Web GUI at a non-loopback origin rides the gated /remote channel and must carry a live paired-device cookie. Off: the desktop stays on its original paths (only useful when that origin is already trusted) and pairing only manages tokens/status.',
162
212
  'settings.publicBaseUrl': 'Public address (optional)',
163
213
  'settings.publicBaseUrlHint': 'The public URL of a tunnel in front of this server, e.g. a Cloudflare Tunnel: https://xxx.trycloudflare.com. When set, the QR link is built from it so a phone anywhere can pair; leave blank for LAN-only usage.',
164
214
  'settings.autoTunnel': 'Auto public tunnel',
@@ -41,6 +41,14 @@ export interface TunnelStatusFrame {
41
41
  url?: string;
42
42
  error?: string;
43
43
  }
44
+ /** One /api posture frame (host half probe; see src/posture.ts). */
45
+ export interface PostureFrame {
46
+ checkedAt: number;
47
+ hosts: {
48
+ host: string;
49
+ exposed: boolean;
50
+ }[];
51
+ }
44
52
  /** One /api/pair/events frame. */
45
53
  export interface PairStateFrame {
46
54
  type: 'state';
@@ -50,8 +58,20 @@ export interface PairStateFrame {
50
58
  tokenExpiresAt?: number;
51
59
  deviceCount: number;
52
60
  onlineCount: number;
61
+ /** Per-device roster (loopback events only). */
62
+ devices?: DeviceFrame[];
53
63
  /** Auto-tunnel status, while the auto-tunnel feature is active. */
54
64
  tunnel?: TunnelStatusFrame;
65
+ /** Latest /api fence posture probe, once a round has completed. */
66
+ posture?: PostureFrame;
67
+ }
68
+ /** One authorized-device row from the loopback status stream. */
69
+ export interface DeviceFrame {
70
+ id: string;
71
+ createdAt: number;
72
+ lastSeenAt: number;
73
+ online: boolean;
74
+ userAgent?: string;
55
75
  }
56
76
  /**
57
77
  * Mint a fresh pairing token (one active token at a time — this invalidates
@@ -76,6 +96,11 @@ export declare function acceptPair(token: string): Promise<{
76
96
  } | AcceptFailure>;
77
97
  /** Revoke mobile access (paired devices + the current token). */
78
98
  export declare function stopPair(): Promise<void>;
99
+ /**
100
+ * Revoke one paired device from the loopback panel.
101
+ * @param deviceId - the session id of the row to drop.
102
+ */
103
+ export declare function revokePair(deviceId: string): Promise<void>;
79
104
  /** Presence heartbeat from a paired phone (unpaired heartbeats 401 harmlessly). */
80
105
  export declare function sendHeartbeat(): Promise<void>;
81
106
  /** Whether the current page URL carries a pairing token / workspace target. */
@@ -83,8 +108,18 @@ export declare function readPairParams(search: string): {
83
108
  pair?: string;
84
109
  workspace?: string;
85
110
  };
111
+ /**
112
+ * Strip one query parameter from the current URL without reloading.
113
+ * @param name - the parameter to remove.
114
+ * @returns the new search string ('' when empty).
115
+ */
116
+ export declare function stripParam(name: string): string;
117
+ /** Convert an issued `/m/` link into the desktop pairing form. */
118
+ export declare function desktopPairUrl(mobileUrl: string): string;
86
119
  /** Human-readable expiry clock, e.g. "10:35". */
87
120
  export declare function formatClock(epochMs: number): string;
121
+ /** Calendar + clock for last-seen timestamps, e.g. "2026-08-19 10:35". */
122
+ export declare function formatLastSeen(epochMs: number): string;
88
123
  /**
89
124
  * Copy text to the clipboard with a fallback for insecure contexts
90
125
  * (plain-HTTP LAN origins lack navigator.clipboard).
@@ -1 +1 @@
1
- {"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAwB;AACxB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,IAAI,CAAA;IACR,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,iBAAiB,CAAA;CACxB;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,qBAAqB,CAAA;AAExG,8BAA8B;AAC9B,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,WAAW,CAAA;CAAE,CAAA;AAEjF,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;IACxC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,cAAc,CAAA;IAC5E,YAAY,EAAE,OAAO,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,mEAAmE;IACnE,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgB9F;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,aAAa,CAAC,CAUrF;AAED,iEAAiE;AACjE,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAG9C;AAED,mFAAmF;AACnF,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAQpF;AAED,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB7D"}
1
+ {"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"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"}
@@ -60,6 +60,21 @@ export async function stopPair() {
60
60
  if (!response.ok)
61
61
  throw new Error(`remote-web-ui: stop failed with ${String(response.status)}`);
62
62
  }
63
+ /**
64
+ * Revoke one paired device from the loopback panel.
65
+ * @param deviceId - the session id of the row to drop.
66
+ */
67
+ export async function revokePair(deviceId) {
68
+ const response = await fetch('/api/pair/revoke', {
69
+ method: 'POST',
70
+ headers: { 'content-type': 'application/json' },
71
+ body: JSON.stringify({ deviceId }),
72
+ });
73
+ if (response.status === 404)
74
+ return;
75
+ if (!response.ok)
76
+ throw new Error(`remote-web-ui: revoke failed with ${String(response.status)}`);
77
+ }
63
78
  /** Presence heartbeat from a paired phone (unpaired heartbeats 401 harmlessly). */
64
79
  export async function sendHeartbeat() {
65
80
  await fetch('/api/pair/heartbeat', { method: 'POST' });
@@ -74,6 +89,22 @@ export function readPairParams(search) {
74
89
  ...(workspace !== null && workspace !== '' ? { workspace } : {}),
75
90
  };
76
91
  }
92
+ /**
93
+ * Strip one query parameter from the current URL without reloading.
94
+ * @param name - the parameter to remove.
95
+ * @returns the new search string ('' when empty).
96
+ */
97
+ export function stripParam(name) {
98
+ const url = new URL(window.location.href);
99
+ url.searchParams.delete(name);
100
+ return url.search;
101
+ }
102
+ /** Convert an issued `/m/` link into the desktop pairing form. */
103
+ export function desktopPairUrl(mobileUrl) {
104
+ const url = new URL(mobileUrl);
105
+ url.pathname = '/';
106
+ return url.href;
107
+ }
77
108
  /** Human-readable expiry clock, e.g. "10:35". */
78
109
  export function formatClock(epochMs) {
79
110
  const date = new Date(epochMs);
@@ -81,6 +112,14 @@ export function formatClock(epochMs) {
81
112
  const minutes = String(date.getMinutes()).padStart(2, '0');
82
113
  return `${hours}:${minutes}`;
83
114
  }
115
+ /** Calendar + clock for last-seen timestamps, e.g. "2026-08-19 10:35". */
116
+ export function formatLastSeen(epochMs) {
117
+ const date = new Date(epochMs);
118
+ const year = String(date.getFullYear());
119
+ const month = String(date.getMonth() + 1).padStart(2, '0');
120
+ const day = String(date.getDate()).padStart(2, '0');
121
+ return `${year}-${month}-${day} ${formatClock(epochMs)}`;
122
+ }
84
123
  /**
85
124
  * Copy text to the clipboard with a fallback for insecure contexts
86
125
  * (plain-HTTP LAN origins lack navigator.clipboard).
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The remote desktop channel — browser half. On a non-loopback origin (LAN
3
+ * address or public tunnel) fenced host routes refuse the request, and
4
+ * pairing is the real access control — so same-origin traffic the desktop
5
+ * issues is rewritten onto this plugin's gated `/remote` prefix (host half
6
+ * in src/remote-api.ts). The host then re-issues the call to 127.0.0.1 so
7
+ * plugin loopback fences pass.
8
+ *
9
+ * The rewrite is deliberately narrow:
10
+ * - loopback origins are untouched (the desktop at 127.0.0.1 keeps original paths);
11
+ * - the pairing routes (`/api/pair/*`) stay where they are — accept must
12
+ * work BEFORE a device is paired;
13
+ * - the update endpoints (`/api/update/*`) stay loopback-only;
14
+ * - desktop-launcher create/shutdown (`/api/dsh-desktop-launcher/*`) stay
15
+ * loopback-only (host-local files and process exit);
16
+ * - the family settings bridge (`/api/dsh-web-ui-settings/*`) stays
17
+ * loopback-only (same plane as SDK settings.*);
18
+ * - `/api/*` (SDK methods and `/api/<plugin>/...` plugin namespaces),
19
+ * `/sidebar/*`, `/git/*`, and `/pet/*` ride the channel;
20
+ * - fetch, EventSource, WebSocket, and img/script/iframe `src` are patched;
21
+ * everything else calls the original unchanged.
22
+ *
23
+ * Pure helpers are exported for unit tests; `installRemoteChannel` patches
24
+ * the given window and returns their restore.
25
+ */
26
+ /** The gated mirror prefix (must match src/remote-methods.ts). */
27
+ export declare const REMOTE_PREFIX = "/remote";
28
+ /** Connection-plugin method prefix under the gated channel. */
29
+ export declare const REMOTE_API_PREFIX = "/remote/api";
30
+ /**
31
+ * Browser-safe loopback classification for the page origin (the SDK client
32
+ * exports its own; this copy keeps the module dependency-free).
33
+ * @param hostname - a location hostname (IPv6 without brackets).
34
+ * @returns true for localhost, IPv6 loopback, or any 127/8 literal.
35
+ */
36
+ export declare function isLoopbackHostname(hostname: string): boolean;
37
+ /**
38
+ * Whether one same-origin path must ride the gated channel (fetch, EventSource,
39
+ * img/script/iframe src).
40
+ * @param pathname - the request URL pathname.
41
+ */
42
+ export declare function shouldRewriteFetchPath(pathname: string): boolean;
43
+ /**
44
+ * Whether one WebSocket path must ride the gated channel.
45
+ * @param pathname - the WebSocket URL pathname.
46
+ */
47
+ export declare function shouldRewriteWsPath(pathname: string): boolean;
48
+ /** The gated twin of one fenced path (`/remote` + original pathname). */
49
+ export declare function rewritePath(pathname: string): string;
50
+ /**
51
+ * Rewrite one raw URL string when it is same-origin and fenced. Relative
52
+ * inputs stay relative so resource loaders do not unexpectedly absolutize.
53
+ */
54
+ export declare function rewriteRawUrl(raw: string, baseHref: string, origin: string): string;
55
+ /** A constructor that exposes a configurable `src` on its prototype. */
56
+ interface SrcConstructor {
57
+ prototype: object;
58
+ }
59
+ /** The subset of window the channel needs (injectable for tests). */
60
+ export interface ChannelWindow {
61
+ fetch: typeof globalThis.fetch;
62
+ WebSocket: typeof WebSocket;
63
+ EventSource?: typeof EventSource;
64
+ HTMLImageElement?: SrcConstructor;
65
+ HTMLScriptElement?: SrcConstructor;
66
+ HTMLIFrameElement?: SrcConstructor;
67
+ location: {
68
+ origin: string;
69
+ href: string;
70
+ };
71
+ }
72
+ /** Options for {@link installRemoteChannel}. */
73
+ export interface RemoteChannelOptions {
74
+ /** Called when a gated call came back unpaired (code `unpaired`). */
75
+ onUnpaired?: () => void;
76
+ /** Called when a gated call succeeded (an unpaired banner can retire). */
77
+ onPaired?: () => void;
78
+ }
79
+ /**
80
+ * Whether a gated 403 is the unpaired-device fence (not a loopback-only
81
+ * method denial, which uses the same status with code `forbidden`).
82
+ */
83
+ export declare function isUnpairedDenied(response: Response): Promise<boolean>;
84
+ /**
85
+ * Patch `fetch`, `EventSource`, `WebSocket`, and resource `src` accessors on
86
+ * one window to route fenced traffic through the gated channel.
87
+ * @param window - the browser window (or a test double).
88
+ * @param options - the unpaired callback.
89
+ * @returns a function restoring the originals.
90
+ */
91
+ export declare function installRemoteChannel(window: ChannelWindow, options?: RemoteChannelOptions): () => void;
92
+ export {};
93
+ //# sourceMappingURL=remote-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAkBvD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI5D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAUhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAcnF;AAED,wEAAwE;AACxE,UAAU,cAAc;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC9B,SAAS,EAAE,OAAO,SAAS,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,WAAW,CAAA;IAChC,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,iBAAiB,CAAC,EAAE,cAAc,CAAA;IAClC,iBAAiB,CAAC,EAAE,cAAc,CAAA;IAClC,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3C;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAoBD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAO3E;AA0BD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,IAAI,CAwE1G"}