@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.
- package/README.i18n.yaml +2 -2
- package/README.md +148 -77
- package/README.zh.md +32 -26
- package/lib/client.js +602 -67
- package/lib/client.js.map +1 -1
- package/lib/index.js +667 -32
- package/lib/mobile.js +3 -1
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FenceNotice.d.ts +21 -0
- package/lib/types/client/FenceNotice.d.ts.map +1 -0
- package/lib/types/client/FenceNotice.js +10 -0
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +18 -11
- package/lib/types/client/RemotePanel.d.ts +10 -4
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +7 -6
- package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -1
- package/lib/types/client/device-name.d.ts +3 -0
- package/lib/types/client/device-name.d.ts.map +1 -0
- package/lib/types/client/device-name.js +30 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +48 -0
- package/lib/types/client/locales.d.ts +51 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +79 -29
- package/lib/types/client/pair-api.d.ts +35 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +39 -0
- package/lib/types/client/remote-channel.d.ts +93 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -0
- package/lib/types/client/remote-channel.js +246 -0
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +5 -2
- package/lib/types/index.d.ts +19 -12
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +73 -5
- package/lib/types/loopback-proxy.d.ts +28 -0
- package/lib/types/loopback-proxy.d.ts.map +1 -0
- package/lib/types/loopback-proxy.js +112 -0
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +6 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +6 -2
- package/lib/types/mobile-routes.js +1 -1
- package/lib/types/pairing.d.ts +63 -11
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +137 -20
- package/lib/types/pairing.persist.test.js +60 -5
- package/lib/types/posture.d.ts +75 -0
- package/lib/types/posture.d.ts.map +1 -0
- package/lib/types/posture.js +116 -0
- package/lib/types/remote-api.d.ts +60 -0
- package/lib/types/remote-api.d.ts.map +1 -0
- package/lib/types/remote-api.js +181 -0
- package/lib/types/remote-methods.d.ts +33 -0
- package/lib/types/remote-methods.d.ts.map +1 -0
- package/lib/types/remote-methods.js +54 -0
- package/lib/types/routes.d.ts +4 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +33 -4
- package/package.json +2 -2
- package/src/client/FenceNotice.tsx +43 -0
- package/src/client/RemoteEntry.tsx +19 -9
- package/src/client/RemotePanel.tsx +83 -10
- package/src/client/RemoteSettingsCard.tsx +16 -0
- package/src/client/device-name.ts +31 -0
- package/src/client/index.ts +46 -0
- package/src/client/locales.ts +79 -29
- package/src/client/pair-api.ts +60 -0
- package/src/client/remote-channel.ts +269 -0
- package/src/client/remote.module.css +240 -16
- package/src/dsh-home.ts +36 -0
- package/src/gate.ts +5 -2
- package/src/index.ts +89 -17
- package/src/loopback-proxy.ts +126 -0
- package/src/mobile/views/SessionListView.tsx +6 -1
- package/src/mobile-api.ts +6 -2
- package/src/mobile-routes.ts +1 -1
- package/src/pairing.persist.test.ts +64 -5
- package/src/pairing.ts +167 -20
- package/src/posture.ts +154 -0
- package/src/remote-api.ts +217 -0
- package/src/remote-methods.ts +62 -0
- package/src/routes.ts +33 -4
package/lib/client.js
CHANGED
|
@@ -1298,6 +1298,19 @@ window.__ModuleLoader__.load({
|
|
|
1298
1298
|
const response = await fetch("/api/pair/stop", { method: "POST" });
|
|
1299
1299
|
if (!response.ok) throw new Error(`remote-web-ui: stop failed with ${String(response.status)}`);
|
|
1300
1300
|
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Revoke one paired device from the loopback panel.
|
|
1303
|
+
* @param deviceId - the session id of the row to drop.
|
|
1304
|
+
*/
|
|
1305
|
+
async function revokePair(deviceId) {
|
|
1306
|
+
const response = await fetch("/api/pair/revoke", {
|
|
1307
|
+
method: "POST",
|
|
1308
|
+
headers: { "content-type": "application/json" },
|
|
1309
|
+
body: JSON.stringify({ deviceId })
|
|
1310
|
+
});
|
|
1311
|
+
if (response.status === 404) return;
|
|
1312
|
+
if (!response.ok) throw new Error(`remote-web-ui: revoke failed with ${String(response.status)}`);
|
|
1313
|
+
}
|
|
1301
1314
|
/** Presence heartbeat from a paired phone (unpaired heartbeats 401 harmlessly). */
|
|
1302
1315
|
async function sendHeartbeat() {
|
|
1303
1316
|
await fetch("/api/pair/heartbeat", { method: "POST" });
|
|
@@ -1312,11 +1325,22 @@ window.__ModuleLoader__.load({
|
|
|
1312
1325
|
...workspace !== null && workspace !== "" ? { workspace } : {}
|
|
1313
1326
|
};
|
|
1314
1327
|
}
|
|
1328
|
+
/** Convert an issued `/m/` link into the desktop pairing form. */
|
|
1329
|
+
function desktopPairUrl(mobileUrl) {
|
|
1330
|
+
const url = new URL(mobileUrl);
|
|
1331
|
+
url.pathname = "/";
|
|
1332
|
+
return url.href;
|
|
1333
|
+
}
|
|
1315
1334
|
/** Human-readable expiry clock, e.g. "10:35". */
|
|
1316
1335
|
function formatClock(epochMs) {
|
|
1317
1336
|
const date = new Date(epochMs);
|
|
1318
1337
|
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
1319
1338
|
}
|
|
1339
|
+
/** Calendar + clock for last-seen timestamps, e.g. "2026-08-19 10:35". */
|
|
1340
|
+
function formatLastSeen(epochMs) {
|
|
1341
|
+
const date = new Date(epochMs);
|
|
1342
|
+
return `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${formatClock(epochMs)}`;
|
|
1343
|
+
}
|
|
1320
1344
|
/**
|
|
1321
1345
|
* Copy text to the clipboard with a fallback for insecure contexts
|
|
1322
1346
|
* (plain-HTTP LAN origins lack navigator.clipboard).
|
|
@@ -1343,8 +1367,18 @@ window.__ModuleLoader__.load({
|
|
|
1343
1367
|
}
|
|
1344
1368
|
}
|
|
1345
1369
|
//#endregion
|
|
1370
|
+
//#region src/client/device-name.ts
|
|
1371
|
+
/** Derive a short, non-sensitive device label from a browser User-Agent. */
|
|
1372
|
+
function deviceNameFromUserAgent(userAgent) {
|
|
1373
|
+
if (userAgent === void 0 || userAgent.trim() === "") return void 0;
|
|
1374
|
+
const os = /Windows NT/i.test(userAgent) ? "Windows" : /Android/i.test(userAgent) ? "Android" : /iPhone|iPad|iPod/i.test(userAgent) ? "iOS" : /Macintosh|Mac OS X/i.test(userAgent) ? "macOS" : /Linux/i.test(userAgent) ? "Linux" : void 0;
|
|
1375
|
+
const browser = /Edg(?:A|iOS)?\//i.test(userAgent) ? "Edge" : /(?:OPR|Opera)\//i.test(userAgent) ? "Opera" : /(?:Chrome|CriOS)\//i.test(userAgent) ? "Chrome" : /(?:Firefox|FxiOS)\//i.test(userAgent) ? "Firefox" : /Safari\//i.test(userAgent) && /Version\//i.test(userAgent) ? "Safari" : void 0;
|
|
1376
|
+
if (os !== void 0 && browser !== void 0) return `${os} · ${browser}`;
|
|
1377
|
+
return os ?? browser;
|
|
1378
|
+
}
|
|
1379
|
+
//#endregion
|
|
1346
1380
|
//#region \0dsh-css:packages/dsh-remote-web-ui/src/client/remote.module.css.mjs
|
|
1347
|
-
const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-update-available]:after{box-sizing:border-box;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-brand-primary);content:\"\";pointer-events:none;border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.fThDlq_trigger[data-wide=wide]{border-radius:8px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;overflow:hidden}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_notice{z-index:2000;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:min(420px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;padding:12px 16px;position:fixed;top:16px;left:50%;transform:translate(-50%)}.fThDlq_noticeTitle{color:var(--dsw-alias-state-error-primary);margin:0;font-weight:500}.fThDlq_noticeDetail{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_addresses{border:none;margin:12px 0 0;padding:0}.fThDlq_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}.fThDlq_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}.fThDlq_address:hover{background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}.fThDlq_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
|
|
1381
|
+
const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-update-available]:after{box-sizing:border-box;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-brand-primary);content:\"\";pointer-events:none;border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.fThDlq_trigger[data-wide=wide]{border-radius:8px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;display:block;overflow:hidden}.fThDlq_pairLinks{flex-direction:column;gap:8px;display:flex}.fThDlq_pairLinkRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:10px;min-width:0;padding:10px 12px;display:flex}.fThDlq_pairLinkText{flex:1;min-width:0}.fThDlq_pairLinkLabel{color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-size:12px;display:block}.fThDlq_copyLink{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);min-height:30px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:5px;padding:0 10px;display:inline-flex}.fThDlq_oneTimeHint{color:var(--dsw-alias-label-caption);margin:0;font-size:12px}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_fencePage{z-index:2000;box-sizing:border-box;background:var(--dsw-alias-bg-base);text-align:center;flex-direction:column;justify-content:center;align-items:center;padding:40px 24px;display:flex;position:fixed;inset:0;overflow:auto}.fThDlq_fenceCard{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:min(520px,100%);box-shadow:var(--dsw-shadow-lv3);text-align:center;border-radius:20px;margin-inline:auto;padding:36px 40px}.fThDlq_fenceMark{background:var(--dsw-alias-state-error-secondary);width:44px;height:44px;color:var(--dsw-alias-state-error-primary);border-radius:50%;place-items:center;margin-inline:auto;font-size:24px;line-height:1;display:grid}.fThDlq_fenceEyebrow{color:var(--dsw-alias-state-error-primary);margin:22px 0 8px;font-size:13px;font-weight:600}.fThDlq_fenceTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:24px;line-height:1.35}.fThDlq_fenceDetail{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:14px;line-height:1.65}.fThDlq_fenceSteps{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;margin:24px auto 0;padding:20px 20px 20px 42px;font-size:14px;line-height:1.65}.fThDlq_fenceSteps li+li{margin-top:8px}.fThDlq_fenceRetry{border:1px solid var(--dsw-alias-brand-primary);background:var(--dsw-alias-brand-primary);color:#fff;width:100%;font:inherit;cursor:pointer;border-radius:10px;margin-top:24px;padding:11px 16px;font-weight:500}.fThDlq_fenceRetry:hover{filter:brightness(.96)}.fThDlq_fenceRetry:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.fThDlq_fenceFootnote{color:var(--dsw-alias-label-tertiary);margin:14px 0 0;font-size:12px;line-height:1.55}.fThDlq_addresses{border:none;margin:12px 0 0;padding:0}.fThDlq_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}.fThDlq_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}.fThDlq_address:hover{background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}.fThDlq_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}.fThDlq_devices{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:8px;padding:12px 16px 14px;display:flex}.fThDlq_devicesTitle{margin:0;font-size:13px;font-weight:500;line-height:20px}.fThDlq_devicesEmpty{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_deviceList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.fThDlq_deviceRow{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.fThDlq_deviceMeta{flex-direction:column;gap:2px;min-width:0;display:flex}.fThDlq_deviceName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.fThDlq_devicePresence{font-size:12px;line-height:18px}.fThDlq_deviceOnline{color:var(--dsw-alias-state-success-primary)}.fThDlq_deviceOffline{color:var(--dsw-alias-label-secondary)}.fThDlq_deviceSeen{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px}.fThDlq_deviceRevoke{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;padding:6px 10px;font-size:12px;transition:background-color .12s,color .12s}.fThDlq_deviceRevoke:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_deviceRevoke:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address,.fThDlq_deviceRevoke{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
|
|
1348
1382
|
const tagId$1 = "@linxin666/dsh-remote-web-ui/remote.module.css";
|
|
1349
1383
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
1350
1384
|
const tag = document.createElement("style");
|
|
@@ -1374,18 +1408,42 @@ window.__ModuleLoader__.load({
|
|
|
1374
1408
|
"cardHeader": "fThDlq_cardHeader",
|
|
1375
1409
|
"cardTitle": "fThDlq_cardTitle",
|
|
1376
1410
|
"close": "fThDlq_close",
|
|
1411
|
+
"copyLink": "fThDlq_copyLink",
|
|
1412
|
+
"deviceList": "fThDlq_deviceList",
|
|
1413
|
+
"deviceMeta": "fThDlq_deviceMeta",
|
|
1414
|
+
"deviceName": "fThDlq_deviceName",
|
|
1415
|
+
"deviceOffline": "fThDlq_deviceOffline",
|
|
1416
|
+
"deviceOnline": "fThDlq_deviceOnline",
|
|
1417
|
+
"devicePresence": "fThDlq_devicePresence",
|
|
1418
|
+
"deviceRevoke": "fThDlq_deviceRevoke",
|
|
1419
|
+
"deviceRow": "fThDlq_deviceRow",
|
|
1420
|
+
"deviceSeen": "fThDlq_deviceSeen",
|
|
1421
|
+
"devices": "fThDlq_devices",
|
|
1422
|
+
"devicesEmpty": "fThDlq_devicesEmpty",
|
|
1423
|
+
"devicesTitle": "fThDlq_devicesTitle",
|
|
1377
1424
|
"entryRow": "fThDlq_entryRow",
|
|
1378
1425
|
"expired": "fThDlq_expired",
|
|
1379
1426
|
"expiry": "fThDlq_expiry",
|
|
1427
|
+
"fenceCard": "fThDlq_fenceCard",
|
|
1428
|
+
"fenceDetail": "fThDlq_fenceDetail",
|
|
1429
|
+
"fenceEyebrow": "fThDlq_fenceEyebrow",
|
|
1430
|
+
"fenceFootnote": "fThDlq_fenceFootnote",
|
|
1431
|
+
"fenceMark": "fThDlq_fenceMark",
|
|
1432
|
+
"fencePage": "fThDlq_fencePage",
|
|
1433
|
+
"fenceRetry": "fThDlq_fenceRetry",
|
|
1434
|
+
"fenceSteps": "fThDlq_fenceSteps",
|
|
1435
|
+
"fenceTitle": "fThDlq_fenceTitle",
|
|
1380
1436
|
"header": "fThDlq_header",
|
|
1381
1437
|
"heading": "fThDlq_heading",
|
|
1382
1438
|
"hint": "fThDlq_hint",
|
|
1383
1439
|
"link": "fThDlq_link",
|
|
1384
1440
|
"mask": "fThDlq_mask",
|
|
1385
|
-
"
|
|
1386
|
-
"noticeDetail": "fThDlq_noticeDetail",
|
|
1387
|
-
"noticeTitle": "fThDlq_noticeTitle",
|
|
1441
|
+
"oneTimeHint": "fThDlq_oneTimeHint",
|
|
1388
1442
|
"overlay": "fThDlq_overlay",
|
|
1443
|
+
"pairLinkLabel": "fThDlq_pairLinkLabel",
|
|
1444
|
+
"pairLinkRow": "fThDlq_pairLinkRow",
|
|
1445
|
+
"pairLinkText": "fThDlq_pairLinkText",
|
|
1446
|
+
"pairLinks": "fThDlq_pairLinks",
|
|
1389
1447
|
"panel": "fThDlq_panel",
|
|
1390
1448
|
"qr": "fThDlq_qr",
|
|
1391
1449
|
"qrWrap": "fThDlq_qrWrap",
|
|
@@ -1444,7 +1502,7 @@ window.__ModuleLoader__.load({
|
|
|
1444
1502
|
* @param props - copy, state, and actions.
|
|
1445
1503
|
* @returns the panel element tree.
|
|
1446
1504
|
*/
|
|
1447
|
-
function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic }) {
|
|
1505
|
+
function RemotePanel({ t, state, copied, onClose, onStop, onRefresh, onCopy, onPickAddress, onPickPublic, onRevoke }) {
|
|
1448
1506
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1449
1507
|
className: remote_module_css_default.panel,
|
|
1450
1508
|
role: "dialog",
|
|
@@ -1499,6 +1557,17 @@ window.__ModuleLoader__.load({
|
|
|
1499
1557
|
children: t("status.unreachableHint")
|
|
1500
1558
|
})]
|
|
1501
1559
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1560
|
+
state.posture !== void 0 && state.posture.hosts.some((host) => host.exposed) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1561
|
+
className: remote_module_css_default.banner,
|
|
1562
|
+
role: "alert",
|
|
1563
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1564
|
+
className: remote_module_css_default.bannerTitle,
|
|
1565
|
+
children: t("posture.exposed")
|
|
1566
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1567
|
+
className: remote_module_css_default.bannerHint,
|
|
1568
|
+
children: t("posture.exposedHint", { hosts: state.posture.hosts.filter((host) => host.exposed).map((host) => host.host).join(", ") })
|
|
1569
|
+
})]
|
|
1570
|
+
}),
|
|
1502
1571
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1503
1572
|
className: remote_module_css_default.card,
|
|
1504
1573
|
children: [
|
|
@@ -1542,10 +1611,49 @@ window.__ModuleLoader__.load({
|
|
|
1542
1611
|
className: remote_module_css_default.hint,
|
|
1543
1612
|
children: state.public ? t("pair.publicHint") : t("pair.hint")
|
|
1544
1613
|
}),
|
|
1614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1615
|
+
className: remote_module_css_default.pairLinks,
|
|
1616
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1617
|
+
className: remote_module_css_default.pairLinkRow,
|
|
1618
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1619
|
+
className: remote_module_css_default.pairLinkText,
|
|
1620
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1621
|
+
className: remote_module_css_default.pairLinkLabel,
|
|
1622
|
+
children: t("pair.phoneLabel")
|
|
1623
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1624
|
+
className: remote_module_css_default.link,
|
|
1625
|
+
title: state.url,
|
|
1626
|
+
children: state.url
|
|
1627
|
+
})]
|
|
1628
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1629
|
+
type: "button",
|
|
1630
|
+
className: remote_module_css_default.copyLink,
|
|
1631
|
+
onClick: () => onCopy("phone", state.url),
|
|
1632
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }), copied === "phone" ? t("action.copied") : t("action.copyPhone")]
|
|
1633
|
+
})]
|
|
1634
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1635
|
+
className: remote_module_css_default.pairLinkRow,
|
|
1636
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1637
|
+
className: remote_module_css_default.pairLinkText,
|
|
1638
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1639
|
+
className: remote_module_css_default.pairLinkLabel,
|
|
1640
|
+
children: t("pair.desktopLabel")
|
|
1641
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1642
|
+
className: remote_module_css_default.link,
|
|
1643
|
+
title: desktopPairUrl(state.url),
|
|
1644
|
+
children: desktopPairUrl(state.url)
|
|
1645
|
+
})]
|
|
1646
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1647
|
+
type: "button",
|
|
1648
|
+
className: remote_module_css_default.copyLink,
|
|
1649
|
+
onClick: () => onCopy("desktop", desktopPairUrl(state.url)),
|
|
1650
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutline16, { size: 14 }), copied === "desktop" ? t("action.copied") : t("action.copyDesktop")]
|
|
1651
|
+
})]
|
|
1652
|
+
})]
|
|
1653
|
+
}),
|
|
1545
1654
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1546
|
-
className: remote_module_css_default.
|
|
1547
|
-
|
|
1548
|
-
children: state.url
|
|
1655
|
+
className: remote_module_css_default.oneTimeHint,
|
|
1656
|
+
children: t("pair.oneTimeHint")
|
|
1549
1657
|
}),
|
|
1550
1658
|
state.phase === "stopped" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1551
1659
|
className: remote_module_css_default.stoppedHint,
|
|
@@ -1602,26 +1710,59 @@ window.__ModuleLoader__.load({
|
|
|
1602
1710
|
}),
|
|
1603
1711
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1604
1712
|
className: remote_module_css_default.actions,
|
|
1605
|
-
children: [
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1713
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1714
|
+
type: "button",
|
|
1715
|
+
className: remote_module_css_default.action,
|
|
1716
|
+
onClick: onStop,
|
|
1717
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconStopFill16, { size: 14 }), t("action.stop")]
|
|
1718
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1719
|
+
type: "button",
|
|
1720
|
+
className: remote_module_css_default.action,
|
|
1721
|
+
onClick: onRefresh,
|
|
1722
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }), t("action.refresh")]
|
|
1723
|
+
})]
|
|
1724
|
+
}),
|
|
1725
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1726
|
+
className: remote_module_css_default.devices,
|
|
1727
|
+
"data-testid": "remote-devices",
|
|
1728
|
+
"aria-label": t("devices.title"),
|
|
1729
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1730
|
+
className: remote_module_css_default.devicesTitle,
|
|
1731
|
+
children: t("devices.title")
|
|
1732
|
+
}), state.devices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1733
|
+
className: remote_module_css_default.devicesEmpty,
|
|
1734
|
+
children: t("devices.empty")
|
|
1735
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1736
|
+
className: remote_module_css_default.deviceList,
|
|
1737
|
+
children: state.devices.map((device) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1738
|
+
className: remote_module_css_default.deviceRow,
|
|
1739
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1740
|
+
className: remote_module_css_default.deviceMeta,
|
|
1741
|
+
children: [
|
|
1742
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1743
|
+
className: remote_module_css_default.deviceName,
|
|
1744
|
+
children: deviceNameFromUserAgent(device.userAgent) ?? t("devices.unknown")
|
|
1745
|
+
}),
|
|
1746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1747
|
+
className: clsx(remote_module_css_default.devicePresence, device.online ? remote_module_css_default.deviceOnline : remote_module_css_default.deviceOffline),
|
|
1748
|
+
children: device.online ? t("devices.online") : t("devices.offline")
|
|
1749
|
+
}),
|
|
1750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1751
|
+
className: remote_module_css_default.deviceSeen,
|
|
1752
|
+
children: t("devices.lastSeen", { time: formatLastSeen(device.lastSeenAt) })
|
|
1753
|
+
})
|
|
1754
|
+
]
|
|
1755
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1756
|
+
type: "button",
|
|
1757
|
+
className: remote_module_css_default.deviceRevoke,
|
|
1758
|
+
"aria-label": t("devices.revoke.label"),
|
|
1759
|
+
onClick: () => {
|
|
1760
|
+
onRevoke(device.id);
|
|
1761
|
+
},
|
|
1762
|
+
children: t("devices.revoke")
|
|
1763
|
+
})]
|
|
1764
|
+
}, device.id))
|
|
1765
|
+
})]
|
|
1625
1766
|
})
|
|
1626
1767
|
] })]
|
|
1627
1768
|
});
|
|
@@ -2071,7 +2212,9 @@ window.__ModuleLoader__.load({
|
|
|
2071
2212
|
phase: frame.phase,
|
|
2072
2213
|
deviceCount: frame.deviceCount,
|
|
2073
2214
|
onlineCount: frame.onlineCount,
|
|
2074
|
-
|
|
2215
|
+
devices: frame.devices ?? [],
|
|
2216
|
+
...frame.tunnel !== void 0 ? { tunnel: frame.tunnel } : {},
|
|
2217
|
+
...frame.posture !== void 0 ? { posture: frame.posture } : {}
|
|
2075
2218
|
};
|
|
2076
2219
|
}
|
|
2077
2220
|
/**
|
|
@@ -2086,7 +2229,7 @@ window.__ModuleLoader__.load({
|
|
|
2086
2229
|
(0, react.useEffect)(() => {
|
|
2087
2230
|
stateRef.current = state;
|
|
2088
2231
|
}, [state]);
|
|
2089
|
-
const [copied, setCopied] = (0, react.useState)(
|
|
2232
|
+
const [copied, setCopied] = (0, react.useState)(void 0);
|
|
2090
2233
|
const eventSource = (0, react.useRef)(void 0);
|
|
2091
2234
|
const openSeq = (0, react.useRef)(0);
|
|
2092
2235
|
const workspaceId = useWorkspaces((s) => s.recentWorkspaceId);
|
|
@@ -2115,6 +2258,7 @@ window.__ModuleLoader__.load({
|
|
|
2115
2258
|
phase: "waiting",
|
|
2116
2259
|
deviceCount: 0,
|
|
2117
2260
|
onlineCount: 0,
|
|
2261
|
+
devices: [],
|
|
2118
2262
|
public: publicBaseUrl !== void 0 && result.url.startsWith(publicBaseUrl),
|
|
2119
2263
|
...publicBaseUrl !== void 0 ? { publicBaseUrl } : {},
|
|
2120
2264
|
address: address ?? result.lanAddresses[0] ?? "",
|
|
@@ -2174,7 +2318,15 @@ window.__ModuleLoader__.load({
|
|
|
2174
2318
|
stopPair().catch(() => {});
|
|
2175
2319
|
setState((previous) => previous.kind === "ready" ? {
|
|
2176
2320
|
...previous,
|
|
2177
|
-
phase: "stopped"
|
|
2321
|
+
phase: "stopped",
|
|
2322
|
+
devices: []
|
|
2323
|
+
} : previous);
|
|
2324
|
+
}, []);
|
|
2325
|
+
const handleRevoke = (0, react.useCallback)((deviceId) => {
|
|
2326
|
+
revokePair(deviceId).catch(() => {});
|
|
2327
|
+
setState((previous) => previous.kind === "ready" ? {
|
|
2328
|
+
...previous,
|
|
2329
|
+
devices: previous.devices.filter((device) => device.id !== deviceId)
|
|
2178
2330
|
} : previous);
|
|
2179
2331
|
}, []);
|
|
2180
2332
|
const handleRefresh = (0, react.useCallback)(() => {
|
|
@@ -2188,16 +2340,15 @@ window.__ModuleLoader__.load({
|
|
|
2188
2340
|
const handlePickPublic = (0, react.useCallback)(() => {
|
|
2189
2341
|
mint().then(setState);
|
|
2190
2342
|
}, [mint]);
|
|
2191
|
-
const handleCopy = (0, react.useCallback)(() => {
|
|
2192
|
-
|
|
2193
|
-
copyText(state.url).then((ok) => {
|
|
2343
|
+
const handleCopy = (0, react.useCallback)((target, url) => {
|
|
2344
|
+
copyText(url).then((ok) => {
|
|
2194
2345
|
if (!ok) return;
|
|
2195
|
-
setCopied(
|
|
2346
|
+
setCopied(target);
|
|
2196
2347
|
window.setTimeout(() => {
|
|
2197
|
-
setCopied(
|
|
2348
|
+
setCopied(void 0);
|
|
2198
2349
|
}, 1500);
|
|
2199
2350
|
});
|
|
2200
|
-
}, [
|
|
2351
|
+
}, []);
|
|
2201
2352
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2202
2353
|
className: remote_module_css_default.entryRow,
|
|
2203
2354
|
"data-rail": wide ? void 0 : "rail",
|
|
@@ -2225,7 +2376,8 @@ window.__ModuleLoader__.load({
|
|
|
2225
2376
|
onRefresh: handleRefresh,
|
|
2226
2377
|
onCopy: handleCopy,
|
|
2227
2378
|
onPickAddress: handlePickAddress,
|
|
2228
|
-
onPickPublic: handlePickPublic
|
|
2379
|
+
onPickPublic: handlePickPublic,
|
|
2380
|
+
onRevoke: handleRevoke
|
|
2229
2381
|
})]
|
|
2230
2382
|
}), document.body)] });
|
|
2231
2383
|
}
|
|
@@ -3046,6 +3198,7 @@ window.__ModuleLoader__.load({
|
|
|
3046
3198
|
numberField("tokenTtlMs"),
|
|
3047
3199
|
numberField("offlineAfterMs"),
|
|
3048
3200
|
numberField("maxDevices"),
|
|
3201
|
+
numberField("idleExpireMs"),
|
|
3049
3202
|
textField("cookieName"),
|
|
3050
3203
|
booleanField("requirePairingForLan"),
|
|
3051
3204
|
textField("publicBaseUrl"),
|
|
@@ -3061,6 +3214,7 @@ window.__ModuleLoader__.load({
|
|
|
3061
3214
|
tokenTtlMs: this.form.field("tokenTtlMs"),
|
|
3062
3215
|
offlineAfterMs: this.form.field("offlineAfterMs"),
|
|
3063
3216
|
maxDevices: this.form.field("maxDevices"),
|
|
3217
|
+
idleExpireMs: this.form.field("idleExpireMs"),
|
|
3064
3218
|
cookieName: this.form.field("cookieName"),
|
|
3065
3219
|
requirePairingForLan: this.form.field("requirePairingForLan"),
|
|
3066
3220
|
publicBaseUrl: this.form.field("publicBaseUrl"),
|
|
@@ -3168,6 +3322,20 @@ window.__ModuleLoader__.load({
|
|
|
3168
3322
|
props.resetField("maxDevices");
|
|
3169
3323
|
}
|
|
3170
3324
|
}),
|
|
3325
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
3326
|
+
id: "settings-remote-idle-expire",
|
|
3327
|
+
label: t("settings.idleExpireMs"),
|
|
3328
|
+
hint: t("settings.idleExpireMsHint"),
|
|
3329
|
+
numeric: true,
|
|
3330
|
+
...fieldProps,
|
|
3331
|
+
...state.idleExpireMs,
|
|
3332
|
+
onEdit: (text) => {
|
|
3333
|
+
props.edit("idleExpireMs", text);
|
|
3334
|
+
},
|
|
3335
|
+
onReset: () => {
|
|
3336
|
+
props.resetField("idleExpireMs");
|
|
3337
|
+
}
|
|
3338
|
+
}),
|
|
3171
3339
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
3172
3340
|
id: "settings-remote-cookie",
|
|
3173
3341
|
label: t("settings.cookieName"),
|
|
@@ -3248,46 +3416,69 @@ window.__ModuleLoader__.load({
|
|
|
3248
3416
|
}
|
|
3249
3417
|
//#endregion
|
|
3250
3418
|
//#region src/client/locales.ts
|
|
3251
|
-
/** `remote` namespace dictionaries: the
|
|
3419
|
+
/** `remote` namespace dictionaries: the remote-access surface copy. */
|
|
3252
3420
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
3253
3421
|
const zh = {
|
|
3254
|
-
"entry.label": "
|
|
3255
|
-
"title": "
|
|
3256
|
-
"subtitle": "
|
|
3257
|
-
"card.title": "
|
|
3422
|
+
"entry.label": "远程访问",
|
|
3423
|
+
"title": "远程访问",
|
|
3424
|
+
"subtitle": "通过手机或另一台电脑配对,远程访问当前工作区",
|
|
3425
|
+
"card.title": "设备配对",
|
|
3258
3426
|
"public.badge": "公网",
|
|
3259
|
-
"status.waiting": "
|
|
3427
|
+
"status.waiting": "等待设备连接",
|
|
3260
3428
|
"status.connected": "已连接 {n} 台设备",
|
|
3261
3429
|
"status.disconnected": "已配对设备离线",
|
|
3262
|
-
"status.stopped": "
|
|
3430
|
+
"status.stopped": "已停止远程访问",
|
|
3263
3431
|
"status.lanRequired": "此功能需要以 --host 0.0.0.0 启动 dsh web,或配置公网地址才能使用",
|
|
3264
3432
|
"status.lanRequiredHint": "当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请用 dsh web --host 0.0.0.0 重新启动,或在设置中填写内网穿透的公网地址。",
|
|
3265
3433
|
"status.loopbackRequired": "配对面板仅限本机使用",
|
|
3266
3434
|
"status.loopbackRequiredHint": "请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。",
|
|
3267
3435
|
"status.unreachable": "无法连接配对服务",
|
|
3268
3436
|
"status.unreachableHint": "请刷新页面后重试。",
|
|
3269
|
-
"pair.hint": "
|
|
3270
|
-
"pair.publicHint": "
|
|
3437
|
+
"pair.hint": "无法扫码?可直接打开下方配对链接",
|
|
3438
|
+
"pair.publicHint": "公网链接:设备无需与本机处于同一网络",
|
|
3271
3439
|
"address.label": "选择二维码指向的网络",
|
|
3272
3440
|
"address.public": "公网地址",
|
|
3273
3441
|
"address.lan": "局域网",
|
|
3274
|
-
"address.hint": "
|
|
3442
|
+
"address.hint": "远程设备不在同一网络时请使用公网地址;局域网地址仅限同一网络内使用。",
|
|
3275
3443
|
"pair.expires": "二维码有效至 {time}",
|
|
3276
3444
|
"pair.expired": "二维码已过期,请刷新",
|
|
3445
|
+
"pair.phoneLabel": "手机配对链接",
|
|
3446
|
+
"pair.desktopLabel": "电脑配对链接",
|
|
3447
|
+
"pair.oneTimeHint": "两条链接共用同一枚一次性令牌;任意设备配对成功后,另一条链接立即失效。",
|
|
3277
3448
|
"pair.failed.title": "配对失败",
|
|
3278
3449
|
"pair.failed.detail": "链接无效或已使用,请回到电脑端刷新二维码后重新扫码。",
|
|
3450
|
+
"fence.unpaired.title": "此设备未配对,无法访问工作区数据",
|
|
3451
|
+
"fence.unpaired.eyebrow": "需要设备配对",
|
|
3452
|
+
"fence.unpaired.hint": "为保护工作区、会话与插件数据,远程电脑必须先通过主电脑授权。",
|
|
3453
|
+
"fence.unpaired.stepDesktop": "在主电脑打开 http://127.0.0.1:3080,进入“远程访问”。",
|
|
3454
|
+
"fence.unpaired.stepLink": "在“设备配对”中复制电脑配对链接。",
|
|
3455
|
+
"fence.unpaired.stepOpen": "在当前浏览器打开该链接,完成授权后即可进入。",
|
|
3456
|
+
"fence.unpaired.retry": "重新检测",
|
|
3457
|
+
"fence.unpaired.footnote": "请勿使用他人提供的配对链接;管理员可随时取消此设备的授权。",
|
|
3458
|
+
"posture.exposed": "/api 通道对未配对设备敞开",
|
|
3459
|
+
"posture.exposedHint": "以下来源的请求未经配对即可访问完整桌面 API:{hosts}。请移除对应来源的 --trusted-host(配对机制已覆盖远程访问),或改为仅绑定 127.0.0.1 并使用隧道。",
|
|
3279
3460
|
"action.stop": "停止",
|
|
3280
3461
|
"action.refresh": "刷新二维码",
|
|
3281
3462
|
"action.copy": "复制链接",
|
|
3463
|
+
"action.copyPhone": "复制手机链接",
|
|
3464
|
+
"action.copyDesktop": "复制电脑链接",
|
|
3282
3465
|
"action.copied": "已复制",
|
|
3283
|
-
"
|
|
3466
|
+
"devices.title": "已授权设备",
|
|
3467
|
+
"devices.empty": "还没有已配对的设备。扫码或打开链接后会出现在这里。",
|
|
3468
|
+
"devices.unknown": "未知设备",
|
|
3469
|
+
"devices.online": "在线",
|
|
3470
|
+
"devices.offline": "离线",
|
|
3471
|
+
"devices.lastSeen": "最近活动 {time}",
|
|
3472
|
+
"devices.revoke": "取消配对",
|
|
3473
|
+
"devices.revoke.label": "取消配对此设备",
|
|
3474
|
+
"stopped.hint": "已停止远程访问。点击\"刷新二维码\"重新开启。",
|
|
3284
3475
|
"tunnel.starting": "公网隧道启动中,二维码将自动变为公网链接…",
|
|
3285
3476
|
"tunnel.failed": "公网隧道启动失败:{error}",
|
|
3286
3477
|
"tunnel.unknownError": "未知错误",
|
|
3287
|
-
"close.label": "
|
|
3288
|
-
"settings.title": "
|
|
3478
|
+
"close.label": "关闭远程访问面板",
|
|
3479
|
+
"settings.title": "远程访问设置",
|
|
3289
3480
|
"settings.description": "配对安全与设备限额。",
|
|
3290
|
-
"settings.enabled": "
|
|
3481
|
+
"settings.enabled": "启用远程访问",
|
|
3291
3482
|
"settings.enabledHint": "关闭后移除侧边栏入口并停用配对路由与局域网栅栏。",
|
|
3292
3483
|
"settings.tokenTtlMs": "配对令牌有效期(毫秒)",
|
|
3293
3484
|
"settings.tokenTtlMsHint": "新生成的二维码链接在此时间后失效。",
|
|
@@ -3295,10 +3486,12 @@ window.__ModuleLoader__.load({
|
|
|
3295
3486
|
"settings.offlineAfterMsHint": "配对设备超过该时长未上报心跳即视为离线。",
|
|
3296
3487
|
"settings.maxDevices": "已配对设备上限",
|
|
3297
3488
|
"settings.maxDevicesHint": "超过上限时淘汰最旧的设备会话。",
|
|
3489
|
+
"settings.idleExpireMs": "空闲过期(毫秒)",
|
|
3490
|
+
"settings.idleExpireMsHint": "超过该时长没有任何心跳或请求的已配对设备会被删除,必须重新扫码。默认 7 天。",
|
|
3298
3491
|
"settings.cookieName": "设备 Cookie 名",
|
|
3299
3492
|
"settings.cookieNameHint": "携带已配对设备标识的 Cookie 名称。",
|
|
3300
3493
|
"settings.requirePairingForLan": "局域网访问要求配对",
|
|
3301
|
-
"settings.requirePairingForLanHint": "
|
|
3494
|
+
"settings.requirePairingForLanHint": "开启:非本机回环的桌面 Web GUI 改走门控的 /remote,必须携带有效配对 Cookie;关闭:桌面继续走普通路径(仅在该来源已被 SDK 信任时有意义),配对只管理令牌与状态。",
|
|
3302
3495
|
"settings.publicBaseUrl": "公网地址(可选)",
|
|
3303
3496
|
"settings.publicBaseUrlHint": "内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。",
|
|
3304
3497
|
"settings.autoTunnel": "自动公网隧道",
|
|
@@ -3358,43 +3551,66 @@ window.__ModuleLoader__.load({
|
|
|
3358
3551
|
};
|
|
3359
3552
|
/** English dictionary, checked complete against the zh key set. */
|
|
3360
3553
|
const en = {
|
|
3361
|
-
"entry.label": "
|
|
3362
|
-
"title": "
|
|
3363
|
-
"subtitle": "
|
|
3364
|
-
"card.title": "
|
|
3554
|
+
"entry.label": "Remote access",
|
|
3555
|
+
"title": "Remote access",
|
|
3556
|
+
"subtitle": "Pair a phone or another computer to access this workspace remotely",
|
|
3557
|
+
"card.title": "Pair a device",
|
|
3365
3558
|
"public.badge": "Public",
|
|
3366
|
-
"status.waiting": "Waiting for a
|
|
3559
|
+
"status.waiting": "Waiting for a device",
|
|
3367
3560
|
"status.connected": "{n} device(s) connected",
|
|
3368
3561
|
"status.disconnected": "Paired devices offline",
|
|
3369
|
-
"status.stopped": "
|
|
3562
|
+
"status.stopped": "Remote access stopped",
|
|
3370
3563
|
"status.lanRequired": "This feature needs dsh web started with --host 0.0.0.0, or a configured public address",
|
|
3371
3564
|
"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.",
|
|
3372
3565
|
"status.loopbackRequired": "The pairing panel works on this machine only",
|
|
3373
3566
|
"status.loopbackRequiredHint": "Open this page at http://127.0.0.1 to mint a QR code; phones use the paired link.",
|
|
3374
3567
|
"status.unreachable": "Cannot reach the pairing service",
|
|
3375
3568
|
"status.unreachableHint": "Refresh the page and try again.",
|
|
3376
|
-
"pair.hint": "Cannot scan? Open the
|
|
3377
|
-
"pair.publicHint": "Public
|
|
3569
|
+
"pair.hint": "Cannot scan? Open one of the pairing links below",
|
|
3570
|
+
"pair.publicHint": "Public links work when the device is not on the same network",
|
|
3378
3571
|
"address.label": "Network the QR code points to",
|
|
3379
3572
|
"address.public": "Public",
|
|
3380
3573
|
"address.lan": "LAN",
|
|
3381
|
-
"address.hint": "Use the public address when the
|
|
3574
|
+
"address.hint": "Use the public address when the remote device is on another network; LAN addresses only work on the same network.",
|
|
3382
3575
|
"pair.expires": "QR code valid until {time}",
|
|
3383
3576
|
"pair.expired": "QR code expired — refresh it",
|
|
3577
|
+
"pair.phoneLabel": "Phone pairing link",
|
|
3578
|
+
"pair.desktopLabel": "Computer pairing link",
|
|
3579
|
+
"pair.oneTimeHint": "Both links share one single-use token. Pairing either device invalidates the other link immediately.",
|
|
3384
3580
|
"pair.failed.title": "Pairing failed",
|
|
3385
3581
|
"pair.failed.detail": "The link is invalid or was already used. Refresh the QR code on your computer and scan again.",
|
|
3582
|
+
"fence.unpaired.title": "This device is not paired and cannot reach workspace data",
|
|
3583
|
+
"fence.unpaired.eyebrow": "Device pairing required",
|
|
3584
|
+
"fence.unpaired.hint": "To protect workspace, session, and plugin data, a remote computer must first be authorized by the primary computer.",
|
|
3585
|
+
"fence.unpaired.stepDesktop": "On the primary computer, open http://127.0.0.1:3080 and go to Remote access.",
|
|
3586
|
+
"fence.unpaired.stepLink": "Copy the computer pairing link under Pair a device.",
|
|
3587
|
+
"fence.unpaired.stepOpen": "Open that link in this browser. After authorization, the workspace will become available.",
|
|
3588
|
+
"fence.unpaired.retry": "Check again",
|
|
3589
|
+
"fence.unpaired.footnote": "Do not use pairing links from people you do not trust. An administrator can revoke this device at any time.",
|
|
3590
|
+
"posture.exposed": "The /api channel is open to unpaired devices",
|
|
3591
|
+
"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.",
|
|
3386
3592
|
"action.stop": "Stop",
|
|
3387
3593
|
"action.refresh": "Refresh QR",
|
|
3388
3594
|
"action.copy": "Copy link",
|
|
3595
|
+
"action.copyPhone": "Copy phone link",
|
|
3596
|
+
"action.copyDesktop": "Copy computer link",
|
|
3389
3597
|
"action.copied": "Copied",
|
|
3390
|
-
"
|
|
3598
|
+
"devices.title": "Authorized devices",
|
|
3599
|
+
"devices.empty": "No paired devices yet. Scan the QR or open the link to add one.",
|
|
3600
|
+
"devices.unknown": "Unknown device",
|
|
3601
|
+
"devices.online": "Online",
|
|
3602
|
+
"devices.offline": "Offline",
|
|
3603
|
+
"devices.lastSeen": "Last active {time}",
|
|
3604
|
+
"devices.revoke": "Unpair",
|
|
3605
|
+
"devices.revoke.label": "Unpair this device",
|
|
3606
|
+
"stopped.hint": "Remote access is stopped. Click \"Refresh QR\" to re-enable it.",
|
|
3391
3607
|
"tunnel.starting": "The public tunnel is starting; the QR code will switch to a public link shortly…",
|
|
3392
3608
|
"tunnel.failed": "The public tunnel failed to start: {error}",
|
|
3393
3609
|
"tunnel.unknownError": "unknown error",
|
|
3394
|
-
"close.label": "Close
|
|
3395
|
-
"settings.title": "
|
|
3610
|
+
"close.label": "Close remote access panel",
|
|
3611
|
+
"settings.title": "Remote access settings",
|
|
3396
3612
|
"settings.description": "Pairing security and device limits.",
|
|
3397
|
-
"settings.enabled": "Enable
|
|
3613
|
+
"settings.enabled": "Enable remote access",
|
|
3398
3614
|
"settings.enabledHint": "When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.",
|
|
3399
3615
|
"settings.tokenTtlMs": "Pairing token lifetime (ms)",
|
|
3400
3616
|
"settings.tokenTtlMsHint": "How long a minted QR link stays valid before it dies.",
|
|
@@ -3402,10 +3618,12 @@ window.__ModuleLoader__.load({
|
|
|
3402
3618
|
"settings.offlineAfterMsHint": "A paired device flips to offline when it has not been seen for this long.",
|
|
3403
3619
|
"settings.maxDevices": "Paired device cap",
|
|
3404
3620
|
"settings.maxDevicesHint": "Hard cap on paired device sessions; the oldest is evicted when full.",
|
|
3621
|
+
"settings.idleExpireMs": "Idle expiry (ms)",
|
|
3622
|
+
"settings.idleExpireMsHint": "A paired device with no heartbeat or request for this long is deleted and must scan again. Default is 7 days.",
|
|
3405
3623
|
"settings.cookieName": "Device cookie name",
|
|
3406
3624
|
"settings.cookieNameHint": "Cookie that carries the paired device id.",
|
|
3407
3625
|
"settings.requirePairingForLan": "Require pairing for LAN access",
|
|
3408
|
-
"settings.requirePairingForLanHint": "On:
|
|
3626
|
+
"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.",
|
|
3409
3627
|
"settings.publicBaseUrl": "Public address (optional)",
|
|
3410
3628
|
"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.",
|
|
3411
3629
|
"settings.autoTunnel": "Auto public tunnel",
|
|
@@ -3552,6 +3770,278 @@ window.__ModuleLoader__.load({
|
|
|
3552
3770
|
page.replaceState(`${url.pathname}${url.search}${url.hash}`);
|
|
3553
3771
|
}
|
|
3554
3772
|
//#endregion
|
|
3773
|
+
//#region src/client/remote-channel.ts
|
|
3774
|
+
/**
|
|
3775
|
+
* The remote desktop channel — browser half. On a non-loopback origin (LAN
|
|
3776
|
+
* address or public tunnel) fenced host routes refuse the request, and
|
|
3777
|
+
* pairing is the real access control — so same-origin traffic the desktop
|
|
3778
|
+
* issues is rewritten onto this plugin's gated `/remote` prefix (host half
|
|
3779
|
+
* in src/remote-api.ts). The host then re-issues the call to 127.0.0.1 so
|
|
3780
|
+
* plugin loopback fences pass.
|
|
3781
|
+
*
|
|
3782
|
+
* The rewrite is deliberately narrow:
|
|
3783
|
+
* - loopback origins are untouched (the desktop at 127.0.0.1 keeps original paths);
|
|
3784
|
+
* - the pairing routes (`/api/pair/*`) stay where they are — accept must
|
|
3785
|
+
* work BEFORE a device is paired;
|
|
3786
|
+
* - the update endpoints (`/api/update/*`) stay loopback-only;
|
|
3787
|
+
* - desktop-launcher create/shutdown (`/api/dsh-desktop-launcher/*`) stay
|
|
3788
|
+
* loopback-only (host-local files and process exit);
|
|
3789
|
+
* - the family settings bridge (`/api/dsh-web-ui-settings/*`) stays
|
|
3790
|
+
* loopback-only (same plane as SDK settings.*);
|
|
3791
|
+
* - `/api/*` (SDK methods and `/api/<plugin>/...` plugin namespaces),
|
|
3792
|
+
* `/sidebar/*`, `/git/*`, and `/pet/*` ride the channel;
|
|
3793
|
+
* - fetch, EventSource, WebSocket, and img/script/iframe `src` are patched;
|
|
3794
|
+
* everything else calls the original unchanged.
|
|
3795
|
+
*
|
|
3796
|
+
* Pure helpers are exported for unit tests; `installRemoteChannel` patches
|
|
3797
|
+
* the given window and returns their restore.
|
|
3798
|
+
*/
|
|
3799
|
+
/** The gated mirror prefix (must match src/remote-methods.ts). */
|
|
3800
|
+
const REMOTE_PREFIX = "/remote";
|
|
3801
|
+
const API_PREFIX = "/api/";
|
|
3802
|
+
const PAIR_PREFIX = "/api/pair/";
|
|
3803
|
+
const UPDATE_PREFIX = "/api/update/";
|
|
3804
|
+
const DESKTOP_LAUNCHER_PREFIX = "/api/dsh-desktop-launcher";
|
|
3805
|
+
const SETTINGS_BRIDGE_PREFIX = "/api/dsh-web-ui-settings";
|
|
3806
|
+
const SIDEBAR_PREFIX = "/sidebar/";
|
|
3807
|
+
const GIT_PREFIX = "/git/";
|
|
3808
|
+
const PET_PREFIX = "/pet/";
|
|
3809
|
+
const WS_PATHS = /* @__PURE__ */ new Set([
|
|
3810
|
+
"/api/events.mux",
|
|
3811
|
+
"/api/events.host",
|
|
3812
|
+
"/sidebar/ws/terminal",
|
|
3813
|
+
"/sidebar/ws/agent-terminals",
|
|
3814
|
+
"/api/dsh-ssh/terminal"
|
|
3815
|
+
]);
|
|
3816
|
+
/**
|
|
3817
|
+
* Browser-safe loopback classification for the page origin (the SDK client
|
|
3818
|
+
* exports its own; this copy keeps the module dependency-free).
|
|
3819
|
+
* @param hostname - a location hostname (IPv6 without brackets).
|
|
3820
|
+
* @returns true for localhost, IPv6 loopback, or any 127/8 literal.
|
|
3821
|
+
*/
|
|
3822
|
+
function isLoopbackHostname(hostname) {
|
|
3823
|
+
if (hostname === "localhost" || hostname === "::1") return true;
|
|
3824
|
+
const parts = hostname.split(".");
|
|
3825
|
+
return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
3826
|
+
}
|
|
3827
|
+
/**
|
|
3828
|
+
* Whether one same-origin path must ride the gated channel (fetch, EventSource,
|
|
3829
|
+
* img/script/iframe src).
|
|
3830
|
+
* @param pathname - the request URL pathname.
|
|
3831
|
+
*/
|
|
3832
|
+
function shouldRewriteFetchPath(pathname) {
|
|
3833
|
+
if (pathname.startsWith(PAIR_PREFIX)) return false;
|
|
3834
|
+
if (pathname.startsWith(UPDATE_PREFIX)) return false;
|
|
3835
|
+
if (pathname === DESKTOP_LAUNCHER_PREFIX || pathname.startsWith(`${DESKTOP_LAUNCHER_PREFIX}/`)) return false;
|
|
3836
|
+
if (pathname === SETTINGS_BRIDGE_PREFIX || pathname.startsWith(`${SETTINGS_BRIDGE_PREFIX}/`)) return false;
|
|
3837
|
+
if (pathname.startsWith(API_PREFIX)) return true;
|
|
3838
|
+
if (pathname.startsWith(SIDEBAR_PREFIX) || pathname === "/sidebar") return true;
|
|
3839
|
+
if (pathname.startsWith(GIT_PREFIX) || pathname === "/git") return true;
|
|
3840
|
+
if (pathname.startsWith(PET_PREFIX) || pathname === "/pet") return true;
|
|
3841
|
+
return false;
|
|
3842
|
+
}
|
|
3843
|
+
/**
|
|
3844
|
+
* Whether one WebSocket path must ride the gated channel.
|
|
3845
|
+
* @param pathname - the WebSocket URL pathname.
|
|
3846
|
+
*/
|
|
3847
|
+
function shouldRewriteWsPath(pathname) {
|
|
3848
|
+
return WS_PATHS.has(pathname);
|
|
3849
|
+
}
|
|
3850
|
+
/** The gated twin of one fenced path (`/remote` + original pathname). */
|
|
3851
|
+
function rewritePath(pathname) {
|
|
3852
|
+
return `${REMOTE_PREFIX}${pathname}`;
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Rewrite one raw URL string when it is same-origin and fenced. Relative
|
|
3856
|
+
* inputs stay relative so resource loaders do not unexpectedly absolutize.
|
|
3857
|
+
*/
|
|
3858
|
+
function rewriteRawUrl(raw, baseHref, origin) {
|
|
3859
|
+
let url;
|
|
3860
|
+
try {
|
|
3861
|
+
url = new URL(raw, baseHref);
|
|
3862
|
+
} catch {
|
|
3863
|
+
return raw;
|
|
3864
|
+
}
|
|
3865
|
+
if (url.origin !== origin) return raw;
|
|
3866
|
+
if (!shouldRewriteFetchPath(url.pathname)) return raw;
|
|
3867
|
+
url.pathname = rewritePath(url.pathname);
|
|
3868
|
+
if (raw.startsWith("/") && !raw.startsWith("//")) return `${url.pathname}${url.search}${url.hash}`;
|
|
3869
|
+
return url.href;
|
|
3870
|
+
}
|
|
3871
|
+
/** Read an unpaired code from either the SDK envelope or a plugin JSON body. */
|
|
3872
|
+
function unpairedCodeOf(value) {
|
|
3873
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
3874
|
+
const record = value;
|
|
3875
|
+
const nested = record.result;
|
|
3876
|
+
if (typeof nested === "object" && nested !== null) {
|
|
3877
|
+
const error = nested.error;
|
|
3878
|
+
if (typeof error === "object" && error !== null && typeof error.code === "string") return error.code;
|
|
3879
|
+
}
|
|
3880
|
+
const error = record.error;
|
|
3881
|
+
if (typeof error === "object" && error !== null && typeof error.code === "string") return error.code;
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* Whether a gated 403 is the unpaired-device fence (not a loopback-only
|
|
3885
|
+
* method denial, which uses the same status with code `forbidden`).
|
|
3886
|
+
*/
|
|
3887
|
+
async function isUnpairedDenied(response) {
|
|
3888
|
+
if (response.status !== 403) return false;
|
|
3889
|
+
try {
|
|
3890
|
+
return unpairedCodeOf(await response.json()) === "unpaired";
|
|
3891
|
+
} catch {
|
|
3892
|
+
return false;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
/**
|
|
3896
|
+
* Wrap a prototype `src` setter so fenced same-origin URLs ride `/remote`.
|
|
3897
|
+
* No-ops when the constructor is missing or `src` is not configurable.
|
|
3898
|
+
*/
|
|
3899
|
+
function patchSrcAccessor(ctor, rewrite) {
|
|
3900
|
+
if (ctor === void 0) return () => {};
|
|
3901
|
+
const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, "src");
|
|
3902
|
+
if (descriptor === void 0 || descriptor.configurable === false) return () => {};
|
|
3903
|
+
if (descriptor.set === void 0) return () => {};
|
|
3904
|
+
const originalSet = descriptor.set;
|
|
3905
|
+
const originalGet = descriptor.get;
|
|
3906
|
+
Object.defineProperty(ctor.prototype, "src", {
|
|
3907
|
+
configurable: true,
|
|
3908
|
+
enumerable: descriptor.enumerable ?? true,
|
|
3909
|
+
get: originalGet,
|
|
3910
|
+
set(value) {
|
|
3911
|
+
originalSet.call(this, rewrite(String(value)));
|
|
3912
|
+
}
|
|
3913
|
+
});
|
|
3914
|
+
return () => {
|
|
3915
|
+
Object.defineProperty(ctor.prototype, "src", descriptor);
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
/**
|
|
3919
|
+
* Patch `fetch`, `EventSource`, `WebSocket`, and resource `src` accessors on
|
|
3920
|
+
* one window to route fenced traffic through the gated channel.
|
|
3921
|
+
* @param window - the browser window (or a test double).
|
|
3922
|
+
* @param options - the unpaired callback.
|
|
3923
|
+
* @returns a function restoring the originals.
|
|
3924
|
+
*/
|
|
3925
|
+
function installRemoteChannel(window, options = {}) {
|
|
3926
|
+
const originalFetch = window.fetch;
|
|
3927
|
+
const OriginalWebSocket = window.WebSocket;
|
|
3928
|
+
const OriginalEventSource = window.EventSource;
|
|
3929
|
+
const sameOrigin = (url) => url.origin === window.location.origin;
|
|
3930
|
+
const rewrite = (raw) => rewriteRawUrl(raw, window.location.href, window.location.origin);
|
|
3931
|
+
const patchedFetch = (input, init) => {
|
|
3932
|
+
const url = new URL(typeof input === "string" || input instanceof URL ? input.toString() : input.url, window.location.href);
|
|
3933
|
+
if (sameOrigin(url) && shouldRewriteFetchPath(url.pathname)) {
|
|
3934
|
+
const rewritten = new URL(url);
|
|
3935
|
+
rewritten.pathname = rewritePath(url.pathname);
|
|
3936
|
+
const next = typeof input === "string" || input instanceof URL ? rewritten.toString() : new Request(rewritten, input);
|
|
3937
|
+
return Promise.resolve(originalFetch.call(window, next, init)).then(async (response) => {
|
|
3938
|
+
if (await isUnpairedDenied(response.clone())) options.onUnpaired?.();
|
|
3939
|
+
else options.onPaired?.();
|
|
3940
|
+
return response;
|
|
3941
|
+
});
|
|
3942
|
+
}
|
|
3943
|
+
return originalFetch.call(window, input, init);
|
|
3944
|
+
};
|
|
3945
|
+
class PatchedWebSocket extends OriginalWebSocket {
|
|
3946
|
+
constructor(url, protocols) {
|
|
3947
|
+
const parsed = new URL(url.toString(), window.location.href);
|
|
3948
|
+
const wsOrigin = parsed.protocol === "wss:" ? `https://${parsed.host}` : parsed.protocol === "ws:" ? `http://${parsed.host}` : "";
|
|
3949
|
+
if (wsOrigin !== "" && wsOrigin === window.location.origin && shouldRewriteWsPath(parsed.pathname)) {
|
|
3950
|
+
const rewritten = new URL(parsed);
|
|
3951
|
+
rewritten.pathname = rewritePath(parsed.pathname);
|
|
3952
|
+
super(rewritten, protocols);
|
|
3953
|
+
return;
|
|
3954
|
+
}
|
|
3955
|
+
super(url, protocols);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
const restoreSrc = [
|
|
3959
|
+
patchSrcAccessor(window.HTMLImageElement, rewrite),
|
|
3960
|
+
patchSrcAccessor(window.HTMLScriptElement, rewrite),
|
|
3961
|
+
patchSrcAccessor(window.HTMLIFrameElement, rewrite)
|
|
3962
|
+
];
|
|
3963
|
+
window.fetch = patchedFetch;
|
|
3964
|
+
window.WebSocket = PatchedWebSocket;
|
|
3965
|
+
if (OriginalEventSource !== void 0) {
|
|
3966
|
+
class PatchedEventSource extends OriginalEventSource {
|
|
3967
|
+
constructor(url, eventSourceInitDict) {
|
|
3968
|
+
const parsed = new URL(url.toString(), window.location.href);
|
|
3969
|
+
if (sameOrigin(parsed) && shouldRewriteFetchPath(parsed.pathname)) {
|
|
3970
|
+
const rewritten = new URL(parsed);
|
|
3971
|
+
rewritten.pathname = rewritePath(parsed.pathname);
|
|
3972
|
+
super(rewritten, eventSourceInitDict);
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
super(url, eventSourceInitDict);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
window.EventSource = PatchedEventSource;
|
|
3979
|
+
}
|
|
3980
|
+
return () => {
|
|
3981
|
+
window.fetch = originalFetch;
|
|
3982
|
+
window.WebSocket = OriginalWebSocket;
|
|
3983
|
+
if (OriginalEventSource !== void 0) window.EventSource = OriginalEventSource;
|
|
3984
|
+
for (const restore of restoreSrc) restore();
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
//#endregion
|
|
3988
|
+
//#region src/client/FenceNotice.tsx
|
|
3989
|
+
/**
|
|
3990
|
+
* Render the unpaired blocking page.
|
|
3991
|
+
* @param props - localized copy.
|
|
3992
|
+
* @returns the notice element.
|
|
3993
|
+
*/
|
|
3994
|
+
function FenceNotice({ t, onRetry }) {
|
|
3995
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3996
|
+
className: remote_module_css_default.fencePage,
|
|
3997
|
+
role: "dialog",
|
|
3998
|
+
"aria-modal": "true",
|
|
3999
|
+
"aria-labelledby": "remote-fence-title",
|
|
4000
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
4001
|
+
className: remote_module_css_default.fenceCard,
|
|
4002
|
+
"data-dsh-plugin": "remote-web-ui",
|
|
4003
|
+
children: [
|
|
4004
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4005
|
+
className: remote_module_css_default.fenceMark,
|
|
4006
|
+
"aria-hidden": "true",
|
|
4007
|
+
children: "×"
|
|
4008
|
+
}),
|
|
4009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4010
|
+
className: remote_module_css_default.fenceEyebrow,
|
|
4011
|
+
children: t("fence.unpaired.eyebrow")
|
|
4012
|
+
}),
|
|
4013
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
4014
|
+
id: "remote-fence-title",
|
|
4015
|
+
className: remote_module_css_default.fenceTitle,
|
|
4016
|
+
children: t("fence.unpaired.title")
|
|
4017
|
+
}),
|
|
4018
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4019
|
+
className: remote_module_css_default.fenceDetail,
|
|
4020
|
+
children: t("fence.unpaired.hint")
|
|
4021
|
+
}),
|
|
4022
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
|
|
4023
|
+
className: remote_module_css_default.fenceSteps,
|
|
4024
|
+
children: [
|
|
4025
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("fence.unpaired.stepDesktop") }),
|
|
4026
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("fence.unpaired.stepLink") }),
|
|
4027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t("fence.unpaired.stepOpen") })
|
|
4028
|
+
]
|
|
4029
|
+
}),
|
|
4030
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4031
|
+
className: remote_module_css_default.fenceRetry,
|
|
4032
|
+
type: "button",
|
|
4033
|
+
onClick: onRetry,
|
|
4034
|
+
children: t("fence.unpaired.retry")
|
|
4035
|
+
}),
|
|
4036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4037
|
+
className: remote_module_css_default.fenceFootnote,
|
|
4038
|
+
children: t("fence.unpaired.footnote")
|
|
4039
|
+
})
|
|
4040
|
+
]
|
|
4041
|
+
})
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
//#endregion
|
|
3555
4045
|
//#region src/client/index.ts
|
|
3556
4046
|
/**
|
|
3557
4047
|
* Mobile remote control — browser half. Registers the `remote` dictionaries,
|
|
@@ -3663,6 +4153,51 @@ window.__ModuleLoader__.load({
|
|
|
3663
4153
|
};
|
|
3664
4154
|
settingsScope.subscribe(syncRuntime);
|
|
3665
4155
|
syncRuntime();
|
|
4156
|
+
let disposeChannel;
|
|
4157
|
+
let fenceNotice;
|
|
4158
|
+
const showFenceNotice = () => {
|
|
4159
|
+
if (fenceNotice !== void 0) return;
|
|
4160
|
+
const node = document.createElement("div");
|
|
4161
|
+
document.body.appendChild(node);
|
|
4162
|
+
const root = (0, react_dom_client.createRoot)(node);
|
|
4163
|
+
root.render((0, react.createElement)(FenceNotice, {
|
|
4164
|
+
t,
|
|
4165
|
+
onRetry: () => {
|
|
4166
|
+
window.location.reload();
|
|
4167
|
+
}
|
|
4168
|
+
}));
|
|
4169
|
+
fenceNotice = {
|
|
4170
|
+
unmount: () => {
|
|
4171
|
+
root.unmount();
|
|
4172
|
+
node.remove();
|
|
4173
|
+
},
|
|
4174
|
+
node
|
|
4175
|
+
};
|
|
4176
|
+
};
|
|
4177
|
+
const hideFenceNotice = () => {
|
|
4178
|
+
fenceNotice?.unmount();
|
|
4179
|
+
fenceNotice = void 0;
|
|
4180
|
+
};
|
|
4181
|
+
const channelActive = () => {
|
|
4182
|
+
if (isLoopbackHostname(window.location.hostname)) return false;
|
|
4183
|
+
const snapshot = settingsScope.getSnapshot();
|
|
4184
|
+
const value = snapshot.status === "ready" ? snapshot.value : void 0;
|
|
4185
|
+
return (value?.enabled ?? true) && (value?.requirePairingForLan ?? true);
|
|
4186
|
+
};
|
|
4187
|
+
const syncChannel = () => {
|
|
4188
|
+
if (channelActive() && disposeChannel === void 0) disposeChannel = ctx.effect(() => {
|
|
4189
|
+
return installRemoteChannel(window, {
|
|
4190
|
+
onUnpaired: showFenceNotice,
|
|
4191
|
+
onPaired: hideFenceNotice
|
|
4192
|
+
});
|
|
4193
|
+
}, "remote-web-ui: remote desktop channel");
|
|
4194
|
+
else if (!channelActive() && disposeChannel !== void 0) {
|
|
4195
|
+
disposeChannel();
|
|
4196
|
+
disposeChannel = void 0;
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
settingsScope.subscribe(syncChannel);
|
|
4200
|
+
syncChannel();
|
|
3666
4201
|
ctx.effect(() => {
|
|
3667
4202
|
const timer = window.setTimeout(() => {
|
|
3668
4203
|
if (sessionStorage.getItem("dsh-remote-pair-failed") === null) return;
|