dsh-mobile 0.3.2 → 0.3.4
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/CHANGELOG.md +19 -0
- package/FUNNEL_THIRD_PARTY_LICENSES.txt +2551 -0
- package/README.en.md +22 -10
- package/README.md +19 -8
- package/SECURITY.md +6 -3
- package/THIRD_PARTY_NOTICES.md +6 -2
- package/bin/dsh-mobile-funnel-win32-x64.exe +0 -0
- package/cordis.patch.yml +3 -0
- package/lib/cli.js +4 -1
- package/lib/cli.js.map +1 -0
- package/lib/client.js +715 -58
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +228 -3
- package/lib/index.mjs +1966 -396
- package/lib/index.mjs.map +1 -0
- package/package.json +17 -11
- package/assets/brand/app-icon-master.png +0 -0
- package/assets/brand/repository-hero.png +0 -0
package/lib/index.mjs
CHANGED
|
@@ -710,7 +710,8 @@ const SUPPORTED_DSH_VERSIONS = Object.freeze([
|
|
|
710
710
|
"0.1.0-rc.6",
|
|
711
711
|
"0.1.0-rc.7",
|
|
712
712
|
"0.1.1-rc.2",
|
|
713
|
-
"0.1.2-alpha.1"
|
|
713
|
+
"0.1.2-alpha.1",
|
|
714
|
+
"0.1.2-alpha.2"
|
|
714
715
|
]);
|
|
715
716
|
/**
|
|
716
717
|
* Reject an unverified DeepSeek Harness Host before opening the LAN listener.
|
|
@@ -1246,7 +1247,10 @@ const EXTENSION_LIMITS = Object.freeze({
|
|
|
1246
1247
|
manifest: 65536,
|
|
1247
1248
|
script: 1048576,
|
|
1248
1249
|
css: 524288,
|
|
1249
|
-
asset: 8388608
|
|
1250
|
+
asset: 8388608,
|
|
1251
|
+
assetFiles: 256,
|
|
1252
|
+
assetBytes: 33554432,
|
|
1253
|
+
assetDepth: 8
|
|
1250
1254
|
});
|
|
1251
1255
|
/** A misbehaving host activation must not wedge the local watcher forever. */
|
|
1252
1256
|
const HOST_ACTIVATION_TIMEOUT_MS = 5e3;
|
|
@@ -1331,7 +1335,7 @@ function normalizeRelativePath(value, field) {
|
|
|
1331
1335
|
if (normalized.split("/").some((part) => part === "" || part === "." || part === "..")) throw new MobileExtensionError("invalid_extension_path", `${field} escapes extension directory`);
|
|
1332
1336
|
return normalized;
|
|
1333
1337
|
}
|
|
1334
|
-
async function regularFile$
|
|
1338
|
+
async function regularFile$2(path, maximum, field) {
|
|
1335
1339
|
let info;
|
|
1336
1340
|
try {
|
|
1337
1341
|
info = await lstat(path);
|
|
@@ -1352,7 +1356,7 @@ async function containedPath(root, relativePath, maximum, field) {
|
|
|
1352
1356
|
const targetReal = await realpath(target);
|
|
1353
1357
|
const relation = relative(rootReal, targetReal);
|
|
1354
1358
|
if (relation === "" || relation.startsWith("..") || isAbsolute(relation)) throw new MobileExtensionError("invalid_extension_path", `${field} escapes extension directory`);
|
|
1355
|
-
return regularFile$
|
|
1359
|
+
return regularFile$2(targetReal, maximum, field);
|
|
1356
1360
|
}
|
|
1357
1361
|
async function optionalFile(root, name, maximum, field) {
|
|
1358
1362
|
try {
|
|
@@ -1390,7 +1394,9 @@ async function assetSnapshot(extensionRootReal) {
|
|
|
1390
1394
|
const assetsReal = await realpath(assetsPath);
|
|
1391
1395
|
assertRealPathWithin(extensionRootReal, assetsReal, "assets");
|
|
1392
1396
|
const snapshots = /* @__PURE__ */ new Map();
|
|
1393
|
-
|
|
1397
|
+
let totalBytes = 0;
|
|
1398
|
+
const visit = async (directoryReal, prefix, depth) => {
|
|
1399
|
+
if (depth > EXTENSION_LIMITS.assetDepth) throw new MobileExtensionError("invalid_extension", "asset tree exceeds its depth limit");
|
|
1394
1400
|
assertRealPathWithin(extensionRootReal, directoryReal, "asset directory");
|
|
1395
1401
|
const handle = await opendir(directoryReal);
|
|
1396
1402
|
const entries = [];
|
|
@@ -1408,11 +1414,13 @@ async function assetSnapshot(extensionRootReal) {
|
|
|
1408
1414
|
assertRealPathWithin(extensionRootReal, targetReal, "asset");
|
|
1409
1415
|
const key = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
1410
1416
|
if (info.isDirectory()) {
|
|
1411
|
-
await visit(targetReal, key);
|
|
1417
|
+
await visit(targetReal, key, depth + 1);
|
|
1412
1418
|
continue;
|
|
1413
1419
|
}
|
|
1414
1420
|
if (!info.isFile() || info.size > EXTENSION_LIMITS.asset) throw new MobileExtensionError("invalid_extension", "asset must be a regular file within its size limit");
|
|
1415
1421
|
const body = await readFile(targetReal);
|
|
1422
|
+
totalBytes += body.byteLength;
|
|
1423
|
+
if (snapshots.size >= EXTENSION_LIMITS.assetFiles || totalBytes > EXTENSION_LIMITS.assetBytes) throw new MobileExtensionError("invalid_extension", "asset tree exceeds its aggregate limit");
|
|
1416
1424
|
snapshots.set(key, Object.freeze({
|
|
1417
1425
|
body,
|
|
1418
1426
|
digest: createHash("sha256").update(body).digest("hex"),
|
|
@@ -1420,12 +1428,12 @@ async function assetSnapshot(extensionRootReal) {
|
|
|
1420
1428
|
}));
|
|
1421
1429
|
}
|
|
1422
1430
|
};
|
|
1423
|
-
await visit(assetsReal, "");
|
|
1431
|
+
await visit(assetsReal, "", 0);
|
|
1424
1432
|
return snapshots;
|
|
1425
1433
|
}
|
|
1426
1434
|
async function extensionFingerprint(directory) {
|
|
1427
1435
|
const root = await realExtensionRoot(directory);
|
|
1428
|
-
const manifestFile = await regularFile$
|
|
1436
|
+
const manifestFile = await regularFile$2(join(root, "extension.json"), EXTENSION_LIMITS.manifest, "extension.json");
|
|
1429
1437
|
const manifestBody = await readFile(manifestFile.path);
|
|
1430
1438
|
const manifest = parseExtensionManifest(JSON.parse(manifestBody.toString("utf8")));
|
|
1431
1439
|
if (manifest.id !== basename(root)) throw new MobileExtensionError("invalid_manifest", "extension id must match its directory name");
|
|
@@ -1915,7 +1923,7 @@ async function abortAndDisposeLocal(entries) {
|
|
|
1915
1923
|
}
|
|
1916
1924
|
async function loadLocalExtension(directory, context, known, parentSignal) {
|
|
1917
1925
|
const root = await realExtensionRoot(directory);
|
|
1918
|
-
const manifestFile = await regularFile$
|
|
1926
|
+
const manifestFile = await regularFile$2(join(root, "extension.json"), EXTENSION_LIMITS.manifest, "extension.json");
|
|
1919
1927
|
const manifest = known?.manifest ?? parseExtensionManifest(JSON.parse(await readFile(manifestFile.path, "utf8")));
|
|
1920
1928
|
if (manifest.id !== basename(root)) throw new MobileExtensionError("invalid_manifest", "extension id must match its directory name");
|
|
1921
1929
|
const scriptBody = known === void 0 ? await optionalFile(root, "mobile.js", EXTENSION_LIMITS.script, "mobile.js").then((path) => path === void 0 ? void 0 : readFile(path)) : known.scriptBody;
|
|
@@ -2042,6 +2050,8 @@ const RUNTIME_MODULE = "@deepseek-ai/dsh-client-runtime";
|
|
|
2042
2050
|
const RENDERER_MODULE = "@deepseek-ai/dsh-client-ui-renderer";
|
|
2043
2051
|
const SIDEBAR_MODULE = "@deepseek-ai/dsh-client-ui-sidebar";
|
|
2044
2052
|
const SETTINGS_MODULE = "@deepseek-ai/dsh-client-ui-settings";
|
|
2053
|
+
const API_GATEWAY_MODULE = "@deepseek-ai/dsh-api-gateway";
|
|
2054
|
+
const API_REMOTES_MODULE = "@deepseek-ai/dsh-api-remotes";
|
|
2045
2055
|
const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
|
|
2046
2056
|
slots: RUNTIME_MODULE,
|
|
2047
2057
|
dependencies: Object.freeze([RUNTIME_MODULE, "@deepseek-ai/dsh-client-ui-theme"])
|
|
@@ -2055,6 +2065,7 @@ const MOBILE_LAYOUT_DEPENDENCY_PROFILES = Object.freeze([Object.freeze({
|
|
|
2055
2065
|
])
|
|
2056
2066
|
})]);
|
|
2057
2067
|
const MOBILE_CSRF_FETCH_BOOTSTRAP = `(()=>{const nativeFetch=window.fetch.bind(window);window.fetch=(input,init)=>{const source=input instanceof Request?input:undefined;const method=String(init?.method??source?.method??'GET').toUpperCase();if(method==='GET'||method==='HEAD')return nativeFetch(input,init);const raw=typeof input==='string'?input:input instanceof URL?input.href:source?.url;if(raw===undefined||new URL(raw,location.href).origin!==location.origin)return nativeFetch(input,init);const headers=new Headers(init?.headers??source?.headers);if(!headers.has(${JSON.stringify(CSRF_HEADER)})){const prefix=${JSON.stringify(`${CSRF_COOKIE}=`)};const token=document.cookie.split(';').map(value=>value.trim()).find(value=>value.startsWith(prefix))?.slice(prefix.length);if(token!==undefined)headers.set(${JSON.stringify(CSRF_HEADER)},token)}return nativeFetch(input,{...init,headers})};})();`;
|
|
2068
|
+
const MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP = `(()=>{if(window.__DSH_TRANSPORT__!==undefined)throw new Error('DSH Mobile cannot replace an existing transport override');window.__DSH_TRANSPORT__={fetch:(input,init)=>window.fetch(input,init),ownsHost:true}})();`;
|
|
2058
2069
|
const PAIR_PAGE = `<!doctype html>
|
|
2059
2070
|
<html lang="en">
|
|
2060
2071
|
<meta charset="utf-8">
|
|
@@ -2089,12 +2100,21 @@ function ensureMobileViewport(html) {
|
|
|
2089
2100
|
function orderAuthenticatedSettings(entries, slotsProvider) {
|
|
2090
2101
|
const mobile = entries.filter((entry) => entry !== null && typeof entry === "object" && entry.id === MOBILE_CLIENT_MODULE);
|
|
2091
2102
|
const settings = entries.filter((entry) => entry !== null && typeof entry === "object" && entry.id === SETTINGS_MODULE);
|
|
2092
|
-
if (mobile.length === 0 || settings.length === 0) return;
|
|
2103
|
+
if (mobile.length === 0 || settings.length === 0) return false;
|
|
2093
2104
|
if (mobile.length !== 1 || settings.length !== 1) throw new Error("upstream DSH mobile settings graph is ambiguous");
|
|
2094
2105
|
if (!Array.isArray(mobile[0]?.inject) || !mobile[0].inject.includes(CONNECTION_MODULE) || !mobile[0].inject.includes(SIDEBAR_MODULE)) throw new Error("dsh-mobile client has unsupported dependencies");
|
|
2095
|
-
if (!Array.isArray(settings[0]?.inject)
|
|
2106
|
+
if (!Array.isArray(settings[0]?.inject)) throw new Error("upstream DSH settings module has unsupported dependencies");
|
|
2107
|
+
const remoteSettings = !settings[0].inject.includes(CONNECTION_MODULE);
|
|
2108
|
+
if (remoteSettings) {
|
|
2109
|
+
if (!settings[0].inject.includes(API_REMOTES_MODULE) || slotsProvider !== RENDERER_MODULE) throw new Error("upstream DSH settings module has unsupported dependencies");
|
|
2110
|
+
const remotes = entries.filter((entry) => entry !== null && typeof entry === "object" && entry.id === API_REMOTES_MODULE);
|
|
2111
|
+
const gateway = entries.filter((entry) => entry !== null && typeof entry === "object" && entry.id === API_GATEWAY_MODULE);
|
|
2112
|
+
if (remotes.length !== 1 || !Array.isArray(remotes[0]?.inject) || !remotes[0].inject.includes(API_GATEWAY_MODULE) || gateway.length !== 1 || !Array.isArray(gateway[0]?.inject) || !gateway[0].inject.includes(CONNECTION_MODULE)) throw new Error("upstream DSH settings Remote graph has unsupported dependencies");
|
|
2113
|
+
if (!gateway[0].inject.includes(MOBILE_CLIENT_MODULE)) gateway[0].inject = [...gateway[0].inject, MOBILE_CLIENT_MODULE];
|
|
2114
|
+
}
|
|
2096
2115
|
mobile[0].inject = [CONNECTION_MODULE, slotsProvider];
|
|
2097
2116
|
if (!settings[0].inject.includes(MOBILE_CLIENT_MODULE)) settings[0].inject = [...settings[0].inject, MOBILE_CLIENT_MODULE];
|
|
2117
|
+
return remoteSettings;
|
|
2098
2118
|
}
|
|
2099
2119
|
function revisionedMobileBatchPath(entries) {
|
|
2100
2120
|
const key = createHash("sha256").update(DSH_MOBILE_VERSION).update(JSON.stringify(entries)).digest("hex");
|
|
@@ -2121,7 +2141,7 @@ function rewriteMobileIndexWithBatch(html) {
|
|
|
2121
2141
|
if (dependencyProfile === void 0) throw new Error("upstream DSH layout module has unsupported dependencies");
|
|
2122
2142
|
layout[0].url = MOBILE_LAYOUT_PATH;
|
|
2123
2143
|
layout[0].rev = `dsh-mobile-layout-${DSH_MOBILE_VERSION}`;
|
|
2124
|
-
orderAuthenticatedSettings(entries, dependencyProfile.slots);
|
|
2144
|
+
const remoteSettings = orderAuthenticatedSettings(entries, dependencyProfile.slots);
|
|
2125
2145
|
let mobileBatch;
|
|
2126
2146
|
if (parsed.batches !== void 0) {
|
|
2127
2147
|
if (!Array.isArray(parsed.batches)) throw new Error("upstream DSH boot manifest batches are malformed");
|
|
@@ -2156,7 +2176,7 @@ function rewriteMobileIndexWithBatch(html) {
|
|
|
2156
2176
|
batches
|
|
2157
2177
|
})).digest("hex").slice(0, 16);
|
|
2158
2178
|
}
|
|
2159
|
-
const replacement = `${MOBILE_CSRF_FETCH_BOOTSTRAP}window.__DSH_MOBILE_FRONTEND__="dedicated";${assignment[0]}${JSON.stringify(parsed)};`;
|
|
2179
|
+
const replacement = `${remoteSettings ? MOBILE_AUTHENTICATED_TRANSPORT_BOOTSTRAP : ""}${MOBILE_CSRF_FETCH_BOOTSTRAP}window.__DSH_MOBILE_FRONTEND__="dedicated";${assignment[0]}${JSON.stringify(parsed)};`;
|
|
2160
2180
|
return Object.freeze({
|
|
2161
2181
|
html: ensureMobileViewport(`${html.slice(0, start)}${replacement}${html.slice(scriptEnd)}`),
|
|
2162
2182
|
...mobileBatch === void 0 ? {} : { batch: mobileBatch }
|
|
@@ -3309,8 +3329,10 @@ var MobileAccessGateway = class {
|
|
|
3309
3329
|
}
|
|
3310
3330
|
}
|
|
3311
3331
|
async sendExtensionResponse(response, result, head) {
|
|
3332
|
+
const status = result.status ?? 200;
|
|
3333
|
+
if (!Number.isSafeInteger(status) || status < 200 || status > 599) throw new MobileExtensionError("invalid_route_response", "extension returned an invalid HTTP status", 500);
|
|
3312
3334
|
const contentType = result.contentType ?? "application/octet-stream";
|
|
3313
|
-
if (!/^[\w!#$&+.^-]+\/[\w!#$&+.^-]+(?:;[\
|
|
3335
|
+
if (contentType.length > 1024 || !/^[\x20-\x7e]+$/u.test(contentType) || !/^[\w!#$&+.^-]+\/[\w!#$&+.^-]+(?:;[\x20-\x7e]*)?$/u.test(contentType)) throw new MobileExtensionError("invalid_route_response", "extension returned an invalid content type", 500);
|
|
3314
3336
|
const safeHeaders = {};
|
|
3315
3337
|
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
3316
3338
|
if (!/^(?:content-disposition|cache-control|etag)$/iu.test(name) || /[\r\n]/u.test(value)) continue;
|
|
@@ -3320,7 +3342,7 @@ var MobileAccessGateway = class {
|
|
|
3320
3342
|
if (typeof result.body === "string" || result.body instanceof Uint8Array) {
|
|
3321
3343
|
const body = typeof result.body === "string" ? Buffer.from(result.body) : Buffer.from(result.body);
|
|
3322
3344
|
if (body.byteLength > 4194304) throw new MobileExtensionError("extension_result_too_large", "extension response is too large", 500);
|
|
3323
|
-
response.writeHead(
|
|
3345
|
+
response.writeHead(status, {
|
|
3324
3346
|
...safeHeaders,
|
|
3325
3347
|
"Content-Type": contentType,
|
|
3326
3348
|
"Content-Length": body.byteLength
|
|
@@ -3329,7 +3351,7 @@ var MobileAccessGateway = class {
|
|
|
3329
3351
|
else response.end(body);
|
|
3330
3352
|
return;
|
|
3331
3353
|
}
|
|
3332
|
-
response.writeHead(
|
|
3354
|
+
response.writeHead(status, {
|
|
3333
3355
|
...safeHeaders,
|
|
3334
3356
|
"Content-Type": contentType
|
|
3335
3357
|
});
|
|
@@ -4160,244 +4182,1434 @@ var MemoryDeviceStore = class {
|
|
|
4160
4182
|
}
|
|
4161
4183
|
};
|
|
4162
4184
|
//#endregion
|
|
4163
|
-
//#region src/
|
|
4164
|
-
const
|
|
4165
|
-
const
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4185
|
+
//#region src/frp-component.ts
|
|
4186
|
+
const FRP_VERSION = "0.70.1";
|
|
4187
|
+
const MAX_ARCHIVE_ENTRIES = 128;
|
|
4188
|
+
const MAX_ARCHIVE_LIST_BYTES = 262144;
|
|
4189
|
+
/** Pinned official FRP release metadata for supported desktop targets. */
|
|
4190
|
+
const FRP_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
4191
|
+
{
|
|
4192
|
+
platform: "win32",
|
|
4193
|
+
arch: "x64",
|
|
4194
|
+
archiveName: "frp.zip",
|
|
4195
|
+
executableName: "frpc.exe",
|
|
4196
|
+
downloadBytes: 13924309,
|
|
4197
|
+
downloadSha256: "531f3cd3cc41c0b4f077b54fe6b7dd83c0ff727e7f0bf412a4c78fa279165de5",
|
|
4198
|
+
downloadUrl: `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_windows_amd64.zip`
|
|
4199
|
+
},
|
|
4200
|
+
{
|
|
4201
|
+
platform: "win32",
|
|
4202
|
+
arch: "arm64",
|
|
4203
|
+
archiveName: "frp.zip",
|
|
4204
|
+
executableName: "frpc.exe",
|
|
4205
|
+
downloadBytes: 12204751,
|
|
4206
|
+
downloadSha256: "74d3acaf0f03ee190dd0462f9b49861dca50b0559c5488af4b36572fc951fcca",
|
|
4207
|
+
downloadUrl: `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_windows_arm64.zip`
|
|
4208
|
+
},
|
|
4209
|
+
{
|
|
4210
|
+
platform: "linux",
|
|
4211
|
+
arch: "x64",
|
|
4212
|
+
archiveName: "frp.tar.gz",
|
|
4213
|
+
executableName: "frpc",
|
|
4214
|
+
downloadBytes: 13924042,
|
|
4215
|
+
downloadSha256: "333da23d1b9009d7c01638e9ba38cf4600f7d37d393f854e96ee1396adefa9a6",
|
|
4216
|
+
downloadUrl: `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_amd64.tar.gz`
|
|
4217
|
+
},
|
|
4218
|
+
{
|
|
4219
|
+
platform: "linux",
|
|
4220
|
+
arch: "arm64",
|
|
4221
|
+
archiveName: "frp.tar.gz",
|
|
4222
|
+
executableName: "frpc",
|
|
4223
|
+
downloadBytes: 12371290,
|
|
4224
|
+
downloadSha256: "3990f396a9a490ee7f0e5f355287750ed41520064ed999eab443b5e9a78d773d",
|
|
4225
|
+
downloadUrl: `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_arm64.tar.gz`
|
|
4226
|
+
},
|
|
4227
|
+
{
|
|
4228
|
+
platform: "darwin",
|
|
4229
|
+
arch: "x64",
|
|
4230
|
+
archiveName: "frp.tar.gz",
|
|
4231
|
+
executableName: "frpc",
|
|
4232
|
+
downloadBytes: 13951979,
|
|
4233
|
+
downloadSha256: "cbf69cf26e5553e914e97d37f5d4367fa30f5f531d073a889465af4719281e25",
|
|
4234
|
+
downloadUrl: `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_darwin_amd64.tar.gz`
|
|
4235
|
+
},
|
|
4236
|
+
{
|
|
4237
|
+
platform: "darwin",
|
|
4238
|
+
arch: "arm64",
|
|
4239
|
+
archiveName: "frp.tar.gz",
|
|
4240
|
+
executableName: "frpc",
|
|
4241
|
+
downloadBytes: 12670664,
|
|
4242
|
+
downloadSha256: "cfa733b5a261c1647edee3c1fc4133d2542989b28f5602e81d47fc821d25c55f",
|
|
4243
|
+
downloadUrl: `https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_darwin_arm64.tar.gz`
|
|
4244
|
+
}
|
|
4245
|
+
].map((release) => [`${release.platform}-${release.arch}`, Object.freeze(release)])));
|
|
4246
|
+
function inside$1(parent, child) {
|
|
4247
|
+
const candidate = relative(parent, child);
|
|
4248
|
+
return candidate !== "" && !candidate.startsWith("..") && !isAbsolute(candidate);
|
|
4195
4249
|
}
|
|
4196
|
-
function
|
|
4197
|
-
if (origin === void 0) return "未分配";
|
|
4250
|
+
async function regularFile$1(file) {
|
|
4198
4251
|
try {
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
return "地址格式无效";
|
|
4252
|
+
const entry = await lstat(file);
|
|
4253
|
+
return entry.isFile() && !entry.isSymbolicLink();
|
|
4254
|
+
} catch (error) {
|
|
4255
|
+
if (error.code === "ENOENT") return false;
|
|
4256
|
+
throw error;
|
|
4205
4257
|
}
|
|
4206
4258
|
}
|
|
4207
|
-
function
|
|
4208
|
-
|
|
4259
|
+
async function replaceDirectory(target, candidate) {
|
|
4260
|
+
const backup = `${target}.previous-${randomBytes(12).toString("hex")}`;
|
|
4261
|
+
let previous = false;
|
|
4209
4262
|
try {
|
|
4210
|
-
const hostname = new URL(origin).hostname;
|
|
4211
|
-
if (hostname.endsWith(".ts.net")) return "*.ts.net";
|
|
4212
|
-
for (const suffix of [
|
|
4213
|
-
".cpolar.cn",
|
|
4214
|
-
".cpolar.io",
|
|
4215
|
-
".cpolar.top",
|
|
4216
|
-
".cpolar.com"
|
|
4217
|
-
]) if (hostname.endsWith(suffix)) return `*${suffix}`;
|
|
4218
|
-
return "公共 HTTPS 地址";
|
|
4219
|
-
} catch {
|
|
4220
|
-
return "地址格式无效";
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
function defaultFirewallProbe(platform = process.platform) {
|
|
4224
|
-
return async (port) => {
|
|
4225
|
-
if (platform !== "win32") return { state: "not-applicable" };
|
|
4226
|
-
if (port === void 0) return { state: "unknown" };
|
|
4227
|
-
const script = [
|
|
4228
|
-
"$specs = @(@{ Name = 'DSH Mobile HTTPS'; Protocol = 'TCP' }, @{ Name = 'DSH Mobile Discovery'; Protocol = 'UDP' })",
|
|
4229
|
-
"$ready = $true",
|
|
4230
|
-
"$specs | ForEach-Object {",
|
|
4231
|
-
" $spec = $_",
|
|
4232
|
-
" $rule = Get-NetFirewallRule -DisplayName $spec.Name -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' } | Select-Object -First 1",
|
|
4233
|
-
" if ($null -eq $rule) { $ready = $false; return }",
|
|
4234
|
-
" $filters = @($rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)",
|
|
4235
|
-
` $matching = @($filters | Where-Object { $_.Protocol -eq $spec.Protocol -and ($_.LocalPort -eq 'Any' -or $_.LocalPort -eq '${String(port)}') })`,
|
|
4236
|
-
" if ($matching.Count -eq 0) { $ready = $false }",
|
|
4237
|
-
"}",
|
|
4238
|
-
"if ($ready) { 'ready' } else { 'missing' }"
|
|
4239
|
-
].join("; ");
|
|
4240
4263
|
try {
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
script
|
|
4246
|
-
], {
|
|
4247
|
-
encoding: "utf8",
|
|
4248
|
-
timeout: 3e3,
|
|
4249
|
-
windowsHide: true
|
|
4250
|
-
})).stdout.trim() === "ready" ? "ready" : "missing" };
|
|
4251
|
-
} catch {
|
|
4252
|
-
return { state: "unknown" };
|
|
4264
|
+
await rename(target, backup);
|
|
4265
|
+
previous = true;
|
|
4266
|
+
} catch (error) {
|
|
4267
|
+
if (error.code !== "ENOENT") throw error;
|
|
4253
4268
|
}
|
|
4254
|
-
|
|
4269
|
+
try {
|
|
4270
|
+
await rename(candidate, target);
|
|
4271
|
+
} catch (error) {
|
|
4272
|
+
if (previous) try {
|
|
4273
|
+
await rename(backup, target);
|
|
4274
|
+
} catch (restoreError) {
|
|
4275
|
+
throw new AggregateError([error, restoreError], "frp_component_replace_failed");
|
|
4276
|
+
}
|
|
4277
|
+
throw error;
|
|
4278
|
+
}
|
|
4279
|
+
if (previous) await rm(backup, {
|
|
4280
|
+
recursive: true,
|
|
4281
|
+
force: true
|
|
4282
|
+
});
|
|
4283
|
+
} finally {
|
|
4284
|
+
await rm(candidate, {
|
|
4285
|
+
recursive: true,
|
|
4286
|
+
force: true
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4255
4289
|
}
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
const hostname = new URL(origin).hostname.toLowerCase();
|
|
4259
|
-
if (hostname.endsWith(".ts.net") || hostname.includes(".cpolar.")) return 1e4;
|
|
4260
|
-
return 5e3;
|
|
4290
|
+
function sha256$1(bytes) {
|
|
4291
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
4261
4292
|
}
|
|
4262
|
-
async function
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4293
|
+
async function runCapture(file, args) {
|
|
4294
|
+
return new Promise((resolveRun, reject) => {
|
|
4295
|
+
execFile(file, [...args], {
|
|
4296
|
+
windowsHide: true,
|
|
4297
|
+
timeout: 12e4,
|
|
4298
|
+
maxBuffer: MAX_ARCHIVE_LIST_BYTES,
|
|
4299
|
+
encoding: "utf8"
|
|
4300
|
+
}, (error, stdout) => {
|
|
4301
|
+
if (error === null) resolveRun(stdout);
|
|
4302
|
+
else reject(error);
|
|
4271
4303
|
});
|
|
4272
|
-
|
|
4273
|
-
if (response.status === 429) return {
|
|
4274
|
-
state: "rate-limited",
|
|
4275
|
-
latencyMs
|
|
4276
|
-
};
|
|
4277
|
-
return response.ok ? {
|
|
4278
|
-
state: "ready",
|
|
4279
|
-
latencyMs
|
|
4280
|
-
} : {
|
|
4281
|
-
state: "unreachable",
|
|
4282
|
-
latencyMs
|
|
4283
|
-
};
|
|
4284
|
-
} catch {
|
|
4285
|
-
let fakeIp = false;
|
|
4286
|
-
try {
|
|
4287
|
-
fakeIp = (await lookup(hostname, { all: true })).some(({ address }) => {
|
|
4288
|
-
const [first, second] = address.split(".").map(Number);
|
|
4289
|
-
return first === 198 && (second === 18 || second === 19);
|
|
4290
|
-
});
|
|
4291
|
-
} catch {}
|
|
4292
|
-
return {
|
|
4293
|
-
state: "unreachable",
|
|
4294
|
-
...fakeIp ? { fakeIp: true } : {}
|
|
4295
|
-
};
|
|
4296
|
-
}
|
|
4304
|
+
});
|
|
4297
4305
|
}
|
|
4298
|
-
function
|
|
4299
|
-
|
|
4306
|
+
function validatedArchiveEntry(rawEntry) {
|
|
4307
|
+
if (rawEntry.length === 0 || rawEntry.includes("\\") || rawEntry.includes("\0") || rawEntry.startsWith("/") || /^[a-zA-Z]:/u.test(rawEntry)) throw new Error("frp_archive_path_invalid");
|
|
4308
|
+
const segments = rawEntry.replace(/\/$/u, "").split("/");
|
|
4309
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error("frp_archive_path_invalid");
|
|
4310
|
+
return segments;
|
|
4300
4311
|
}
|
|
4301
|
-
/**
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
} else checks.push(check("network", "info", "network-fixed", "局域网网卡", "当前使用固定网络配置。"));
|
|
4312
|
-
if (snapshot.lan.running && snapshot.lan.origin !== void 0) {
|
|
4313
|
-
const endpointSuffix = maskLanOrigin(snapshot.lan.origin);
|
|
4314
|
-
checks.push(check("lan", "ok", "lan-ready", "局域网网关", `已监听 ${endpointSuffix},配对入口可用。`, void 0, { endpointSuffix }));
|
|
4315
|
-
} else checks.push(check("lan", "info", "lan-off", "局域网网关", "当前未开启。", "需要手机直连时开启局域网访问。"));
|
|
4316
|
-
if (firewall.state === "ready") checks.push(check("firewall", "ok", "firewall-ready", "Windows 防火墙", "局域网 TCP 与发现规则已启用。"));
|
|
4317
|
-
else if (firewall.state === "missing") checks.push(check("firewall", "warning", "firewall-missing", "Windows 防火墙", "未找到完整的局域网放行规则。", "以管理员身份重新运行 dsh-mobile setup。"));
|
|
4318
|
-
else if (firewall.state === "unknown") checks.push(check("firewall", "info", "firewall-unknown", "Windows 防火墙", "系统未允许插件读取防火墙状态。", "若手机找不到电脑,以管理员身份重新运行 setup。"));
|
|
4319
|
-
if (!snapshot.remote.running || snapshot.remote.state === "off") checks.push(check("remote", "info", "remote-off", "远程通道", "当前未启用。", void 0, { provider: snapshot.remote.provider }));
|
|
4320
|
-
else if (snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0) {
|
|
4321
|
-
const endpointSuffix = remoteSuffix(snapshot.remote.origin);
|
|
4322
|
-
const facts = {
|
|
4323
|
-
provider: snapshot.remote.provider,
|
|
4324
|
-
endpointSuffix,
|
|
4325
|
-
...remoteObservation.latencyMs === void 0 ? {} : { latencyMs: remoteObservation.latencyMs }
|
|
4326
|
-
};
|
|
4327
|
-
if (remoteObservation.state === "ready") checks.push(check("remote", "ok", "remote-ready", "远程通道", `${snapshot.remote.provider} 公共地址 ${endpointSuffix} 可达,往返约 ${String(remoteObservation.latencyMs ?? 0)} ms。`, void 0, facts));
|
|
4328
|
-
else if (remoteObservation.state === "rate-limited") checks.push(check("remote", "warning", "remote-rate-limited", "远程通道", "公共地址可达,但本次检查观察到服务限流。", "稍后重试;旧会话会按需加载以减少流量。", facts));
|
|
4329
|
-
else if (snapshot.remote.provider === "tailscale" && remoteObservation.fakeIp === true) checks.push(check("remote", "error", "remote-fake-ip", "远程通道", "Tailscale 地址被当前 VPN 或 DNS 代理接管,但 TLS 链路未建立。", "切换 VPN 节点或代理模式;仍失败时改用 cpolar。", facts));
|
|
4330
|
-
else checks.push(check("remote", "error", "remote-unreachable", "远程通道", "提供方显示已就绪,但公共地址暂不可达。", "点击“重新连接”;仍失败时检查提供方状态。", facts));
|
|
4331
|
-
} else if (snapshot.remote.state === "starting" || snapshot.remote.state === "connecting" || snapshot.remote.state === "needs-login") {
|
|
4332
|
-
const needsLogin = snapshot.remote.state === "needs-login";
|
|
4333
|
-
checks.push(check("remote", "warning", needsLogin ? "remote-needs-login" : "remote-connecting", "远程通道", needsLogin ? "等待完成 Tailscale 登录。" : "仍在建立连接。", needsLogin ? "返回远程页继续登录。" : "等待片刻后重新检查。", { provider: snapshot.remote.provider }));
|
|
4334
|
-
} else {
|
|
4335
|
-
const controllerCode = snapshot.remote.errorCode ?? snapshot.remote.state;
|
|
4336
|
-
checks.push(check("remote", "error", "remote-controller-error", "远程通道", `连接未建立(${controllerCode})。`, REMOTE_ERROR_GUIDANCE[controllerCode] ?? "返回远程页点击“重新连接”。", {
|
|
4337
|
-
provider: snapshot.remote.provider,
|
|
4338
|
-
controllerCode
|
|
4339
|
-
}));
|
|
4312
|
+
/** Select exactly one nested frpc executable from a safe archive listing. */
|
|
4313
|
+
function selectFrpExecutableEntry(entries, executableName) {
|
|
4314
|
+
if (entries.length === 0 || entries.length > MAX_ARCHIVE_ENTRIES) throw new Error("frp_archive_entries_invalid");
|
|
4315
|
+
let executableEntry;
|
|
4316
|
+
for (const entry of entries) {
|
|
4317
|
+
const segments = validatedArchiveEntry(entry);
|
|
4318
|
+
if (segments.length >= 2 && segments.at(-1) === executableName) {
|
|
4319
|
+
if (executableEntry !== void 0) throw new Error("frp_archive_executable_ambiguous");
|
|
4320
|
+
executableEntry = entry.replace(/\/$/u, "");
|
|
4321
|
+
}
|
|
4340
4322
|
}
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
].join("\n");
|
|
4352
|
-
return Object.freeze({
|
|
4353
|
-
version: 1,
|
|
4354
|
-
generatedAt: Date.now(),
|
|
4355
|
-
overall,
|
|
4356
|
-
versions: Object.freeze({
|
|
4357
|
-
plugin: DSH_MOBILE_VERSION,
|
|
4358
|
-
dsh: snapshot.dshVersion,
|
|
4359
|
-
minimumAndroidApp: MINIMUM_ANDROID_APP_VERSION
|
|
4360
|
-
}),
|
|
4361
|
-
summary,
|
|
4362
|
-
checks: Object.freeze(checks),
|
|
4363
|
-
report
|
|
4323
|
+
if (executableEntry === void 0) throw new Error("frp_archive_executable_missing");
|
|
4324
|
+
return executableEntry;
|
|
4325
|
+
}
|
|
4326
|
+
async function defaultExtractArtifact$1(archive, destination, executableName) {
|
|
4327
|
+
const tar = process.platform === "win32" ? "tar.exe" : "tar";
|
|
4328
|
+
const executableEntry = selectFrpExecutableEntry((await runCapture(tar, ["-tf", archive])).split(/\r?\n/u).filter((entry) => entry.length > 0), executableName);
|
|
4329
|
+
const unpacked = join(destination, "archive");
|
|
4330
|
+
await mkdir(unpacked, {
|
|
4331
|
+
recursive: true,
|
|
4332
|
+
mode: 448
|
|
4364
4333
|
});
|
|
4334
|
+
await runCapture(tar, [
|
|
4335
|
+
"-xf",
|
|
4336
|
+
archive,
|
|
4337
|
+
"-C",
|
|
4338
|
+
unpacked,
|
|
4339
|
+
executableEntry
|
|
4340
|
+
]);
|
|
4341
|
+
const extracted = join(unpacked, ...validatedArchiveEntry(executableEntry));
|
|
4342
|
+
if (!await regularFile$1(extracted)) throw new Error("frp_archive_executable_invalid");
|
|
4343
|
+
await copyFile(extracted, join(destination, executableName));
|
|
4365
4344
|
}
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
const
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4345
|
+
async function defaultFetchArtifact$1(artifact, signal) {
|
|
4346
|
+
const response = await fetch(artifact.downloadUrl, {
|
|
4347
|
+
redirect: "follow",
|
|
4348
|
+
signal
|
|
4349
|
+
});
|
|
4350
|
+
if (!response.ok) throw new Error(`frp_download_http_${String(response.status)}`);
|
|
4351
|
+
const finalUrl = new URL(response.url);
|
|
4352
|
+
const officialHost = finalUrl.hostname === "github.com" || finalUrl.hostname.endsWith(".githubusercontent.com");
|
|
4353
|
+
if (finalUrl.protocol !== "https:" || !officialHost) throw new Error("frp_download_origin_invalid");
|
|
4354
|
+
const lengthHeader = response.headers.get("content-length");
|
|
4355
|
+
const declaredLength = lengthHeader === null ? void 0 : Number(lengthHeader);
|
|
4356
|
+
if (declaredLength !== void 0 && (!Number.isFinite(declaredLength) || declaredLength !== artifact.downloadBytes)) throw new Error("frp_download_size_mismatch");
|
|
4357
|
+
if (response.body === null) throw new Error("frp_download_empty");
|
|
4358
|
+
const chunks = [];
|
|
4359
|
+
let received = 0;
|
|
4360
|
+
const reader = response.body.getReader();
|
|
4361
|
+
while (true) {
|
|
4362
|
+
const result = await reader.read();
|
|
4363
|
+
if (result.done) break;
|
|
4364
|
+
received += result.value.byteLength;
|
|
4365
|
+
if (received > artifact.downloadBytes) {
|
|
4366
|
+
await reader.cancel();
|
|
4367
|
+
throw new Error("frp_download_size_mismatch");
|
|
4368
|
+
}
|
|
4369
|
+
chunks.push(result.value);
|
|
4370
|
+
}
|
|
4371
|
+
if (received !== artifact.downloadBytes) throw new Error("frp_download_size_mismatch");
|
|
4372
|
+
const bytes = new Uint8Array(received);
|
|
4373
|
+
let offset = 0;
|
|
4374
|
+
for (const chunk of chunks) {
|
|
4375
|
+
bytes.set(chunk, offset);
|
|
4376
|
+
offset += chunk.byteLength;
|
|
4377
|
+
}
|
|
4378
|
+
return bytes;
|
|
4379
|
+
}
|
|
4380
|
+
async function defaultInspectExecutable(executable) {
|
|
4381
|
+
return (await runCapture(executable, ["--version"])).trim();
|
|
4382
|
+
}
|
|
4383
|
+
/** Owns the optional official frpc binary inside the DSH Mobile state directory. */
|
|
4384
|
+
var FrpComponentManager = class {
|
|
4385
|
+
executable;
|
|
4386
|
+
componentRoot;
|
|
4387
|
+
componentStorage;
|
|
4388
|
+
logRoot;
|
|
4389
|
+
stagingRoot;
|
|
4390
|
+
artifact;
|
|
4391
|
+
fetchArtifact;
|
|
4392
|
+
extractArtifact;
|
|
4393
|
+
inspectExecutable;
|
|
4394
|
+
installed = false;
|
|
4395
|
+
installedBytes = 0;
|
|
4396
|
+
errorCode;
|
|
4397
|
+
queue = Promise.resolve();
|
|
4398
|
+
constructor(options) {
|
|
4399
|
+
const stateDirectory = resolve(options.stateDirectory);
|
|
4400
|
+
if (!isAbsolute(stateDirectory)) throw new Error("frp state directory must be absolute");
|
|
4401
|
+
const platform = options.platform ?? process.platform;
|
|
4402
|
+
const arch = options.arch ?? process.arch;
|
|
4403
|
+
this.artifact = FRP_COMPONENT_RELEASES[`${platform}-${arch}`];
|
|
4404
|
+
this.componentRoot = join(stateDirectory, "components", "frp");
|
|
4405
|
+
this.componentStorage = join(this.componentRoot, FRP_VERSION);
|
|
4406
|
+
this.executable = join(this.componentStorage, platform === "win32" ? "frpc.exe" : "frpc");
|
|
4407
|
+
this.logRoot = join(stateDirectory, "logs", "frp");
|
|
4408
|
+
this.stagingRoot = join(stateDirectory, "staging", "frp");
|
|
4409
|
+
for (const child of [
|
|
4410
|
+
this.componentRoot,
|
|
4411
|
+
this.componentStorage,
|
|
4412
|
+
this.logRoot,
|
|
4413
|
+
this.stagingRoot
|
|
4414
|
+
]) if (!inside$1(stateDirectory, child)) throw new Error("frp component path escaped its state directory");
|
|
4415
|
+
this.fetchArtifact = options.fetchArtifact ?? defaultFetchArtifact$1;
|
|
4416
|
+
this.extractArtifact = options.extractArtifact ?? defaultExtractArtifact$1;
|
|
4417
|
+
this.inspectExecutable = options.inspectExecutable ?? defaultInspectExecutable;
|
|
4418
|
+
}
|
|
4419
|
+
/** Inspect the managed executable without relying on global FRP installations. */
|
|
4420
|
+
async initialize() {
|
|
4421
|
+
this.installed = await regularFile$1(this.executable);
|
|
4422
|
+
this.installedBytes = this.installed ? (await stat(this.executable)).size : 0;
|
|
4423
|
+
if (this.installed) try {
|
|
4424
|
+
if (await this.inspectExecutable(this.executable) !== FRP_VERSION) throw new Error("frp_component_version_mismatch");
|
|
4425
|
+
this.errorCode = void 0;
|
|
4426
|
+
} catch {
|
|
4427
|
+
this.installed = false;
|
|
4428
|
+
this.errorCode = "frp_component_invalid";
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
/** Return component metadata without exposing configuration or credentials. */
|
|
4432
|
+
status() {
|
|
4433
|
+
return Object.freeze({
|
|
4434
|
+
supported: this.artifact !== void 0,
|
|
4435
|
+
installed: this.installed,
|
|
4436
|
+
version: FRP_VERSION,
|
|
4437
|
+
downloadBytes: this.artifact?.downloadBytes ?? 0,
|
|
4438
|
+
installedBytes: this.installedBytes,
|
|
4439
|
+
sourceUrl: this.artifact?.downloadUrl ?? "https://github.com/fatedier/frp/releases",
|
|
4440
|
+
releasePage: `https://github.com/fatedier/frp/releases/tag/v${FRP_VERSION}`,
|
|
4441
|
+
storagePath: this.componentRoot,
|
|
4442
|
+
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
/** Download, verify, and extract only frpc after explicit confirmation. */
|
|
4446
|
+
install() {
|
|
4447
|
+
return this.enqueue(async () => {
|
|
4448
|
+
const artifact = this.artifact;
|
|
4449
|
+
if (artifact === void 0) throw new Error("frp_component_unsupported");
|
|
4450
|
+
await mkdir(this.stagingRoot, {
|
|
4451
|
+
recursive: true,
|
|
4452
|
+
mode: 448
|
|
4453
|
+
});
|
|
4454
|
+
const staging = await mkdtemp(join(this.stagingRoot, "install-"));
|
|
4455
|
+
try {
|
|
4456
|
+
const controller = new AbortController();
|
|
4457
|
+
const timeout = setTimeout(() => {
|
|
4458
|
+
controller.abort();
|
|
4459
|
+
}, 12e4);
|
|
4460
|
+
timeout.unref();
|
|
4461
|
+
let bytes;
|
|
4462
|
+
try {
|
|
4463
|
+
bytes = await this.fetchArtifact(artifact, controller.signal);
|
|
4464
|
+
} finally {
|
|
4465
|
+
clearTimeout(timeout);
|
|
4466
|
+
}
|
|
4467
|
+
if (bytes.byteLength !== artifact.downloadBytes) throw new Error("frp_download_size_mismatch");
|
|
4468
|
+
if (sha256$1(bytes) !== artifact.downloadSha256) throw new Error("frp_download_hash_mismatch");
|
|
4469
|
+
const archive = join(staging, artifact.archiveName);
|
|
4470
|
+
await writeFile(archive, bytes, {
|
|
4471
|
+
flag: "wx",
|
|
4472
|
+
mode: 384
|
|
4473
|
+
});
|
|
4474
|
+
await this.extractArtifact(archive, staging, artifact.executableName);
|
|
4475
|
+
const extracted = join(staging, artifact.executableName);
|
|
4476
|
+
if (!await regularFile$1(extracted)) throw new Error("frp_executable_missing");
|
|
4477
|
+
await chmod(extracted, 448);
|
|
4478
|
+
if (await this.inspectExecutable(extracted) !== FRP_VERSION) throw new Error("frp_component_version_mismatch");
|
|
4479
|
+
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
4480
|
+
await mkdir(candidate, {
|
|
4481
|
+
recursive: true,
|
|
4482
|
+
mode: 448
|
|
4483
|
+
});
|
|
4484
|
+
const candidateExecutable = join(candidate, artifact.executableName);
|
|
4485
|
+
await copyFile(extracted, candidateExecutable);
|
|
4486
|
+
await chmod(candidateExecutable, 448);
|
|
4487
|
+
await replaceDirectory(this.componentStorage, candidate);
|
|
4488
|
+
this.installed = true;
|
|
4489
|
+
this.installedBytes = (await stat(this.executable)).size;
|
|
4490
|
+
this.errorCode = void 0;
|
|
4491
|
+
} finally {
|
|
4492
|
+
await rm(staging, {
|
|
4493
|
+
recursive: true,
|
|
4494
|
+
force: true
|
|
4495
|
+
});
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
4498
|
+
}
|
|
4499
|
+
/** Remove all FRP executable, staging, and log files owned by DSH Mobile. */
|
|
4500
|
+
purge() {
|
|
4501
|
+
return this.enqueue(async () => {
|
|
4502
|
+
await Promise.all([
|
|
4503
|
+
rm(this.componentRoot, {
|
|
4504
|
+
recursive: true,
|
|
4505
|
+
force: true
|
|
4506
|
+
}),
|
|
4507
|
+
rm(this.logRoot, {
|
|
4508
|
+
recursive: true,
|
|
4509
|
+
force: true
|
|
4510
|
+
}),
|
|
4511
|
+
rm(this.stagingRoot, {
|
|
4512
|
+
recursive: true,
|
|
4513
|
+
force: true
|
|
4514
|
+
})
|
|
4515
|
+
]);
|
|
4516
|
+
this.installed = false;
|
|
4517
|
+
this.installedBytes = 0;
|
|
4518
|
+
this.errorCode = void 0;
|
|
4519
|
+
});
|
|
4520
|
+
}
|
|
4521
|
+
enqueue(operation) {
|
|
4522
|
+
const task = this.queue.then(operation, operation);
|
|
4523
|
+
this.queue = task.then(() => void 0, () => void 0);
|
|
4524
|
+
return task.then(() => this.status());
|
|
4525
|
+
}
|
|
4526
|
+
};
|
|
4527
|
+
//#endregion
|
|
4528
|
+
//#region src/frp-template.ts
|
|
4529
|
+
/** Loopback-only HTTP vhost port used between Caddy and frps. */
|
|
4530
|
+
const FRP_VHOST_HTTP_PORT = 7080;
|
|
4531
|
+
function publicDnsHostname(value) {
|
|
4532
|
+
return value.length <= 253 && value.includes(".") && !/^[0-9.]+$/u.test(value) && !value.includes(":") && value.split(".").every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label));
|
|
4533
|
+
}
|
|
4534
|
+
/** Build the only supported frps and Caddy configuration from validated user inputs. */
|
|
4535
|
+
function createRestrictedFrpServerTemplate(serverPort, token, publicOrigin) {
|
|
4536
|
+
if (!Number.isSafeInteger(serverPort) || serverPort < 1 || serverPort > 65535 || token.length < 16 || token.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(token)) throw new Error("frp_template_input_invalid");
|
|
4537
|
+
let url;
|
|
4538
|
+
try {
|
|
4539
|
+
url = new URL(publicOrigin);
|
|
4540
|
+
} catch {
|
|
4541
|
+
throw new Error("frp_template_input_invalid");
|
|
4542
|
+
}
|
|
4543
|
+
if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || !publicDnsHostname(url.hostname)) throw new Error("frp_template_input_invalid");
|
|
4544
|
+
return [
|
|
4545
|
+
"# frps.toml",
|
|
4546
|
+
`bindPort = ${String(serverPort)}`,
|
|
4547
|
+
"proxyBindAddr = \"127.0.0.1\"",
|
|
4548
|
+
`vhostHTTPPort = ${String(FRP_VHOST_HTTP_PORT)}`,
|
|
4549
|
+
"auth.method = \"token\"",
|
|
4550
|
+
`auth.token = ${JSON.stringify(token)}`,
|
|
4551
|
+
"",
|
|
4552
|
+
"# Caddyfile",
|
|
4553
|
+
`${url.hostname} {`,
|
|
4554
|
+
` reverse_proxy 127.0.0.1:${String(FRP_VHOST_HTTP_PORT)}`,
|
|
4555
|
+
"}",
|
|
4556
|
+
""
|
|
4557
|
+
].join("\n");
|
|
4558
|
+
}
|
|
4559
|
+
//#endregion
|
|
4560
|
+
//#region src/frp-config.ts
|
|
4561
|
+
const MAX_SETTINGS_BYTES = 8192;
|
|
4562
|
+
function hostname$1(value) {
|
|
4563
|
+
if (value.length > 253 || !value.includes(".")) return false;
|
|
4564
|
+
return value.split(".").every((label) => label.length >= 1 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label));
|
|
4565
|
+
}
|
|
4566
|
+
/** Validate the FRP server hostname or IP address. */
|
|
4567
|
+
function validateFrpServerAddress(value) {
|
|
4568
|
+
if (typeof value !== "string" || value !== value.trim() || value.length === 0 || value.length > 253 || /[\s\u0000-\u001f\u007f/\\@?#]/u.test(value)) throw new Error("frp_server_address_invalid");
|
|
4569
|
+
const normalized = value.toLowerCase().replace(/\.$/u, "");
|
|
4570
|
+
if (isIP(normalized) === 0 && !hostname$1(normalized)) throw new Error("frp_server_address_invalid");
|
|
4571
|
+
return normalized;
|
|
4572
|
+
}
|
|
4573
|
+
/** Validate the FRP control port. */
|
|
4574
|
+
function validateFrpServerPort(value) {
|
|
4575
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > 65535) throw new Error("frp_server_port_invalid");
|
|
4576
|
+
return Number(value);
|
|
4577
|
+
}
|
|
4578
|
+
/** Validate a high-entropy FRP token before durable storage. */
|
|
4579
|
+
function validateFrpToken(value) {
|
|
4580
|
+
if (typeof value !== "string" || value.length < 16 || value.length > 512 || /[\s\u0000-\u001f\u007f]/u.test(value)) throw new Error("frp_token_invalid");
|
|
4581
|
+
return value;
|
|
4582
|
+
}
|
|
4583
|
+
/** Validate the public HTTPS origin used by Caddy and Android pairing. */
|
|
4584
|
+
function validateFrpPublicOrigin(value) {
|
|
4585
|
+
if (typeof value !== "string" || value.length > 512) throw new Error("frp_public_origin_invalid");
|
|
4586
|
+
let url;
|
|
4587
|
+
try {
|
|
4588
|
+
url = new URL(value);
|
|
4589
|
+
} catch {
|
|
4590
|
+
throw new Error("frp_public_origin_invalid");
|
|
4591
|
+
}
|
|
4592
|
+
if (url.protocol !== "https:" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "" || isIP(url.hostname) !== 0 || !hostname$1(url.hostname)) throw new Error("frp_public_origin_invalid");
|
|
4593
|
+
return url.origin;
|
|
4594
|
+
}
|
|
4595
|
+
/** Parse FRP settings at the loopback request and filesystem boundaries. */
|
|
4596
|
+
function parseFrpSettings(value) {
|
|
4597
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("frp_settings_invalid");
|
|
4598
|
+
const record = value;
|
|
4599
|
+
if (Reflect.ownKeys(record).some((key) => ![
|
|
4600
|
+
"version",
|
|
4601
|
+
"serverAddress",
|
|
4602
|
+
"serverPort",
|
|
4603
|
+
"token",
|
|
4604
|
+
"publicOrigin"
|
|
4605
|
+
].includes(String(key)))) throw new Error("frp_settings_invalid");
|
|
4606
|
+
if (record.version !== void 0 && record.version !== 1) throw new Error("frp_settings_invalid");
|
|
4607
|
+
return Object.freeze({
|
|
4608
|
+
version: 1,
|
|
4609
|
+
serverAddress: validateFrpServerAddress(record.serverAddress),
|
|
4610
|
+
serverPort: validateFrpServerPort(record.serverPort),
|
|
4611
|
+
token: validateFrpToken(record.token),
|
|
4612
|
+
publicOrigin: validateFrpPublicOrigin(record.publicOrigin)
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
function tomlString(value) {
|
|
4616
|
+
return JSON.stringify(value);
|
|
4617
|
+
}
|
|
4618
|
+
/** Build the single-purpose frpc configuration for the current loopback gateway. */
|
|
4619
|
+
function createFrpcToml(settings, localPort) {
|
|
4620
|
+
if (!Number.isSafeInteger(localPort) || localPort < 1 || localPort > 65535) throw new Error("frp_local_port_invalid");
|
|
4621
|
+
const hostnameValue = new URL(settings.publicOrigin).hostname;
|
|
4622
|
+
return [
|
|
4623
|
+
`serverAddr = ${tomlString(settings.serverAddress)}`,
|
|
4624
|
+
`serverPort = ${String(settings.serverPort)}`,
|
|
4625
|
+
"auth.method = \"token\"",
|
|
4626
|
+
`auth.token = ${tomlString(settings.token)}`,
|
|
4627
|
+
"transport.tls.enable = true",
|
|
4628
|
+
"",
|
|
4629
|
+
"[[proxies]]",
|
|
4630
|
+
"name = \"dsh-mobile\"",
|
|
4631
|
+
"type = \"http\"",
|
|
4632
|
+
"localIP = \"127.0.0.1\"",
|
|
4633
|
+
`localPort = ${String(localPort)}`,
|
|
4634
|
+
`customDomains = [${tomlString(hostnameValue)}]`,
|
|
4635
|
+
"transport.useEncryption = true",
|
|
4636
|
+
"transport.useCompression = true",
|
|
4637
|
+
""
|
|
4638
|
+
].join("\n");
|
|
4639
|
+
}
|
|
4640
|
+
/** Build the matching restricted frps and Caddy templates for one VPS. */
|
|
4641
|
+
function createFrpServerTemplate(settings) {
|
|
4642
|
+
return createRestrictedFrpServerTemplate(settings.serverPort, settings.token, settings.publicOrigin);
|
|
4643
|
+
}
|
|
4644
|
+
async function atomicPrivateWrite(file, body) {
|
|
4645
|
+
const directory = dirname(file);
|
|
4646
|
+
await mkdir(directory, {
|
|
4647
|
+
recursive: true,
|
|
4648
|
+
mode: 448
|
|
4649
|
+
});
|
|
4650
|
+
try {
|
|
4651
|
+
const current = await lstat(file);
|
|
4652
|
+
if (!current.isFile() || current.isSymbolicLink()) throw new Error("frp_config_target_invalid");
|
|
4653
|
+
} catch (error) {
|
|
4654
|
+
if (error.code !== "ENOENT") throw error;
|
|
4655
|
+
}
|
|
4656
|
+
const temporary = join(directory, `.${basename(file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
4657
|
+
try {
|
|
4658
|
+
await writeFile(temporary, body, {
|
|
4659
|
+
encoding: "utf8",
|
|
4660
|
+
flag: "wx",
|
|
4661
|
+
mode: 384
|
|
4662
|
+
});
|
|
4663
|
+
await rename(temporary, file);
|
|
4664
|
+
await restrictPrivateFile(file);
|
|
4665
|
+
} catch (error) {
|
|
4666
|
+
await rm(temporary, { force: true });
|
|
4667
|
+
throw error;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
/** Owns private FRP settings and generation-specific frpc configuration. */
|
|
4671
|
+
var FrpConfigStore = class {
|
|
4672
|
+
stateRoot;
|
|
4673
|
+
settingsFile;
|
|
4674
|
+
runtimeConfigFile;
|
|
4675
|
+
settingsValue;
|
|
4676
|
+
errorCode;
|
|
4677
|
+
constructor(stateDirectory) {
|
|
4678
|
+
if (!isAbsolute(stateDirectory)) throw new Error("frp config state directory must be absolute");
|
|
4679
|
+
this.stateRoot = resolve(stateDirectory);
|
|
4680
|
+
this.settingsFile = join(this.stateRoot, "settings.json");
|
|
4681
|
+
this.runtimeConfigFile = join(this.stateRoot, "frpc.toml");
|
|
4682
|
+
}
|
|
4683
|
+
/** Load private settings while rejecting links, oversized files, and unknown fields. */
|
|
4684
|
+
async initialize() {
|
|
4685
|
+
let entry;
|
|
4686
|
+
try {
|
|
4687
|
+
entry = await lstat(this.settingsFile);
|
|
4688
|
+
} catch (error) {
|
|
4689
|
+
if (error.code === "ENOENT") return;
|
|
4690
|
+
throw error;
|
|
4691
|
+
}
|
|
4692
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_SETTINGS_BYTES) {
|
|
4693
|
+
this.errorCode = "frp_config_invalid";
|
|
4694
|
+
return;
|
|
4695
|
+
}
|
|
4696
|
+
await restrictPrivateFile(this.settingsFile);
|
|
4697
|
+
try {
|
|
4698
|
+
this.settingsValue = parseFrpSettings(JSON.parse(await readFile(this.settingsFile, "utf8")));
|
|
4699
|
+
this.errorCode = void 0;
|
|
4700
|
+
} catch {
|
|
4701
|
+
this.settingsValue = void 0;
|
|
4702
|
+
this.errorCode = "frp_config_invalid";
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
/** Return configuration metadata without exposing the FRP token. */
|
|
4706
|
+
status() {
|
|
4707
|
+
const settings = this.settingsValue;
|
|
4708
|
+
return Object.freeze({
|
|
4709
|
+
configured: settings !== void 0,
|
|
4710
|
+
...settings === void 0 ? {} : {
|
|
4711
|
+
serverAddress: settings.serverAddress,
|
|
4712
|
+
serverPort: settings.serverPort,
|
|
4713
|
+
publicOrigin: settings.publicOrigin
|
|
4714
|
+
},
|
|
4715
|
+
vhostHttpPort: FRP_VHOST_HTTP_PORT,
|
|
4716
|
+
storagePath: this.stateRoot,
|
|
4717
|
+
...this.errorCode === void 0 ? {} : { errorCode: this.errorCode }
|
|
4718
|
+
});
|
|
4719
|
+
}
|
|
4720
|
+
/** Return private settings only to the provider lifecycle. */
|
|
4721
|
+
settings() {
|
|
4722
|
+
return this.settingsValue;
|
|
4723
|
+
}
|
|
4724
|
+
/** Atomically replace private FRP settings. */
|
|
4725
|
+
async configure(value) {
|
|
4726
|
+
const settings = parseFrpSettings(value);
|
|
4727
|
+
await atomicPrivateWrite(this.settingsFile, `${JSON.stringify(settings)}\n`);
|
|
4728
|
+
await rm(this.runtimeConfigFile, { force: true });
|
|
4729
|
+
this.settingsValue = settings;
|
|
4730
|
+
this.errorCode = void 0;
|
|
4731
|
+
return this.status();
|
|
4732
|
+
}
|
|
4733
|
+
/** Materialize the private generation-specific frpc configuration. */
|
|
4734
|
+
async writeRuntimeConfig(localPort) {
|
|
4735
|
+
const settings = this.settingsValue;
|
|
4736
|
+
if (settings === void 0) throw new Error("frp_config_missing");
|
|
4737
|
+
await atomicPrivateWrite(this.runtimeConfigFile, createFrpcToml(settings, localPort));
|
|
4738
|
+
return this.runtimeConfigFile;
|
|
4739
|
+
}
|
|
4740
|
+
/** Remove only configuration files owned by the FRP provider. */
|
|
4741
|
+
async purge() {
|
|
4742
|
+
await rm(this.stateRoot, {
|
|
4743
|
+
recursive: true,
|
|
4744
|
+
force: true
|
|
4745
|
+
});
|
|
4746
|
+
this.settingsValue = void 0;
|
|
4747
|
+
this.errorCode = void 0;
|
|
4748
|
+
return this.status();
|
|
4749
|
+
}
|
|
4750
|
+
};
|
|
4751
|
+
//#endregion
|
|
4752
|
+
//#region src/remote.ts
|
|
4753
|
+
const REMOTE_PROVIDERS = [
|
|
4754
|
+
"tailscale",
|
|
4755
|
+
"cpolar",
|
|
4756
|
+
"frp"
|
|
4757
|
+
];
|
|
4758
|
+
function aggregateErrors(errors, message) {
|
|
4759
|
+
if (errors.length === 0) return void 0;
|
|
4760
|
+
if (errors.length === 1 && errors[0] instanceof Error) return errors[0];
|
|
4761
|
+
return new AggregateError(errors, message);
|
|
4762
|
+
}
|
|
4763
|
+
/** Settle independent remote cleanup work before reporting any collected failure. */
|
|
4764
|
+
async function settleRemoteResources(steps, message = "remote resource cleanup failed") {
|
|
4765
|
+
const failure = aggregateErrors((await Promise.allSettled(steps.map(async (step) => step()))).filter((result) => result.status === "rejected").map((result) => result.reason), message);
|
|
4766
|
+
if (failure !== void 0) throw failure;
|
|
4767
|
+
}
|
|
4768
|
+
/**
|
|
4769
|
+
* Serialize all provider mutations and preserve the single-provider invariant.
|
|
4770
|
+
* Operations read the selected controller only after reaching the front of the queue.
|
|
4771
|
+
*/
|
|
4772
|
+
var RemoteProviderCoordinator = class {
|
|
4773
|
+
controllers;
|
|
4774
|
+
store;
|
|
4775
|
+
selectedValue;
|
|
4776
|
+
queue = Promise.resolve();
|
|
4777
|
+
constructor(selected, controllers, store) {
|
|
4778
|
+
this.controllers = controllers;
|
|
4779
|
+
this.store = store;
|
|
4780
|
+
this.selectedValue = selected;
|
|
4781
|
+
}
|
|
4782
|
+
/** Return the durable provider currently selected by the desktop UI. */
|
|
4783
|
+
get selected() {
|
|
4784
|
+
return this.selectedValue;
|
|
4785
|
+
}
|
|
4786
|
+
/** Return the controller selected when this method is called. */
|
|
4787
|
+
controller() {
|
|
4788
|
+
return this.controllers[this.selectedValue];
|
|
4789
|
+
}
|
|
4790
|
+
/** Run a provider-owned mutation after all earlier provider work settles. */
|
|
4791
|
+
mutate(operation) {
|
|
4792
|
+
return this.enqueue(() => operation(this.controller()));
|
|
4793
|
+
}
|
|
4794
|
+
/** Disable the previous provider, persist the new selection, and retain rollback on write failure. */
|
|
4795
|
+
select(provider) {
|
|
4796
|
+
return this.enqueue(async () => {
|
|
4797
|
+
if (provider === this.selectedValue) return;
|
|
4798
|
+
const previous = this.controllers[this.selectedValue];
|
|
4799
|
+
const restore = previous.status().enabled;
|
|
4800
|
+
if (restore) await previous.setEnabled(false);
|
|
4801
|
+
try {
|
|
4802
|
+
await this.store.save({
|
|
4803
|
+
version: 1,
|
|
4804
|
+
provider
|
|
4805
|
+
});
|
|
4806
|
+
this.selectedValue = provider;
|
|
4807
|
+
} catch (error) {
|
|
4808
|
+
if (restore) try {
|
|
4809
|
+
await previous.setEnabled(true);
|
|
4810
|
+
} catch (restoreError) {
|
|
4811
|
+
throw new AggregateError([error, restoreError], "remote provider selection rollback failed");
|
|
4812
|
+
}
|
|
4813
|
+
throw error;
|
|
4814
|
+
}
|
|
4815
|
+
});
|
|
4816
|
+
}
|
|
4817
|
+
enqueue(operation) {
|
|
4818
|
+
const task = this.queue.then(() => this.runAndEnforce(operation), () => this.runAndEnforce(operation));
|
|
4819
|
+
this.queue = task.then(() => void 0, () => void 0);
|
|
4820
|
+
return task;
|
|
4821
|
+
}
|
|
4822
|
+
async runAndEnforce(operation) {
|
|
4823
|
+
let value;
|
|
4824
|
+
let operationError;
|
|
4825
|
+
try {
|
|
4826
|
+
value = await operation();
|
|
4827
|
+
} catch (error) {
|
|
4828
|
+
operationError = error;
|
|
4829
|
+
}
|
|
4830
|
+
const results = await Promise.allSettled(REMOTE_PROVIDERS.filter((provider) => provider !== this.selectedValue).map((provider) => this.controllers[provider].setEnabled(false)));
|
|
4831
|
+
const failure = aggregateErrors([...operationError === void 0 ? [] : [operationError], ...results.filter((result) => result.status === "rejected").map((result) => result.reason)], "remote provider operation failed");
|
|
4832
|
+
if (failure !== void 0) throw failure;
|
|
4833
|
+
return value;
|
|
4834
|
+
}
|
|
4835
|
+
};
|
|
4836
|
+
/** Stop an owned provider process and do not report completion before its close event. */
|
|
4837
|
+
async function terminateRemoteProcess(child, gracefulTimeoutMs = 1500, forcedTimeoutMs = 1500) {
|
|
4838
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
4839
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
4840
|
+
let gracefulTimer;
|
|
4841
|
+
let forcedTimer;
|
|
4842
|
+
let settled = false;
|
|
4843
|
+
const finish = (error) => {
|
|
4844
|
+
if (settled) return;
|
|
4845
|
+
settled = true;
|
|
4846
|
+
if (gracefulTimer !== void 0) clearTimeout(gracefulTimer);
|
|
4847
|
+
if (forcedTimer !== void 0) clearTimeout(forcedTimer);
|
|
4848
|
+
child.off("close", onClose);
|
|
4849
|
+
if (error === void 0) resolveClose();
|
|
4850
|
+
else rejectClose(error);
|
|
4851
|
+
};
|
|
4852
|
+
const onClose = () => {
|
|
4853
|
+
finish();
|
|
4854
|
+
};
|
|
4855
|
+
child.once("close", onClose);
|
|
4856
|
+
try {
|
|
4857
|
+
child.kill("SIGTERM");
|
|
4858
|
+
} catch (error) {
|
|
4859
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4862
|
+
if (settled) return;
|
|
4863
|
+
gracefulTimer = setTimeout(() => {
|
|
4864
|
+
try {
|
|
4865
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
4866
|
+
} catch (error) {
|
|
4867
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
4868
|
+
return;
|
|
4869
|
+
}
|
|
4870
|
+
if (settled) return;
|
|
4871
|
+
forcedTimer = setTimeout(() => {
|
|
4872
|
+
finish(/* @__PURE__ */ new Error("remote_process_stop_timeout"));
|
|
4873
|
+
}, forcedTimeoutMs);
|
|
4874
|
+
forcedTimer.unref();
|
|
4875
|
+
}, gracefulTimeoutMs);
|
|
4876
|
+
gracefulTimer.unref();
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
/** Validate the provider selection loaded across the filesystem boundary. */
|
|
4880
|
+
function parseRemoteProviderState(value) {
|
|
4881
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("remote provider state must be an object");
|
|
4882
|
+
const record = value;
|
|
4883
|
+
if (record.version !== 1 || record.provider !== "tailscale" && record.provider !== "cpolar" && record.provider !== "frp" || Reflect.ownKeys(record).some((key) => key !== "version" && key !== "provider")) throw new Error("remote provider state has an unsupported format");
|
|
4884
|
+
return Object.freeze({
|
|
4885
|
+
version: 1,
|
|
4886
|
+
provider: record.provider
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
/** Atomic selection store whose absent-file state uses the configured default. */
|
|
4890
|
+
var JsonRemoteProviderStore = class {
|
|
4891
|
+
file;
|
|
4892
|
+
defaultProvider;
|
|
4893
|
+
constructor(file, defaultProvider) {
|
|
4894
|
+
this.file = file;
|
|
4895
|
+
this.defaultProvider = defaultProvider;
|
|
4896
|
+
}
|
|
4897
|
+
async load() {
|
|
4898
|
+
let stat;
|
|
4899
|
+
try {
|
|
4900
|
+
stat = await lstat(this.file);
|
|
4901
|
+
} catch (error) {
|
|
4902
|
+
if (error.code === "ENOENT") return Object.freeze({
|
|
4903
|
+
version: 1,
|
|
4904
|
+
provider: this.defaultProvider
|
|
4905
|
+
});
|
|
4906
|
+
throw error;
|
|
4907
|
+
}
|
|
4908
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096) throw new Error("remote provider state must be a regular file no larger than 4 KiB");
|
|
4909
|
+
await restrictPrivateFile(this.file);
|
|
4910
|
+
let parsed;
|
|
4911
|
+
try {
|
|
4912
|
+
parsed = JSON.parse(await readFile(this.file, "utf8"));
|
|
4913
|
+
} catch (error) {
|
|
4914
|
+
throw new Error("remote provider state is not valid JSON", { cause: error });
|
|
4915
|
+
}
|
|
4916
|
+
return parseRemoteProviderState(parsed);
|
|
4917
|
+
}
|
|
4918
|
+
async save(state) {
|
|
4919
|
+
const validated = parseRemoteProviderState(state);
|
|
4920
|
+
const directory = dirname(this.file);
|
|
4921
|
+
await mkdir(directory, {
|
|
4922
|
+
recursive: true,
|
|
4923
|
+
mode: 448
|
|
4924
|
+
});
|
|
4925
|
+
try {
|
|
4926
|
+
const current = await lstat(this.file);
|
|
4927
|
+
if (!current.isFile() || current.isSymbolicLink()) throw new Error("remote provider state target must remain a regular file");
|
|
4928
|
+
} catch (error) {
|
|
4929
|
+
if (error.code !== "ENOENT") throw error;
|
|
4930
|
+
}
|
|
4931
|
+
const temporary = join(directory, `.${basename(this.file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
4932
|
+
try {
|
|
4933
|
+
await writeFile(temporary, `${JSON.stringify(validated)}\n`, {
|
|
4934
|
+
encoding: "utf8",
|
|
4935
|
+
flag: "wx",
|
|
4936
|
+
mode: 384
|
|
4937
|
+
});
|
|
4938
|
+
await rename(temporary, this.file);
|
|
4939
|
+
await restrictPrivateFile(this.file);
|
|
4940
|
+
} catch (error) {
|
|
4941
|
+
await rm(temporary, { force: true });
|
|
4942
|
+
throw error;
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
};
|
|
4946
|
+
/** Resolve the first-run provider without letting environment values bypass validation. */
|
|
4947
|
+
function configuredRemoteProvider(environment) {
|
|
4948
|
+
const value = environment.DSH_MOBILE_REMOTE_PROVIDER ?? "tailscale";
|
|
4949
|
+
if (value !== "tailscale" && value !== "cpolar" && value !== "frp") throw new Error("DSH_MOBILE_REMOTE_PROVIDER must be tailscale, cpolar, or frp");
|
|
4950
|
+
return value;
|
|
4951
|
+
}
|
|
4952
|
+
//#endregion
|
|
4953
|
+
//#region src/frp.ts
|
|
4954
|
+
const START_TIMEOUT_MS$1 = 45e3;
|
|
4955
|
+
const DISCOVERY_REQUEST_TIMEOUT_MS = 5e3;
|
|
4956
|
+
const DISCOVERY_RETRY_MS = 1e3;
|
|
4957
|
+
const MAX_DISCOVERY_BYTES = 16384;
|
|
4958
|
+
const VHOST_PROBE_TIMEOUT_MS = 1500;
|
|
4959
|
+
function publicStatus$2(status) {
|
|
4960
|
+
return Object.freeze({
|
|
4961
|
+
enabled: status.enabled,
|
|
4962
|
+
state: status.state,
|
|
4963
|
+
...status.origin === void 0 ? {} : { origin: status.origin },
|
|
4964
|
+
...status.errorCode === void 0 ? {} : { errorCode: status.errorCode }
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
async function defaultVerifyConfig(executable, configFile) {
|
|
4968
|
+
await new Promise((resolveRun, reject) => {
|
|
4969
|
+
execFile(executable, [
|
|
4970
|
+
"verify",
|
|
4971
|
+
"-c",
|
|
4972
|
+
configFile
|
|
4973
|
+
], {
|
|
4974
|
+
windowsHide: true,
|
|
4975
|
+
timeout: 3e4,
|
|
4976
|
+
maxBuffer: 65536
|
|
4977
|
+
}, (error) => {
|
|
4978
|
+
if (error === null) resolveRun();
|
|
4979
|
+
else reject(error);
|
|
4980
|
+
});
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
function defaultLaunchClient(executable, configFile) {
|
|
4984
|
+
return spawn(executable, ["-c", configFile], {
|
|
4985
|
+
shell: false,
|
|
4986
|
+
stdio: [
|
|
4987
|
+
"pipe",
|
|
4988
|
+
"pipe",
|
|
4989
|
+
"pipe"
|
|
4990
|
+
],
|
|
4991
|
+
windowsHide: true
|
|
4992
|
+
});
|
|
4993
|
+
}
|
|
4994
|
+
async function defaultProbeVhostExposure(serverAddress, port) {
|
|
4995
|
+
return new Promise((resolveProbe) => {
|
|
4996
|
+
const socket = connect({
|
|
4997
|
+
host: serverAddress,
|
|
4998
|
+
port
|
|
4999
|
+
});
|
|
5000
|
+
let finished = false;
|
|
5001
|
+
const finish = (exposed) => {
|
|
5002
|
+
if (finished) return;
|
|
5003
|
+
finished = true;
|
|
5004
|
+
clearTimeout(timer);
|
|
5005
|
+
socket.destroy();
|
|
5006
|
+
resolveProbe(exposed);
|
|
5007
|
+
};
|
|
5008
|
+
const timer = setTimeout(() => {
|
|
5009
|
+
finish(false);
|
|
5010
|
+
}, VHOST_PROBE_TIMEOUT_MS);
|
|
5011
|
+
timer.unref();
|
|
5012
|
+
socket.once("connect", () => {
|
|
5013
|
+
finish(true);
|
|
5014
|
+
});
|
|
5015
|
+
socket.once("error", () => {
|
|
5016
|
+
finish(false);
|
|
5017
|
+
});
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
async function boundedResponseBytes(response) {
|
|
5021
|
+
if (response.body === null) throw new Error("frp_discovery_invalid");
|
|
5022
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
5023
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_DISCOVERY_BYTES) throw new Error("frp_discovery_invalid");
|
|
5024
|
+
const reader = response.body.getReader();
|
|
5025
|
+
const chunks = [];
|
|
5026
|
+
let received = 0;
|
|
5027
|
+
while (true) {
|
|
5028
|
+
const result = await reader.read();
|
|
5029
|
+
if (result.done) break;
|
|
5030
|
+
received += result.value.byteLength;
|
|
5031
|
+
if (received > MAX_DISCOVERY_BYTES) {
|
|
5032
|
+
await reader.cancel();
|
|
5033
|
+
throw new Error("frp_discovery_invalid");
|
|
5034
|
+
}
|
|
5035
|
+
chunks.push(result.value);
|
|
5036
|
+
}
|
|
5037
|
+
const bytes = new Uint8Array(received);
|
|
5038
|
+
let offset = 0;
|
|
5039
|
+
for (const chunk of chunks) {
|
|
5040
|
+
bytes.set(chunk, offset);
|
|
5041
|
+
offset += chunk.byteLength;
|
|
5042
|
+
}
|
|
5043
|
+
return bytes;
|
|
5044
|
+
}
|
|
5045
|
+
async function defaultProbeDiscovery(origin, expectedInstanceId, signal) {
|
|
5046
|
+
const requestController = new AbortController();
|
|
5047
|
+
const abort = () => {
|
|
5048
|
+
requestController.abort();
|
|
5049
|
+
};
|
|
5050
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
5051
|
+
const timeout = setTimeout(abort, DISCOVERY_REQUEST_TIMEOUT_MS);
|
|
5052
|
+
timeout.unref();
|
|
5053
|
+
try {
|
|
5054
|
+
const response = await fetch(`${origin}/mobile-access/discovery`, {
|
|
5055
|
+
method: "GET",
|
|
5056
|
+
redirect: "error",
|
|
5057
|
+
cache: "no-store",
|
|
5058
|
+
signal: requestController.signal,
|
|
5059
|
+
headers: { accept: "application/json" }
|
|
5060
|
+
});
|
|
5061
|
+
if (!response.ok) return false;
|
|
5062
|
+
let value;
|
|
5063
|
+
try {
|
|
5064
|
+
value = JSON.parse(new TextDecoder().decode(await boundedResponseBytes(response)));
|
|
5065
|
+
} catch {
|
|
5066
|
+
throw new Error("frp_discovery_invalid");
|
|
5067
|
+
}
|
|
5068
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("frp_discovery_invalid");
|
|
5069
|
+
const actual = value.instanceId;
|
|
5070
|
+
if (typeof actual !== "string") throw new Error("frp_discovery_invalid");
|
|
5071
|
+
if (actual !== expectedInstanceId) throw new Error("frp_discovery_mismatch");
|
|
5072
|
+
return true;
|
|
5073
|
+
} finally {
|
|
5074
|
+
clearTimeout(timeout);
|
|
5075
|
+
signal.removeEventListener("abort", abort);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
/** Owns frpc, its generation-specific configuration, and the remote gateway. */
|
|
5079
|
+
var FrpController = class {
|
|
5080
|
+
options;
|
|
5081
|
+
enabled = false;
|
|
5082
|
+
initialized = false;
|
|
5083
|
+
disposed = false;
|
|
5084
|
+
child;
|
|
5085
|
+
gatewayValue;
|
|
5086
|
+
generation = 0;
|
|
5087
|
+
latest = publicStatus$2({
|
|
5088
|
+
enabled: false,
|
|
5089
|
+
state: "off"
|
|
5090
|
+
});
|
|
5091
|
+
queue = Promise.resolve();
|
|
5092
|
+
startupAbort;
|
|
5093
|
+
constructor(options) {
|
|
5094
|
+
this.options = options;
|
|
5095
|
+
if (!isAbsolute(options.executable)) throw new Error("frpc executable path must be absolute");
|
|
5096
|
+
if (!/^[a-f0-9]{64}$/u.test(options.instanceId)) throw new Error("FRP instance ID is invalid");
|
|
5097
|
+
}
|
|
5098
|
+
/** Restore the remembered FRP switch without changing LAN or other providers. */
|
|
5099
|
+
async initialize() {
|
|
5100
|
+
const state = await this.options.store.load();
|
|
5101
|
+
this.enabled = state.enabled;
|
|
5102
|
+
this.initialized = true;
|
|
5103
|
+
if (this.enabled) await this.start();
|
|
5104
|
+
else this.publish({
|
|
5105
|
+
enabled: false,
|
|
5106
|
+
state: "off"
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
5109
|
+
/** Return the active FRP-backed DSH gateway. */
|
|
5110
|
+
gateway() {
|
|
5111
|
+
return this.gatewayValue;
|
|
5112
|
+
}
|
|
5113
|
+
/** Return state safe for the desktop control UI. */
|
|
5114
|
+
status() {
|
|
5115
|
+
return publicStatus$2(this.latest);
|
|
5116
|
+
}
|
|
5117
|
+
/** Enable or disable FRP without changing LAN or another provider. */
|
|
5118
|
+
async setEnabled(enabled) {
|
|
5119
|
+
if (!this.initialized || this.disposed) throw new Error("FRP controller is unavailable");
|
|
5120
|
+
await this.enqueue(async () => {
|
|
5121
|
+
if (this.enabled === enabled && (enabled === false || this.child !== void 0)) return;
|
|
5122
|
+
if (!enabled) await this.stop();
|
|
5123
|
+
this.enabled = enabled;
|
|
5124
|
+
await this.options.store.save({
|
|
5125
|
+
version: 1,
|
|
5126
|
+
enabled
|
|
5127
|
+
});
|
|
5128
|
+
if (enabled) await this.start();
|
|
5129
|
+
else this.publish({
|
|
5130
|
+
enabled: false,
|
|
5131
|
+
state: "off"
|
|
5132
|
+
});
|
|
5133
|
+
});
|
|
5134
|
+
return this.status();
|
|
5135
|
+
}
|
|
5136
|
+
/** Restart FRP while retaining its private server settings and devices. */
|
|
5137
|
+
async reconnect() {
|
|
5138
|
+
if (!this.initialized || this.disposed) throw new Error("FRP controller is unavailable");
|
|
5139
|
+
await this.enqueue(async () => {
|
|
5140
|
+
if (!this.enabled) {
|
|
5141
|
+
this.enabled = true;
|
|
5142
|
+
await this.options.store.save({
|
|
5143
|
+
version: 1,
|
|
5144
|
+
enabled: true
|
|
5145
|
+
});
|
|
5146
|
+
}
|
|
5147
|
+
await this.stop();
|
|
5148
|
+
await this.start();
|
|
5149
|
+
});
|
|
5150
|
+
return this.status();
|
|
5151
|
+
}
|
|
5152
|
+
/** Disable FRP without deleting its explicitly managed component or settings. */
|
|
5153
|
+
async reset() {
|
|
5154
|
+
if (!this.initialized || this.disposed) throw new Error("FRP controller is unavailable");
|
|
5155
|
+
await this.enqueue(async () => {
|
|
5156
|
+
await this.stop();
|
|
5157
|
+
this.enabled = false;
|
|
5158
|
+
await this.options.store.save({
|
|
5159
|
+
version: 1,
|
|
5160
|
+
enabled: false
|
|
5161
|
+
});
|
|
5162
|
+
this.publish({
|
|
5163
|
+
enabled: false,
|
|
5164
|
+
state: "off"
|
|
5165
|
+
});
|
|
5166
|
+
});
|
|
5167
|
+
return this.status();
|
|
5168
|
+
}
|
|
5169
|
+
/** Stop all FRP resources without changing the remembered switch. */
|
|
5170
|
+
async close() {
|
|
5171
|
+
if (this.disposed) return;
|
|
5172
|
+
this.disposed = true;
|
|
5173
|
+
await this.enqueue(() => this.stop());
|
|
5174
|
+
}
|
|
5175
|
+
enqueue(operation) {
|
|
5176
|
+
const task = this.queue.then(operation, operation);
|
|
5177
|
+
this.queue = task.then(() => void 0, () => void 0);
|
|
5178
|
+
return task;
|
|
5179
|
+
}
|
|
5180
|
+
publish(status) {
|
|
5181
|
+
this.latest = publicStatus$2(status);
|
|
5182
|
+
try {
|
|
5183
|
+
this.options.onStatus?.(this.status());
|
|
5184
|
+
} catch {}
|
|
5185
|
+
}
|
|
5186
|
+
async start() {
|
|
5187
|
+
const generation = ++this.generation;
|
|
5188
|
+
let executableEntry;
|
|
5189
|
+
try {
|
|
5190
|
+
executableEntry = await lstat(this.options.executable);
|
|
5191
|
+
} catch {
|
|
5192
|
+
this.publish({
|
|
5193
|
+
enabled: true,
|
|
5194
|
+
state: "unavailable",
|
|
5195
|
+
errorCode: "frp_component_missing"
|
|
5196
|
+
});
|
|
5197
|
+
return;
|
|
5198
|
+
}
|
|
5199
|
+
if (!executableEntry.isFile() || executableEntry.isSymbolicLink()) {
|
|
5200
|
+
this.publish({
|
|
5201
|
+
enabled: true,
|
|
5202
|
+
state: "unavailable",
|
|
5203
|
+
errorCode: "frp_component_invalid"
|
|
5204
|
+
});
|
|
5205
|
+
return;
|
|
5206
|
+
}
|
|
5207
|
+
const settings = this.options.config.settings();
|
|
5208
|
+
if (settings === void 0) {
|
|
5209
|
+
this.publish({
|
|
5210
|
+
enabled: true,
|
|
5211
|
+
state: "unavailable",
|
|
5212
|
+
errorCode: "frp_config_missing"
|
|
5213
|
+
});
|
|
5214
|
+
return;
|
|
5215
|
+
}
|
|
5216
|
+
this.publish({
|
|
5217
|
+
enabled: true,
|
|
5218
|
+
state: "starting",
|
|
5219
|
+
origin: settings.publicOrigin
|
|
5220
|
+
});
|
|
5221
|
+
let exposed;
|
|
5222
|
+
try {
|
|
5223
|
+
exposed = await (this.options.probeVhostExposure ?? defaultProbeVhostExposure)(settings.serverAddress, FRP_VHOST_HTTP_PORT);
|
|
5224
|
+
} catch {
|
|
5225
|
+
this.publish({
|
|
5226
|
+
enabled: true,
|
|
5227
|
+
state: "error",
|
|
5228
|
+
origin: settings.publicOrigin,
|
|
5229
|
+
errorCode: "frp_vhost_probe_failed"
|
|
5230
|
+
});
|
|
5231
|
+
return;
|
|
5232
|
+
}
|
|
5233
|
+
if (exposed) {
|
|
5234
|
+
this.publish({
|
|
5235
|
+
enabled: true,
|
|
5236
|
+
state: "error",
|
|
5237
|
+
origin: settings.publicOrigin,
|
|
5238
|
+
errorCode: "frp_vhost_publicly_reachable"
|
|
5239
|
+
});
|
|
5240
|
+
return;
|
|
5241
|
+
}
|
|
5242
|
+
let gateway;
|
|
5243
|
+
try {
|
|
5244
|
+
gateway = await this.options.createGateway(settings.publicOrigin);
|
|
5245
|
+
} catch {
|
|
5246
|
+
this.publish({
|
|
5247
|
+
enabled: true,
|
|
5248
|
+
state: "error",
|
|
5249
|
+
origin: settings.publicOrigin,
|
|
5250
|
+
errorCode: "gateway_start_failed"
|
|
5251
|
+
});
|
|
5252
|
+
return;
|
|
5253
|
+
}
|
|
5254
|
+
if (generation !== this.generation || !this.enabled) {
|
|
5255
|
+
await gateway.close();
|
|
5256
|
+
return;
|
|
5257
|
+
}
|
|
5258
|
+
this.gatewayValue = gateway;
|
|
5259
|
+
let configFile;
|
|
5260
|
+
try {
|
|
5261
|
+
configFile = await this.options.config.writeRuntimeConfig(gateway.address().port);
|
|
5262
|
+
await (this.options.verifyConfig ?? defaultVerifyConfig)(this.options.executable, configFile);
|
|
5263
|
+
} catch {
|
|
5264
|
+
await this.failGeneration(generation, "frp_config_verify_failed");
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
5267
|
+
if (generation !== this.generation || !this.enabled) return;
|
|
5268
|
+
let child;
|
|
5269
|
+
try {
|
|
5270
|
+
child = (this.options.launchClient ?? defaultLaunchClient)(this.options.executable, configFile);
|
|
5271
|
+
} catch {
|
|
5272
|
+
await this.failGeneration(generation, "frp_launch_failed");
|
|
5273
|
+
return;
|
|
5274
|
+
}
|
|
5275
|
+
this.child = child;
|
|
5276
|
+
child.stdout.resume();
|
|
5277
|
+
child.stderr.resume();
|
|
5278
|
+
child.once("error", () => {
|
|
5279
|
+
this.enqueue(() => this.failGeneration(generation, "frp_launch_failed"));
|
|
5280
|
+
});
|
|
5281
|
+
child.once("close", (code) => {
|
|
5282
|
+
if (generation !== this.generation || this.child !== child) return;
|
|
5283
|
+
this.child = void 0;
|
|
5284
|
+
if (this.enabled) this.enqueue(() => this.failGeneration(generation, code === 0 ? "frp_stopped" : "frp_exited"));
|
|
5285
|
+
});
|
|
5286
|
+
this.publish({
|
|
5287
|
+
enabled: true,
|
|
5288
|
+
state: "connecting",
|
|
5289
|
+
origin: settings.publicOrigin
|
|
5290
|
+
});
|
|
5291
|
+
const controller = new AbortController();
|
|
5292
|
+
this.startupAbort = controller;
|
|
5293
|
+
this.waitForDiscovery(generation, settings.publicOrigin, controller.signal);
|
|
5294
|
+
}
|
|
5295
|
+
async waitForDiscovery(generation, origin, signal) {
|
|
5296
|
+
const deadline = Date.now() + (this.options.startTimeoutMs ?? START_TIMEOUT_MS$1);
|
|
5297
|
+
const probe = this.options.probeDiscovery ?? defaultProbeDiscovery;
|
|
5298
|
+
while (!signal.aborted && Date.now() < deadline) {
|
|
5299
|
+
try {
|
|
5300
|
+
if (await probe(origin, this.options.instanceId, signal)) {
|
|
5301
|
+
await this.enqueue(async () => {
|
|
5302
|
+
if (generation !== this.generation || signal.aborted || !this.enabled) return;
|
|
5303
|
+
this.startupAbort = void 0;
|
|
5304
|
+
this.publish({
|
|
5305
|
+
enabled: true,
|
|
5306
|
+
state: "ready",
|
|
5307
|
+
origin
|
|
5308
|
+
});
|
|
5309
|
+
});
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5312
|
+
} catch (error) {
|
|
5313
|
+
if (signal.aborted) return;
|
|
5314
|
+
if (error instanceof Error && (error.message === "frp_discovery_mismatch" || error.message === "frp_discovery_invalid")) {
|
|
5315
|
+
await this.enqueue(() => this.failGeneration(generation, error.message));
|
|
5316
|
+
return;
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
await new Promise((resolveWait) => {
|
|
5320
|
+
let finished = false;
|
|
5321
|
+
const finish = () => {
|
|
5322
|
+
if (finished) return;
|
|
5323
|
+
finished = true;
|
|
5324
|
+
clearTimeout(timer);
|
|
5325
|
+
signal.removeEventListener("abort", finish);
|
|
5326
|
+
resolveWait();
|
|
5327
|
+
};
|
|
5328
|
+
const timer = setTimeout(finish, this.options.retryIntervalMs ?? DISCOVERY_RETRY_MS);
|
|
5329
|
+
timer.unref();
|
|
5330
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
5331
|
+
});
|
|
5332
|
+
}
|
|
5333
|
+
if (!signal.aborted) await this.enqueue(() => this.failGeneration(generation, "frp_start_timeout"));
|
|
5334
|
+
}
|
|
5335
|
+
async failGeneration(generation, code) {
|
|
5336
|
+
if (generation !== this.generation) return;
|
|
5337
|
+
await this.stopProcessAndGateway();
|
|
5338
|
+
if (this.enabled) this.publish({
|
|
5339
|
+
enabled: true,
|
|
5340
|
+
state: "error",
|
|
5341
|
+
errorCode: code
|
|
5342
|
+
});
|
|
5343
|
+
}
|
|
5344
|
+
async stop() {
|
|
5345
|
+
++this.generation;
|
|
5346
|
+
await this.stopProcessAndGateway();
|
|
5347
|
+
}
|
|
5348
|
+
async stopProcessAndGateway() {
|
|
5349
|
+
this.startupAbort?.abort();
|
|
5350
|
+
this.startupAbort = void 0;
|
|
5351
|
+
const child = this.child;
|
|
5352
|
+
this.child = void 0;
|
|
5353
|
+
const gateway = this.gatewayValue;
|
|
5354
|
+
this.gatewayValue = void 0;
|
|
5355
|
+
await settleRemoteResources([
|
|
5356
|
+
() => child !== void 0 && child.exitCode === null ? terminateRemoteProcess(child) : void 0,
|
|
5357
|
+
() => gateway?.close(),
|
|
5358
|
+
() => rm(this.options.config.runtimeConfigFile, { force: true })
|
|
5359
|
+
], "FRP resource cleanup failed");
|
|
5360
|
+
}
|
|
5361
|
+
};
|
|
5362
|
+
//#endregion
|
|
5363
|
+
//#region src/diagnostics.ts
|
|
5364
|
+
const execFile$2 = promisify(execFile);
|
|
5365
|
+
const REMOTE_ERROR_GUIDANCE = Object.freeze({
|
|
5366
|
+
component_missing: "重新安装完整插件包。",
|
|
5367
|
+
funnel_permission_required: "继续完成 Tailscale Funnel 授权。",
|
|
5368
|
+
funnel_https_required: "继续完成 Tailscale HTTPS 授权。",
|
|
5369
|
+
funnel_start_failed: "重新打开授权页并允许 Funnel。",
|
|
5370
|
+
funnel_start_timeout: "检查网络后点击“重新连接”。",
|
|
5371
|
+
tailscale_dns_missing: "确认 Tailscale 登录仍有效后重新连接。",
|
|
5372
|
+
sidecar_launch_failed: "重新安装完整插件包后重试。",
|
|
5373
|
+
sidecar_stopped: "点击“重新连接”。",
|
|
5374
|
+
sidecar_exited: "点击“重新连接”;仍失败时复制诊断报告。",
|
|
5375
|
+
control_channel_failed: "点击“重新连接”。",
|
|
5376
|
+
cpolar_component_missing: "先安装 cpolar 官方组件。",
|
|
5377
|
+
cpolar_component_invalid: "彻底移除 cpolar 组件后重新安装。",
|
|
5378
|
+
cpolar_config_missing: "保存 cpolar Authtoken 后重试。",
|
|
5379
|
+
cpolar_config_invalid: "重新保存 cpolar Authtoken。",
|
|
5380
|
+
cpolar_start_timeout: "检查网络后点击“重新连接”。",
|
|
5381
|
+
cpolar_stopped: "点击“重新连接”。",
|
|
5382
|
+
cpolar_exited: "点击“重新连接”;仍失败时复制诊断报告。",
|
|
5383
|
+
frp_component_missing: "先安装 FRP 官方组件。",
|
|
5384
|
+
frp_component_invalid: "彻底清理 FRP 组件后重新安装。",
|
|
5385
|
+
frp_config_missing: "先保存自建 FRP 连接配置。",
|
|
5386
|
+
frp_config_verify_failed: "检查服务器地址、端口、Token 和公开域名。",
|
|
5387
|
+
frp_vhost_publicly_reachable: "将 frps 的 HTTP vhost 监听限制到 127.0.0.1。",
|
|
5388
|
+
frp_vhost_probe_failed: "确认 VPS 地址可解析后重新连接。",
|
|
5389
|
+
frp_launch_failed: "重新安装 FRP 官方组件后重试。",
|
|
5390
|
+
frp_start_timeout: "确认 frps、Caddy 和域名解析正常后重新连接。",
|
|
5391
|
+
frp_discovery_mismatch: "公开域名连接到了另一台电脑,请核对 Caddy 与 frps 配置。",
|
|
5392
|
+
frp_discovery_invalid: "公开域名返回了非 DSH Mobile 响应。",
|
|
5393
|
+
frp_stopped: "点击“重新连接”。",
|
|
5394
|
+
frp_exited: "检查 VPS 配置后重新连接;仍失败时复制诊断报告。",
|
|
5395
|
+
gateway_start_failed: "确认 DSH 正在运行后重新连接。"
|
|
5396
|
+
});
|
|
5397
|
+
function check(id, status, reason, label, detail, action, facts) {
|
|
5398
|
+
return Object.freeze({
|
|
5399
|
+
id,
|
|
5400
|
+
status,
|
|
5401
|
+
reason,
|
|
5402
|
+
...facts === void 0 ? {} : { facts: Object.freeze(facts) },
|
|
5403
|
+
label,
|
|
5404
|
+
detail,
|
|
5405
|
+
...action === void 0 ? {} : { action }
|
|
5406
|
+
});
|
|
5407
|
+
}
|
|
5408
|
+
function maskLanOrigin(origin) {
|
|
5409
|
+
if (origin === void 0) return "未分配";
|
|
5410
|
+
try {
|
|
5411
|
+
const url = new URL(origin);
|
|
5412
|
+
const octets = url.hostname.split(".");
|
|
5413
|
+
const host = octets.length === 4 ? `${octets[0]}.${octets[1]}.${octets[2]}.x` : "局域网地址";
|
|
5414
|
+
return `${url.protocol}//${host}${url.port === "" ? "" : `:${url.port}`}`;
|
|
5415
|
+
} catch {
|
|
5416
|
+
return "地址格式无效";
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
function remoteSuffix(origin) {
|
|
5420
|
+
if (origin === void 0) return "未分配";
|
|
5421
|
+
try {
|
|
5422
|
+
const hostname = new URL(origin).hostname;
|
|
5423
|
+
if (hostname.endsWith(".ts.net")) return "*.ts.net";
|
|
5424
|
+
for (const suffix of [
|
|
5425
|
+
".cpolar.cn",
|
|
5426
|
+
".cpolar.io",
|
|
5427
|
+
".cpolar.top",
|
|
5428
|
+
".cpolar.com"
|
|
5429
|
+
]) if (hostname.endsWith(suffix)) return `*${suffix}`;
|
|
5430
|
+
return "公共 HTTPS 地址";
|
|
5431
|
+
} catch {
|
|
5432
|
+
return "地址格式无效";
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
function defaultFirewallProbe(platform = process.platform) {
|
|
5436
|
+
return async (port) => {
|
|
5437
|
+
if (platform !== "win32") return { state: "not-applicable" };
|
|
5438
|
+
if (port === void 0) return { state: "unknown" };
|
|
5439
|
+
const script = [
|
|
5440
|
+
"$specs = @(@{ Name = 'DSH Mobile HTTPS'; Protocol = 'TCP' }, @{ Name = 'DSH Mobile Discovery'; Protocol = 'UDP' })",
|
|
5441
|
+
"$ready = $true",
|
|
5442
|
+
"$specs | ForEach-Object {",
|
|
5443
|
+
" $spec = $_",
|
|
5444
|
+
" $rule = Get-NetFirewallRule -DisplayName $spec.Name -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' } | Select-Object -First 1",
|
|
5445
|
+
" if ($null -eq $rule) { $ready = $false; return }",
|
|
5446
|
+
" $filters = @($rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)",
|
|
5447
|
+
` $matching = @($filters | Where-Object { $_.Protocol -eq $spec.Protocol -and ($_.LocalPort -eq 'Any' -or $_.LocalPort -eq '${String(port)}') })`,
|
|
5448
|
+
" if ($matching.Count -eq 0) { $ready = $false }",
|
|
5449
|
+
"}",
|
|
5450
|
+
"if ($ready) { 'ready' } else { 'missing' }"
|
|
5451
|
+
].join("; ");
|
|
5452
|
+
try {
|
|
5453
|
+
return { state: (await execFile$2("powershell.exe", [
|
|
5454
|
+
"-NoProfile",
|
|
5455
|
+
"-NonInteractive",
|
|
5456
|
+
"-Command",
|
|
5457
|
+
script
|
|
5458
|
+
], {
|
|
5459
|
+
encoding: "utf8",
|
|
5460
|
+
timeout: 3e3,
|
|
5461
|
+
windowsHide: true
|
|
5462
|
+
})).stdout.trim() === "ready" ? "ready" : "missing" };
|
|
5463
|
+
} catch {
|
|
5464
|
+
return { state: "unknown" };
|
|
5465
|
+
}
|
|
5466
|
+
};
|
|
5467
|
+
}
|
|
5468
|
+
/** Allow remote relays enough time to answer without making diagnostics unbounded. */
|
|
5469
|
+
function remoteDiagnosticTimeoutMs(origin) {
|
|
5470
|
+
const hostname = new URL(origin).hostname.toLowerCase();
|
|
5471
|
+
if (hostname.endsWith(".ts.net") || hostname.includes(".cpolar.")) return 1e4;
|
|
5472
|
+
return 1e4;
|
|
5473
|
+
}
|
|
5474
|
+
async function defaultRemoteProbe(origin) {
|
|
5475
|
+
if (origin === void 0) return { state: "not-applicable" };
|
|
5476
|
+
const hostname = new URL(origin).hostname;
|
|
5477
|
+
const started = performance.now();
|
|
5478
|
+
try {
|
|
5479
|
+
const response = await fetch(new URL("/mobile-access/health", origin), {
|
|
5480
|
+
cache: "no-store",
|
|
5481
|
+
redirect: "error",
|
|
5482
|
+
signal: AbortSignal.timeout(remoteDiagnosticTimeoutMs(origin))
|
|
5483
|
+
});
|
|
5484
|
+
const latencyMs = Math.max(0, Math.round(performance.now() - started));
|
|
5485
|
+
if (response.status === 429) return {
|
|
5486
|
+
state: "rate-limited",
|
|
5487
|
+
latencyMs
|
|
5488
|
+
};
|
|
5489
|
+
return response.ok ? {
|
|
5490
|
+
state: "ready",
|
|
5491
|
+
latencyMs
|
|
5492
|
+
} : {
|
|
5493
|
+
state: "unreachable",
|
|
5494
|
+
latencyMs
|
|
5495
|
+
};
|
|
5496
|
+
} catch {
|
|
5497
|
+
let fakeIp = false;
|
|
5498
|
+
try {
|
|
5499
|
+
fakeIp = (await lookup(hostname, { all: true })).some(({ address }) => {
|
|
5500
|
+
const [first, second] = address.split(".").map(Number);
|
|
5501
|
+
return first === 198 && (second === 18 || second === 19);
|
|
5502
|
+
});
|
|
5503
|
+
} catch {}
|
|
5504
|
+
return {
|
|
5505
|
+
state: "unreachable",
|
|
5506
|
+
...fakeIp ? { fakeIp: true } : {}
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
function reportLine(entry) {
|
|
5511
|
+
return `[${entry.status.toUpperCase()}] ${entry.label}: ${entry.detail}${entry.action === void 0 ? "" : ` ${entry.action}`}`;
|
|
5512
|
+
}
|
|
5513
|
+
/** Run bounded read-only checks and return a report safe to paste into an issue. */
|
|
5514
|
+
async function collectConnectionDiagnostics(snapshot, probes = {}) {
|
|
5515
|
+
const checks = [];
|
|
5516
|
+
const remoteProbe = snapshot.remote.running && snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0 ? (probes.remote ?? defaultRemoteProbe)(snapshot.remote.origin) : Promise.resolve({ state: "not-applicable" });
|
|
5517
|
+
const [firewall, remoteObservation] = await Promise.all([(probes.firewall ?? defaultFirewallProbe())(snapshot.lan.port), remoteProbe]);
|
|
5518
|
+
checks.push(check("versions", "ok", "versions-current", "版本兼容", `插件 ${DSH_MOBILE_VERSION},DSH ${snapshot.dshVersion},Android App 最低 ${MINIMUM_ANDROID_APP_VERSION}。`));
|
|
5519
|
+
if (snapshot.lan.networkError !== void 0) checks.push(check("network", "error", "network-unavailable", "局域网网卡", "已保存的网卡当前不可用。", "重新运行 dsh-mobile setup。"));
|
|
5520
|
+
else if (snapshot.lan.configuredInterface !== void 0) {
|
|
5521
|
+
const interfaceName = snapshot.lan.interfaceName ?? snapshot.lan.configuredInterface;
|
|
5522
|
+
checks.push(check("network", "ok", "network-interface", "局域网网卡", `正在跟随 ${interfaceName}。`, void 0, { interfaceName }));
|
|
5523
|
+
} else checks.push(check("network", "info", "network-fixed", "局域网网卡", "当前使用固定网络配置。"));
|
|
5524
|
+
if (snapshot.lan.running && snapshot.lan.origin !== void 0) {
|
|
5525
|
+
const endpointSuffix = maskLanOrigin(snapshot.lan.origin);
|
|
5526
|
+
checks.push(check("lan", "ok", "lan-ready", "局域网网关", `已监听 ${endpointSuffix},配对入口可用。`, void 0, { endpointSuffix }));
|
|
5527
|
+
} else checks.push(check("lan", "info", "lan-off", "局域网网关", "当前未开启。", "需要手机直连时开启局域网访问。"));
|
|
5528
|
+
if (firewall.state === "ready") checks.push(check("firewall", "ok", "firewall-ready", "Windows 防火墙", "局域网 TCP 与发现规则已启用。"));
|
|
5529
|
+
else if (firewall.state === "missing") checks.push(check("firewall", "warning", "firewall-missing", "Windows 防火墙", "未找到完整的局域网放行规则。", "以管理员身份重新运行 dsh-mobile setup。"));
|
|
5530
|
+
else if (firewall.state === "unknown") checks.push(check("firewall", "info", "firewall-unknown", "Windows 防火墙", "系统未允许插件读取防火墙状态。", "若手机找不到电脑,以管理员身份重新运行 setup。"));
|
|
5531
|
+
if (!snapshot.remote.running || snapshot.remote.state === "off") checks.push(check("remote", "info", "remote-off", "远程通道", "当前未启用。", void 0, { provider: snapshot.remote.provider }));
|
|
5532
|
+
else if (snapshot.remote.state === "ready" && snapshot.remote.origin !== void 0) {
|
|
5533
|
+
const endpointSuffix = remoteSuffix(snapshot.remote.origin);
|
|
5534
|
+
const facts = {
|
|
5535
|
+
provider: snapshot.remote.provider,
|
|
5536
|
+
endpointSuffix,
|
|
5537
|
+
...remoteObservation.latencyMs === void 0 ? {} : { latencyMs: remoteObservation.latencyMs }
|
|
5538
|
+
};
|
|
5539
|
+
if (remoteObservation.state === "ready") checks.push(check("remote", "ok", "remote-ready", "远程通道", `${snapshot.remote.provider} 公共地址 ${endpointSuffix} 可达,往返约 ${String(remoteObservation.latencyMs ?? 0)} ms。`, void 0, facts));
|
|
5540
|
+
else if (remoteObservation.state === "rate-limited") checks.push(check("remote", "warning", "remote-rate-limited", "远程通道", "公共地址可达,但本次检查观察到服务限流。", "稍后重试;旧会话会按需加载以减少流量。", facts));
|
|
5541
|
+
else if (snapshot.remote.provider === "tailscale" && remoteObservation.fakeIp === true) checks.push(check("remote", "error", "remote-fake-ip", "远程通道", "Tailscale 地址被当前 VPN 或 DNS 代理接管,但 TLS 链路未建立。", "切换 VPN 节点或代理模式;仍失败时改用 cpolar。", facts));
|
|
5542
|
+
else checks.push(check("remote", "error", "remote-unreachable", "远程通道", "提供方显示已就绪,但公共地址暂不可达。", "点击“重新连接”;仍失败时检查提供方状态。", facts));
|
|
5543
|
+
} else if (snapshot.remote.state === "starting" || snapshot.remote.state === "connecting" || snapshot.remote.state === "needs-login") {
|
|
5544
|
+
const needsLogin = snapshot.remote.state === "needs-login";
|
|
5545
|
+
checks.push(check("remote", "warning", needsLogin ? "remote-needs-login" : "remote-connecting", "远程通道", needsLogin ? "等待完成 Tailscale 登录。" : "仍在建立连接。", needsLogin ? "返回远程页继续登录。" : "等待片刻后重新检查。", { provider: snapshot.remote.provider }));
|
|
5546
|
+
} else {
|
|
5547
|
+
const controllerCode = snapshot.remote.errorCode ?? snapshot.remote.state;
|
|
5548
|
+
checks.push(check("remote", "error", "remote-controller-error", "远程通道", `连接未建立(${controllerCode})。`, REMOTE_ERROR_GUIDANCE[controllerCode] ?? "返回远程页点击“重新连接”。", {
|
|
5549
|
+
provider: snapshot.remote.provider,
|
|
5550
|
+
controllerCode
|
|
5551
|
+
}));
|
|
5552
|
+
}
|
|
5553
|
+
checks.push(check("phone-network", "info", "phone-network-unknown", "手机网络", "电脑无法判断路由器是否隔离了手机。", "局域网仍失败时,确认手机与电脑在同一网络,并关闭访客网络或 AP 隔离。"));
|
|
5554
|
+
const overall = checks.some((entry) => entry.status === "error") ? "error" : checks.some((entry) => entry.status === "warning") ? "attention" : "ok";
|
|
5555
|
+
const summary = overall === "ok" ? "连接基础检查正常。" : overall === "attention" ? "发现需要留意的项目。" : "发现会影响连接的问题。";
|
|
5556
|
+
const report = [
|
|
5557
|
+
"DSH Mobile 诊断报告",
|
|
5558
|
+
`生成时间: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
5559
|
+
`版本: plugin=${DSH_MOBILE_VERSION}; dsh=${snapshot.dshVersion}; min-app=${MINIMUM_ANDROID_APP_VERSION}`,
|
|
5560
|
+
`LAN: ${snapshot.lan.running ? "on" : "off"}; endpoint=${maskLanOrigin(snapshot.lan.origin)}`,
|
|
5561
|
+
`Remote: provider=${snapshot.remote.provider}; state=${snapshot.remote.state}; endpoint=${remoteSuffix(snapshot.remote.origin)}`,
|
|
5562
|
+
...checks.map(reportLine)
|
|
5563
|
+
].join("\n");
|
|
5564
|
+
return Object.freeze({
|
|
5565
|
+
version: 1,
|
|
5566
|
+
generatedAt: Date.now(),
|
|
5567
|
+
overall,
|
|
5568
|
+
versions: Object.freeze({
|
|
5569
|
+
plugin: DSH_MOBILE_VERSION,
|
|
5570
|
+
dsh: snapshot.dshVersion,
|
|
5571
|
+
minimumAndroidApp: MINIMUM_ANDROID_APP_VERSION
|
|
5572
|
+
}),
|
|
5573
|
+
summary,
|
|
5574
|
+
checks: Object.freeze(checks),
|
|
5575
|
+
report
|
|
5576
|
+
});
|
|
5577
|
+
}
|
|
5578
|
+
//#endregion
|
|
5579
|
+
//#region src/mobile-guide.ts
|
|
5580
|
+
/**
|
|
5581
|
+
* Instructions handed to the DSH agent when the user runs `/mobile <task>`.
|
|
5582
|
+
* The agent edits files under the DSH home; this text is what tells it the
|
|
5583
|
+
* layout of the mobile-access customization surface so it does not guess.
|
|
5584
|
+
*/
|
|
5585
|
+
const MOBILE_CUSTOMIZATION_GUIDE = `你在为用户定制 DSH Mobile 的手机端。DSH Mobile 是一个把电脑上的 DeepSeek Harness 带到手机浏览器的插件,手机端界面和能力都来自本机文件。
|
|
5586
|
+
|
|
5587
|
+
所有改动只允许在 $DSH_HOME/mobile-access/ 目录内进行,绝不修改 DeepSeek Harness 的源码或其他目录。$DSH_HOME 是 DeepSeek Harness 的配置目录(通常为 ~/.dsh),先确认它的实际路径再操作。
|
|
5588
|
+
|
|
5589
|
+
手机端的能力分两层,按用户需求选择改动目标:
|
|
5590
|
+
|
|
5591
|
+
1. 界面与交互 —— 只改外观和交互,不需要碰电脑的文件或程序:
|
|
5592
|
+
- $DSH_HOME/mobile-access/mobile.css:手机端样式
|
|
5593
|
+
- $DSH_HOME/mobile-access/mobile.js:手机端脚本,用 window.dshMobile.register(({ root }) => { ... }) 把内容挂载到 root,返回清理函数
|
|
5594
|
+
- 保存后手机端几秒内自动应用,无需重启
|
|
5595
|
+
|
|
5596
|
+
2. 电脑端能力 —— 手机需要读电脑文件、执行命令或访问硬件时,创建扩展:
|
|
5597
|
+
- 目录:$DSH_HOME/mobile-access/extensions/<id>/,id 用小写字母数字和连字符(如 media-remote)
|
|
5598
|
+
- extension.json:{"schemaVersion":1,"id":"<id>","name":"显示名","version":"0.1.0","description":"说明"}
|
|
5599
|
+
- host.mjs:电脑端 Node.js 代码(可信本地代码,可读写文件、执行命令)。导出默认函数 (api) => { ... },用 api.action('名称', { input, run }) 注册动作、api.route({ method, path, handle }) 注册路由、api.effect(fn) 注册清理
|
|
5600
|
+
- mobile.js:手机端脚本,用 window.dshMobile.define({ apiVersion:1, id:'<id>', activate(api) { ... } }),activate 返回清理函数
|
|
5601
|
+
- mobile.css:手机端样式(可选)
|
|
5602
|
+
- assets/:手机端静态资源(可选)
|
|
5603
|
+
- mobile.js 里用 api.host.invoke('动作名', 输入) 调 host.mjs 的 action,api.host.fetch('/路由路径') 调 route,api.host.assetUrl('相对路径') 生成与当前版本绑定的资源地址
|
|
5604
|
+
- 也可以先用命令生成模板:dsh plugin --profile web exec dsh-mobile extension create <id> --name "<名称>",再在模板上改
|
|
5605
|
+
|
|
5606
|
+
安全约束:
|
|
5607
|
+
- host.mjs 拥有电脑用户的完整权限,绝不能放入不可信代码,也不要让手机端无条件执行任意命令
|
|
5608
|
+
- 所有改动只限 $DSH_HOME/mobile-access/,不要动 DeepSeek Harness 源码
|
|
5609
|
+
|
|
5610
|
+
请执行用户需求:外观或交互类改 mobile.css / mobile.js;需要电脑能力的创建或修改扩展。完成后简要说明改了什么、手机端会有什么变化。`;
|
|
5611
|
+
//#endregion
|
|
5612
|
+
//#region src/funnel.ts
|
|
4401
5613
|
const MAX_PROTOCOL_LINE_BYTES = 16384;
|
|
4402
5614
|
const FUNNEL_START_TIMEOUT_MS = 45e3;
|
|
4403
5615
|
function publicStatus$1(status) {
|
|
@@ -4748,28 +5960,12 @@ var FunnelController = class {
|
|
|
4748
5960
|
this.clearStartTimer();
|
|
4749
5961
|
const child = this.child;
|
|
4750
5962
|
this.child = void 0;
|
|
4751
|
-
child?.stdin.end();
|
|
4752
|
-
if (child !== void 0 && child.exitCode === null) {
|
|
4753
|
-
child.kill("SIGTERM");
|
|
4754
|
-
await new Promise((resolveClose) => {
|
|
4755
|
-
let completed = false;
|
|
4756
|
-
const finish = () => {
|
|
4757
|
-
if (completed) return;
|
|
4758
|
-
completed = true;
|
|
4759
|
-
clearTimeout(timer);
|
|
4760
|
-
resolveClose();
|
|
4761
|
-
};
|
|
4762
|
-
const timer = setTimeout(() => {
|
|
4763
|
-
if (child.exitCode === null) child.kill("SIGKILL");
|
|
4764
|
-
finish();
|
|
4765
|
-
}, 1500);
|
|
4766
|
-
timer.unref();
|
|
4767
|
-
child.once("close", finish);
|
|
4768
|
-
});
|
|
4769
|
-
}
|
|
4770
5963
|
const gateway = this.gatewayValue;
|
|
4771
5964
|
this.gatewayValue = void 0;
|
|
4772
|
-
await
|
|
5965
|
+
await settleRemoteResources([async () => {
|
|
5966
|
+
child?.stdin.end();
|
|
5967
|
+
if (child !== void 0 && child.exitCode === null) await terminateRemoteProcess(child);
|
|
5968
|
+
}, () => gateway?.close()], "Funnel resource cleanup failed");
|
|
4773
5969
|
}
|
|
4774
5970
|
clearStartTimer() {
|
|
4775
5971
|
if (this.startTimer === void 0) return;
|
|
@@ -5140,30 +6336,15 @@ var CpolarController = class {
|
|
|
5140
6336
|
this.startupTimer = void 0;
|
|
5141
6337
|
const reservation = this.reservation;
|
|
5142
6338
|
this.reservation = void 0;
|
|
5143
|
-
await reservation?.release();
|
|
5144
6339
|
const child = this.child;
|
|
5145
6340
|
this.child = void 0;
|
|
5146
|
-
if (child !== void 0 && child.exitCode === null) {
|
|
5147
|
-
child.kill("SIGTERM");
|
|
5148
|
-
await new Promise((resolveClose) => {
|
|
5149
|
-
let completed = false;
|
|
5150
|
-
const finish = () => {
|
|
5151
|
-
if (completed) return;
|
|
5152
|
-
completed = true;
|
|
5153
|
-
clearTimeout(timer);
|
|
5154
|
-
resolveClose();
|
|
5155
|
-
};
|
|
5156
|
-
const timer = setTimeout(() => {
|
|
5157
|
-
if (child.exitCode === null) child.kill("SIGKILL");
|
|
5158
|
-
finish();
|
|
5159
|
-
}, 1500);
|
|
5160
|
-
timer.unref();
|
|
5161
|
-
child.once("close", finish);
|
|
5162
|
-
});
|
|
5163
|
-
}
|
|
5164
6341
|
const gateway = this.gatewayValue;
|
|
5165
6342
|
this.gatewayValue = void 0;
|
|
5166
|
-
await
|
|
6343
|
+
await settleRemoteResources([
|
|
6344
|
+
() => reservation?.release(),
|
|
6345
|
+
() => child !== void 0 && child.exitCode === null ? terminateRemoteProcess(child) : void 0,
|
|
6346
|
+
() => gateway?.close()
|
|
6347
|
+
], "cpolar resource cleanup failed");
|
|
5167
6348
|
}
|
|
5168
6349
|
};
|
|
5169
6350
|
//#endregion
|
|
@@ -5435,80 +6616,386 @@ var CpolarComponentManager = class {
|
|
|
5435
6616
|
}
|
|
5436
6617
|
};
|
|
5437
6618
|
//#endregion
|
|
5438
|
-
//#region src/
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
6619
|
+
//#region src/release-update.ts
|
|
6620
|
+
const PACKAGE_NAME = "dsh-mobile";
|
|
6621
|
+
const NPM_LATEST_URL = "https://registry.npmjs.org/dsh-mobile/latest";
|
|
6622
|
+
const GITHUB_LATEST_URL = "https://github.com/saya-ch/dsh-mobile/releases/latest";
|
|
6623
|
+
const GITHUB_RELEASES_URL = "https://github.com/saya-ch/dsh-mobile/releases";
|
|
6624
|
+
const STATUS_CACHE_MS = 6e5;
|
|
6625
|
+
const REQUEST_TIMEOUT_MS = 8e3;
|
|
6626
|
+
const UPDATE_TIMEOUT_MS = 12e4;
|
|
6627
|
+
const UPDATE_TERMINATION_GRACE_MS = 1500;
|
|
6628
|
+
const NUMERIC_VERSION_IDENTIFIER = "(?:0|[1-9]\\d*)";
|
|
6629
|
+
const WILDCARD_VERSION_IDENTIFIER = "(?:[xX*])";
|
|
6630
|
+
const RANGE_VERSION = `(?:${`${NUMERIC_VERSION_IDENTIFIER}\\.${NUMERIC_VERSION_IDENTIFIER}\\.${NUMERIC_VERSION_IDENTIFIER}(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?`}|${`(?:${WILDCARD_VERSION_IDENTIFIER}|${NUMERIC_VERSION_IDENTIFIER}(?:\\.(?:${WILDCARD_VERSION_IDENTIFIER}|${NUMERIC_VERSION_IDENTIFIER}(?:\\.(?:${WILDCARD_VERSION_IDENTIFIER}|${NUMERIC_VERSION_IDENTIFIER}))?))?)`})`;
|
|
6631
|
+
const COMPARATOR = new RegExp(`^(?:<=|>=|<|>|=|~|\\^)?${RANGE_VERSION}$`, "u");
|
|
6632
|
+
const HYPHEN_RANGE = new RegExp(`^${RANGE_VERSION} +[-] +${RANGE_VERSION}$`, "u");
|
|
6633
|
+
const DIST_TAG = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/u;
|
|
6634
|
+
function parseSemver(value) {
|
|
6635
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
|
|
6636
|
+
if (match === null) return void 0;
|
|
6637
|
+
const core = [
|
|
6638
|
+
Number(match[1]),
|
|
6639
|
+
Number(match[2]),
|
|
6640
|
+
Number(match[3])
|
|
6641
|
+
];
|
|
6642
|
+
if (core.some((part) => !Number.isSafeInteger(part))) return void 0;
|
|
6643
|
+
const prerelease = match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/u.test(part) ? Number(part) : part);
|
|
6644
|
+
if (prerelease.some((part) => typeof part === "number" && !Number.isSafeInteger(part))) return void 0;
|
|
5444
6645
|
return Object.freeze({
|
|
5445
|
-
|
|
5446
|
-
|
|
6646
|
+
core,
|
|
6647
|
+
prerelease: Object.freeze(prerelease)
|
|
5447
6648
|
});
|
|
5448
6649
|
}
|
|
5449
|
-
/**
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
6650
|
+
/** Compare two strict SemVer strings, including prerelease precedence. */
|
|
6651
|
+
function comparePluginVersions(left, right) {
|
|
6652
|
+
const a = parseSemver(left);
|
|
6653
|
+
const b = parseSemver(right);
|
|
6654
|
+
if (a === void 0 || b === void 0) return void 0;
|
|
6655
|
+
for (let index = 0; index < a.core.length; index += 1) {
|
|
6656
|
+
const difference = a.core[index] - b.core[index];
|
|
6657
|
+
if (difference !== 0) return Math.sign(difference);
|
|
6658
|
+
}
|
|
6659
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
6660
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
6661
|
+
for (let index = 0; index < length; index += 1) {
|
|
6662
|
+
const leftPart = a.prerelease[index];
|
|
6663
|
+
const rightPart = b.prerelease[index];
|
|
6664
|
+
if (leftPart === void 0 || rightPart === void 0) return leftPart === void 0 ? -1 : 1;
|
|
6665
|
+
if (leftPart === rightPart) continue;
|
|
6666
|
+
if (typeof leftPart === "number" && typeof rightPart === "number") return Math.sign(leftPart - rightPart);
|
|
6667
|
+
if (typeof leftPart === "number") return -1;
|
|
6668
|
+
if (typeof rightPart === "number") return 1;
|
|
6669
|
+
return leftPart < rightPart ? -1 : 1;
|
|
6670
|
+
}
|
|
6671
|
+
return 0;
|
|
6672
|
+
}
|
|
6673
|
+
function isComparatorSet(value) {
|
|
6674
|
+
if (HYPHEN_RANGE.test(value)) return true;
|
|
6675
|
+
const comparators = value.replace(/(<=|>=|<|>|=|~|\^) +/gu, "$1").split(/ +/u);
|
|
6676
|
+
return comparators.length > 0 && comparators.every((comparator) => COMPARATOR.test(comparator));
|
|
6677
|
+
}
|
|
6678
|
+
function isNpmVersionRange(value) {
|
|
6679
|
+
if (!/^[0-9xX*<>=~^|.+\- ]+$/u.test(value)) return false;
|
|
6680
|
+
const alternatives = value.split(/ *\|\| */u);
|
|
6681
|
+
return alternatives.length > 0 && alternatives.every((alternative) => alternative !== "" && isComparatorSet(alternative));
|
|
6682
|
+
}
|
|
6683
|
+
/** Return whether pnpm may safely replace this profile dependency from an npm version, range, or tag. */
|
|
6684
|
+
function isRegistryPluginSpec(value) {
|
|
6685
|
+
if (typeof value !== "string" || value.trim() !== value || value === "" || /[\u0000-\u001f\u007f]/u.test(value)) return false;
|
|
6686
|
+
if (/\.(?:tgz|tar(?:\.gz)?)$/iu.test(value)) return false;
|
|
6687
|
+
return parseSemver(value) !== void 0 || isNpmVersionRange(value) || DIST_TAG.test(value);
|
|
6688
|
+
}
|
|
6689
|
+
/** Resolve the DSH profile named by the current launcher arguments. */
|
|
6690
|
+
function launchedProfileName(argv) {
|
|
6691
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
6692
|
+
if (argv[index] === "--profile") {
|
|
6693
|
+
const candidate = argv[index + 1];
|
|
6694
|
+
if (candidate !== void 0 && /^[\w.-]+$/u.test(candidate)) return candidate;
|
|
6695
|
+
}
|
|
6696
|
+
const match = /^--profile=([\w.-]+)$/u.exec(argv[index] ?? "");
|
|
6697
|
+
if (match?.[1] !== void 0) return match[1];
|
|
5456
6698
|
}
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
6699
|
+
return "web";
|
|
6700
|
+
}
|
|
6701
|
+
/** Resolve the launcher-owned Desktop profile, or the CLI profile outside Desktop. */
|
|
6702
|
+
function releaseProfileDirectory(ctx, dshHome, argv) {
|
|
6703
|
+
const desktopProfiles = ctx.get("desktopProfiles");
|
|
6704
|
+
const desktopDirectory = desktopProfiles?.current?.dir;
|
|
6705
|
+
if (typeof desktopDirectory === "string" && isAbsolute(desktopDirectory)) return desktopDirectory;
|
|
6706
|
+
if (desktopProfiles !== void 0 || ctx.get("desktopRuntime") !== void 0) return void 0;
|
|
6707
|
+
return join(dshHome, "profiles", launchedProfileName(argv));
|
|
6708
|
+
}
|
|
6709
|
+
async function profileDependencySpec(profileDirectory) {
|
|
6710
|
+
try {
|
|
6711
|
+
const value = JSON.parse(await readFile(join(profileDirectory, "package.json"), "utf8")).dependencies?.[PACKAGE_NAME];
|
|
6712
|
+
return typeof value === "string" ? value : void 0;
|
|
6713
|
+
} catch {
|
|
6714
|
+
return;
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
async function fetchNpmVersion(fetcher) {
|
|
6718
|
+
const response = await fetcher(NPM_LATEST_URL, {
|
|
6719
|
+
headers: {
|
|
6720
|
+
accept: "application/json",
|
|
6721
|
+
"user-agent": "dsh-mobile-release-check"
|
|
6722
|
+
},
|
|
6723
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
6724
|
+
});
|
|
6725
|
+
if (!response.ok) return void 0;
|
|
6726
|
+
const payload = await response.json();
|
|
6727
|
+
return typeof payload.version === "string" && parseSemver(payload.version) !== void 0 ? payload.version : void 0;
|
|
6728
|
+
}
|
|
6729
|
+
function githubReleaseVersion(location, responseUrl) {
|
|
6730
|
+
let url;
|
|
6731
|
+
try {
|
|
6732
|
+
url = new URL(location ?? responseUrl, GITHUB_LATEST_URL);
|
|
6733
|
+
} catch {
|
|
6734
|
+
return;
|
|
6735
|
+
}
|
|
6736
|
+
if (url.origin !== "https://github.com" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") return void 0;
|
|
6737
|
+
if (!url.pathname.startsWith("/saya-ch/dsh-mobile/releases/tag/v")) return void 0;
|
|
6738
|
+
let version;
|
|
6739
|
+
try {
|
|
6740
|
+
version = decodeURIComponent(url.pathname.slice(34));
|
|
6741
|
+
} catch {
|
|
6742
|
+
return;
|
|
6743
|
+
}
|
|
6744
|
+
return parseSemver(version) === void 0 ? void 0 : version;
|
|
6745
|
+
}
|
|
6746
|
+
function androidReleaseDownloadUrl(version) {
|
|
6747
|
+
if (version === void 0) return GITHUB_RELEASES_URL;
|
|
6748
|
+
const tag = `v${version}`;
|
|
6749
|
+
return `https://github.com/saya-ch/dsh-mobile/releases/download/${encodeURIComponent(tag)}/dsh-mobile-android-${encodeURIComponent(tag)}.apk`;
|
|
6750
|
+
}
|
|
6751
|
+
async function fetchAndroidVersion(fetcher) {
|
|
6752
|
+
const response = await fetcher(GITHUB_LATEST_URL, {
|
|
6753
|
+
method: "GET",
|
|
6754
|
+
redirect: "manual",
|
|
6755
|
+
headers: {
|
|
6756
|
+
accept: "text/html",
|
|
6757
|
+
"user-agent": "dsh-mobile-release-check"
|
|
6758
|
+
},
|
|
6759
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
6760
|
+
});
|
|
6761
|
+
return githubReleaseVersion(response.headers.get("location"), response.url);
|
|
6762
|
+
}
|
|
6763
|
+
async function readProfileInstalledVersion(profileDirectory) {
|
|
6764
|
+
try {
|
|
6765
|
+
const manifestPath = createRequire(join(profileDirectory, "package.json")).resolve(`${PACKAGE_NAME}/package.json`);
|
|
6766
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
6767
|
+
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
6768
|
+
} catch {
|
|
6769
|
+
return;
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
function childCompletion(child) {
|
|
6773
|
+
return new Promise((resolveCompletion, rejectCompletion) => {
|
|
6774
|
+
child.once("error", rejectCompletion);
|
|
6775
|
+
child.once("close", (code, signal) => {
|
|
6776
|
+
resolveCompletion({
|
|
6777
|
+
code,
|
|
6778
|
+
signal
|
|
5465
6779
|
});
|
|
5466
|
-
|
|
6780
|
+
});
|
|
6781
|
+
});
|
|
6782
|
+
}
|
|
6783
|
+
function createDeadline(timeoutMs) {
|
|
6784
|
+
let timer;
|
|
6785
|
+
return {
|
|
6786
|
+
promise: new Promise((resolveTimeout) => {
|
|
6787
|
+
timer = setTimeout(resolveTimeout, timeoutMs);
|
|
6788
|
+
timer.unref();
|
|
6789
|
+
}),
|
|
6790
|
+
cancel: () => {
|
|
6791
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
6792
|
+
timer = void 0;
|
|
5467
6793
|
}
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
6794
|
+
};
|
|
6795
|
+
}
|
|
6796
|
+
async function taskkillProcessTree(pid) {
|
|
6797
|
+
if ((await childCompletion(spawn("taskkill.exe", [
|
|
6798
|
+
"/PID",
|
|
6799
|
+
String(pid),
|
|
6800
|
+
"/T",
|
|
6801
|
+
"/F"
|
|
6802
|
+
], {
|
|
6803
|
+
shell: false,
|
|
6804
|
+
windowsHide: true,
|
|
6805
|
+
stdio: "ignore"
|
|
6806
|
+
}))).code !== 0) throw new Error("plugin_update_tree_termination_failed");
|
|
6807
|
+
}
|
|
6808
|
+
async function completionWithin(completion, timeoutMs) {
|
|
6809
|
+
let timer;
|
|
6810
|
+
try {
|
|
6811
|
+
return await Promise.race([completion.then(() => true, () => true), new Promise((resolveTimeout) => {
|
|
6812
|
+
timer = setTimeout(() => {
|
|
6813
|
+
resolveTimeout(false);
|
|
6814
|
+
}, timeoutMs);
|
|
6815
|
+
timer.unref();
|
|
6816
|
+
})]);
|
|
6817
|
+
} finally {
|
|
6818
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6821
|
+
function processMissing(error) {
|
|
6822
|
+
return error.code === "ESRCH";
|
|
6823
|
+
}
|
|
6824
|
+
async function terminateProcessTree(child, completion, platform) {
|
|
6825
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
6826
|
+
const pid = child.pid;
|
|
6827
|
+
if (pid === void 0) {
|
|
6828
|
+
child.kill("SIGKILL");
|
|
6829
|
+
if (!await completionWithin(completion, UPDATE_TERMINATION_GRACE_MS)) throw new Error("plugin_update_tree_termination_timeout");
|
|
6830
|
+
return;
|
|
6831
|
+
}
|
|
6832
|
+
if (platform === "win32") {
|
|
5471
6833
|
try {
|
|
5472
|
-
|
|
6834
|
+
await taskkillProcessTree(pid);
|
|
5473
6835
|
} catch (error) {
|
|
5474
|
-
|
|
6836
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
6837
|
+
if (!await completionWithin(completion, UPDATE_TERMINATION_GRACE_MS)) throw new AggregateError([error, /* @__PURE__ */ new Error("plugin_update_tree_termination_timeout")], "plugin update tree termination failed");
|
|
6838
|
+
throw error;
|
|
5475
6839
|
}
|
|
5476
|
-
|
|
6840
|
+
if (!await completionWithin(completion, UPDATE_TERMINATION_GRACE_MS)) {
|
|
6841
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
6842
|
+
if (!await completionWithin(completion, UPDATE_TERMINATION_GRACE_MS)) throw new Error("plugin_update_tree_termination_timeout");
|
|
6843
|
+
}
|
|
6844
|
+
return;
|
|
5477
6845
|
}
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
6846
|
+
try {
|
|
6847
|
+
process.kill(-pid, "SIGTERM");
|
|
6848
|
+
} catch (error) {
|
|
6849
|
+
if (!processMissing(error)) throw error;
|
|
6850
|
+
if (!await completionWithin(completion, UPDATE_TERMINATION_GRACE_MS)) throw new Error("plugin_update_tree_termination_timeout");
|
|
6851
|
+
return;
|
|
6852
|
+
}
|
|
6853
|
+
if (await completionWithin(completion, UPDATE_TERMINATION_GRACE_MS)) return;
|
|
6854
|
+
try {
|
|
6855
|
+
process.kill(-pid, "SIGKILL");
|
|
6856
|
+
} catch (error) {
|
|
6857
|
+
if (!processMissing(error)) throw error;
|
|
6858
|
+
}
|
|
6859
|
+
if (!await completionWithin(completion, UPDATE_TERMINATION_GRACE_MS)) throw new Error("plugin_update_tree_termination_timeout");
|
|
6860
|
+
}
|
|
6861
|
+
function startUpdateProcess(request) {
|
|
6862
|
+
const child = spawn(request.command, [...request.args], {
|
|
6863
|
+
cwd: request.cwd,
|
|
6864
|
+
detached: request.detached,
|
|
6865
|
+
shell: request.shell,
|
|
6866
|
+
windowsHide: true,
|
|
6867
|
+
stdio: [
|
|
6868
|
+
"ignore",
|
|
6869
|
+
"ignore",
|
|
6870
|
+
"pipe"
|
|
6871
|
+
]
|
|
6872
|
+
});
|
|
6873
|
+
const completion = childCompletion(child);
|
|
6874
|
+
return {
|
|
6875
|
+
completion,
|
|
6876
|
+
...child.stderr === null ? {} : { stderr: child.stderr },
|
|
6877
|
+
terminateTree: async () => terminateProcessTree(child, completion, request.platform)
|
|
6878
|
+
};
|
|
6879
|
+
}
|
|
6880
|
+
function updateFailure(cause) {
|
|
6881
|
+
return cause === void 0 ? /* @__PURE__ */ new Error("plugin_update_failed") : new Error("plugin_update_failed", { cause });
|
|
6882
|
+
}
|
|
6883
|
+
async function runPnpmUpdate(profileDirectory, version, runtime = {}) {
|
|
6884
|
+
if (parseSemver(version) === void 0) throw new Error("plugin_update_unavailable");
|
|
6885
|
+
const platform = runtime.platform ?? process.platform;
|
|
6886
|
+
const packageSpec = `${PACKAGE_NAME}@${version}`;
|
|
6887
|
+
const managed = (runtime.start ?? startUpdateProcess)({
|
|
6888
|
+
command: platform === "win32" ? runtime.windowsCommandInterpreter ?? process.env.ComSpec ?? "cmd.exe" : "pnpm",
|
|
6889
|
+
args: platform === "win32" ? [
|
|
6890
|
+
"/d",
|
|
6891
|
+
"/s",
|
|
6892
|
+
"/c",
|
|
6893
|
+
"pnpm.cmd",
|
|
6894
|
+
"add",
|
|
6895
|
+
packageSpec
|
|
6896
|
+
] : ["add", packageSpec],
|
|
6897
|
+
cwd: profileDirectory,
|
|
6898
|
+
detached: platform !== "win32",
|
|
6899
|
+
platform,
|
|
6900
|
+
shell: false
|
|
6901
|
+
});
|
|
6902
|
+
let diagnostics = "";
|
|
6903
|
+
managed.stderr?.on("data", (chunk) => {
|
|
6904
|
+
if (diagnostics.length < 4096) diagnostics += Buffer.from(chunk).toString("utf8").slice(0, 4096 - diagnostics.length);
|
|
6905
|
+
});
|
|
6906
|
+
const completion = managed.completion.then((result) => ({
|
|
6907
|
+
kind: "exit",
|
|
6908
|
+
result
|
|
6909
|
+
}), (error) => ({
|
|
6910
|
+
kind: "error",
|
|
6911
|
+
error
|
|
6912
|
+
}));
|
|
6913
|
+
const deadline = (runtime.deadline ?? createDeadline)(runtime.timeoutMs ?? UPDATE_TIMEOUT_MS);
|
|
6914
|
+
const first = await Promise.race([completion, deadline.promise.then(() => ({ kind: "timeout" }))]);
|
|
6915
|
+
deadline.cancel();
|
|
6916
|
+
if (first.kind === "error") throw updateFailure(first.error);
|
|
6917
|
+
if (first.kind === "exit") {
|
|
6918
|
+
if (first.result.code === 0) return;
|
|
6919
|
+
const detail = diagnostics.trim() || `pnpm exited with ${first.result.signal ?? String(first.result.code)}`;
|
|
6920
|
+
throw updateFailure(new Error(detail));
|
|
6921
|
+
}
|
|
6922
|
+
let terminationError;
|
|
6923
|
+
try {
|
|
6924
|
+
await managed.terminateTree();
|
|
6925
|
+
} catch (error) {
|
|
6926
|
+
terminationError = error;
|
|
6927
|
+
}
|
|
6928
|
+
if (terminationError !== void 0) throw updateFailure(terminationError);
|
|
6929
|
+
const stopped = await completion;
|
|
6930
|
+
if (stopped.kind === "error") throw updateFailure(stopped.error);
|
|
6931
|
+
throw updateFailure(/* @__PURE__ */ new Error("plugin update timed out"));
|
|
6932
|
+
}
|
|
6933
|
+
/** Cached npm/GitHub release lookup and guarded profile-local package update. */
|
|
6934
|
+
var PluginReleaseManager = class {
|
|
6935
|
+
profileDirectory;
|
|
6936
|
+
installedVersion;
|
|
6937
|
+
fetcher;
|
|
6938
|
+
runner;
|
|
6939
|
+
installedVersionReader;
|
|
6940
|
+
now;
|
|
6941
|
+
cache;
|
|
6942
|
+
activeUpdate;
|
|
6943
|
+
constructor(options) {
|
|
6944
|
+
this.profileDirectory = options.profileDirectory;
|
|
6945
|
+
this.installedVersion = options.installedVersion ?? DSH_MOBILE_VERSION;
|
|
6946
|
+
this.fetcher = options.fetch ?? globalThis.fetch;
|
|
6947
|
+
this.runner = options.runUpdate ?? ((profileDirectory, version) => runPnpmUpdate(profileDirectory, version, options.updateProcess));
|
|
6948
|
+
this.installedVersionReader = options.readInstalledVersion ?? readProfileInstalledVersion;
|
|
6949
|
+
this.now = options.now ?? Date.now;
|
|
6950
|
+
}
|
|
6951
|
+
/** Read cached release metadata and suppress external lookup failures. */
|
|
6952
|
+
async status(force = false) {
|
|
6953
|
+
if (!force && this.cache !== void 0 && this.cache.expiresAt > this.now()) return this.cache.status;
|
|
6954
|
+
const updateSupported = isRegistryPluginSpec(this.profileDirectory === void 0 ? void 0 : await profileDependencySpec(this.profileDirectory));
|
|
6955
|
+
const [npmResult, androidResult] = await Promise.allSettled([fetchNpmVersion(this.fetcher), fetchAndroidVersion(this.fetcher)]);
|
|
6956
|
+
const latestVersion = npmResult.status === "fulfilled" ? npmResult.value : void 0;
|
|
6957
|
+
const androidVersion = androidResult.status === "fulfilled" ? androidResult.value : void 0;
|
|
6958
|
+
const comparison = latestVersion === void 0 ? void 0 : comparePluginVersions(latestVersion, this.installedVersion);
|
|
6959
|
+
const status = Object.freeze({
|
|
6960
|
+
installedVersion: this.installedVersion,
|
|
6961
|
+
...latestVersion === void 0 ? {} : { latestVersion },
|
|
6962
|
+
updateAvailable: updateSupported && comparison === 1,
|
|
6963
|
+
updateSupported,
|
|
6964
|
+
...androidVersion === void 0 ? {} : { androidVersion },
|
|
6965
|
+
androidDownloadUrl: androidReleaseDownloadUrl(androidVersion)
|
|
5484
6966
|
});
|
|
6967
|
+
this.cache = {
|
|
6968
|
+
expiresAt: this.now() + STATUS_CACHE_MS,
|
|
6969
|
+
status
|
|
6970
|
+
};
|
|
6971
|
+
return status;
|
|
6972
|
+
}
|
|
6973
|
+
/** Install the latest npm release into the active profile, then require a DSH restart. */
|
|
6974
|
+
async update() {
|
|
6975
|
+
if (this.activeUpdate !== void 0) return this.activeUpdate;
|
|
6976
|
+
this.activeUpdate = this.updateOnce();
|
|
5485
6977
|
try {
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
if (error.code !== "ENOENT") throw error;
|
|
5490
|
-
}
|
|
5491
|
-
const temporary = join(directory, `.${basename(this.file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
5492
|
-
try {
|
|
5493
|
-
await writeFile(temporary, `${JSON.stringify(validated)}\n`, {
|
|
5494
|
-
encoding: "utf8",
|
|
5495
|
-
flag: "wx",
|
|
5496
|
-
mode: 384
|
|
5497
|
-
});
|
|
5498
|
-
await rename(temporary, this.file);
|
|
5499
|
-
await restrictPrivateFile(this.file);
|
|
5500
|
-
} catch (error) {
|
|
5501
|
-
await rm(temporary, { force: true });
|
|
5502
|
-
throw error;
|
|
6978
|
+
return await this.activeUpdate;
|
|
6979
|
+
} finally {
|
|
6980
|
+
this.activeUpdate = void 0;
|
|
5503
6981
|
}
|
|
5504
6982
|
}
|
|
6983
|
+
async updateOnce() {
|
|
6984
|
+
const profileDirectory = this.profileDirectory;
|
|
6985
|
+
if (profileDirectory === void 0) throw new Error("plugin_update_unsupported");
|
|
6986
|
+
const status = await this.status(true);
|
|
6987
|
+
if (!status.updateSupported) throw new Error("plugin_update_unsupported");
|
|
6988
|
+
if (!status.updateAvailable || status.latestVersion === void 0) throw new Error("plugin_update_unavailable");
|
|
6989
|
+
await this.runner(profileDirectory, status.latestVersion);
|
|
6990
|
+
const installed = await this.installedVersionReader(profileDirectory);
|
|
6991
|
+
if (installed !== status.latestVersion) throw new Error("plugin_update_failed");
|
|
6992
|
+
this.cache = void 0;
|
|
6993
|
+
return Object.freeze({
|
|
6994
|
+
installedVersion: installed,
|
|
6995
|
+
restartRequired: true
|
|
6996
|
+
});
|
|
6997
|
+
}
|
|
5505
6998
|
};
|
|
5506
|
-
/** Resolve the first-run provider without letting environment values bypass validation. */
|
|
5507
|
-
function configuredRemoteProvider(environment) {
|
|
5508
|
-
const value = environment.DSH_MOBILE_REMOTE_PROVIDER ?? "tailscale";
|
|
5509
|
-
if (value !== "tailscale" && value !== "cpolar") throw new Error("DSH_MOBILE_REMOTE_PROVIDER must be tailscale or cpolar");
|
|
5510
|
-
return value;
|
|
5511
|
-
}
|
|
5512
6999
|
promisify(execFile);
|
|
5513
7000
|
const VIRTUAL_INTERFACE_MARKERS = [
|
|
5514
7001
|
"bridge",
|
|
@@ -5721,6 +7208,17 @@ const inject = [
|
|
|
5721
7208
|
"commands",
|
|
5722
7209
|
"connection"
|
|
5723
7210
|
];
|
|
7211
|
+
/** Run cleanup steps in ownership order and report every failure after all steps settle. */
|
|
7212
|
+
async function settleCleanupSteps(steps) {
|
|
7213
|
+
const errors = [];
|
|
7214
|
+
for (const step of steps) try {
|
|
7215
|
+
await step();
|
|
7216
|
+
} catch (error) {
|
|
7217
|
+
errors.push(error);
|
|
7218
|
+
}
|
|
7219
|
+
if (errors.length === 1 && errors[0] instanceof Error) throw errors[0];
|
|
7220
|
+
if (errors.length > 0) throw new AggregateError(errors, "DSH Mobile cleanup failed");
|
|
7221
|
+
}
|
|
5724
7222
|
function upstreamAuthenticatedUrl(ctx, upstreamOrigin) {
|
|
5725
7223
|
const connection = ctx.connection;
|
|
5726
7224
|
return typeof connection?.authenticatedUrl === "function" ? connection.authenticatedUrl(upstreamOrigin.origin) : void 0;
|
|
@@ -5738,6 +7236,16 @@ function mapAdminError(error) {
|
|
|
5738
7236
|
if (error instanceof Error && error.message.startsWith("saved LAN interface ")) return new HttpError(409, "network_interface_unavailable");
|
|
5739
7237
|
if (error instanceof Error && error.message === "cpolar_authtoken_invalid") return new HttpError(400, "cpolar_authtoken_invalid");
|
|
5740
7238
|
if (error instanceof Error && error.message.startsWith("cpolar_")) return new HttpError(409, error.message);
|
|
7239
|
+
if (error instanceof Error && [
|
|
7240
|
+
"frp_server_address_invalid",
|
|
7241
|
+
"frp_server_port_invalid",
|
|
7242
|
+
"frp_token_invalid",
|
|
7243
|
+
"frp_public_origin_invalid",
|
|
7244
|
+
"frp_settings_invalid"
|
|
7245
|
+
].includes(error.message)) return new HttpError(400, error.message);
|
|
7246
|
+
if (error instanceof Error && error.message.startsWith("frp_")) return new HttpError(409, error.message);
|
|
7247
|
+
if (error instanceof Error && error.message === "plugin_update_failed") return new HttpError(500, error.message);
|
|
7248
|
+
if (error instanceof Error && error.message.startsWith("plugin_update_")) return new HttpError(409, error.message);
|
|
5741
7249
|
return new HttpError(500, "internal_error");
|
|
5742
7250
|
}
|
|
5743
7251
|
const SETUP_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -5829,7 +7337,7 @@ function remoteGatewayConfig(template, publicOrigin, stateFile, instanceId, list
|
|
|
5829
7337
|
discovery: false
|
|
5830
7338
|
});
|
|
5831
7339
|
}
|
|
5832
|
-
function remoteControlPayload(provider, status, gateway, providerStatuses, cpolarComponent) {
|
|
7340
|
+
function remoteControlPayload(provider, status, gateway, providerStatuses, cpolarComponent, frpComponent, frpConfiguration) {
|
|
5833
7341
|
return {
|
|
5834
7342
|
provider,
|
|
5835
7343
|
running: status.enabled,
|
|
@@ -5850,6 +7358,13 @@ function remoteControlPayload(provider, status, gateway, providerStatuses, cpola
|
|
|
5850
7358
|
running: providerStatuses.cpolar.enabled,
|
|
5851
7359
|
state: providerStatuses.cpolar.state,
|
|
5852
7360
|
component: cpolarComponent
|
|
7361
|
+
},
|
|
7362
|
+
frp: {
|
|
7363
|
+
bundled: false,
|
|
7364
|
+
running: providerStatuses.frp.enabled,
|
|
7365
|
+
state: providerStatuses.frp.state,
|
|
7366
|
+
component: frpComponent,
|
|
7367
|
+
configuration: frpConfiguration
|
|
5853
7368
|
}
|
|
5854
7369
|
}
|
|
5855
7370
|
};
|
|
@@ -5865,10 +7380,16 @@ async function apply(ctx, config) {
|
|
|
5865
7380
|
const instanceId = await stableInstanceId(loaded, template);
|
|
5866
7381
|
const stateDirectory = dirname(template.stateFile);
|
|
5867
7382
|
const remoteDirectory = join(stateDirectory, "remote");
|
|
7383
|
+
const configuredDshHome = process.env.DSH_HOME?.trim();
|
|
7384
|
+
const releaseManager = new PluginReleaseManager({ profileDirectory: releaseProfileDirectory(ctx, configuredDshHome === void 0 || configuredDshHome === "" ? dirname(stateDirectory) : resolve(configuredDshHome), process.argv.slice(2)) });
|
|
5868
7385
|
const remoteProviderStore = new JsonRemoteProviderStore(join(remoteDirectory, "provider.json"), configuredRemoteProvider(process.env));
|
|
5869
|
-
|
|
7386
|
+
const initialRemoteProvider = (await remoteProviderStore.load()).provider;
|
|
5870
7387
|
const cpolarComponent = new CpolarComponentManager({ stateDirectory });
|
|
5871
7388
|
await cpolarComponent.initialize();
|
|
7389
|
+
const frpComponent = new FrpComponentManager({ stateDirectory });
|
|
7390
|
+
await frpComponent.initialize();
|
|
7391
|
+
const frpConfig = new FrpConfigStore(join(remoteDirectory, "frp", "config"));
|
|
7392
|
+
await frpConfig.initialize();
|
|
5872
7393
|
const unregisterBuiltin = mobileAccess.registerExtension({
|
|
5873
7394
|
schemaVersion: 1,
|
|
5874
7395
|
id: "computer-images",
|
|
@@ -5930,7 +7451,7 @@ async function apply(ctx, config) {
|
|
|
5930
7451
|
const lanController = new MobileAccessGatewayController(new JsonMobileAccessControlStore(parseControlFile(config.controlFile), config.initiallyEnabled), startRuntime);
|
|
5931
7452
|
const remoteDeviceFile = join(remoteDirectory, "devices.json");
|
|
5932
7453
|
const legacyCpolarDeviceFile = join(remoteDirectory, "cpolar", "devices.json");
|
|
5933
|
-
if (
|
|
7454
|
+
if (initialRemoteProvider === "cpolar") try {
|
|
5934
7455
|
await lstat(remoteDeviceFile);
|
|
5935
7456
|
} catch (error) {
|
|
5936
7457
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -5948,6 +7469,7 @@ async function apply(ctx, config) {
|
|
|
5948
7469
|
};
|
|
5949
7470
|
const tailscaleStore = new JsonMobileAccessControlStore(join(remoteDirectory, "control.json"), false);
|
|
5950
7471
|
const cpolarStore = new JsonMobileAccessControlStore(join(remoteDirectory, "cpolar", "control.json"), false);
|
|
7472
|
+
const frpStore = new JsonMobileAccessControlStore(join(remoteDirectory, "frp", "control.json"), false);
|
|
5951
7473
|
const remoteControllers = {
|
|
5952
7474
|
tailscale: new FunnelController({
|
|
5953
7475
|
store: tailscaleStore,
|
|
@@ -5962,29 +7484,22 @@ async function apply(ctx, config) {
|
|
|
5962
7484
|
configFile: cpolarComponent.configFile,
|
|
5963
7485
|
region: "cn",
|
|
5964
7486
|
createGateway: createRemoteGateway
|
|
7487
|
+
}),
|
|
7488
|
+
frp: new FrpController({
|
|
7489
|
+
store: frpStore,
|
|
7490
|
+
executable: frpComponent.executable,
|
|
7491
|
+
config: frpConfig,
|
|
7492
|
+
instanceId,
|
|
7493
|
+
createGateway: createRemoteGateway
|
|
5965
7494
|
})
|
|
5966
7495
|
};
|
|
5967
|
-
const
|
|
5968
|
-
const
|
|
7496
|
+
const remoteProviders = new RemoteProviderCoordinator(initialRemoteProvider, remoteControllers, remoteProviderStore);
|
|
7497
|
+
const remoteController = () => remoteProviders.controller();
|
|
7498
|
+
const remotePayload = () => remoteControlPayload(remoteProviders.selected, remoteController().status(), remoteController().gateway(), {
|
|
5969
7499
|
tailscale: remoteControllers.tailscale.status(),
|
|
5970
|
-
cpolar: remoteControllers.cpolar.status()
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
if (provider === remoteProvider) return;
|
|
5974
|
-
const previous = remoteControllers[remoteProvider];
|
|
5975
|
-
const restore = previous.status().enabled;
|
|
5976
|
-
if (restore) await previous.setEnabled(false);
|
|
5977
|
-
try {
|
|
5978
|
-
await remoteProviderStore.save({
|
|
5979
|
-
version: 1,
|
|
5980
|
-
provider
|
|
5981
|
-
});
|
|
5982
|
-
remoteProvider = provider;
|
|
5983
|
-
} catch (error) {
|
|
5984
|
-
if (restore) await previous.setEnabled(true);
|
|
5985
|
-
throw error;
|
|
5986
|
-
}
|
|
5987
|
-
};
|
|
7500
|
+
cpolar: remoteControllers.cpolar.status(),
|
|
7501
|
+
frp: remoteControllers.frp.status()
|
|
7502
|
+
}, cpolarComponent.status(), frpComponent.status(), frpConfig.status());
|
|
5988
7503
|
const lanPayload = () => ({
|
|
5989
7504
|
running: lanController.isRunning(),
|
|
5990
7505
|
origin: lanGateway?.address().origin,
|
|
@@ -6015,7 +7530,7 @@ async function apply(ctx, config) {
|
|
|
6015
7530
|
...networkError === void 0 ? {} : { networkError }
|
|
6016
7531
|
},
|
|
6017
7532
|
remote: {
|
|
6018
|
-
provider:
|
|
7533
|
+
provider: remoteProviders.selected,
|
|
6019
7534
|
running: remote.enabled,
|
|
6020
7535
|
state: remote.state,
|
|
6021
7536
|
...remote.origin === void 0 ? {} : { origin: remote.origin },
|
|
@@ -6040,6 +7555,15 @@ async function apply(ctx, config) {
|
|
|
6040
7555
|
sendJson(response, 200, await diagnosticsPayload(), false);
|
|
6041
7556
|
return;
|
|
6042
7557
|
}
|
|
7558
|
+
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/release`) {
|
|
7559
|
+
sendJson(response, 200, await releaseManager.status(), false);
|
|
7560
|
+
return;
|
|
7561
|
+
}
|
|
7562
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/release/update`) {
|
|
7563
|
+
await readJsonObject(request, 4096);
|
|
7564
|
+
sendJson(response, 200, await releaseManager.update(), false);
|
|
7565
|
+
return;
|
|
7566
|
+
}
|
|
6043
7567
|
if (request.method === "POST" && lanControl) {
|
|
6044
7568
|
const body = await readJsonObject(request, 4096);
|
|
6045
7569
|
if (typeof body.running !== "boolean") throw new HttpError(400, "bad_request");
|
|
@@ -6053,47 +7577,75 @@ async function apply(ctx, config) {
|
|
|
6053
7577
|
}
|
|
6054
7578
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/provider`) {
|
|
6055
7579
|
const body = await readJsonObject(request, 4096);
|
|
6056
|
-
if (body.provider !== "tailscale" && body.provider !== "cpolar") throw new HttpError(400, "bad_request");
|
|
6057
|
-
await
|
|
7580
|
+
if (body.provider !== "tailscale" && body.provider !== "cpolar" && body.provider !== "frp") throw new HttpError(400, "bad_request");
|
|
7581
|
+
await remoteProviders.select(body.provider);
|
|
6058
7582
|
sendJson(response, 200, remotePayload(), false);
|
|
6059
7583
|
return;
|
|
6060
7584
|
}
|
|
6061
7585
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/cpolar/component/install`) {
|
|
6062
7586
|
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
6063
|
-
await cpolarComponent.install();
|
|
7587
|
+
await remoteProviders.mutate(async () => cpolarComponent.install());
|
|
6064
7588
|
sendJson(response, 200, remotePayload(), false);
|
|
6065
7589
|
return;
|
|
6066
7590
|
}
|
|
6067
7591
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/cpolar/configure`) {
|
|
6068
7592
|
const body = await readJsonObject(request, 4096);
|
|
6069
|
-
await cpolarComponent.configure(body.authtoken);
|
|
7593
|
+
await remoteProviders.mutate(async () => cpolarComponent.configure(body.authtoken));
|
|
6070
7594
|
sendJson(response, 200, remotePayload(), false);
|
|
6071
7595
|
return;
|
|
6072
7596
|
}
|
|
6073
7597
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/cpolar/component/purge`) {
|
|
6074
7598
|
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
6075
|
-
await
|
|
6076
|
-
|
|
7599
|
+
await remoteProviders.mutate(async () => {
|
|
7600
|
+
await remoteControllers.cpolar.setEnabled(false);
|
|
7601
|
+
await cpolarComponent.purge();
|
|
7602
|
+
});
|
|
6077
7603
|
sendJson(response, 200, remotePayload(), false);
|
|
6078
7604
|
return;
|
|
6079
7605
|
}
|
|
6080
|
-
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/
|
|
7606
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/frp/component/install`) {
|
|
7607
|
+
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
7608
|
+
await remoteProviders.mutate(async () => frpComponent.install());
|
|
7609
|
+
sendJson(response, 200, remotePayload(), false);
|
|
7610
|
+
return;
|
|
7611
|
+
}
|
|
7612
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/frp/configure`) {
|
|
6081
7613
|
const body = await readJsonObject(request, 4096);
|
|
6082
|
-
|
|
6083
|
-
|
|
7614
|
+
await remoteProviders.mutate(async () => {
|
|
7615
|
+
await frpConfig.configure(body);
|
|
7616
|
+
if (remoteControllers.frp.status().enabled) await remoteControllers.frp.reconnect();
|
|
7617
|
+
});
|
|
7618
|
+
sendJson(response, 200, remotePayload(), false);
|
|
7619
|
+
return;
|
|
7620
|
+
}
|
|
7621
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/frp/component/purge`) {
|
|
7622
|
+
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
7623
|
+
await remoteProviders.mutate(async () => {
|
|
7624
|
+
await remoteControllers.frp.setEnabled(false);
|
|
7625
|
+
await Promise.all([frpComponent.purge(), frpConfig.purge()]);
|
|
7626
|
+
});
|
|
7627
|
+
sendJson(response, 200, remotePayload(), false);
|
|
7628
|
+
return;
|
|
7629
|
+
}
|
|
7630
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/control`) {
|
|
7631
|
+
const running = (await readJsonObject(request, 4096)).running;
|
|
7632
|
+
if (typeof running !== "boolean") throw new HttpError(400, "bad_request");
|
|
7633
|
+
await remoteProviders.mutate(async (controller) => controller.setEnabled(running));
|
|
6084
7634
|
sendJson(response, 200, remotePayload(), false);
|
|
6085
7635
|
return;
|
|
6086
7636
|
}
|
|
6087
7637
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/reconnect`) {
|
|
6088
7638
|
await readJsonObject(request, 4096);
|
|
6089
|
-
await
|
|
7639
|
+
await remoteProviders.mutate(async (controller) => controller.reconnect());
|
|
6090
7640
|
sendJson(response, 200, remotePayload(), false);
|
|
6091
7641
|
return;
|
|
6092
7642
|
}
|
|
6093
7643
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/reset`) {
|
|
6094
7644
|
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
6095
|
-
await
|
|
6096
|
-
|
|
7645
|
+
await remoteProviders.mutate(async (controller) => {
|
|
7646
|
+
await controller.reset();
|
|
7647
|
+
await rm(remoteDeviceFile, { force: true });
|
|
7648
|
+
});
|
|
6097
7649
|
sendJson(response, 200, remotePayload(), false);
|
|
6098
7650
|
return;
|
|
6099
7651
|
}
|
|
@@ -6152,36 +7704,54 @@ async function apply(ctx, config) {
|
|
|
6152
7704
|
try {
|
|
6153
7705
|
await mobileAccess.startLocal(template.extensionsDir, ctx);
|
|
6154
7706
|
await lanController.initialize();
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
7707
|
+
const stores = {
|
|
7708
|
+
tailscale: tailscaleStore,
|
|
7709
|
+
cpolar: cpolarStore,
|
|
7710
|
+
frp: frpStore
|
|
7711
|
+
};
|
|
7712
|
+
await Promise.all(Object.keys(stores).filter((provider) => provider !== remoteProviders.selected).map((provider) => stores[provider].save({
|
|
6160
7713
|
version: 1,
|
|
6161
7714
|
enabled: false
|
|
6162
|
-
});
|
|
6163
|
-
|
|
6164
|
-
|
|
7715
|
+
})));
|
|
7716
|
+
for (const provider of [
|
|
7717
|
+
"tailscale",
|
|
7718
|
+
"cpolar",
|
|
7719
|
+
"frp"
|
|
7720
|
+
]) await remoteControllers[provider].initialize();
|
|
6165
7721
|
} catch (error) {
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
7722
|
+
try {
|
|
7723
|
+
await settleCleanupSteps([
|
|
7724
|
+
unregister,
|
|
7725
|
+
disposeMobileCommand,
|
|
7726
|
+
async () => {
|
|
7727
|
+
const failures = (await Promise.allSettled(Object.values(remoteControllers).map((controller) => controller.close()))).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
7728
|
+
if (failures.length > 0) throw new AggregateError(failures, "remote provider cleanup failed");
|
|
7729
|
+
},
|
|
7730
|
+
() => lanController.close(),
|
|
7731
|
+
() => mobileAccess.stopLocal(),
|
|
7732
|
+
unregisterBuiltin
|
|
7733
|
+
]);
|
|
7734
|
+
} catch (cleanupError) {
|
|
7735
|
+
throw new AggregateError([error, cleanupError], "DSH Mobile initialization and cleanup failed");
|
|
7736
|
+
}
|
|
6172
7737
|
throw error;
|
|
6173
7738
|
}
|
|
6174
7739
|
return async () => {
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
7740
|
+
await settleCleanupSteps([
|
|
7741
|
+
unregister,
|
|
7742
|
+
disposeMobileCommand,
|
|
7743
|
+
async () => {
|
|
7744
|
+
const failures = (await Promise.allSettled(Object.values(remoteControllers).map((controller) => controller.close()))).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
7745
|
+
if (failures.length > 0) throw new AggregateError(failures, "remote provider cleanup failed");
|
|
7746
|
+
},
|
|
7747
|
+
() => lanController.close(),
|
|
7748
|
+
() => mobileAccess.stopLocal(),
|
|
7749
|
+
unregisterBuiltin
|
|
7750
|
+
]);
|
|
6181
7751
|
};
|
|
6182
|
-
}, "dsh-mobile: independent LAN and selectable remote
|
|
7752
|
+
}, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
|
|
6183
7753
|
}
|
|
6184
7754
|
//#endregion
|
|
6185
|
-
export { AUTH_PREFIX, AccessController, AccessError, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, DEVICE_COOKIE, EXTENSION_LIMITS, JsonDeviceStore, JsonMobileAccessControlStore, LOCAL_ADMIN_PREFIX, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, RequestTrustPolicy, SESSION_COOKIE, SUPPORTED_DSH_VERSIONS, WS_PATHS, addressAllowed, apply, assertExtensionId, assertSupportedDshVersion, createMobileAccessService, inject, isLoopbackAddress, name, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseGatewayConfig, parseMobileAccessControlState, resolveAuthority, rewriteMobileIndex };
|
|
7755
|
+
export { AUTH_PREFIX, AccessController, AccessError, BoundedRateLimiter, CSRF_COOKIE, CSRF_HEADER, Config, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_COMPONENT_RELEASES, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, RequestTrustPolicy, SESSION_COOKIE, SUPPORTED_DSH_VERSIONS, WS_PATHS, addressAllowed, apply, assertExtensionId, assertSupportedDshVersion, configuredRemoteProvider, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, inject, isLoopbackAddress, name, parseAuthority, parseCidr, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseRemoteProviderState, resolveAuthority, rewriteMobileIndex, validateFrpPublicOrigin, validateFrpServerAddress, validateFrpServerPort, validateFrpToken };
|
|
6186
7756
|
|
|
6187
7757
|
//# sourceMappingURL=index.mjs.map
|