dsh-mobile 0.6.2 → 0.6.3
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 +14 -0
- package/CONTRIBUTORS.md +11 -4
- package/README.en.md +11 -7
- package/README.md +11 -7
- package/SECURITY.md +3 -2
- package/apps/mobile/README.md +14 -2
- package/apps/mobile/release.json +3 -3
- package/cordis.patch.yml +2 -1
- package/docs/ATTACH_EXISTING_FRPS.md +1 -1
- package/docs/CADDY_MANAGED.en.md +15 -1
- package/docs/CADDY_MANAGED.md +15 -1
- package/docs/CLOUDFLARE_TUNNEL.en.md +2 -2
- package/docs/CLOUDFLARE_TUNNEL.md +3 -3
- package/docs/EXTENSION_WORKERS.en.md +49 -0
- package/docs/EXTENSION_WORKERS.md +49 -0
- package/docs/MOBILE_COMPOSER_TOOLBAR.en.md +19 -0
- package/docs/MOBILE_COMPOSER_TOOLBAR.md +19 -0
- package/docs/README.md +2 -0
- package/docs/SELF_HOSTED_FRP.en.md +1 -1
- package/docs/SELF_HOSTED_FRP.md +1 -1
- package/docs/TRUSTED_NETWORKS.en.md +1 -1
- package/docs/TRUSTED_NETWORKS.md +1 -1
- package/lib/cli.js +1 -0
- package/lib/cli.js.map +1 -1
- package/lib/client.js +864 -97
- package/lib/client.js.map +1 -1
- package/lib/extension-worker-runtime.mjs +1646 -0
- package/lib/extension-worker-runtime.mjs.map +1 -0
- package/lib/index.d.mts +199 -7
- package/lib/index.mjs +1594 -533
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +15 -6
- package/packages/question-fixes/LICENSE +201 -0
- package/packages/question-fixes/package.json +9 -2
- package/node_modules/dsh-mobile-question-fixes/lib/client.js +0 -78
- package/node_modules/dsh-mobile-question-fixes/lib/client.js.map +0 -1
- package/node_modules/dsh-mobile-question-fixes/lib/index.mjs +0 -14
- package/node_modules/dsh-mobile-question-fixes/lib/index.mjs.map +0 -1
- package/node_modules/dsh-mobile-question-fixes/package.json +0 -25
package/lib/index.mjs
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
6
|
import { connect, createServer } from "node:net";
|
|
7
|
-
import { chmod, copyFile, lstat, mkdir, mkdtemp, opendir, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
8
8
|
import { execFile, spawn } from "node:child_process";
|
|
9
9
|
import { createSocket } from "node:dgram";
|
|
10
10
|
import { homedir, hostname, networkInterfaces, tmpdir } from "node:os";
|
|
11
11
|
import { createServer as createServer$1, request } from "node:http";
|
|
12
12
|
import { createServer as createServer$2, request as request$1 } from "node:https";
|
|
13
13
|
import { Readable, Transform, finished } from "node:stream";
|
|
14
|
-
import { pipeline } from "node:stream/promises";
|
|
14
|
+
import { finished as finished$1, pipeline } from "node:stream/promises";
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
import { createGzip, gzip } from "node:zlib";
|
|
17
17
|
import Bonjour from "bonjour-service";
|
|
18
18
|
import * as QRCode from "qrcode";
|
|
19
19
|
import WebSocket, { WebSocketServer } from "ws";
|
|
20
20
|
import { Logger, Service } from "@deepseek-ai/cordis";
|
|
21
|
+
import { Worker } from "node:worker_threads";
|
|
21
22
|
import { connect as connect$1 } from "node:tls";
|
|
22
23
|
import { generate } from "selfsigned";
|
|
23
24
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm/message";
|
|
24
25
|
import { lookup } from "node:dns/promises";
|
|
25
|
-
import {
|
|
26
|
+
import { constants } from "node:fs";
|
|
26
27
|
//#region src/access.ts
|
|
27
28
|
/** Stable error categories converted to deliberately terse HTTP responses. */
|
|
28
29
|
var AccessError = class extends Error {
|
|
@@ -860,9 +861,69 @@ var WebSocketPathStore = class {
|
|
|
860
861
|
}
|
|
861
862
|
};
|
|
862
863
|
//#endregion
|
|
864
|
+
//#region src/extension-worker-config.ts
|
|
865
|
+
function integer$1(value, name, fallback, minimum, maximum) {
|
|
866
|
+
const resolved = value ?? fallback;
|
|
867
|
+
if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) throw new Error(`hostExecution.${name} must be an integer from ${minimum} through ${maximum}`);
|
|
868
|
+
return resolved;
|
|
869
|
+
}
|
|
870
|
+
/** Resolve configuration once; reject malformed modes, ids and incoherent stream windows. */
|
|
871
|
+
function resolveHostExecution(raw) {
|
|
872
|
+
if (raw !== void 0 && (raw === null || typeof raw !== "object" || Array.isArray(raw))) throw new Error("hostExecution must be an object");
|
|
873
|
+
const value = raw ?? {};
|
|
874
|
+
const mode = value.mode ?? "in-process";
|
|
875
|
+
if (mode !== "worker" && mode !== "in-process") throw new Error("hostExecution.mode is invalid");
|
|
876
|
+
if (value.extensions !== void 0 && (!Array.isArray(value.extensions) || value.extensions.some((id) => typeof id !== "string" || !/^[a-z][a-z0-9-]{0,63}$/u.test(id)) || new Set(value.extensions).size !== value.extensions.length)) throw new Error("hostExecution.extensions must contain unique local extension ids");
|
|
877
|
+
const streamWindowBytes = integer$1(value.streamWindowBytes, "streamWindowBytes", 262144, 1024, 4194304);
|
|
878
|
+
const streamChunkBytes = integer$1(value.streamChunkBytes, "streamChunkBytes", 65536, 1024, streamWindowBytes);
|
|
879
|
+
const streamAggregateBytes = integer$1(value.streamAggregateBytes, "streamAggregateBytes", 1048576, streamWindowBytes, 33554432);
|
|
880
|
+
return Object.freeze({
|
|
881
|
+
mode,
|
|
882
|
+
extensions: value.extensions === void 0 ? void 0 : Object.freeze([...value.extensions]),
|
|
883
|
+
maxWorkers: integer$1(value.maxWorkers, "maxWorkers", 8, 1, 128),
|
|
884
|
+
maxRegistrations: integer$1(value.maxRegistrations, "maxRegistrations", 256, 1, 4096),
|
|
885
|
+
maxOldGenerationSizeMb: integer$1(value.maxOldGenerationSizeMb, "maxOldGenerationSizeMb", 128, 16, 4096),
|
|
886
|
+
maxYoungGenerationSizeMb: integer$1(value.maxYoungGenerationSizeMb, "maxYoungGenerationSizeMb", 16, 1, 512),
|
|
887
|
+
stackSizeMb: integer$1(value.stackSizeMb, "stackSizeMb", 4, 1, 64),
|
|
888
|
+
activationTimeoutMs: integer$1(value.activationTimeoutMs, "activationTimeoutMs", 5e3, 1, 3e5),
|
|
889
|
+
operationTimeoutMs: integer$1(value.operationTimeoutMs, "operationTimeoutMs", 3e4, 1, 3e5),
|
|
890
|
+
cancelGraceMs: integer$1(value.cancelGraceMs, "cancelGraceMs", 250, 1, 3e4),
|
|
891
|
+
streamCancelGraceMs: integer$1(value.streamCancelGraceMs, "streamCancelGraceMs", 1e3, 1, 3e4),
|
|
892
|
+
disposeGraceMs: integer$1(value.disposeGraceMs, "disposeGraceMs", 2e3, 1, 3e4),
|
|
893
|
+
resultMaxBytes: integer$1(value.resultMaxBytes, "resultMaxBytes", 4194304, 1024, 4194304),
|
|
894
|
+
streamWindowBytes,
|
|
895
|
+
streamChunkBytes,
|
|
896
|
+
streamAggregateBytes,
|
|
897
|
+
logWindowMs: integer$1(value.logWindowMs, "logWindowMs", 1e3, 1, 6e4),
|
|
898
|
+
logMessagesPerWindow: integer$1(value.logMessagesPerWindow, "logMessagesPerWindow", 64, 1, 1024),
|
|
899
|
+
logMessageBytes: integer$1(value.logMessageBytes, "logMessageBytes", 4096, 128, 65536)
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
863
903
|
//#region src/config.ts
|
|
864
904
|
/** Loader-facing defaults; {@link parseGatewayConfig} enforces cross-field security rules. */
|
|
865
905
|
const Config = z.object({
|
|
906
|
+
hostExecution: z.object({
|
|
907
|
+
mode: z.union([z.const("in-process"), z.const("worker")]),
|
|
908
|
+
extensions: z.union([z.const(void 0), z.array(String)]),
|
|
909
|
+
maxWorkers: z.natural(),
|
|
910
|
+
maxRegistrations: z.natural(),
|
|
911
|
+
maxOldGenerationSizeMb: z.natural(),
|
|
912
|
+
maxYoungGenerationSizeMb: z.natural(),
|
|
913
|
+
stackSizeMb: z.natural(),
|
|
914
|
+
activationTimeoutMs: z.natural(),
|
|
915
|
+
operationTimeoutMs: z.natural(),
|
|
916
|
+
cancelGraceMs: z.natural(),
|
|
917
|
+
streamCancelGraceMs: z.natural(),
|
|
918
|
+
disposeGraceMs: z.natural(),
|
|
919
|
+
resultMaxBytes: z.natural(),
|
|
920
|
+
streamWindowBytes: z.natural(),
|
|
921
|
+
streamChunkBytes: z.natural(),
|
|
922
|
+
streamAggregateBytes: z.natural(),
|
|
923
|
+
logWindowMs: z.natural(),
|
|
924
|
+
logMessagesPerWindow: z.natural(),
|
|
925
|
+
logMessageBytes: z.natural()
|
|
926
|
+
}),
|
|
866
927
|
setupFile: z.string().hidden(),
|
|
867
928
|
publicOrigin: z.string(),
|
|
868
929
|
listenHost: z.string(),
|
|
@@ -977,7 +1038,9 @@ function parsePublicOrigin$1(value) {
|
|
|
977
1038
|
});
|
|
978
1039
|
}
|
|
979
1040
|
function parseTls(value, listenHost) {
|
|
1041
|
+
if (value !== void 0 && (value === null || typeof value !== "object" || Array.isArray(value))) throw new Error("tls must be an object");
|
|
980
1042
|
const mode = value?.mode ?? "provided";
|
|
1043
|
+
if (value?.mode === null || mode !== "provided" && mode !== "disabled") throw new Error("tls.mode must be provided or disabled");
|
|
981
1044
|
if (mode === "disabled") {
|
|
982
1045
|
if (!isLoopbackAddress(listenHost)) throw new Error("TLS may be disabled only on an IP loopback listener");
|
|
983
1046
|
return Object.freeze({ mode });
|
|
@@ -1024,6 +1087,7 @@ function parseGatewayConfig(raw) {
|
|
|
1024
1087
|
if (sessionTtlMs > deviceTtlMs) throw new Error("sessionTtlMs must not exceed deviceTtlMs");
|
|
1025
1088
|
return Object.freeze({
|
|
1026
1089
|
listenHost,
|
|
1090
|
+
hostExecution: resolveHostExecution(value.hostExecution),
|
|
1027
1091
|
listenPort,
|
|
1028
1092
|
upstreamOrigin,
|
|
1029
1093
|
authorities: Object.freeze(authorities),
|
|
@@ -2258,6 +2322,660 @@ async function readComputerImage(path, signal) {
|
|
|
2258
2322
|
}
|
|
2259
2323
|
}
|
|
2260
2324
|
//#endregion
|
|
2325
|
+
//#region src/extension-worker.ts
|
|
2326
|
+
/** Supervise opt-in local hosts without executing extension code on the DSH thread. */
|
|
2327
|
+
/** JSON bytes serialized and size-checked inside the worker. */
|
|
2328
|
+
var PreparedJsonResult = class {
|
|
2329
|
+
bytes;
|
|
2330
|
+
constructor(bytes) {
|
|
2331
|
+
this.bytes = bytes;
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
/** Identify worker results without invoking an extension serializer in the gateway. */
|
|
2335
|
+
function isPreparedJson(value) {
|
|
2336
|
+
return value instanceof PreparedJsonResult;
|
|
2337
|
+
}
|
|
2338
|
+
/** Resolve the installed standalone entry adjacent to the plugin bundle. */
|
|
2339
|
+
function defaultExtensionWorkerModule() {
|
|
2340
|
+
return new URL("./extension-worker-runtime.mjs", import.meta.url);
|
|
2341
|
+
}
|
|
2342
|
+
/** Every thread counts until actual exit, including activation, retirement and termination. */
|
|
2343
|
+
var ExtensionWorkerBudget = class {
|
|
2344
|
+
maximum;
|
|
2345
|
+
count = 0;
|
|
2346
|
+
constructor(maximum) {
|
|
2347
|
+
this.maximum = maximum;
|
|
2348
|
+
}
|
|
2349
|
+
get current() {
|
|
2350
|
+
return this.count;
|
|
2351
|
+
}
|
|
2352
|
+
reserve() {
|
|
2353
|
+
if (this.count >= this.maximum) throw new MobileExtensionError("extension_worker_limit", "local extension worker limit reached", 503);
|
|
2354
|
+
this.count += 1;
|
|
2355
|
+
let released = false;
|
|
2356
|
+
return () => {
|
|
2357
|
+
if (!released) {
|
|
2358
|
+
released = true;
|
|
2359
|
+
this.count -= 1;
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
/** Diagnostic count; production admission uses an instance-owned budget. */
|
|
2365
|
+
const activeWorkerCount = { current: 0 };
|
|
2366
|
+
const LIFECYCLE_ID = "lifecycle";
|
|
2367
|
+
/** Node accepts quoted NODE_OPTIONS words; inspect option names without exposing their values. */
|
|
2368
|
+
function nodeOptionWords(value) {
|
|
2369
|
+
const words = [];
|
|
2370
|
+
let word = "";
|
|
2371
|
+
let quoted = false;
|
|
2372
|
+
let escaped = false;
|
|
2373
|
+
for (const character of value) {
|
|
2374
|
+
if (escaped) {
|
|
2375
|
+
word += character;
|
|
2376
|
+
escaped = false;
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
if (character === "\\") {
|
|
2380
|
+
escaped = true;
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (character === "\"") {
|
|
2384
|
+
quoted = !quoted;
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
if (/\s/u.test(character) && !quoted) {
|
|
2388
|
+
if (word !== "") {
|
|
2389
|
+
words.push(word);
|
|
2390
|
+
word = "";
|
|
2391
|
+
}
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
word += character;
|
|
2395
|
+
}
|
|
2396
|
+
if (word !== "") words.push(word);
|
|
2397
|
+
return words;
|
|
2398
|
+
}
|
|
2399
|
+
/** Known parent V8 heap flags take precedence over Worker resourceLimits. */
|
|
2400
|
+
function parentWorkerHeapOverrides(argv, nodeOptions) {
|
|
2401
|
+
const heapFlags = /* @__PURE__ */ new Set([
|
|
2402
|
+
"--max-old-space-size",
|
|
2403
|
+
"--max-semi-space-size",
|
|
2404
|
+
"--max-heap-size",
|
|
2405
|
+
"--max-old-space-size-percentage"
|
|
2406
|
+
]);
|
|
2407
|
+
const found = /* @__PURE__ */ new Set();
|
|
2408
|
+
for (const argument of [...argv, ...nodeOptionWords(nodeOptions ?? "")]) {
|
|
2409
|
+
const name = argument.split("=", 1)[0].replaceAll("_", "-");
|
|
2410
|
+
if (heapFlags.has(name)) found.add(name);
|
|
2411
|
+
}
|
|
2412
|
+
return Object.freeze([...found]);
|
|
2413
|
+
}
|
|
2414
|
+
const initialNodeOptions = process.env.NODE_OPTIONS;
|
|
2415
|
+
var ExtensionWorkerHost = class {
|
|
2416
|
+
options;
|
|
2417
|
+
limits;
|
|
2418
|
+
worker;
|
|
2419
|
+
pending = /* @__PURE__ */ new Map();
|
|
2420
|
+
routePending = /* @__PURE__ */ new Map();
|
|
2421
|
+
streams = /* @__PURE__ */ new Map();
|
|
2422
|
+
detachedRoutes = /* @__PURE__ */ new Set();
|
|
2423
|
+
executionGrace = /* @__PURE__ */ new Map();
|
|
2424
|
+
exited;
|
|
2425
|
+
unavailableController = new AbortController();
|
|
2426
|
+
termination;
|
|
2427
|
+
activation;
|
|
2428
|
+
dead = false;
|
|
2429
|
+
disposed = false;
|
|
2430
|
+
maxBufferedBytes = 0;
|
|
2431
|
+
cancelledStreams = 0;
|
|
2432
|
+
logWindowStart = Date.now();
|
|
2433
|
+
logCount = 0;
|
|
2434
|
+
stdioBytes = 0;
|
|
2435
|
+
stdioResume;
|
|
2436
|
+
constructor(options) {
|
|
2437
|
+
this.options = options;
|
|
2438
|
+
this.limits = resolveHostExecution(options.limits);
|
|
2439
|
+
const overrides = new Set(parentWorkerHeapOverrides(process.execArgv, initialNodeOptions));
|
|
2440
|
+
for (const flag of parentWorkerHeapOverrides([], process.env.NODE_OPTIONS)) overrides.add(flag);
|
|
2441
|
+
if (overrides.size > 0) throw new MobileExtensionError("extension_worker_heap_override", `worker heap limits require launching DSH without ${[...overrides].join(", ")}`, 503);
|
|
2442
|
+
const release = options.budget?.reserve() ?? (() => void 0);
|
|
2443
|
+
try {
|
|
2444
|
+
this.worker = new Worker(options.workerModule, {
|
|
2445
|
+
workerData: { limits: this.limits },
|
|
2446
|
+
stdout: true,
|
|
2447
|
+
stderr: true,
|
|
2448
|
+
resourceLimits: {
|
|
2449
|
+
maxOldGenerationSizeMb: this.limits.maxOldGenerationSizeMb,
|
|
2450
|
+
maxYoungGenerationSizeMb: this.limits.maxYoungGenerationSizeMb,
|
|
2451
|
+
stackSizeMb: this.limits.stackSizeMb
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
} catch (error) {
|
|
2455
|
+
release();
|
|
2456
|
+
throw error;
|
|
2457
|
+
}
|
|
2458
|
+
const drainConsole = (chunk) => {
|
|
2459
|
+
this.stdioBytes += chunk.byteLength;
|
|
2460
|
+
if (this.stdioBytes < this.limits.logMessageBytes * this.limits.logMessagesPerWindow || this.stdioResume !== void 0) return;
|
|
2461
|
+
this.worker.stdout?.pause();
|
|
2462
|
+
this.worker.stderr?.pause();
|
|
2463
|
+
this.stdioResume = setTimeout(() => {
|
|
2464
|
+
this.stdioResume = void 0;
|
|
2465
|
+
this.stdioBytes = 0;
|
|
2466
|
+
if (!this.dead) {
|
|
2467
|
+
this.worker.stdout?.resume();
|
|
2468
|
+
this.worker.stderr?.resume();
|
|
2469
|
+
}
|
|
2470
|
+
}, this.limits.logWindowMs);
|
|
2471
|
+
this.stdioResume.unref();
|
|
2472
|
+
};
|
|
2473
|
+
this.worker.stdout?.on("data", drainConsole);
|
|
2474
|
+
this.worker.stderr?.on("data", drainConsole);
|
|
2475
|
+
this.worker.unref();
|
|
2476
|
+
activeWorkerCount.current += 1;
|
|
2477
|
+
this.exited = new Promise((resolve) => {
|
|
2478
|
+
this.worker.once("exit", () => {
|
|
2479
|
+
this.dead = true;
|
|
2480
|
+
if (this.stdioResume !== void 0) clearTimeout(this.stdioResume);
|
|
2481
|
+
this.markUnavailable();
|
|
2482
|
+
release();
|
|
2483
|
+
activeWorkerCount.current -= 1;
|
|
2484
|
+
for (const timer of this.executionGrace.values()) clearTimeout(timer);
|
|
2485
|
+
this.executionGrace.clear();
|
|
2486
|
+
const error = this.unavailable();
|
|
2487
|
+
for (const entry of this.pending.values()) {
|
|
2488
|
+
entry.cleanup();
|
|
2489
|
+
entry.reject(error);
|
|
2490
|
+
}
|
|
2491
|
+
for (const entry of this.routePending.values()) {
|
|
2492
|
+
entry.cleanup();
|
|
2493
|
+
entry.reject(error);
|
|
2494
|
+
}
|
|
2495
|
+
this.pending.clear();
|
|
2496
|
+
this.routePending.clear();
|
|
2497
|
+
this.detachedRoutes.clear();
|
|
2498
|
+
for (const live of this.streams.values()) {
|
|
2499
|
+
if (live.cancelTimer !== void 0) clearTimeout(live.cancelTimer);
|
|
2500
|
+
live.bridge.destroy(error);
|
|
2501
|
+
}
|
|
2502
|
+
this.streams.clear();
|
|
2503
|
+
resolve();
|
|
2504
|
+
});
|
|
2505
|
+
});
|
|
2506
|
+
this.worker.on("message", (raw) => {
|
|
2507
|
+
try {
|
|
2508
|
+
this.handleMessage(raw);
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
this.terminate("invalid-worker-message");
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
this.worker.on("error", () => {
|
|
2514
|
+
this.terminate("worker-error");
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
/** A stopped generation never silently respawns. */
|
|
2518
|
+
available() {
|
|
2519
|
+
return !this.dead && this.termination === void 0 && !this.disposed && !this.unavailableController.signal.aborted;
|
|
2520
|
+
}
|
|
2521
|
+
/** Activate once and receive executable-free registration metadata. */
|
|
2522
|
+
async activate() {
|
|
2523
|
+
let timer;
|
|
2524
|
+
try {
|
|
2525
|
+
const reply = new Promise((resolve, reject) => {
|
|
2526
|
+
this.pending.set(LIFECYCLE_ID, {
|
|
2527
|
+
resolve,
|
|
2528
|
+
reject,
|
|
2529
|
+
cleanup: () => void 0
|
|
2530
|
+
});
|
|
2531
|
+
this.post({
|
|
2532
|
+
kind: "activate",
|
|
2533
|
+
id: LIFECYCLE_ID,
|
|
2534
|
+
hostFile: this.options.hostFile,
|
|
2535
|
+
generation: this.options.generation,
|
|
2536
|
+
manifest: this.options.manifest
|
|
2537
|
+
});
|
|
2538
|
+
});
|
|
2539
|
+
await Promise.race([reply, new Promise((_, reject) => {
|
|
2540
|
+
timer = setTimeout(() => reject(new MobileExtensionError("host_load_timeout", "extension worker activation timed out", 500)), this.options.activationTimeoutMs ?? this.limits.activationTimeoutMs);
|
|
2541
|
+
})]);
|
|
2542
|
+
if (this.activation === void 0) throw new MobileExtensionError("host_activation_failed", "extension worker activation failed", 500);
|
|
2543
|
+
return this.activation;
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
await this.terminate("activation-failed");
|
|
2546
|
+
throw error;
|
|
2547
|
+
} finally {
|
|
2548
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
actionMetadata() {
|
|
2552
|
+
return this.activation?.actions ?? [];
|
|
2553
|
+
}
|
|
2554
|
+
routeMetadata() {
|
|
2555
|
+
return this.activation?.routes ?? [];
|
|
2556
|
+
}
|
|
2557
|
+
/** Underlying action: settles only when execution completes or the thread exits. */
|
|
2558
|
+
invokeExecution(action, input, caller) {
|
|
2559
|
+
if (!this.available()) return Promise.reject(this.unavailable());
|
|
2560
|
+
if (caller.signal.aborted) return Promise.reject(caller.signal.reason);
|
|
2561
|
+
const id = randomUUID();
|
|
2562
|
+
return new Promise((resolve, reject) => {
|
|
2563
|
+
const onAbort = () => {
|
|
2564
|
+
this.post({
|
|
2565
|
+
kind: "cancel",
|
|
2566
|
+
id
|
|
2567
|
+
});
|
|
2568
|
+
this.armExecutionGrace(id);
|
|
2569
|
+
};
|
|
2570
|
+
caller.signal.addEventListener("abort", onAbort, { once: true });
|
|
2571
|
+
this.pending.set(id, {
|
|
2572
|
+
resolve,
|
|
2573
|
+
reject,
|
|
2574
|
+
cleanup: () => {
|
|
2575
|
+
caller.signal.removeEventListener("abort", onAbort);
|
|
2576
|
+
this.settleExecutionGrace(id);
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
try {
|
|
2580
|
+
this.post({
|
|
2581
|
+
kind: "invoke",
|
|
2582
|
+
id,
|
|
2583
|
+
action,
|
|
2584
|
+
deviceId: caller.deviceId,
|
|
2585
|
+
input
|
|
2586
|
+
});
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
this.pending.get(id)?.cleanup();
|
|
2589
|
+
this.pending.delete(id);
|
|
2590
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
/** Direct caller budget; service dispatch observes invokeExecution separately. */
|
|
2595
|
+
invoke(action, input, caller) {
|
|
2596
|
+
return this.callerBudget((signal) => this.invokeExecution(action, input, {
|
|
2597
|
+
...caller,
|
|
2598
|
+
signal
|
|
2599
|
+
}), caller.signal, this.actionMetadata().find((entry) => entry.name === action)?.timeoutMs ?? this.limits.operationTimeoutMs, "extension_action_timeout");
|
|
2600
|
+
}
|
|
2601
|
+
/** Underlying route: late streams are cancelled and observed to confirmed end. */
|
|
2602
|
+
handleRouteExecution(routeIndex, request) {
|
|
2603
|
+
if (!this.available()) return Promise.reject(this.unavailable());
|
|
2604
|
+
if (request.signal.aborted) return Promise.reject(request.signal.reason);
|
|
2605
|
+
const id = randomUUID();
|
|
2606
|
+
return new Promise((resolve, reject) => {
|
|
2607
|
+
const onAbort = () => {
|
|
2608
|
+
this.post({
|
|
2609
|
+
kind: "cancel",
|
|
2610
|
+
id
|
|
2611
|
+
});
|
|
2612
|
+
if (this.routePending.has(id)) {
|
|
2613
|
+
this.detachedRoutes.add(id);
|
|
2614
|
+
this.armExecutionGrace(id);
|
|
2615
|
+
}
|
|
2616
|
+
this.cancelStream(id);
|
|
2617
|
+
};
|
|
2618
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
2619
|
+
this.routePending.set(id, {
|
|
2620
|
+
resolve,
|
|
2621
|
+
reject,
|
|
2622
|
+
cleanup: () => {
|
|
2623
|
+
request.signal.removeEventListener("abort", onAbort);
|
|
2624
|
+
this.settleExecutionGrace(id);
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
try {
|
|
2628
|
+
this.post({
|
|
2629
|
+
kind: "route",
|
|
2630
|
+
id,
|
|
2631
|
+
routeIndex,
|
|
2632
|
+
method: request.method,
|
|
2633
|
+
path: request.pathname,
|
|
2634
|
+
query: [...request.query.entries()],
|
|
2635
|
+
headers: request.headers,
|
|
2636
|
+
body: new Uint8Array(request.body),
|
|
2637
|
+
deviceId: request.deviceId
|
|
2638
|
+
});
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
this.routePending.get(id)?.cleanup();
|
|
2641
|
+
this.routePending.delete(id);
|
|
2642
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
/** Handler budget ends at stream-start; stream lifetime remains cancellation-bound. */
|
|
2647
|
+
handleRoute(routeIndex, request) {
|
|
2648
|
+
return this.callerBudget((signal) => this.handleRouteExecution(routeIndex, {
|
|
2649
|
+
...request,
|
|
2650
|
+
signal
|
|
2651
|
+
}), request.signal, this.routeMetadata()[routeIndex]?.timeoutMs ?? this.limits.operationTimeoutMs, "extension_route_timeout", true);
|
|
2652
|
+
}
|
|
2653
|
+
async callerBudget(start, external, timeoutMs, code, holdForStream = false) {
|
|
2654
|
+
const controller = new AbortController();
|
|
2655
|
+
let timer;
|
|
2656
|
+
let detach;
|
|
2657
|
+
const onAbort = () => {
|
|
2658
|
+
controller.abort(external.reason);
|
|
2659
|
+
detach?.(external.reason ?? new MobileExtensionError("extension_cancelled", "caller cancelled", 409));
|
|
2660
|
+
};
|
|
2661
|
+
const onUnavailable = () => {
|
|
2662
|
+
detach?.(this.unavailableController.signal.reason);
|
|
2663
|
+
};
|
|
2664
|
+
if (external.aborted) throw external.reason;
|
|
2665
|
+
if (!this.available()) throw this.unavailable();
|
|
2666
|
+
external.addEventListener("abort", onAbort, { once: true });
|
|
2667
|
+
this.unavailableController.signal.addEventListener("abort", onUnavailable, { once: true });
|
|
2668
|
+
const release = () => {
|
|
2669
|
+
external.removeEventListener("abort", onAbort);
|
|
2670
|
+
this.unavailableController.signal.removeEventListener("abort", onUnavailable);
|
|
2671
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2672
|
+
};
|
|
2673
|
+
let retained = false;
|
|
2674
|
+
try {
|
|
2675
|
+
const result = await Promise.race([start(controller.signal), new Promise((_, reject) => {
|
|
2676
|
+
detach = reject;
|
|
2677
|
+
timer = setTimeout(() => {
|
|
2678
|
+
const error = new MobileExtensionError(code, "extension operation timed out", 500);
|
|
2679
|
+
controller.abort(error);
|
|
2680
|
+
reject(error);
|
|
2681
|
+
}, timeoutMs);
|
|
2682
|
+
})]);
|
|
2683
|
+
if (holdForStream && result !== null && typeof result === "object" && "body" in result && result.body instanceof Readable) {
|
|
2684
|
+
retained = true;
|
|
2685
|
+
result.body.once("close", release);
|
|
2686
|
+
result.body.once("end", release);
|
|
2687
|
+
}
|
|
2688
|
+
return result;
|
|
2689
|
+
} finally {
|
|
2690
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2691
|
+
if (!retained) release();
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
streamStats() {
|
|
2695
|
+
return {
|
|
2696
|
+
activeStreams: [...this.streams.values()].filter((live) => !live.ended).length,
|
|
2697
|
+
maxBufferedBytes: this.maxBufferedBytes,
|
|
2698
|
+
cancelledStreams: this.cancelledStreams,
|
|
2699
|
+
bufferedBytes: this.totalUnacked()
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
/** Graceful effects cleanup followed by termination; completion requires actual exit. */
|
|
2703
|
+
async dispose() {
|
|
2704
|
+
if (!this.available()) {
|
|
2705
|
+
await this.terminate("dispose-unavailable");
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
this.disposed = true;
|
|
2709
|
+
let timer;
|
|
2710
|
+
try {
|
|
2711
|
+
const reply = new Promise((resolve, reject) => {
|
|
2712
|
+
this.pending.set(LIFECYCLE_ID, {
|
|
2713
|
+
resolve,
|
|
2714
|
+
reject,
|
|
2715
|
+
cleanup: () => void 0
|
|
2716
|
+
});
|
|
2717
|
+
this.post({
|
|
2718
|
+
kind: "dispose",
|
|
2719
|
+
id: LIFECYCLE_ID
|
|
2720
|
+
});
|
|
2721
|
+
});
|
|
2722
|
+
await Promise.race([reply, new Promise((resolve) => {
|
|
2723
|
+
timer = setTimeout(resolve, this.limits.disposeGraceMs);
|
|
2724
|
+
})]);
|
|
2725
|
+
} finally {
|
|
2726
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2727
|
+
await this.terminate("disposed");
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
whenExited() {
|
|
2731
|
+
return this.exited;
|
|
2732
|
+
}
|
|
2733
|
+
/** Stop admission now, retain execution/budget ownership until confirmed thread exit. */
|
|
2734
|
+
terminate(reason) {
|
|
2735
|
+
this.markUnavailable();
|
|
2736
|
+
this.termination ??= (async () => {
|
|
2737
|
+
if (!this.dead) await this.worker.terminate();
|
|
2738
|
+
await this.exited;
|
|
2739
|
+
})();
|
|
2740
|
+
this.termination.catch(() => void 0);
|
|
2741
|
+
return this.termination;
|
|
2742
|
+
}
|
|
2743
|
+
unavailable() {
|
|
2744
|
+
return new MobileExtensionError("extension_host_unavailable", `extension ${this.options.manifest.id} worker is unavailable; explicit recovery is required`, 503);
|
|
2745
|
+
}
|
|
2746
|
+
markUnavailable() {
|
|
2747
|
+
if (this.unavailableController.signal.aborted) return;
|
|
2748
|
+
const error = this.unavailable();
|
|
2749
|
+
this.unavailableController.abort(error);
|
|
2750
|
+
try {
|
|
2751
|
+
this.options.onUnavailable?.(error);
|
|
2752
|
+
} catch (cause) {}
|
|
2753
|
+
}
|
|
2754
|
+
armExecutionGrace(id) {
|
|
2755
|
+
if (this.executionGrace.has(id) || !this.available()) return;
|
|
2756
|
+
const timer = setTimeout(() => {
|
|
2757
|
+
this.executionGrace.delete(id);
|
|
2758
|
+
if (this.pending.has(id) || this.routePending.has(id)) this.terminate("cancel-unresponsive");
|
|
2759
|
+
}, this.limits.cancelGraceMs);
|
|
2760
|
+
timer.unref();
|
|
2761
|
+
this.executionGrace.set(id, timer);
|
|
2762
|
+
}
|
|
2763
|
+
settleExecutionGrace(id) {
|
|
2764
|
+
const timer = this.executionGrace.get(id);
|
|
2765
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2766
|
+
this.executionGrace.delete(id);
|
|
2767
|
+
}
|
|
2768
|
+
post(message) {
|
|
2769
|
+
if (!this.dead && this.termination === void 0) this.worker.postMessage(message);
|
|
2770
|
+
}
|
|
2771
|
+
handleMessage(raw) {
|
|
2772
|
+
if (this.dead || raw === null || typeof raw !== "object") return;
|
|
2773
|
+
const message = raw;
|
|
2774
|
+
if (message.kind === "log") {
|
|
2775
|
+
if (message.level !== "debug" && message.level !== "info" && message.level !== "warn" && message.level !== "error") return;
|
|
2776
|
+
if (!Array.isArray(message.args) || message.args.some((arg) => typeof arg !== "string")) return;
|
|
2777
|
+
if (Date.now() - this.logWindowStart >= this.limits.logWindowMs) {
|
|
2778
|
+
this.logWindowStart = Date.now();
|
|
2779
|
+
this.logCount = 0;
|
|
2780
|
+
}
|
|
2781
|
+
if (++this.logCount > this.limits.logMessagesPerWindow || Buffer.byteLength(message.args.join("")) > this.limits.logMessageBytes) return;
|
|
2782
|
+
try {
|
|
2783
|
+
this.options.logger[message.level](...message.args);
|
|
2784
|
+
} catch (error) {}
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
if (typeof message.runtimeId !== "string") return;
|
|
2788
|
+
if (this.activation !== void 0 && message.runtimeId !== this.activation.runtimeId) return;
|
|
2789
|
+
if (message.kind === "activated") {
|
|
2790
|
+
if (this.activation !== void 0 || !Array.isArray(message.actions) || !Array.isArray(message.routes)) throw new Error("invalid activation metadata");
|
|
2791
|
+
const actions = message.actions;
|
|
2792
|
+
const routes = message.routes;
|
|
2793
|
+
if (actions.length + routes.length > this.limits.maxRegistrations) throw new Error("worker registration limit exceeded");
|
|
2794
|
+
for (const action of actions) if (typeof action?.name !== "string" || !/^[a-z][a-z0-9-]{0,63}$/u.test(action.name)) throw new Error("invalid action metadata");
|
|
2795
|
+
for (const route of routes) if (typeof route?.method !== "string" || typeof route.path !== "string") throw new Error("invalid route metadata");
|
|
2796
|
+
this.activation = Object.freeze({
|
|
2797
|
+
runtimeId: message.runtimeId,
|
|
2798
|
+
actions: Object.freeze(actions),
|
|
2799
|
+
routes: Object.freeze(routes)
|
|
2800
|
+
});
|
|
2801
|
+
const pending = this.pending.get(LIFECYCLE_ID);
|
|
2802
|
+
this.pending.delete(LIFECYCLE_ID);
|
|
2803
|
+
pending?.cleanup();
|
|
2804
|
+
pending?.resolve(new PreparedJsonResult(Buffer.alloc(0)));
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
if (typeof message.id !== "string") return;
|
|
2808
|
+
const id = message.id;
|
|
2809
|
+
if (message.kind === "result" || message.kind === "error") {
|
|
2810
|
+
const pending = this.pending.get(id);
|
|
2811
|
+
this.pending.delete(id);
|
|
2812
|
+
pending?.cleanup();
|
|
2813
|
+
if (message.kind === "result") {
|
|
2814
|
+
if (!(message.bytes instanceof Uint8Array) || message.bytes.byteLength > this.limits.resultMaxBytes) {
|
|
2815
|
+
pending?.reject(new MobileExtensionError("extension_failed", "invalid worker result", 500));
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
pending?.resolve(new PreparedJsonResult(Buffer.from(message.bytes)));
|
|
2819
|
+
} else {
|
|
2820
|
+
const error = new MobileExtensionError(typeof message.code === "string" ? message.code.slice(0, 64) : "extension_failed", typeof message.message === "string" ? message.message.slice(0, 500) : "extension failed", typeof message.status === "number" && Number.isInteger(message.status) && message.status >= 400 && message.status <= 599 ? message.status : 500);
|
|
2821
|
+
pending?.reject(error);
|
|
2822
|
+
const route = this.routePending.get(id);
|
|
2823
|
+
this.routePending.delete(id);
|
|
2824
|
+
this.detachedRoutes.delete(id);
|
|
2825
|
+
route?.cleanup();
|
|
2826
|
+
route?.reject(error);
|
|
2827
|
+
}
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
if (message.kind === "route-response" || message.kind === "stream-start") {
|
|
2831
|
+
const route = this.routePending.get(id);
|
|
2832
|
+
if (route === void 0) {
|
|
2833
|
+
if (message.kind === "stream-start") this.post({
|
|
2834
|
+
kind: "stream-cancel",
|
|
2835
|
+
id
|
|
2836
|
+
});
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (message.kind === "stream-start" && this.detachedRoutes.has(id)) {
|
|
2840
|
+
this.cancelledStreams += 1;
|
|
2841
|
+
this.post({
|
|
2842
|
+
kind: "stream-cancel",
|
|
2843
|
+
id
|
|
2844
|
+
});
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
const response = responseMetadata(message);
|
|
2848
|
+
this.routePending.delete(id);
|
|
2849
|
+
this.detachedRoutes.delete(id);
|
|
2850
|
+
if (message.kind === "route-response") {
|
|
2851
|
+
route.cleanup();
|
|
2852
|
+
if (!(message.bytes instanceof Uint8Array) || message.bytes.byteLength > this.limits.resultMaxBytes) {
|
|
2853
|
+
route.reject(new MobileExtensionError("invalid_route_response", "invalid worker route response", 500));
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
route.resolve({
|
|
2857
|
+
...response,
|
|
2858
|
+
body: Buffer.from(message.bytes)
|
|
2859
|
+
});
|
|
2860
|
+
} else {
|
|
2861
|
+
this.settleExecutionGrace(id);
|
|
2862
|
+
const live = this.openStream(id, route.cleanup);
|
|
2863
|
+
route.resolve({
|
|
2864
|
+
...response,
|
|
2865
|
+
body: live.bridge
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
if (message.kind === "stream-chunk") {
|
|
2871
|
+
const live = this.streams.get(id);
|
|
2872
|
+
if (live === void 0 || live.ended || live.bridge.destroyed) return;
|
|
2873
|
+
if (!(message.bytes instanceof Uint8Array) || message.bytes.byteLength > this.limits.streamChunkBytes || live.unacked + message.bytes.byteLength > this.limits.streamWindowBytes || this.totalUnacked() + message.bytes.byteLength > this.limits.streamAggregateBytes) throw new Error("worker exceeded stream credit");
|
|
2874
|
+
live.unacked += message.bytes.byteLength;
|
|
2875
|
+
this.maxBufferedBytes = Math.max(this.maxBufferedBytes, this.totalUnacked());
|
|
2876
|
+
live.bridge.push(Buffer.from(message.bytes));
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
if (message.kind === "stream-end" || message.kind === "stream-error") {
|
|
2880
|
+
this.settleExecutionGrace(id);
|
|
2881
|
+
const detached = this.routePending.get(id);
|
|
2882
|
+
if (this.detachedRoutes.delete(id)) {
|
|
2883
|
+
this.routePending.delete(id);
|
|
2884
|
+
detached?.cleanup();
|
|
2885
|
+
detached?.reject(new MobileExtensionError("extension_cancelled", "caller detached", 409));
|
|
2886
|
+
}
|
|
2887
|
+
const live = this.streams.get(id);
|
|
2888
|
+
if (live === void 0) return;
|
|
2889
|
+
live.ended = true;
|
|
2890
|
+
if (live.cancelTimer !== void 0) clearTimeout(live.cancelTimer);
|
|
2891
|
+
if (message.kind === "stream-error") live.bridge.destroy(new Error(typeof message.message === "string" ? message.message.slice(0, 500) : "worker stream failed"));
|
|
2892
|
+
else if (live.bridge.destroyed) this.streams.delete(id);
|
|
2893
|
+
else live.bridge.push(null);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
totalUnacked() {
|
|
2897
|
+
let total = 0;
|
|
2898
|
+
for (const live of this.streams.values()) total += live.unacked;
|
|
2899
|
+
return total;
|
|
2900
|
+
}
|
|
2901
|
+
openStream(id, cleanup) {
|
|
2902
|
+
const live = {
|
|
2903
|
+
bridge: new WorkerStreamBridge((bytes) => {
|
|
2904
|
+
live.unacked -= bytes;
|
|
2905
|
+
this.post({
|
|
2906
|
+
kind: "stream-ack",
|
|
2907
|
+
id,
|
|
2908
|
+
bytes
|
|
2909
|
+
});
|
|
2910
|
+
}),
|
|
2911
|
+
unacked: 0,
|
|
2912
|
+
ended: false
|
|
2913
|
+
};
|
|
2914
|
+
live.bridge.on("error", () => void 0);
|
|
2915
|
+
const settle = () => {
|
|
2916
|
+
cleanup();
|
|
2917
|
+
if (live.ended) {
|
|
2918
|
+
if (live.unacked > 0) this.post({
|
|
2919
|
+
kind: "stream-cancel",
|
|
2920
|
+
id
|
|
2921
|
+
});
|
|
2922
|
+
this.streams.delete(id);
|
|
2923
|
+
} else this.cancelStream(id);
|
|
2924
|
+
};
|
|
2925
|
+
live.bridge.once("end", settle);
|
|
2926
|
+
live.bridge.once("close", settle);
|
|
2927
|
+
this.streams.set(id, live);
|
|
2928
|
+
return live;
|
|
2929
|
+
}
|
|
2930
|
+
cancelStream(id) {
|
|
2931
|
+
const live = this.streams.get(id);
|
|
2932
|
+
if (live === void 0 || live.ended || live.cancelTimer !== void 0) return;
|
|
2933
|
+
this.cancelledStreams += 1;
|
|
2934
|
+
this.post({
|
|
2935
|
+
kind: "stream-cancel",
|
|
2936
|
+
id
|
|
2937
|
+
});
|
|
2938
|
+
live.cancelTimer = setTimeout(() => {
|
|
2939
|
+
if (!live.ended) this.terminate("stream-cancel-unresponsive");
|
|
2940
|
+
}, this.limits.streamCancelGraceMs);
|
|
2941
|
+
live.cancelTimer.unref();
|
|
2942
|
+
live.bridge.destroy();
|
|
2943
|
+
}
|
|
2944
|
+
};
|
|
2945
|
+
function responseMetadata(message) {
|
|
2946
|
+
const headers = {};
|
|
2947
|
+
let bytes = typeof message.contentType === "string" ? Buffer.byteLength(message.contentType) : 0;
|
|
2948
|
+
if (message.headers !== void 0) {
|
|
2949
|
+
if (message.headers === null || typeof message.headers !== "object" || Array.isArray(message.headers)) throw new Error("invalid response headers");
|
|
2950
|
+
const entries = Object.entries(message.headers);
|
|
2951
|
+
if (entries.length > 3) throw new Error("invalid response header count");
|
|
2952
|
+
for (const [key, value] of entries) {
|
|
2953
|
+
if (typeof value !== "string" || !/^(?:content-disposition|cache-control|etag)$/iu.test(key)) throw new Error("invalid response header");
|
|
2954
|
+
bytes += Buffer.byteLength(key) + Buffer.byteLength(value);
|
|
2955
|
+
if (bytes > 16384) throw new Error("invalid response header bytes");
|
|
2956
|
+
headers[key] = value;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
return {
|
|
2960
|
+
...typeof message.status === "number" ? { status: message.status } : {},
|
|
2961
|
+
...typeof message.contentType === "string" ? { contentType: message.contentType } : {},
|
|
2962
|
+
...message.headers === void 0 ? {} : { headers }
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
/** Credit belongs to bytes actually removed by the consumer, including reads after source EOF. */
|
|
2966
|
+
var WorkerStreamBridge = class extends Readable {
|
|
2967
|
+
consume;
|
|
2968
|
+
constructor(consume) {
|
|
2969
|
+
super({ highWaterMark: 16384 });
|
|
2970
|
+
this.consume = consume;
|
|
2971
|
+
}
|
|
2972
|
+
_read() {}
|
|
2973
|
+
emit(event, ...args) {
|
|
2974
|
+
if (event === "data" && Buffer.isBuffer(args[0])) this.consume(args[0].byteLength);
|
|
2975
|
+
return super.emit(event, ...args);
|
|
2976
|
+
}
|
|
2977
|
+
};
|
|
2978
|
+
//#endregion
|
|
2261
2979
|
//#region src/extensions.ts
|
|
2262
2980
|
/** Maximum sizes enforced at the local-extension filesystem boundary. */
|
|
2263
2981
|
const EXTENSION_LIMITS = Object.freeze({
|
|
@@ -2599,6 +3317,11 @@ var MobileAccessService = class extends Service {
|
|
|
2599
3317
|
localRefreshAbort;
|
|
2600
3318
|
localLifecycle = 0;
|
|
2601
3319
|
localClosed = true;
|
|
3320
|
+
localExecution;
|
|
3321
|
+
localExecutionKey;
|
|
3322
|
+
workerBudget;
|
|
3323
|
+
disposal = /* @__PURE__ */ new Set();
|
|
3324
|
+
recoveries = /* @__PURE__ */ new Map();
|
|
2602
3325
|
constructor(ctx) {
|
|
2603
3326
|
super(ctx, "mobileAccess");
|
|
2604
3327
|
}
|
|
@@ -2660,10 +3383,65 @@ var MobileAccessService = class extends Service {
|
|
|
2660
3383
|
}
|
|
2661
3384
|
/** Return loaded and failed local extension counts without exposing host errors. */
|
|
2662
3385
|
status() {
|
|
3386
|
+
const unavailable = [...this.local.values()].filter((entry) => entry.worker !== void 0 && !entry.worker.available()).length;
|
|
2663
3387
|
return Object.freeze({
|
|
2664
|
-
loaded: this.registered.size + this.local.size,
|
|
2665
|
-
failed: this.failures.size
|
|
3388
|
+
loaded: this.registered.size + this.local.size - unavailable,
|
|
3389
|
+
failed: this.failures.size + unavailable
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
/** Local Host status does not reveal filesystem paths, credentials or extension errors. */
|
|
3393
|
+
hostStatus() {
|
|
3394
|
+
const limits = resolveHostExecution(this.localExecution?.hostExecution);
|
|
3395
|
+
return {
|
|
3396
|
+
hosts: [...this.local.values()].map((entry) => ({
|
|
3397
|
+
id: entry.manifest.id,
|
|
3398
|
+
name: entry.manifest.name,
|
|
3399
|
+
mode: entry.worker === void 0 ? "in-process" : "worker",
|
|
3400
|
+
state: entry.worker === void 0 || entry.worker.available() ? "ready" : "unavailable",
|
|
3401
|
+
generation: entry.digest
|
|
3402
|
+
})),
|
|
3403
|
+
limits: {
|
|
3404
|
+
maxWorkers: limits.maxWorkers,
|
|
3405
|
+
workers: this.workerBudget?.current ?? 0
|
|
3406
|
+
}
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
/** Explicitly replace one unavailable worker without replaying requests or changing its files. */
|
|
3410
|
+
recoverExtension(id) {
|
|
3411
|
+
assertExtensionId(id);
|
|
3412
|
+
const existing = this.recoveries.get(id);
|
|
3413
|
+
if (existing !== void 0) return existing;
|
|
3414
|
+
const recovering = (async () => {
|
|
3415
|
+
if (this.localRefreshing !== void 0) await this.localRefreshing;
|
|
3416
|
+
const previous = this.local.get(id);
|
|
3417
|
+
if (this.localClosed || previous?.worker === void 0 || this.localContext === void 0) throw new MobileExtensionError("extension_recovery_unavailable", "local worker recovery is unavailable", 409);
|
|
3418
|
+
if (previous.worker.available()) throw new MobileExtensionError("extension_host_running", "extension worker is already running", 409);
|
|
3419
|
+
await previous.worker.terminate("explicit-recovery");
|
|
3420
|
+
const lifecycle = this.localLifecycle;
|
|
3421
|
+
const fingerprint = await extensionFingerprint(previous.directory);
|
|
3422
|
+
const fresh = await loadLocalExtension(previous.directory, this.localContext, fingerprint, void 0, this.localExecution);
|
|
3423
|
+
try {
|
|
3424
|
+
const confirmed = await extensionFingerprint(previous.directory);
|
|
3425
|
+
if (this.localClosed || this.localLifecycle !== lifecycle || this.local.get(id) !== previous || confirmed.digest !== fingerprint.digest) throw new MobileExtensionError("extension_changed_during_activation", "extension changed during recovery", 409);
|
|
3426
|
+
} catch (error) {
|
|
3427
|
+
await abortAndDisposeLocal([fresh]);
|
|
3428
|
+
throw error;
|
|
3429
|
+
}
|
|
3430
|
+
this.local.set(id, fresh);
|
|
3431
|
+
this.failures.delete(id);
|
|
3432
|
+
this.queueDisposal([previous]);
|
|
3433
|
+
this.updateContentHash();
|
|
3434
|
+
})().finally(() => {
|
|
3435
|
+
if (this.recoveries.get(id) === recovering) this.recoveries.delete(id);
|
|
3436
|
+
});
|
|
3437
|
+
this.recoveries.set(id, recovering);
|
|
3438
|
+
return recovering;
|
|
3439
|
+
}
|
|
3440
|
+
queueDisposal(entries) {
|
|
3441
|
+
const pending = abortAndDisposeLocal(entries).finally(() => {
|
|
3442
|
+
this.disposal.delete(pending);
|
|
2666
3443
|
});
|
|
3444
|
+
this.disposal.add(pending);
|
|
2667
3445
|
}
|
|
2668
3446
|
/** Locate one active extension. */
|
|
2669
3447
|
extension(id, generation) {
|
|
@@ -2753,7 +3531,7 @@ var MobileAccessService = class extends Service {
|
|
|
2753
3531
|
});
|
|
2754
3532
|
}, {
|
|
2755
3533
|
external: external?.signal ?? context.signal,
|
|
2756
|
-
timeoutMs: action.timeoutMs ?? 3e4,
|
|
3534
|
+
timeoutMs: action.timeoutMs ?? ("host" in extension && extension.worker !== void 0 ? resolveHostExecution(this.localExecution?.hostExecution).operationTimeoutMs : 3e4),
|
|
2757
3535
|
timeoutError: () => new MobileExtensionError("extension_action_timeout", `extension ${id} action ${actionName} timed out`, 500),
|
|
2758
3536
|
onDetach: () => {
|
|
2759
3537
|
this.abandoned.set(id, (this.abandoned.get(id) ?? 0) + 1);
|
|
@@ -2791,7 +3569,7 @@ var MobileAccessService = class extends Service {
|
|
|
2791
3569
|
signal
|
|
2792
3570
|
}), {
|
|
2793
3571
|
external: external?.signal ?? request.signal,
|
|
2794
|
-
timeoutMs: route.timeoutMs ?? 3e4,
|
|
3572
|
+
timeoutMs: route.timeoutMs ?? ("host" in extension && extension.worker !== void 0 ? resolveHostExecution(this.localExecution?.hostExecution).operationTimeoutMs : 3e4),
|
|
2795
3573
|
timeoutError: () => new MobileExtensionError("extension_route_timeout", `extension ${id} route ${method} ${pathname} timed out`, 500),
|
|
2796
3574
|
onDetach: () => {
|
|
2797
3575
|
this.abandoned.set(id, (this.abandoned.get(id) ?? 0) + 1);
|
|
@@ -2823,13 +3601,24 @@ var MobileAccessService = class extends Service {
|
|
|
2823
3601
|
}
|
|
2824
3602
|
}
|
|
2825
3603
|
/** Start the local directory watcher; an absent directory is intentionally inert. */
|
|
2826
|
-
async startLocal(root, context) {
|
|
3604
|
+
async startLocal(root, context, options) {
|
|
2827
3605
|
const targetRoot = resolve(root);
|
|
2828
|
-
|
|
3606
|
+
const limits = resolveHostExecution(options?.hostExecution);
|
|
3607
|
+
const executionKey = JSON.stringify({
|
|
3608
|
+
limits,
|
|
3609
|
+
module: options?.hostExecution?.workerModule
|
|
3610
|
+
});
|
|
3611
|
+
if (this.localRoot !== void 0 && (resolve(this.localRoot) !== targetRoot || this.localExecutionKey !== executionKey)) await this.stopLocal();
|
|
2829
3612
|
if (this.localTimer !== void 0) clearInterval(this.localTimer);
|
|
2830
3613
|
const lifecycle = ++this.localLifecycle;
|
|
3614
|
+
this.workerBudget ??= new ExtensionWorkerBudget(limits.maxWorkers);
|
|
2831
3615
|
this.localRoot = targetRoot;
|
|
2832
3616
|
this.localContext = context;
|
|
3617
|
+
this.localExecution = {
|
|
3618
|
+
...options,
|
|
3619
|
+
workerBudget: this.workerBudget
|
|
3620
|
+
};
|
|
3621
|
+
this.localExecutionKey = executionKey;
|
|
2833
3622
|
this.localClosed = false;
|
|
2834
3623
|
await mkdir(this.localRoot, { recursive: true });
|
|
2835
3624
|
if (this.localClosed || this.localLifecycle !== lifecycle || this.localRoot !== targetRoot || this.localContext !== context) return;
|
|
@@ -2863,6 +3652,8 @@ var MobileAccessService = class extends Service {
|
|
|
2863
3652
|
this.failures.clear();
|
|
2864
3653
|
this.updateContentHash();
|
|
2865
3654
|
await abortAndDisposeLocal(late);
|
|
3655
|
+
await Promise.allSettled([...this.disposal, ...this.recoveries.values()]);
|
|
3656
|
+
this.workerBudget = void 0;
|
|
2866
3657
|
if (this.localTimer !== void 0) clearInterval(this.localTimer);
|
|
2867
3658
|
this.localTimer = void 0;
|
|
2868
3659
|
}
|
|
@@ -2906,7 +3697,7 @@ var MobileAccessService = class extends Service {
|
|
|
2906
3697
|
const previous = current?.digest === fingerprint.digest ? current : retired?.digest === fingerprint.digest ? retired : void 0;
|
|
2907
3698
|
if (previous?.digest === fingerprint.digest) staged.push(previous);
|
|
2908
3699
|
else {
|
|
2909
|
-
const fresh = await loadLocalExtension(directory, this.localContext, fingerprint, signal);
|
|
3700
|
+
const fresh = await loadLocalExtension(directory, this.localContext, fingerprint, signal, this.localExecution);
|
|
2910
3701
|
try {
|
|
2911
3702
|
signal.throwIfAborted();
|
|
2912
3703
|
if ((await extensionFingerprint(directory)).digest !== fingerprint.digest) throw new MobileExtensionError("extension_changed_during_activation", `extension ${fingerprint.manifest.id} changed during activation`, 409);
|
|
@@ -2957,13 +3748,15 @@ var MobileAccessService = class extends Service {
|
|
|
2957
3748
|
for (const name of names) this.failures.delete(name);
|
|
2958
3749
|
for (const failure of this.failures.keys()) if (failure !== "local" && !names.includes(failure)) this.failures.delete(failure);
|
|
2959
3750
|
this.failures.delete("local");
|
|
2960
|
-
if (removed.length > 0)
|
|
3751
|
+
if (removed.length > 0) this.queueDisposal(removed);
|
|
2961
3752
|
this.updateContentHash();
|
|
2962
3753
|
} catch (error) {
|
|
2963
3754
|
await abortAndDisposeLocal(stagedFresh);
|
|
2964
3755
|
if (this.localClosed || signal.aborted) return;
|
|
2965
3756
|
const message = error instanceof Error ? error.message : String(error);
|
|
3757
|
+
const changedFailure = this.failures.get(failingName) !== message;
|
|
2966
3758
|
this.failures.set(failingName, message);
|
|
3759
|
+
if (changedFailure && error instanceof MobileExtensionError && error.code === "extension_worker_heap_override") this.ctx.logger.warn("local extension worker refused: %s", error.message);
|
|
2967
3760
|
if (!(error instanceof MobileExtensionError)) this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
2968
3761
|
}
|
|
2969
3762
|
}
|
|
@@ -2973,12 +3766,12 @@ var MobileAccessService = class extends Service {
|
|
|
2973
3766
|
if (previous !== void 0) {
|
|
2974
3767
|
clearTimeout(previous.timer);
|
|
2975
3768
|
this.retired.delete(active.manifest.id);
|
|
2976
|
-
|
|
3769
|
+
this.queueDisposal([previous.active]);
|
|
2977
3770
|
}
|
|
2978
3771
|
const timer = setTimeout(() => {
|
|
2979
3772
|
if (this.retired.get(active.manifest.id)?.active !== active) return;
|
|
2980
3773
|
this.retired.delete(active.manifest.id);
|
|
2981
|
-
|
|
3774
|
+
this.queueDisposal([active]);
|
|
2982
3775
|
}, RETIRED_GENERATION_TTL_MS);
|
|
2983
3776
|
timer.unref();
|
|
2984
3777
|
this.retired.set(active.manifest.id, {
|
|
@@ -3100,8 +3893,9 @@ async function abortAndDisposeLocal(entries) {
|
|
|
3100
3893
|
pending.push(...invokeCleanups(entry.cleanups));
|
|
3101
3894
|
}
|
|
3102
3895
|
await settleBounded(pending, HOST_TEARDOWN_TIMEOUT_MS);
|
|
3896
|
+
await Promise.all(entries.flatMap((entry) => entry.worker === void 0 ? [] : [entry.worker.dispose()]));
|
|
3103
3897
|
}
|
|
3104
|
-
async function loadLocalExtension(directory, context, known, parentSignal) {
|
|
3898
|
+
async function loadLocalExtension(directory, context, known, parentSignal, execution) {
|
|
3105
3899
|
const root = await realExtensionRoot(directory);
|
|
3106
3900
|
const manifestFile = await regularFile$3(join(root, "extension.json"), EXTENSION_LIMITS.manifest, "extension.json");
|
|
3107
3901
|
const manifest = known?.manifest ?? parseExtensionManifest(JSON.parse(await readFile(manifestFile.path, "utf8")));
|
|
@@ -3153,28 +3947,73 @@ async function loadLocalExtension(directory, context, known, parentSignal) {
|
|
|
3153
3947
|
}
|
|
3154
3948
|
};
|
|
3155
3949
|
try {
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3950
|
+
const limits = resolveHostExecution(execution?.hostExecution);
|
|
3951
|
+
const worker = limits.mode === "worker" && (limits.extensions === void 0 || limits.extensions.includes(manifest.id)) && hostFile !== void 0 ? new ExtensionWorkerHost({
|
|
3952
|
+
workerModule: execution?.hostExecution?.workerModule ?? defaultExtensionWorkerModule(),
|
|
3953
|
+
hostFile,
|
|
3954
|
+
manifest,
|
|
3955
|
+
generation: known?.digest ?? createHash("sha256").update(manifest.id).digest("hex"),
|
|
3956
|
+
logger: workerLoggerAdapter(context),
|
|
3957
|
+
limits,
|
|
3958
|
+
...execution?.workerBudget === void 0 ? {} : { budget: execution.workerBudget },
|
|
3959
|
+
onUnavailable: (error) => {
|
|
3960
|
+
controller.abort(error);
|
|
3961
|
+
}
|
|
3962
|
+
}) : void 0;
|
|
3963
|
+
let host;
|
|
3964
|
+
if (worker !== void 0) {
|
|
3965
|
+
try {
|
|
3966
|
+
await withActivationTimeout(worker.activate(), manifest.id, controller.signal);
|
|
3967
|
+
} catch (error) {
|
|
3968
|
+
await worker.terminate("activation-failed");
|
|
3969
|
+
throw error;
|
|
3970
|
+
}
|
|
3971
|
+
const proxyActions = {};
|
|
3972
|
+
for (const metadata of worker.actionMetadata()) proxyActions[metadata.name] = {
|
|
3973
|
+
...metadata.timeoutMs === void 0 ? {} : { timeoutMs: metadata.timeoutMs },
|
|
3974
|
+
run: (actionContext, input) => worker.invokeExecution(metadata.name, input, actionContext)
|
|
3975
|
+
};
|
|
3976
|
+
const proxyRoutes = worker.routeMetadata().map((metadata, index) => ({
|
|
3977
|
+
method: metadata.method,
|
|
3978
|
+
path: metadata.path,
|
|
3979
|
+
...metadata.kind === void 0 ? {} : { kind: metadata.kind },
|
|
3980
|
+
...metadata.timeoutMs === void 0 ? {} : { timeoutMs: metadata.timeoutMs },
|
|
3981
|
+
handle: (request) => worker.handleRouteExecution(index, request)
|
|
3982
|
+
}));
|
|
3983
|
+
try {
|
|
3984
|
+
host = validateDefinition({
|
|
3985
|
+
...manifest,
|
|
3986
|
+
...Object.keys(proxyActions).length === 0 ? {} : { actions: proxyActions },
|
|
3987
|
+
...proxyRoutes.length === 0 ? {} : { routes: proxyRoutes }
|
|
3988
|
+
});
|
|
3989
|
+
} catch (error) {
|
|
3990
|
+
await worker.terminate("definition-invalid");
|
|
3991
|
+
throw error;
|
|
3992
|
+
}
|
|
3993
|
+
} else {
|
|
3994
|
+
const activate = async () => {
|
|
3160
3995
|
controller.signal.throwIfAborted();
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3996
|
+
if (hostFile !== void 0) {
|
|
3997
|
+
const digest = createHash("sha256").update(await readFile(hostFile)).digest("hex");
|
|
3998
|
+
controller.signal.throwIfAborted();
|
|
3999
|
+
let imported;
|
|
4000
|
+
try {
|
|
4001
|
+
imported = await import(`${pathToFileURL(hostFile).href}?dsh_generation=${digest}`);
|
|
4002
|
+
} catch {
|
|
4003
|
+
throw new MobileExtensionError("host_load_failed", `could not load ${manifest.id}/host.mjs`, 500);
|
|
4004
|
+
}
|
|
4005
|
+
controller.signal.throwIfAborted();
|
|
4006
|
+
if (imported.default !== void 0) await imported.default(api);
|
|
3166
4007
|
}
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
routes
|
|
3177
|
-
});
|
|
4008
|
+
await Promise.all(pendingEffects);
|
|
4009
|
+
};
|
|
4010
|
+
await withActivationTimeout(activate(), manifest.id, controller.signal);
|
|
4011
|
+
host = validateDefinition({
|
|
4012
|
+
...manifest,
|
|
4013
|
+
actions,
|
|
4014
|
+
routes
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
3178
4017
|
activationOpen = false;
|
|
3179
4018
|
const digest = known?.digest ?? createHash("sha256").update(manifest.id).digest("hex");
|
|
3180
4019
|
return Object.freeze({
|
|
@@ -3186,7 +4025,8 @@ async function loadLocalExtension(directory, context, known, parentSignal) {
|
|
|
3186
4025
|
host,
|
|
3187
4026
|
controller,
|
|
3188
4027
|
cleanups: Object.freeze(cleanups),
|
|
3189
|
-
digest
|
|
4028
|
+
digest,
|
|
4029
|
+
...worker === void 0 ? {} : { worker }
|
|
3190
4030
|
});
|
|
3191
4031
|
} catch (error) {
|
|
3192
4032
|
activationOpen = false;
|
|
@@ -3198,6 +4038,23 @@ async function loadLocalExtension(directory, context, known, parentSignal) {
|
|
|
3198
4038
|
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
3199
4039
|
}
|
|
3200
4040
|
}
|
|
4041
|
+
/** Forward the worker's logger-only context adapter into the plugin logger. */
|
|
4042
|
+
function workerLoggerAdapter(context) {
|
|
4043
|
+
return {
|
|
4044
|
+
debug: (...args) => {
|
|
4045
|
+
context.logger.debug(...args);
|
|
4046
|
+
},
|
|
4047
|
+
info: (...args) => {
|
|
4048
|
+
context.logger.info(...args);
|
|
4049
|
+
},
|
|
4050
|
+
warn: (...args) => {
|
|
4051
|
+
context.logger.warn(...args);
|
|
4052
|
+
},
|
|
4053
|
+
error: (...args) => {
|
|
4054
|
+
context.logger.error(...args);
|
|
4055
|
+
}
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
3201
4058
|
/** Construct the service in a Cordis plugin without importing DSH internals. */
|
|
3202
4059
|
function createMobileAccessService(ctx) {
|
|
3203
4060
|
return new MobileAccessService(ctx);
|
|
@@ -4992,7 +5849,8 @@ var MobileAccessGateway = class {
|
|
|
4992
5849
|
}
|
|
4993
5850
|
const stockFrontend = new URL(target.raw, this.address().origin).searchParams.get("frontend") === "stock";
|
|
4994
5851
|
const acceptsHtml = request.headers.accept?.split(",").some((value) => value.trim().split(";", 1)[0] === "text/html") ?? false;
|
|
4995
|
-
|
|
5852
|
+
const frontendDocument = target.decodedPathname === "/" || target.decodedPathname === "/index.html";
|
|
5853
|
+
if (request.method === "GET" && frontendDocument && acceptsHtml && !stockFrontend) {
|
|
4996
5854
|
await this.proxyMobileIndex(request, response, authorization);
|
|
4997
5855
|
return;
|
|
4998
5856
|
}
|
|
@@ -5101,14 +5959,27 @@ var MobileAccessGateway = class {
|
|
|
5101
5959
|
signal: abort.signal,
|
|
5102
5960
|
deviceId: authorization.deviceId
|
|
5103
5961
|
}, generation);
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5962
|
+
if (isPreparedJson(result)) {
|
|
5963
|
+
if (result.bytes.byteLength > 4194304) throw new MobileExtensionError("extension_result_too_large", "extension result is too large", 500);
|
|
5964
|
+
if (!response.headersSent && !response.destroyed) {
|
|
5965
|
+
const body = Buffer.concat([result.bytes, Buffer.from("\n")]);
|
|
5966
|
+
setSecurityHeaders(response, this.tlsEnabled);
|
|
5967
|
+
response.writeHead(200, {
|
|
5968
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
5969
|
+
"Content-Length": body.byteLength
|
|
5970
|
+
});
|
|
5971
|
+
response.end(body);
|
|
5972
|
+
}
|
|
5973
|
+
} else {
|
|
5974
|
+
let serialized;
|
|
5975
|
+
try {
|
|
5976
|
+
serialized = Buffer.from(JSON.stringify(result));
|
|
5977
|
+
} catch {
|
|
5978
|
+
throw new MobileExtensionError("extension_failed", "extension action failed", 500);
|
|
5979
|
+
}
|
|
5980
|
+
if (serialized.byteLength > 4194304) throw new MobileExtensionError("extension_result_too_large", "extension result is too large", 500);
|
|
5981
|
+
sendJson(response, 200, result, this.tlsEnabled);
|
|
5109
5982
|
}
|
|
5110
|
-
if (serialized.byteLength > 4194304) throw new MobileExtensionError("extension_result_too_large", "extension result is too large", 500);
|
|
5111
|
-
sendJson(response, 200, result, this.tlsEnabled);
|
|
5112
5983
|
} finally {
|
|
5113
5984
|
generationSignal?.removeEventListener("abort", onGenerationAbort);
|
|
5114
5985
|
abort.abort();
|
|
@@ -5371,7 +6242,7 @@ var MobileAccessGateway = class {
|
|
|
5371
6242
|
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
5372
6243
|
port: Number(this.config.upstreamOrigin.port),
|
|
5373
6244
|
method: "GET",
|
|
5374
|
-
path: "/",
|
|
6245
|
+
path: incoming?.url ?? "/",
|
|
5375
6246
|
headers,
|
|
5376
6247
|
agent: false
|
|
5377
6248
|
});
|
|
@@ -6436,12 +7307,180 @@ var MemoryDeviceStore = class {
|
|
|
6436
7307
|
}
|
|
6437
7308
|
};
|
|
6438
7309
|
//#endregion
|
|
6439
|
-
//#region src/
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
const
|
|
6443
|
-
|
|
6444
|
-
const
|
|
7310
|
+
//#region src/managed-files.ts
|
|
7311
|
+
/** Refuse link-shaped ancestors inside an owned state root before accessing a child. */
|
|
7312
|
+
async function assertManagedParents(root, target, prefix) {
|
|
7313
|
+
const parent = resolve(root);
|
|
7314
|
+
const candidate = resolve(target);
|
|
7315
|
+
const offset = relative(parent, candidate);
|
|
7316
|
+
if (!isAbsolute(root) || offset !== "" && (offset.startsWith("..") || isAbsolute(offset))) throw new Error(`${prefix}_path_invalid`);
|
|
7317
|
+
const paths = [parent];
|
|
7318
|
+
let cursor = parent;
|
|
7319
|
+
if (offset !== "") for (const segment of relative(parent, dirname(candidate)).split(/[\\/]/u).filter(Boolean)) {
|
|
7320
|
+
cursor = join(cursor, segment);
|
|
7321
|
+
paths.push(cursor);
|
|
7322
|
+
}
|
|
7323
|
+
for (const path of paths) {
|
|
7324
|
+
let entry;
|
|
7325
|
+
try {
|
|
7326
|
+
entry = await lstat(path);
|
|
7327
|
+
} catch (error) {
|
|
7328
|
+
if (error.code === "ENOENT") continue;
|
|
7329
|
+
throw error;
|
|
7330
|
+
}
|
|
7331
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error(`${prefix}_path_invalid`);
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
/** Retry Windows handle contention while checking ownership before every mutation. */
|
|
7335
|
+
async function mutateManagedPath(validate, mutate) {
|
|
7336
|
+
let original;
|
|
7337
|
+
for (let attempt = 0;; attempt++) {
|
|
7338
|
+
await validate();
|
|
7339
|
+
try {
|
|
7340
|
+
await mutate();
|
|
7341
|
+
return;
|
|
7342
|
+
} catch (error) {
|
|
7343
|
+
const code = error.code;
|
|
7344
|
+
if (process.platform !== "win32" || code !== "EPERM" && code !== "EBUSY") throw error;
|
|
7345
|
+
if (attempt === 0) original = error;
|
|
7346
|
+
if (attempt === 7) throw original;
|
|
7347
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(50 * 2 ** attempt, 400)));
|
|
7348
|
+
}
|
|
7349
|
+
}
|
|
7350
|
+
}
|
|
7351
|
+
/** Move only owned paths, retaining parent checks on every Windows retry. */
|
|
7352
|
+
async function renameManagedPath(root, source, destination, prefix) {
|
|
7353
|
+
if (resolve(source) === resolve(root) || resolve(destination) === resolve(root)) throw new Error(`${prefix}_path_invalid`);
|
|
7354
|
+
await mutateManagedPath(async () => {
|
|
7355
|
+
await assertManagedParents(root, source, prefix);
|
|
7356
|
+
await assertManagedParents(root, destination, prefix);
|
|
7357
|
+
}, () => rename(source, destination));
|
|
7358
|
+
}
|
|
7359
|
+
/** Create a real owned directory; secure new directories without changing existing child ACLs. */
|
|
7360
|
+
async function ensureManagedDirectory(root, directory, prefix) {
|
|
7361
|
+
await assertManagedParents(root, directory, prefix);
|
|
7362
|
+
const created = await mkdir(directory, {
|
|
7363
|
+
recursive: true,
|
|
7364
|
+
mode: 448
|
|
7365
|
+
});
|
|
7366
|
+
const entry = await lstat(directory);
|
|
7367
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error(`${prefix}_path_invalid`);
|
|
7368
|
+
if (created !== void 0) await restrictPrivateFile(directory, 448);
|
|
7369
|
+
}
|
|
7370
|
+
/** Delete an owned child tree; remove links themselves and never delete the root. */
|
|
7371
|
+
async function removeManagedTree(root, target, prefix) {
|
|
7372
|
+
if (resolve(target) === resolve(root)) throw new Error(`${prefix}_path_invalid`);
|
|
7373
|
+
await assertManagedParents(root, target, prefix);
|
|
7374
|
+
let entry;
|
|
7375
|
+
try {
|
|
7376
|
+
entry = await lstat(target);
|
|
7377
|
+
} catch (error) {
|
|
7378
|
+
if (error.code === "ENOENT") return;
|
|
7379
|
+
throw error;
|
|
7380
|
+
}
|
|
7381
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
7382
|
+
await mutateManagedPath(() => assertManagedParents(root, target, prefix), () => unlink(target));
|
|
7383
|
+
return;
|
|
7384
|
+
}
|
|
7385
|
+
for (const name of await readdir(target)) await removeManagedTree(root, join(target, name), prefix);
|
|
7386
|
+
await mutateManagedPath(() => assertManagedParents(root, target, prefix), () => rmdir(target));
|
|
7387
|
+
}
|
|
7388
|
+
/** Validate every selected child, then await all deletion work even when one fails. */
|
|
7389
|
+
async function removeManagedPaths(root, paths, prefix) {
|
|
7390
|
+
for (const path of paths) {
|
|
7391
|
+
if (resolve(path) === resolve(root)) throw new Error(`${prefix}_path_invalid`);
|
|
7392
|
+
await assertManagedParents(root, path, prefix);
|
|
7393
|
+
}
|
|
7394
|
+
const errors = (await Promise.allSettled(paths.map((path) => removeManagedTree(root, path, prefix)))).filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
7395
|
+
if (errors.length === 1) throw errors[0];
|
|
7396
|
+
if (errors.length > 1) throw new AggregateError(errors, `${prefix}_cleanup_failed`);
|
|
7397
|
+
}
|
|
7398
|
+
/** Atomically replace one sensitive file beneath real owned directories. */
|
|
7399
|
+
async function writeManagedPrivateFile(root, file, body, prefix) {
|
|
7400
|
+
await ensureManagedDirectory(root, dirname(file), prefix);
|
|
7401
|
+
try {
|
|
7402
|
+
const entry = await lstat(file);
|
|
7403
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error(`${prefix}_config_target_invalid`);
|
|
7404
|
+
} catch (error) {
|
|
7405
|
+
if (error.code !== "ENOENT") throw error;
|
|
7406
|
+
}
|
|
7407
|
+
const temporary = join(dirname(file), "." + basename(file) + "." + randomBytes(12).toString("hex") + ".tmp");
|
|
7408
|
+
let failed = false;
|
|
7409
|
+
try {
|
|
7410
|
+
await writeFile(temporary, body, {
|
|
7411
|
+
encoding: "utf8",
|
|
7412
|
+
flag: "wx",
|
|
7413
|
+
mode: 384
|
|
7414
|
+
});
|
|
7415
|
+
await restrictPrivateFile(temporary);
|
|
7416
|
+
await renameManagedPath(root, temporary, file, prefix);
|
|
7417
|
+
} catch (error) {
|
|
7418
|
+
failed = true;
|
|
7419
|
+
throw error;
|
|
7420
|
+
} finally {
|
|
7421
|
+
try {
|
|
7422
|
+
await removeManagedTree(root, temporary, prefix);
|
|
7423
|
+
} catch (error) {
|
|
7424
|
+
if (!failed) throw error;
|
|
7425
|
+
}
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7428
|
+
/** Promote a verified directory, restoring the previous version if promotion fails. */
|
|
7429
|
+
async function replaceManagedDirectory(root, target, candidate, prefix, operations = {}) {
|
|
7430
|
+
if (resolve(target) === resolve(root) || resolve(candidate) === resolve(root)) throw new Error(`${prefix}_path_invalid`);
|
|
7431
|
+
const move = async (source, destination) => {
|
|
7432
|
+
await assertManagedParents(root, source, prefix);
|
|
7433
|
+
await assertManagedParents(root, destination, prefix);
|
|
7434
|
+
if (operations.move === void 0) await renameManagedPath(root, source, destination, prefix);
|
|
7435
|
+
else await operations.move(source, destination);
|
|
7436
|
+
};
|
|
7437
|
+
const remove = operations.remove ?? ((path) => removeManagedTree(root, path, prefix));
|
|
7438
|
+
const backup = join(dirname(target), `.${basename(target)}.previous-${randomBytes(12).toString("hex")}`);
|
|
7439
|
+
let previous = false;
|
|
7440
|
+
let promoted = false;
|
|
7441
|
+
try {
|
|
7442
|
+
await assertManagedParents(root, join(candidate, "publication-check"), prefix);
|
|
7443
|
+
await assertManagedParents(root, target, prefix);
|
|
7444
|
+
try {
|
|
7445
|
+
const entry = await lstat(target);
|
|
7446
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error(`${prefix}_path_invalid`);
|
|
7447
|
+
await move(target, backup);
|
|
7448
|
+
previous = true;
|
|
7449
|
+
} catch (error) {
|
|
7450
|
+
if (error.code !== "ENOENT") throw error;
|
|
7451
|
+
}
|
|
7452
|
+
try {
|
|
7453
|
+
await move(candidate, target);
|
|
7454
|
+
promoted = true;
|
|
7455
|
+
} catch (error) {
|
|
7456
|
+
if (previous) try {
|
|
7457
|
+
await move(backup, target);
|
|
7458
|
+
previous = false;
|
|
7459
|
+
} catch (restoreError) {
|
|
7460
|
+
throw new AggregateError([error, restoreError], `${prefix}_component_replace_failed`);
|
|
7461
|
+
}
|
|
7462
|
+
throw error;
|
|
7463
|
+
}
|
|
7464
|
+
} finally {
|
|
7465
|
+
try {
|
|
7466
|
+
await remove(candidate);
|
|
7467
|
+
} catch (_error) {
|
|
7468
|
+
process.emitWarning(`${prefix} candidate cleanup failed`, { code: "DSH_MOBILE_COMPONENT_CLEANUP_FAILED" });
|
|
7469
|
+
}
|
|
7470
|
+
if (promoted && previous) try {
|
|
7471
|
+
await remove(backup);
|
|
7472
|
+
} catch (_error) {
|
|
7473
|
+
process.emitWarning(`${prefix} previous component cleanup failed`, { code: "DSH_MOBILE_COMPONENT_CLEANUP_FAILED" });
|
|
7474
|
+
}
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
//#endregion
|
|
7478
|
+
//#region src/frp-component.ts
|
|
7479
|
+
const FRP_VERSION$1 = "0.70.1";
|
|
7480
|
+
const MAX_ARCHIVE_ENTRIES$1 = 128;
|
|
7481
|
+
const MAX_ARCHIVE_LIST_BYTES$1 = 262144;
|
|
7482
|
+
/** Pinned official FRP release metadata for supported desktop targets. */
|
|
7483
|
+
const FRP_COMPONENT_RELEASES = Object.freeze(Object.fromEntries([
|
|
6445
7484
|
{
|
|
6446
7485
|
platform: "win32",
|
|
6447
7486
|
arch: "x64",
|
|
@@ -6510,37 +7549,6 @@ async function regularFile$2(file) {
|
|
|
6510
7549
|
throw error;
|
|
6511
7550
|
}
|
|
6512
7551
|
}
|
|
6513
|
-
async function replaceDirectory(target, candidate) {
|
|
6514
|
-
const backup = `${target}.previous-${randomBytes(12).toString("hex")}`;
|
|
6515
|
-
let previous = false;
|
|
6516
|
-
try {
|
|
6517
|
-
try {
|
|
6518
|
-
await rename(target, backup);
|
|
6519
|
-
previous = true;
|
|
6520
|
-
} catch (error) {
|
|
6521
|
-
if (error.code !== "ENOENT") throw error;
|
|
6522
|
-
}
|
|
6523
|
-
try {
|
|
6524
|
-
await rename(candidate, target);
|
|
6525
|
-
} catch (error) {
|
|
6526
|
-
if (previous) try {
|
|
6527
|
-
await rename(backup, target);
|
|
6528
|
-
} catch (restoreError) {
|
|
6529
|
-
throw new AggregateError([error, restoreError], "frp_component_replace_failed");
|
|
6530
|
-
}
|
|
6531
|
-
throw error;
|
|
6532
|
-
}
|
|
6533
|
-
if (previous) await rm(backup, {
|
|
6534
|
-
recursive: true,
|
|
6535
|
-
force: true
|
|
6536
|
-
});
|
|
6537
|
-
} finally {
|
|
6538
|
-
await rm(candidate, {
|
|
6539
|
-
recursive: true,
|
|
6540
|
-
force: true
|
|
6541
|
-
});
|
|
6542
|
-
}
|
|
6543
|
-
}
|
|
6544
7552
|
function sha256$2(bytes) {
|
|
6545
7553
|
return createHash("sha256").update(bytes).digest("hex");
|
|
6546
7554
|
}
|
|
@@ -6641,6 +7649,7 @@ var FrpComponentManager = class {
|
|
|
6641
7649
|
componentStorage;
|
|
6642
7650
|
logRoot;
|
|
6643
7651
|
stagingRoot;
|
|
7652
|
+
stateDirectory;
|
|
6644
7653
|
artifact;
|
|
6645
7654
|
fetchArtifact;
|
|
6646
7655
|
extractArtifact;
|
|
@@ -6650,8 +7659,9 @@ var FrpComponentManager = class {
|
|
|
6650
7659
|
errorCode;
|
|
6651
7660
|
queue = Promise.resolve();
|
|
6652
7661
|
constructor(options) {
|
|
7662
|
+
if (!isAbsolute(options.stateDirectory)) throw new Error("frp state directory must be absolute");
|
|
6653
7663
|
const stateDirectory = resolve(options.stateDirectory);
|
|
6654
|
-
|
|
7664
|
+
this.stateDirectory = stateDirectory;
|
|
6655
7665
|
const platform = options.platform ?? process.platform;
|
|
6656
7666
|
const arch = options.arch ?? process.arch;
|
|
6657
7667
|
this.artifact = FRP_COMPONENT_RELEASES[`${platform}-${arch}`];
|
|
@@ -6672,13 +7682,19 @@ var FrpComponentManager = class {
|
|
|
6672
7682
|
}
|
|
6673
7683
|
/** Inspect the managed executable without relying on global FRP installations. */
|
|
6674
7684
|
async initialize() {
|
|
6675
|
-
this.installed =
|
|
6676
|
-
this.installedBytes =
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
this.
|
|
6680
|
-
|
|
7685
|
+
this.installed = false;
|
|
7686
|
+
this.installedBytes = 0;
|
|
7687
|
+
this.errorCode = void 0;
|
|
7688
|
+
try {
|
|
7689
|
+
await assertManagedParents(this.stateDirectory, this.executable, "frp");
|
|
7690
|
+
this.installed = await regularFile$2(this.executable);
|
|
7691
|
+
this.installedBytes = this.installed ? (await stat(this.executable)).size : 0;
|
|
7692
|
+
if (this.installed) {
|
|
7693
|
+
if (await this.inspectExecutable(this.executable) !== FRP_VERSION$1) throw new Error("frp_component_version_mismatch");
|
|
7694
|
+
}
|
|
7695
|
+
} catch (_error) {
|
|
6681
7696
|
this.installed = false;
|
|
7697
|
+
this.installedBytes = 0;
|
|
6682
7698
|
this.errorCode = "frp_component_invalid";
|
|
6683
7699
|
}
|
|
6684
7700
|
}
|
|
@@ -6701,10 +7717,8 @@ var FrpComponentManager = class {
|
|
|
6701
7717
|
return this.enqueue(async () => {
|
|
6702
7718
|
const artifact = this.artifact;
|
|
6703
7719
|
if (artifact === void 0) throw new Error("frp_component_unsupported");
|
|
6704
|
-
await
|
|
6705
|
-
|
|
6706
|
-
mode: 448
|
|
6707
|
-
});
|
|
7720
|
+
await assertManagedParents(this.stateDirectory, this.executable, "frp");
|
|
7721
|
+
await ensureManagedDirectory(this.stateDirectory, this.stagingRoot, "frp");
|
|
6708
7722
|
const staging = await mkdtemp(join(this.stagingRoot, "install-"));
|
|
6709
7723
|
try {
|
|
6710
7724
|
const controller = new AbortController();
|
|
@@ -6731,42 +7745,36 @@ var FrpComponentManager = class {
|
|
|
6731
7745
|
await chmod(extracted, 448);
|
|
6732
7746
|
if (await this.inspectExecutable(extracted) !== FRP_VERSION$1) throw new Error("frp_component_version_mismatch");
|
|
6733
7747
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
6734
|
-
await
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
7748
|
+
await ensureManagedDirectory(this.stateDirectory, candidate, "frp");
|
|
7749
|
+
try {
|
|
7750
|
+
const candidateExecutable = join(candidate, artifact.executableName);
|
|
7751
|
+
await copyFile(extracted, candidateExecutable);
|
|
7752
|
+
await chmod(candidateExecutable, 448);
|
|
7753
|
+
await replaceManagedDirectory(this.stateDirectory, this.componentStorage, candidate, "frp");
|
|
7754
|
+
} catch (error) {
|
|
7755
|
+
try {
|
|
7756
|
+
await removeManagedTree(this.stateDirectory, candidate, "frp");
|
|
7757
|
+
} catch (_cleanupError) {
|
|
7758
|
+
process.emitWarning("FRP candidate cleanup failed", { code: "DSH_MOBILE_COMPONENT_CLEANUP_FAILED" });
|
|
7759
|
+
}
|
|
7760
|
+
throw error;
|
|
7761
|
+
}
|
|
6742
7762
|
this.installed = true;
|
|
6743
7763
|
this.installedBytes = (await stat(this.executable)).size;
|
|
6744
7764
|
this.errorCode = void 0;
|
|
6745
7765
|
} finally {
|
|
6746
|
-
await
|
|
6747
|
-
recursive: true,
|
|
6748
|
-
force: true
|
|
6749
|
-
});
|
|
7766
|
+
await removeManagedTree(this.stateDirectory, staging, "frp");
|
|
6750
7767
|
}
|
|
6751
7768
|
});
|
|
6752
7769
|
}
|
|
6753
7770
|
/** Remove all FRP executable, staging, and log files owned by DSH Mobile. */
|
|
6754
7771
|
purge() {
|
|
6755
7772
|
return this.enqueue(async () => {
|
|
6756
|
-
await
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
rm(this.logRoot, {
|
|
6762
|
-
recursive: true,
|
|
6763
|
-
force: true
|
|
6764
|
-
}),
|
|
6765
|
-
rm(this.stagingRoot, {
|
|
6766
|
-
recursive: true,
|
|
6767
|
-
force: true
|
|
6768
|
-
})
|
|
6769
|
-
]);
|
|
7773
|
+
await removeManagedPaths(this.stateDirectory, [
|
|
7774
|
+
this.componentRoot,
|
|
7775
|
+
this.logRoot,
|
|
7776
|
+
this.stagingRoot
|
|
7777
|
+
], "frp");
|
|
6770
7778
|
this.installed = false;
|
|
6771
7779
|
this.installedBytes = 0;
|
|
6772
7780
|
this.errorCode = void 0;
|
|
@@ -7188,34 +8196,9 @@ function createFrpServerTemplate(settings) {
|
|
|
7188
8196
|
entryTls: resolveFrpEntryTls(settings)
|
|
7189
8197
|
});
|
|
7190
8198
|
}
|
|
7191
|
-
async function atomicPrivateWrite$2(file, body) {
|
|
7192
|
-
const directory = dirname(file);
|
|
7193
|
-
await mkdir(directory, {
|
|
7194
|
-
recursive: true,
|
|
7195
|
-
mode: 448
|
|
7196
|
-
});
|
|
7197
|
-
try {
|
|
7198
|
-
const current = await lstat(file);
|
|
7199
|
-
if (!current.isFile() || current.isSymbolicLink()) throw new Error("frp_config_target_invalid");
|
|
7200
|
-
} catch (error) {
|
|
7201
|
-
if (error.code !== "ENOENT") throw error;
|
|
7202
|
-
}
|
|
7203
|
-
const temporary = join(directory, `.${basename(file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
7204
|
-
try {
|
|
7205
|
-
await writeFile(temporary, body, {
|
|
7206
|
-
encoding: "utf8",
|
|
7207
|
-
flag: "wx",
|
|
7208
|
-
mode: 384
|
|
7209
|
-
});
|
|
7210
|
-
await rename(temporary, file);
|
|
7211
|
-
await restrictPrivateFile(file);
|
|
7212
|
-
} catch (error) {
|
|
7213
|
-
await rm(temporary, { force: true });
|
|
7214
|
-
throw error;
|
|
7215
|
-
}
|
|
7216
|
-
}
|
|
7217
8199
|
/** Owns private FRP settings and generation-specific frpc configuration. */
|
|
7218
8200
|
var FrpConfigStore = class {
|
|
8201
|
+
ownedRoot;
|
|
7219
8202
|
stateRoot;
|
|
7220
8203
|
settingsFile;
|
|
7221
8204
|
runtimeConfigFile;
|
|
@@ -7229,7 +8212,8 @@ var FrpConfigStore = class {
|
|
|
7229
8212
|
proxyName;
|
|
7230
8213
|
settingsValue;
|
|
7231
8214
|
errorCode;
|
|
7232
|
-
constructor(stateDirectory, proxyName = FRP_DEFAULT_PROXY_NAME) {
|
|
8215
|
+
constructor(stateDirectory, proxyName = FRP_DEFAULT_PROXY_NAME, ownedRoot = stateDirectory) {
|
|
8216
|
+
this.ownedRoot = ownedRoot;
|
|
7233
8217
|
if (!isAbsolute(stateDirectory)) throw new Error("frp config state directory must be absolute");
|
|
7234
8218
|
this.stateRoot = resolve(stateDirectory);
|
|
7235
8219
|
this.settingsFile = join(this.stateRoot, "settings.json");
|
|
@@ -7238,6 +8222,14 @@ var FrpConfigStore = class {
|
|
|
7238
8222
|
}
|
|
7239
8223
|
/** Load private settings while rejecting links, oversized files, and unknown fields. */
|
|
7240
8224
|
async initialize() {
|
|
8225
|
+
this.settingsValue = void 0;
|
|
8226
|
+
this.errorCode = void 0;
|
|
8227
|
+
try {
|
|
8228
|
+
await assertManagedParents(this.ownedRoot, this.settingsFile, "frp");
|
|
8229
|
+
} catch (_error) {
|
|
8230
|
+
this.errorCode = "frp_config_invalid";
|
|
8231
|
+
return;
|
|
8232
|
+
}
|
|
7241
8233
|
let entry;
|
|
7242
8234
|
try {
|
|
7243
8235
|
entry = await lstat(this.settingsFile);
|
|
@@ -7292,8 +8284,8 @@ var FrpConfigStore = class {
|
|
|
7292
8284
|
/** Atomically replace private FRP settings. */
|
|
7293
8285
|
async configure(value) {
|
|
7294
8286
|
const settings = parseFrpSettings(value);
|
|
7295
|
-
await
|
|
7296
|
-
await
|
|
8287
|
+
await writeManagedPrivateFile(this.ownedRoot, this.settingsFile, `${JSON.stringify(settings)}\n`, "frp");
|
|
8288
|
+
await removeManagedTree(this.ownedRoot, this.runtimeConfigFile, "frp");
|
|
7297
8289
|
this.settingsValue = settings;
|
|
7298
8290
|
this.errorCode = void 0;
|
|
7299
8291
|
return this.status();
|
|
@@ -7302,15 +8294,12 @@ var FrpConfigStore = class {
|
|
|
7302
8294
|
async writeRuntimeConfig(localPort) {
|
|
7303
8295
|
const settings = this.settingsValue;
|
|
7304
8296
|
if (settings === void 0) throw new Error("frp_config_missing");
|
|
7305
|
-
await
|
|
8297
|
+
await writeManagedPrivateFile(this.ownedRoot, this.runtimeConfigFile, createFrpcToml(settings, localPort, this.proxyName), "frp");
|
|
7306
8298
|
return this.runtimeConfigFile;
|
|
7307
8299
|
}
|
|
7308
8300
|
/** Remove only configuration files owned by the FRP provider. */
|
|
7309
8301
|
async purge() {
|
|
7310
|
-
await
|
|
7311
|
-
recursive: true,
|
|
7312
|
-
force: true
|
|
7313
|
-
});
|
|
8302
|
+
await removeManagedPaths(this.ownedRoot, [this.settingsFile, this.runtimeConfigFile], "frp");
|
|
7314
8303
|
this.settingsValue = void 0;
|
|
7315
8304
|
this.errorCode = void 0;
|
|
7316
8305
|
return this.status();
|
|
@@ -8197,25 +9186,11 @@ async function readIngressIdentity(file) {
|
|
|
8197
9186
|
throw new Error("frp_ingress_ca_invalid", { cause: error });
|
|
8198
9187
|
}
|
|
8199
9188
|
}
|
|
8200
|
-
async function writeIngressIdentity(file, fingerprint) {
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
caFingerprint: fingerprint
|
|
8206
|
-
})}\n`, {
|
|
8207
|
-
flag: "wx",
|
|
8208
|
-
mode: 384
|
|
8209
|
-
});
|
|
8210
|
-
await rename(temporary, file);
|
|
8211
|
-
await restrictPrivateFile(file);
|
|
8212
|
-
} finally {
|
|
8213
|
-
try {
|
|
8214
|
-
await unlink(temporary);
|
|
8215
|
-
} catch (error) {
|
|
8216
|
-
if (error.code !== "ENOENT") throw error;
|
|
8217
|
-
}
|
|
8218
|
-
}
|
|
9189
|
+
async function writeIngressIdentity(file, fingerprint, ownedRoot) {
|
|
9190
|
+
await writeManagedPrivateFile(ownedRoot, file, `${JSON.stringify({
|
|
9191
|
+
version: 1,
|
|
9192
|
+
caFingerprint: fingerprint
|
|
9193
|
+
})}\n`, "frp_ingress");
|
|
8219
9194
|
}
|
|
8220
9195
|
function coversPublicIp(certificate, address) {
|
|
8221
9196
|
return (certificate.subjectAltName ?? "").split(",").some((entry) => entry.trim() === `IP Address:${address}`);
|
|
@@ -8227,9 +9202,11 @@ function coversPublicIp(certificate, address) {
|
|
|
8227
9202
|
* replaced silently. The leaf is re-signed when expiring or no longer valid for
|
|
8228
9203
|
* the public IPv4, and the running gateway reloads it after a renewal check.
|
|
8229
9204
|
*/
|
|
8230
|
-
async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now(), expectedCaFingerprint) {
|
|
9205
|
+
async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now(), expectedCaFingerprint, ownedRoot = dirname(stateFile)) {
|
|
8231
9206
|
if (resolveFrpMode(settings) !== "attach" || !isFrpSelfSignedIngress(settings)) throw new Error("frp_entry_tls_invalid");
|
|
8232
9207
|
const paths = frpIngressPaths(stateFile);
|
|
9208
|
+
await assertManagedParents(ownedRoot, paths.statusFile, "frp_ingress");
|
|
9209
|
+
await assertManagedParents(ownedRoot, paths.caCertFile, "frp_ingress");
|
|
8233
9210
|
try {
|
|
8234
9211
|
const directory = await lstat(paths.directory);
|
|
8235
9212
|
if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error("frp_ingress_directory_invalid");
|
|
@@ -8246,10 +9223,7 @@ async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now()
|
|
|
8246
9223
|
if (caCertExists !== caKeyExists || !caCertExists && (previousFingerprint !== void 0 || leafCertExists || leafKeyExists)) throw new Error("frp_ingress_ca_invalid");
|
|
8247
9224
|
if (caCertExists && (await readCertificateRenewal(paths.caCertFile, now)).state === "expired") throw new Error("frp_ingress_ca_expired");
|
|
8248
9225
|
if (previousFingerprint === void 0) {
|
|
8249
|
-
await
|
|
8250
|
-
recursive: true,
|
|
8251
|
-
mode: 448
|
|
8252
|
-
});
|
|
9226
|
+
await ensureManagedDirectory(ownedRoot, dirname(paths.statusFile), "frp_ingress");
|
|
8253
9227
|
try {
|
|
8254
9228
|
await writeFile(paths.statusFile, "{\"version\":1,\"pending\":true}\n", {
|
|
8255
9229
|
flag: "wx",
|
|
@@ -8269,6 +9243,7 @@ async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now()
|
|
|
8269
9243
|
};
|
|
8270
9244
|
let ca;
|
|
8271
9245
|
try {
|
|
9246
|
+
await assertManagedParents(ownedRoot, paths.caCertFile, "frp_ingress");
|
|
8272
9247
|
ca = expectedCaFingerprint === void 0 ? await ensureManagedCa(caFiles) : await readManagedCa(caFiles);
|
|
8273
9248
|
} catch (error) {
|
|
8274
9249
|
throw new Error("frp_ingress_ca_invalid", { cause: error });
|
|
@@ -8276,7 +9251,7 @@ async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now()
|
|
|
8276
9251
|
const caFingerprint = ca.fingerprint256.replaceAll(":", "").toLowerCase();
|
|
8277
9252
|
if (previousFingerprint !== void 0 && caFingerprint !== previousFingerprint) throw new Error("frp_ingress_ca_changed");
|
|
8278
9253
|
if (expectedCaFingerprint !== void 0 && caFingerprint !== expectedCaFingerprint) throw new Error("frp_ingress_ca_changed");
|
|
8279
|
-
if (previousFingerprint === void 0) await writeIngressIdentity(paths.statusFile, caFingerprint);
|
|
9254
|
+
if (previousFingerprint === void 0) await writeIngressIdentity(paths.statusFile, caFingerprint, ownedRoot);
|
|
8280
9255
|
const publicHost = new URL(settings.publicOrigin).hostname;
|
|
8281
9256
|
let leaf;
|
|
8282
9257
|
try {
|
|
@@ -8285,6 +9260,7 @@ async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now()
|
|
|
8285
9260
|
leaf = void 0;
|
|
8286
9261
|
}
|
|
8287
9262
|
if (!(leaf !== void 0 && Date.parse(leaf.validTo) > now + 864e5 && coversPublicIp(leaf, publicHost) && leaf.verify(ca.publicKey))) {
|
|
9263
|
+
await assertManagedParents(ownedRoot, paths.certFile, "frp_ingress");
|
|
8288
9264
|
await issueServerCertificate({
|
|
8289
9265
|
caCertFile: paths.caCertFile,
|
|
8290
9266
|
caKeyFile: paths.caKeyFile
|
|
@@ -8314,7 +9290,7 @@ async function ensureFrpIngressCertificate(settings, stateFile, now = Date.now()
|
|
|
8314
9290
|
* No secret ever leaves this function: the CA is reported by fingerprint only,
|
|
8315
9291
|
* and the token is never read.
|
|
8316
9292
|
*/
|
|
8317
|
-
async function frpIngressSelfCheck(settings, stateFile) {
|
|
9293
|
+
async function frpIngressSelfCheck(settings, stateFile, ownedRoot = dirname(stateFile)) {
|
|
8318
9294
|
const paths = frpIngressPaths(stateFile);
|
|
8319
9295
|
const base = {
|
|
8320
9296
|
mode: resolveFrpMode(settings),
|
|
@@ -8331,6 +9307,8 @@ async function frpIngressSelfCheck(settings, stateFile) {
|
|
|
8331
9307
|
}
|
|
8332
9308
|
};
|
|
8333
9309
|
if (!isFrpSelfSignedIngress(settings)) return Object.freeze(base);
|
|
9310
|
+
await assertManagedParents(ownedRoot, paths.caCertFile, "frp_ingress");
|
|
9311
|
+
await assertManagedParents(ownedRoot, paths.certFile, "frp_ingress");
|
|
8334
9312
|
let caFingerprint;
|
|
8335
9313
|
try {
|
|
8336
9314
|
caFingerprint = new X509Certificate(await readFile(paths.caCertFile)).fingerprint256.replaceAll(":", "").toLowerCase();
|
|
@@ -8345,8 +9323,10 @@ async function frpIngressSelfCheck(settings, stateFile) {
|
|
|
8345
9323
|
});
|
|
8346
9324
|
}
|
|
8347
9325
|
/** Remove only the five files owned by the local self-signed FRP entry. */
|
|
8348
|
-
async function purgeFrpIngressCertificates(stateFile) {
|
|
9326
|
+
async function purgeFrpIngressCertificates(stateFile, ownedRoot = dirname(stateFile)) {
|
|
8349
9327
|
const paths = frpIngressPaths(stateFile);
|
|
9328
|
+
await assertManagedParents(ownedRoot, paths.statusFile, "frp_ingress");
|
|
9329
|
+
await assertManagedParents(ownedRoot, paths.directory, "frp_ingress");
|
|
8350
9330
|
try {
|
|
8351
9331
|
const marker = await lstat(paths.statusFile);
|
|
8352
9332
|
if (!marker.isFile() && !marker.isSymbolicLink()) throw new Error("frp_ingress_file_invalid");
|
|
@@ -8372,6 +9352,7 @@ async function purgeFrpIngressCertificates(stateFile) {
|
|
|
8372
9352
|
paths.certFile,
|
|
8373
9353
|
paths.keyFile
|
|
8374
9354
|
]) {
|
|
9355
|
+
await assertManagedParents(ownedRoot, file, "frp_ingress");
|
|
8375
9356
|
let entry;
|
|
8376
9357
|
try {
|
|
8377
9358
|
entry = await lstat(file);
|
|
@@ -8383,6 +9364,7 @@ async function purgeFrpIngressCertificates(stateFile) {
|
|
|
8383
9364
|
await unlink(file);
|
|
8384
9365
|
}
|
|
8385
9366
|
try {
|
|
9367
|
+
await assertManagedParents(ownedRoot, paths.directory, "frp_ingress");
|
|
8386
9368
|
await rmdir(paths.directory);
|
|
8387
9369
|
} catch (error) {
|
|
8388
9370
|
if (error.code === "ENOENT") return;
|
|
@@ -9303,39 +10285,15 @@ function parseOriginSettings(value) {
|
|
|
9303
10285
|
allowedCidrs: validateOriginAllowedCidrs(record.allowedCidrs, listenHost)
|
|
9304
10286
|
});
|
|
9305
10287
|
}
|
|
9306
|
-
async function atomicPrivateWrite$1(file, body) {
|
|
9307
|
-
const directory = dirname(file);
|
|
9308
|
-
await mkdir(directory, {
|
|
9309
|
-
recursive: true,
|
|
9310
|
-
mode: 448
|
|
9311
|
-
});
|
|
9312
|
-
try {
|
|
9313
|
-
const current = await lstat(file);
|
|
9314
|
-
if (!current.isFile() || current.isSymbolicLink()) throw new Error("origin_config_target_invalid");
|
|
9315
|
-
} catch (error) {
|
|
9316
|
-
if (error.code !== "ENOENT") throw error;
|
|
9317
|
-
}
|
|
9318
|
-
const temporary = join(directory, "." + basename(file) + "." + randomBytes(12).toString("hex") + ".tmp");
|
|
9319
|
-
try {
|
|
9320
|
-
await writeFile(temporary, body, {
|
|
9321
|
-
encoding: "utf8",
|
|
9322
|
-
flag: "wx",
|
|
9323
|
-
mode: 384
|
|
9324
|
-
});
|
|
9325
|
-
await rename(temporary, file);
|
|
9326
|
-
await restrictPrivateFile(file);
|
|
9327
|
-
} catch (error) {
|
|
9328
|
-
await rm(temporary, { force: true });
|
|
9329
|
-
throw error;
|
|
9330
|
-
}
|
|
9331
|
-
}
|
|
9332
10288
|
/** Owns only origin settings; shared paired-device storage is never removed. */
|
|
9333
10289
|
var OriginConfigStore = class {
|
|
10290
|
+
ownedRoot;
|
|
9334
10291
|
stateRoot;
|
|
9335
10292
|
settingsFile;
|
|
9336
10293
|
settingsValue;
|
|
9337
10294
|
errorCode;
|
|
9338
|
-
constructor(stateDirectory) {
|
|
10295
|
+
constructor(stateDirectory, ownedRoot = stateDirectory) {
|
|
10296
|
+
this.ownedRoot = ownedRoot;
|
|
9339
10297
|
if (!isAbsolute(stateDirectory)) throw new Error("origin config state directory must be absolute");
|
|
9340
10298
|
this.stateRoot = resolve(stateDirectory);
|
|
9341
10299
|
this.settingsFile = join(this.stateRoot, "settings.json");
|
|
@@ -9343,6 +10301,12 @@ var OriginConfigStore = class {
|
|
|
9343
10301
|
async initialize() {
|
|
9344
10302
|
this.settingsValue = void 0;
|
|
9345
10303
|
this.errorCode = void 0;
|
|
10304
|
+
try {
|
|
10305
|
+
await assertManagedParents(this.ownedRoot, this.settingsFile, "origin");
|
|
10306
|
+
} catch (_error) {
|
|
10307
|
+
this.errorCode = "origin_config_invalid";
|
|
10308
|
+
return;
|
|
10309
|
+
}
|
|
9346
10310
|
let entry;
|
|
9347
10311
|
try {
|
|
9348
10312
|
entry = await lstat(this.settingsFile);
|
|
@@ -9381,13 +10345,13 @@ var OriginConfigStore = class {
|
|
|
9381
10345
|
}
|
|
9382
10346
|
async configure(value) {
|
|
9383
10347
|
const settings = parseOriginSettings(value);
|
|
9384
|
-
await
|
|
10348
|
+
await writeManagedPrivateFile(this.ownedRoot, this.settingsFile, JSON.stringify(settings) + "\n", "origin");
|
|
9385
10349
|
this.settingsValue = settings;
|
|
9386
10350
|
this.errorCode = void 0;
|
|
9387
10351
|
return this.status();
|
|
9388
10352
|
}
|
|
9389
10353
|
async purge() {
|
|
9390
|
-
await
|
|
10354
|
+
await removeManagedTree(this.ownedRoot, this.settingsFile, "origin");
|
|
9391
10355
|
this.settingsValue = void 0;
|
|
9392
10356
|
this.errorCode = void 0;
|
|
9393
10357
|
return this.status();
|
|
@@ -9805,6 +10769,7 @@ var CloudflaredComponentManager = class {
|
|
|
9805
10769
|
stateRoot;
|
|
9806
10770
|
logRoot;
|
|
9807
10771
|
stagingRoot;
|
|
10772
|
+
stateDirectory;
|
|
9808
10773
|
platform;
|
|
9809
10774
|
arch;
|
|
9810
10775
|
release;
|
|
@@ -9814,8 +10779,9 @@ var CloudflaredComponentManager = class {
|
|
|
9814
10779
|
errorCode;
|
|
9815
10780
|
queue = Promise.resolve();
|
|
9816
10781
|
constructor(options) {
|
|
10782
|
+
if (!isAbsolute(options.stateDirectory)) throw new Error("cloudflared state directory must be absolute");
|
|
9817
10783
|
const stateDirectory = resolve(options.stateDirectory);
|
|
9818
|
-
|
|
10784
|
+
this.stateDirectory = stateDirectory;
|
|
9819
10785
|
this.platform = options.platform ?? process.platform;
|
|
9820
10786
|
this.arch = options.arch ?? process.arch;
|
|
9821
10787
|
this.release = lookupRelease(this.platform, this.arch);
|
|
@@ -9839,9 +10805,16 @@ var CloudflaredComponentManager = class {
|
|
|
9839
10805
|
/** Inspect the managed binary without using any global cloudflared state. */
|
|
9840
10806
|
async initialize() {
|
|
9841
10807
|
const release = this.release;
|
|
10808
|
+
this.installed = false;
|
|
9842
10809
|
this.errorCode = void 0;
|
|
9843
|
-
|
|
9844
|
-
|
|
10810
|
+
try {
|
|
10811
|
+
await assertManagedParents(this.stateDirectory, this.executable, "cloudflared");
|
|
10812
|
+
this.installed = release !== void 0 && await regularFile$1(this.executable, release.executableBytes);
|
|
10813
|
+
if (this.installed && release !== void 0 && await sha256$1(this.executable) !== release.executableSha256) {
|
|
10814
|
+
this.installed = false;
|
|
10815
|
+
this.errorCode = "cloudflared_component_invalid";
|
|
10816
|
+
}
|
|
10817
|
+
} catch (_error) {
|
|
9845
10818
|
this.installed = false;
|
|
9846
10819
|
this.errorCode = "cloudflared_component_invalid";
|
|
9847
10820
|
}
|
|
@@ -9867,10 +10840,8 @@ var CloudflaredComponentManager = class {
|
|
|
9867
10840
|
return this.enqueue(async () => {
|
|
9868
10841
|
const release = this.release;
|
|
9869
10842
|
if (release === void 0) throw new Error("cloudflared_component_unsupported");
|
|
9870
|
-
await
|
|
9871
|
-
|
|
9872
|
-
mode: 448
|
|
9873
|
-
});
|
|
10843
|
+
await assertManagedParents(this.stateDirectory, this.executable, "cloudflared");
|
|
10844
|
+
await ensureManagedDirectory(this.stateDirectory, this.stagingRoot, "cloudflared");
|
|
9874
10845
|
const staging = await mkdtemp(join(this.stagingRoot, "install-"));
|
|
9875
10846
|
try {
|
|
9876
10847
|
const controller = new AbortController();
|
|
@@ -9906,48 +10877,43 @@ var CloudflaredComponentManager = class {
|
|
|
9906
10877
|
await chmod(staged, 448);
|
|
9907
10878
|
if (!await regularFile$1(staged, release.executableBytes) || await sha256$1(staged) !== release.executableSha256) throw new Error("cloudflared_executable_hash_mismatch");
|
|
9908
10879
|
const candidate = join(this.componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
9909
|
-
await
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
10880
|
+
await ensureManagedDirectory(this.stateDirectory, candidate, "cloudflared");
|
|
10881
|
+
try {
|
|
10882
|
+
await copyFile(staged, join(candidate, release.executableName));
|
|
10883
|
+
await chmod(join(candidate, release.executableName), 448);
|
|
10884
|
+
await replaceManagedDirectory(this.stateDirectory, this.componentStorage, candidate, "cloudflared");
|
|
10885
|
+
} catch (error) {
|
|
10886
|
+
try {
|
|
10887
|
+
await removeManagedTree(this.stateDirectory, candidate, "cloudflared");
|
|
10888
|
+
} catch (_cleanupError) {
|
|
10889
|
+
process.emitWarning("cloudflared candidate cleanup failed", { code: "DSH_MOBILE_COMPONENT_CLEANUP_FAILED" });
|
|
10890
|
+
}
|
|
10891
|
+
throw error;
|
|
10892
|
+
}
|
|
9920
10893
|
this.installed = true;
|
|
9921
10894
|
this.errorCode = void 0;
|
|
10895
|
+
} catch (error) {
|
|
10896
|
+
try {
|
|
10897
|
+
await assertManagedParents(this.stateDirectory, this.executable, "cloudflared");
|
|
10898
|
+
this.installed = await regularFile$1(this.executable, release.executableBytes) && await sha256$1(this.executable) === release.executableSha256;
|
|
10899
|
+
} catch (_inspectionError) {
|
|
10900
|
+
this.installed = false;
|
|
10901
|
+
}
|
|
10902
|
+
throw error;
|
|
9922
10903
|
} finally {
|
|
9923
|
-
await
|
|
9924
|
-
recursive: true,
|
|
9925
|
-
force: true
|
|
9926
|
-
});
|
|
10904
|
+
await removeManagedTree(this.stateDirectory, staging, "cloudflared");
|
|
9927
10905
|
}
|
|
9928
10906
|
});
|
|
9929
10907
|
}
|
|
9930
10908
|
/** Remove every cloudflared file owned by DSH Mobile without touching global state. */
|
|
9931
10909
|
purge() {
|
|
9932
10910
|
return this.enqueue(async () => {
|
|
9933
|
-
await
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
recursive: true,
|
|
9940
|
-
force: true
|
|
9941
|
-
}),
|
|
9942
|
-
rm(this.logRoot, {
|
|
9943
|
-
recursive: true,
|
|
9944
|
-
force: true
|
|
9945
|
-
}),
|
|
9946
|
-
rm(this.stagingRoot, {
|
|
9947
|
-
recursive: true,
|
|
9948
|
-
force: true
|
|
9949
|
-
})
|
|
9950
|
-
]);
|
|
10911
|
+
await removeManagedPaths(this.stateDirectory, [
|
|
10912
|
+
this.componentRoot,
|
|
10913
|
+
this.stateRoot,
|
|
10914
|
+
this.logRoot,
|
|
10915
|
+
this.stagingRoot
|
|
10916
|
+
], "cloudflared");
|
|
9951
10917
|
this.installed = false;
|
|
9952
10918
|
this.errorCode = void 0;
|
|
9953
10919
|
});
|
|
@@ -10589,34 +11555,9 @@ function mergeSavedCloudflaredTunnelSettings(partial, saved) {
|
|
|
10589
11555
|
port
|
|
10590
11556
|
});
|
|
10591
11557
|
}
|
|
10592
|
-
async function atomicPrivateWrite(file, body) {
|
|
10593
|
-
const directory = dirname(file);
|
|
10594
|
-
await mkdir(directory, {
|
|
10595
|
-
recursive: true,
|
|
10596
|
-
mode: 448
|
|
10597
|
-
});
|
|
10598
|
-
try {
|
|
10599
|
-
const current = await lstat(file);
|
|
10600
|
-
if (!current.isFile() || current.isSymbolicLink()) throw new Error("cloudflared_tunnel_target_invalid");
|
|
10601
|
-
} catch (error) {
|
|
10602
|
-
if (error.code !== "ENOENT") throw error;
|
|
10603
|
-
}
|
|
10604
|
-
const temporary = join(directory, `.${basename(file)}.${randomBytes(12).toString("hex")}.tmp`);
|
|
10605
|
-
try {
|
|
10606
|
-
await writeFile(temporary, body, {
|
|
10607
|
-
encoding: "utf8",
|
|
10608
|
-
flag: "wx",
|
|
10609
|
-
mode: 384
|
|
10610
|
-
});
|
|
10611
|
-
await rename(temporary, file);
|
|
10612
|
-
await restrictPrivateFile(file);
|
|
10613
|
-
} catch (error) {
|
|
10614
|
-
await rm(temporary, { force: true });
|
|
10615
|
-
throw error;
|
|
10616
|
-
}
|
|
10617
|
-
}
|
|
10618
11558
|
/** Owns the private cloudflared tunnel configuration for one DSH installation. */
|
|
10619
11559
|
var CloudflaredTunnelStore = class {
|
|
11560
|
+
ownedRoot;
|
|
10620
11561
|
stateRoot;
|
|
10621
11562
|
settingsFile;
|
|
10622
11563
|
settingsValue = Object.freeze({
|
|
@@ -10624,13 +11565,25 @@ var CloudflaredTunnelStore = class {
|
|
|
10624
11565
|
mode: "quick"
|
|
10625
11566
|
});
|
|
10626
11567
|
errorCode;
|
|
10627
|
-
constructor(stateDirectory) {
|
|
11568
|
+
constructor(stateDirectory, ownedRoot = stateDirectory) {
|
|
11569
|
+
this.ownedRoot = ownedRoot;
|
|
10628
11570
|
if (!isAbsolute(stateDirectory)) throw new Error("cloudflared tunnel state directory must be absolute");
|
|
10629
11571
|
this.stateRoot = resolve(stateDirectory);
|
|
10630
11572
|
this.settingsFile = join(this.stateRoot, "tunnel.json");
|
|
10631
11573
|
}
|
|
10632
11574
|
/** Load private settings while rejecting links, oversized files and unknown fields. */
|
|
10633
11575
|
async initialize() {
|
|
11576
|
+
this.settingsValue = Object.freeze({
|
|
11577
|
+
version: 1,
|
|
11578
|
+
mode: "quick"
|
|
11579
|
+
});
|
|
11580
|
+
this.errorCode = void 0;
|
|
11581
|
+
try {
|
|
11582
|
+
await assertManagedParents(this.ownedRoot, this.settingsFile, "cloudflared");
|
|
11583
|
+
} catch (_error) {
|
|
11584
|
+
this.errorCode = "cloudflared_tunnel_config_invalid";
|
|
11585
|
+
return;
|
|
11586
|
+
}
|
|
10634
11587
|
let entry;
|
|
10635
11588
|
try {
|
|
10636
11589
|
entry = await lstat(this.settingsFile);
|
|
@@ -10675,15 +11628,20 @@ var CloudflaredTunnelStore = class {
|
|
|
10675
11628
|
/** Atomically replace the tunnel configuration. */
|
|
10676
11629
|
async configure(value) {
|
|
10677
11630
|
const settings = parseCloudflaredTunnelSettings(value);
|
|
10678
|
-
if (settings.mode === "quick") await
|
|
10679
|
-
else
|
|
11631
|
+
if (settings.mode === "quick") await removeManagedTree(this.ownedRoot, this.settingsFile, "cloudflared");
|
|
11632
|
+
else try {
|
|
11633
|
+
await writeManagedPrivateFile(this.ownedRoot, this.settingsFile, `${JSON.stringify(settings)}\n`, "cloudflared");
|
|
11634
|
+
} catch (error) {
|
|
11635
|
+
if (error instanceof Error && error.message === "cloudflared_config_target_invalid") throw new Error("cloudflared_tunnel_target_invalid", { cause: error });
|
|
11636
|
+
throw error;
|
|
11637
|
+
}
|
|
10680
11638
|
this.settingsValue = settings;
|
|
10681
11639
|
this.errorCode = void 0;
|
|
10682
11640
|
return this.status();
|
|
10683
11641
|
}
|
|
10684
11642
|
/** Forget a named tunnel and its connector token. */
|
|
10685
11643
|
async purge() {
|
|
10686
|
-
await
|
|
11644
|
+
await removeManagedTree(this.ownedRoot, this.settingsFile, "cloudflared");
|
|
10687
11645
|
this.settingsValue = Object.freeze({
|
|
10688
11646
|
version: 1,
|
|
10689
11647
|
mode: "quick"
|
|
@@ -11242,7 +12200,7 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
|
|
|
11242
12200
|
function buildMobileGuide(state) {
|
|
11243
12201
|
const styleLine = state.hasCustomCss ? "存在(当前生效的自定义样式)" : "不存在(使用内置默认样式)";
|
|
11244
12202
|
const scriptLine = state.hasCustomJs ? "存在(当前生效的自定义脚本)" : "不存在(无自定义脚本)";
|
|
11245
|
-
const extensionLines = state.extensions.length === 0 ? "(无)" : state.extensions.map((entry) => `- ${entry.id}(${entry.name} v${entry.version}
|
|
12203
|
+
const extensionLines = state.extensions.length === 0 ? "(无)" : state.extensions.map((entry) => `- ${entry.id}(${entry.name} v${entry.version})${entry.executionMode === void 0 ? "" : ` · ${entry.executionMode}${entry.executionState === "unavailable" ? " · 当前不可用,需要显式重启扩展" : ""}`}`).join("\n");
|
|
11246
12204
|
const failureLine = state.failedExtensionCount > 0 ? `注意:${state.failedExtensionCount} 个扩展的电脑端 host 激活失败,如改动相关扩展请先检查其 host.mjs 与 extension.json。` : "";
|
|
11247
12205
|
return `${`## 手机端当前状态(改动前必读,避免覆盖已有定制)
|
|
11248
12206
|
|
|
@@ -11276,16 +12234,18 @@ const MOBILE_CUSTOMIZATION_GUIDE_BODY = `你在为用户定制 DSH Mobile 的手
|
|
|
11276
12234
|
- 目录:$DSH_HOME/mobile-access/extensions/<id>/,id 用小写字母数字和连字符(如 media-remote)
|
|
11277
12235
|
- extension.json:{"schemaVersion":1,"id":"<id>","name":"显示名","version":"0.1.0","description":"说明"}
|
|
11278
12236
|
- host.mjs:电脑端 Node.js 代码(可信本地代码,可读写文件、执行命令)。导出默认函数 (api) => { ... },用 api.action('名称', { input, run }) 注册动作、api.route({ method, path, handle }) 注册路由、api.effect(fn) 注册清理。input 可以直接使用 api.schema.object(...) 等 Schemastery schema,也可以传入带 parse(value) 的适配器;两种形式都会在动作执行前校验并规范化输入。
|
|
12237
|
+
- 执行方式见上方实际状态。默认 in-process;插件配置可按扩展选择 worker。Worker 中 api.context 只有 logger,不能调用 Cordis 服务;api.manifest、schema、action、route、effect 仍可用。不要把需要 Cordis 的扩展偷偷改成 Worker,也不要在 extension.json 中添加执行模式字段。
|
|
11279
12238
|
- 动作与路由的执行有时间预算:默认 30 秒(输入校验也计入),可用 api.action('名称', { input, run, timeoutMs }) 或 api.route({ ..., timeoutMs }) 按操作覆盖(1–300000 的整数毫秒)。超时或调用方取消后,调用方收到 500 extension_action_timeout / extension_route_timeout(或取消原因);这只表示调用方不再等待,底层代码可能仍在运行并已产生副作用,请在 run / handle 中响应 signal 实现协作式取消。路由的预算到 handle() 返回响应为止,不影响已返回流的存活。若某扩展存在调用方已离开(超时或取消)但仍未结束的操作,后续对该扩展的新调用会收到 503 extension_busy,直到该操作真正结束。
|
|
11280
12239
|
- mobile.js:手机端脚本,用 window.dshMobile.define({ apiVersion:1, id:'<id>', activate(api) { ... } }),activate 返回清理函数
|
|
11281
12240
|
- mobile.css:手机端样式(可选)
|
|
11282
12241
|
- assets/:手机端静态资源(可选)
|
|
11283
12242
|
- mobile.js 里用 api.host.invoke('动作名', 输入) 调 host.mjs 的 action(请求会按 application/json 发送),api.host.fetch('/路由路径') 调 route,api.host.assetUrl('相对路径') 生成与当前版本绑定的资源地址
|
|
11284
|
-
-
|
|
12243
|
+
- DSH Web 可先用命令生成模板:dsh plugin --profile web exec dsh-mobile extension create <id> --name "<名称>",再在模板上改;Desktop 请在上方实际定制目录创建文件,不要误改 Web profile
|
|
11285
12244
|
|
|
11286
12245
|
安全约束:
|
|
11287
12246
|
- host.mjs 拥有电脑用户的完整权限,绝不能放入不可信代码,也不要让手机端无条件执行任意命令
|
|
11288
|
-
-
|
|
12247
|
+
- in-process 的同步阻塞会卡住网关事件循环,超时不能打断;Worker 可约束 JavaScript 阻塞,但不是安全沙箱,不约束全部原生内存,也不回滚副作用。两种模式都应响应 signal,避免编写不可取消的同步循环
|
|
12248
|
+
- Worker 终止后其他未完成调用会失败,已开始的操作不会自动重试。相同内容的停止实例需要操作者在电脑端“移动访问 → 扩展需要重启”中确认恢复;不要通过无意义改文件或自动重放原动作实现恢复
|
|
11289
12249
|
- 所有改动只限 $DSH_HOME/mobile-access/,不要动 DeepSeek Harness 源码
|
|
11290
12250
|
|
|
11291
12251
|
完成前请自检:
|
|
@@ -11313,10 +12273,16 @@ const LINUX_ARTIFACTS = Object.freeze({
|
|
|
11313
12273
|
var VpsSshError = class extends Error {
|
|
11314
12274
|
stdout;
|
|
11315
12275
|
stderr;
|
|
12276
|
+
timedOut;
|
|
12277
|
+
exitCode;
|
|
12278
|
+
signal;
|
|
11316
12279
|
constructor(message, stdout, stderr, options) {
|
|
11317
12280
|
super(message, options);
|
|
11318
12281
|
this.stdout = stdout;
|
|
11319
12282
|
this.stderr = stderr;
|
|
12283
|
+
this.timedOut = options?.timedOut ?? false;
|
|
12284
|
+
this.exitCode = options?.exitCode;
|
|
12285
|
+
this.signal = options?.signal;
|
|
11320
12286
|
}
|
|
11321
12287
|
};
|
|
11322
12288
|
function shellQuote(value) {
|
|
@@ -11427,7 +12393,7 @@ function sshSessionOptions(knownHostsFile) {
|
|
|
11427
12393
|
`-o UserKnownHostsFile=${knownHostsFile}`
|
|
11428
12394
|
];
|
|
11429
12395
|
}
|
|
11430
|
-
/**
|
|
12396
|
+
/** Recognize usable public-key scan output without authenticating to the server. */
|
|
11431
12397
|
function tryParseKeyscanOutput(output) {
|
|
11432
12398
|
try {
|
|
11433
12399
|
return parseKeyscanOutput(output);
|
|
@@ -11466,58 +12432,20 @@ async function defaultRunKeyscan(input, serverAddress) {
|
|
|
11466
12432
|
}
|
|
11467
12433
|
return first?.stdout ?? "";
|
|
11468
12434
|
}
|
|
11469
|
-
/**
|
|
11470
|
-
* Read the server's public host keys over an authenticated connection with a
|
|
11471
|
-
* throwaway known_hosts file. Fallback for keyscan binaries that cannot
|
|
11472
|
-
* negotiate with modern servers; output feeds the same confirm-and-pin pipeline.
|
|
11473
|
-
*/
|
|
11474
|
-
async function defaultRunSshFetch(input, serverAddress) {
|
|
11475
|
-
const ssh = process.platform === "win32" ? "ssh.exe" : "ssh";
|
|
11476
|
-
const sshUser = validSshUser(input.sshUser);
|
|
11477
|
-
const sshPort = validSshPort(input.sshPort);
|
|
11478
|
-
const sshKeyPath = validSshKeyPath(input.sshKeyPath);
|
|
11479
|
-
const { stdout } = await runProcess(ssh, [
|
|
11480
|
-
"-o",
|
|
11481
|
-
"BatchMode=yes",
|
|
11482
|
-
"-o",
|
|
11483
|
-
"ConnectTimeout=15",
|
|
11484
|
-
"-o",
|
|
11485
|
-
"StrictHostKeyChecking=no",
|
|
11486
|
-
`-o UserKnownHostsFile=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
|
|
11487
|
-
...sshKeyPath === void 0 ? [] : ["-i", sshKeyPath],
|
|
11488
|
-
"-p",
|
|
11489
|
-
String(sshPort),
|
|
11490
|
-
`${sshUser}@${serverAddress}`,
|
|
11491
|
-
"cat /etc/ssh/ssh_host_*_key.pub"
|
|
11492
|
-
], void 0, 3e4).catch((error) => {
|
|
11493
|
-
throw new VpsSshError("vps_host_key_unavailable", "", error instanceof Error ? error.message : String(error));
|
|
11494
|
-
});
|
|
11495
|
-
const lines = [];
|
|
11496
|
-
for (const line of stdout.split(/\r?\n/u)) {
|
|
11497
|
-
const match = /^(ssh-rsa|ecdsa-sha2-nistp\d+|ssh-ed25519)\s+([A-Za-z0-9+/]+={0,2})(\s|$)/u.exec(line.trim());
|
|
11498
|
-
if (match?.[1] !== void 0 && match[2] !== void 0) lines.push(`${serverAddress} ${match[1]} ${match[2]}`);
|
|
11499
|
-
}
|
|
11500
|
-
return lines.length === 0 ? stdout : `${lines.join("\n")}\n`;
|
|
11501
|
-
}
|
|
11502
|
-
/**
|
|
11503
|
-
* Scan host keys with keyscan first, then fall back to an authenticated read
|
|
11504
|
-
* when the local keyscan binary cannot negotiate with the server. Both paths
|
|
11505
|
-
* feed the same confirm-and-pin pipeline, so a fallback never weakens the
|
|
11506
|
-
* user-confirmation gate.
|
|
11507
|
-
*/
|
|
12435
|
+
/** Obtain public host keys without using a private key, agent or unverified authenticated connection. */
|
|
11508
12436
|
async function scanHostKeys(input, serverAddress, options) {
|
|
11509
|
-
const
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11520
|
-
|
|
12437
|
+
const runKeyscan = options.runKeyscan ?? defaultRunKeyscan;
|
|
12438
|
+
let scanned;
|
|
12439
|
+
try {
|
|
12440
|
+
scanned = await runKeyscan({
|
|
12441
|
+
sshUser: input.sshUser,
|
|
12442
|
+
sshPort: input.sshPort
|
|
12443
|
+
}, serverAddress);
|
|
12444
|
+
} catch (error) {
|
|
12445
|
+
throw new Error("vps_host_key_unavailable", { cause: error });
|
|
12446
|
+
}
|
|
12447
|
+
if (parseKeyscanOutput(scanned).length === 0) throw new Error("vps_host_key_unavailable");
|
|
12448
|
+
return scanned;
|
|
11521
12449
|
}
|
|
11522
12450
|
/**
|
|
11523
12451
|
* Fetch the server's current host keys and return them with OpenSSH-style
|
|
@@ -11880,7 +12808,8 @@ check caddy ok "Caddy 已加载 ${publicHost}。"
|
|
|
11880
12808
|
echo DSH_MOBILE_DEPLOYMENT_OK
|
|
11881
12809
|
`;
|
|
11882
12810
|
}
|
|
11883
|
-
|
|
12811
|
+
/** Run one owned SSH/transfer utility; reject only after its process and stdio have closed. */
|
|
12812
|
+
async function runVpsProcess(command, args, stdin, timeoutMs = SSH_TIMEOUT_MS) {
|
|
11884
12813
|
return new Promise((resolveRun, rejectRun) => {
|
|
11885
12814
|
const child = spawn(command, args, {
|
|
11886
12815
|
windowsHide: true,
|
|
@@ -11893,9 +12822,23 @@ async function runProcess(command, args, stdin, timeoutMs = SSH_TIMEOUT_MS) {
|
|
|
11893
12822
|
let stdout = "";
|
|
11894
12823
|
let stderr = "";
|
|
11895
12824
|
const append = (current, chunk) => `${current}${chunk.toString("utf8")}`.slice(-98304);
|
|
11896
|
-
|
|
12825
|
+
let timedOut = false;
|
|
12826
|
+
let failure;
|
|
12827
|
+
let failureCode = "vps_ssh_unavailable";
|
|
12828
|
+
let forceTimer;
|
|
12829
|
+
const terminate = () => {
|
|
11897
12830
|
child.kill();
|
|
11898
|
-
|
|
12831
|
+
forceTimer ??= setTimeout(() => {
|
|
12832
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
12833
|
+
child.stdin.destroy();
|
|
12834
|
+
child.stdout.destroy();
|
|
12835
|
+
child.stderr.destroy();
|
|
12836
|
+
}, 1e3);
|
|
12837
|
+
forceTimer.unref();
|
|
12838
|
+
};
|
|
12839
|
+
const timer = setTimeout(() => {
|
|
12840
|
+
timedOut = true;
|
|
12841
|
+
terminate();
|
|
11899
12842
|
}, timeoutMs);
|
|
11900
12843
|
timer.unref();
|
|
11901
12844
|
child.stdout.on("data", (chunk) => {
|
|
@@ -11905,12 +12848,32 @@ async function runProcess(command, args, stdin, timeoutMs = SSH_TIMEOUT_MS) {
|
|
|
11905
12848
|
stderr = append(stderr, Buffer.from(chunk));
|
|
11906
12849
|
});
|
|
11907
12850
|
child.once("error", (error) => {
|
|
11908
|
-
|
|
11909
|
-
rejectRun(new VpsSshError("vps_ssh_unavailable", stdout, stderr, { cause: error }));
|
|
12851
|
+
failure = error;
|
|
11910
12852
|
});
|
|
11911
|
-
child.
|
|
12853
|
+
child.stdin.on("error", (error) => {
|
|
12854
|
+
failure = error;
|
|
12855
|
+
failureCode = "vps_ssh_input_failed";
|
|
12856
|
+
terminate();
|
|
12857
|
+
});
|
|
12858
|
+
const failedOutput = (error) => {
|
|
12859
|
+
failure = error;
|
|
12860
|
+
failureCode = "vps_ssh_transport_failed";
|
|
12861
|
+
terminate();
|
|
12862
|
+
};
|
|
12863
|
+
child.stdout.on("error", failedOutput);
|
|
12864
|
+
child.stderr.on("error", failedOutput);
|
|
12865
|
+
child.once("close", (code, signal) => {
|
|
11912
12866
|
clearTimeout(timer);
|
|
11913
|
-
if (
|
|
12867
|
+
if (forceTimer !== void 0) clearTimeout(forceTimer);
|
|
12868
|
+
const facts = {
|
|
12869
|
+
timedOut,
|
|
12870
|
+
exitCode: code,
|
|
12871
|
+
signal,
|
|
12872
|
+
...failure === void 0 ? {} : { cause: failure }
|
|
12873
|
+
};
|
|
12874
|
+
if (timedOut) rejectRun(new VpsSshError("vps_ssh_timeout", stdout, stderr, facts));
|
|
12875
|
+
else if (failure !== void 0) rejectRun(new VpsSshError(child.pid === void 0 ? "vps_ssh_unavailable" : failureCode, stdout, stderr, facts));
|
|
12876
|
+
else if (code !== 0) rejectRun(new VpsSshError(stderr.includes("Permission denied") ? "vps_ssh_auth_failed" : "vps_deploy_failed", stdout, stderr, facts));
|
|
11914
12877
|
else resolveRun({
|
|
11915
12878
|
stdout,
|
|
11916
12879
|
stderr
|
|
@@ -11919,8 +12882,10 @@ async function runProcess(command, args, stdin, timeoutMs = SSH_TIMEOUT_MS) {
|
|
|
11919
12882
|
child.stdin.end(stdin, "utf8");
|
|
11920
12883
|
});
|
|
11921
12884
|
}
|
|
12885
|
+
const runProcess = runVpsProcess;
|
|
11922
12886
|
async function downloadArtifact(artifact, file) {
|
|
11923
|
-
|
|
12887
|
+
const curl = process.platform === "win32" ? "curl.exe" : "curl";
|
|
12888
|
+
await runProcess(curl, [
|
|
11924
12889
|
...process.platform === "win32" ? ["--ipv4"] : [],
|
|
11925
12890
|
"--fail",
|
|
11926
12891
|
"--location",
|
|
@@ -12375,12 +13340,27 @@ var FunnelController = class {
|
|
|
12375
13340
|
});
|
|
12376
13341
|
queue = Promise.resolve();
|
|
12377
13342
|
startTimer;
|
|
13343
|
+
ownedRoot;
|
|
12378
13344
|
constructor(options) {
|
|
12379
13345
|
this.options = options;
|
|
12380
13346
|
if (!isAbsolute(options.executable) || !isAbsolute(options.stateDirectory)) throw new Error("Funnel paths must be absolute");
|
|
13347
|
+
this.ownedRoot = options.ownedRoot ?? dirname(resolve(options.stateDirectory));
|
|
13348
|
+
if (!isAbsolute(this.ownedRoot) || resolve(this.ownedRoot) === resolve(options.stateDirectory)) throw new Error("Funnel state must be a child of its owned root");
|
|
12381
13349
|
}
|
|
12382
13350
|
/** Restore the remote switch without coupling it to LAN availability. */
|
|
12383
13351
|
async initialize() {
|
|
13352
|
+
try {
|
|
13353
|
+
await this.assertStateParents();
|
|
13354
|
+
} catch (_error) {
|
|
13355
|
+
this.enabled = false;
|
|
13356
|
+
this.initialized = true;
|
|
13357
|
+
this.publish({
|
|
13358
|
+
enabled: false,
|
|
13359
|
+
state: "unavailable",
|
|
13360
|
+
errorCode: "funnel_state_invalid"
|
|
13361
|
+
});
|
|
13362
|
+
return;
|
|
13363
|
+
}
|
|
12384
13364
|
const state = await this.options.store.load();
|
|
12385
13365
|
this.enabled = state.enabled;
|
|
12386
13366
|
this.initialized = true;
|
|
@@ -12404,6 +13384,17 @@ var FunnelController = class {
|
|
|
12404
13384
|
await this.enqueue(async () => {
|
|
12405
13385
|
if (this.enabled === enabled && (enabled === false || this.child !== void 0)) return;
|
|
12406
13386
|
if (!enabled) await this.stop();
|
|
13387
|
+
try {
|
|
13388
|
+
await this.assertStateParents();
|
|
13389
|
+
} catch (error) {
|
|
13390
|
+
this.enabled = false;
|
|
13391
|
+
this.publish({
|
|
13392
|
+
enabled: false,
|
|
13393
|
+
state: "unavailable",
|
|
13394
|
+
errorCode: "funnel_state_invalid"
|
|
13395
|
+
});
|
|
13396
|
+
throw error;
|
|
13397
|
+
}
|
|
12407
13398
|
this.enabled = enabled;
|
|
12408
13399
|
await this.options.store.save({
|
|
12409
13400
|
version: 1,
|
|
@@ -12421,6 +13412,17 @@ var FunnelController = class {
|
|
|
12421
13412
|
async reconnect() {
|
|
12422
13413
|
if (!this.initialized || this.disposed) throw new Error("Funnel controller is unavailable");
|
|
12423
13414
|
await this.enqueue(async () => {
|
|
13415
|
+
await this.stop();
|
|
13416
|
+
try {
|
|
13417
|
+
await this.assertStateParents();
|
|
13418
|
+
} catch (error) {
|
|
13419
|
+
this.publish({
|
|
13420
|
+
enabled: this.enabled,
|
|
13421
|
+
state: "unavailable",
|
|
13422
|
+
errorCode: "funnel_state_invalid"
|
|
13423
|
+
});
|
|
13424
|
+
throw error;
|
|
13425
|
+
}
|
|
12424
13426
|
if (!this.enabled) {
|
|
12425
13427
|
this.enabled = true;
|
|
12426
13428
|
await this.options.store.save({
|
|
@@ -12428,7 +13430,6 @@ var FunnelController = class {
|
|
|
12428
13430
|
enabled: true
|
|
12429
13431
|
});
|
|
12430
13432
|
}
|
|
12431
|
-
await this.stop();
|
|
12432
13433
|
await this.start();
|
|
12433
13434
|
});
|
|
12434
13435
|
return this.status();
|
|
@@ -12439,14 +13440,21 @@ var FunnelController = class {
|
|
|
12439
13440
|
await this.enqueue(async () => {
|
|
12440
13441
|
await this.stop();
|
|
12441
13442
|
this.enabled = false;
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
})
|
|
13443
|
+
try {
|
|
13444
|
+
await removeManagedTree(this.ownedRoot, this.options.stateDirectory, "funnel");
|
|
13445
|
+
await this.assertStateParents();
|
|
13446
|
+
await this.options.store.save({
|
|
13447
|
+
version: 1,
|
|
13448
|
+
enabled: false
|
|
13449
|
+
});
|
|
13450
|
+
} catch (error) {
|
|
13451
|
+
this.publish({
|
|
13452
|
+
enabled: false,
|
|
13453
|
+
state: "off",
|
|
13454
|
+
errorCode: "funnel_state_invalid"
|
|
13455
|
+
});
|
|
13456
|
+
throw error;
|
|
13457
|
+
}
|
|
12450
13458
|
this.publish({
|
|
12451
13459
|
enabled: false,
|
|
12452
13460
|
state: "off"
|
|
@@ -12471,6 +13479,15 @@ var FunnelController = class {
|
|
|
12471
13479
|
this.options.onStatus?.(this.status());
|
|
12472
13480
|
} catch {}
|
|
12473
13481
|
}
|
|
13482
|
+
async assertStateParents() {
|
|
13483
|
+
await assertManagedParents(this.ownedRoot, this.options.stateDirectory, "funnel");
|
|
13484
|
+
try {
|
|
13485
|
+
const entry = await lstat(this.options.stateDirectory);
|
|
13486
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("funnel_path_invalid");
|
|
13487
|
+
} catch (error) {
|
|
13488
|
+
if (error.code !== "ENOENT") throw error;
|
|
13489
|
+
}
|
|
13490
|
+
}
|
|
12474
13491
|
async start() {
|
|
12475
13492
|
const generation = ++this.generation;
|
|
12476
13493
|
let entry;
|
|
@@ -12492,6 +13509,17 @@ var FunnelController = class {
|
|
|
12492
13509
|
});
|
|
12493
13510
|
return;
|
|
12494
13511
|
}
|
|
13512
|
+
try {
|
|
13513
|
+
await this.assertStateParents();
|
|
13514
|
+
await ensureManagedDirectory(this.ownedRoot, this.options.stateDirectory, "funnel");
|
|
13515
|
+
} catch (_error) {
|
|
13516
|
+
this.publish({
|
|
13517
|
+
enabled: true,
|
|
13518
|
+
state: "unavailable",
|
|
13519
|
+
errorCode: "funnel_state_invalid"
|
|
13520
|
+
});
|
|
13521
|
+
return;
|
|
13522
|
+
}
|
|
12495
13523
|
this.buffer = "";
|
|
12496
13524
|
this.publish({
|
|
12497
13525
|
enabled: true,
|
|
@@ -13147,20 +14175,19 @@ async function pathExists(path) {
|
|
|
13147
14175
|
/** Replace only this version's verified binary, retaining the previous directory until promotion succeeds. */
|
|
13148
14176
|
async function publishVerifiedCpolarExecutable(extracted, componentRoot, componentStorage, executableName, operations = {}) {
|
|
13149
14177
|
if (dirname(componentStorage) !== componentRoot || !inside(componentRoot, componentStorage) || basename(executableName) !== executableName || executableName === "." || executableName === "..") throw new Error("cpolar component storage escaped its private root");
|
|
13150
|
-
const move =
|
|
13151
|
-
|
|
13152
|
-
|
|
13153
|
-
|
|
13154
|
-
|
|
14178
|
+
const move = async (from, to) => {
|
|
14179
|
+
await assertManagedParents(componentRoot, from, "cpolar");
|
|
14180
|
+
await assertManagedParents(componentRoot, to, "cpolar");
|
|
14181
|
+
if (operations.move === void 0) await renameManagedPath(componentRoot, from, to, "cpolar");
|
|
14182
|
+
else await operations.move(from, to);
|
|
14183
|
+
};
|
|
14184
|
+
const remove = operations.remove ?? ((path) => removeManagedTree(componentRoot, path, "cpolar"));
|
|
13155
14185
|
const candidate = join(componentRoot, `.install-${randomBytes(12).toString("hex")}`);
|
|
13156
14186
|
const backup = join(componentRoot, `.previous-${randomBytes(12).toString("hex")}`);
|
|
13157
14187
|
let backupContainsPrevious = false;
|
|
13158
14188
|
let replacementInstalled = false;
|
|
13159
14189
|
try {
|
|
13160
|
-
await
|
|
13161
|
-
recursive: true,
|
|
13162
|
-
mode: 448
|
|
13163
|
-
});
|
|
14190
|
+
await ensureManagedDirectory(componentRoot, candidate, "cpolar");
|
|
13164
14191
|
await copyFile(extracted, join(candidate, executableName));
|
|
13165
14192
|
await chmod(join(candidate, executableName), 448);
|
|
13166
14193
|
if (await pathExists(componentStorage)) {
|
|
@@ -13194,10 +14221,7 @@ async function publishVerifiedCpolarExecutable(extracted, componentRoot, compone
|
|
|
13194
14221
|
}
|
|
13195
14222
|
}
|
|
13196
14223
|
/** Remove only this install's staging directory without replacing a preceding failure. */
|
|
13197
|
-
async function cleanupCpolarInstallStaging(staging, installFailed, remove = (path) =>
|
|
13198
|
-
recursive: true,
|
|
13199
|
-
force: true
|
|
13200
|
-
})) {
|
|
14224
|
+
async function cleanupCpolarInstallStaging(staging, installFailed, remove = (path) => removeManagedTree(dirname(path), path, "cpolar")) {
|
|
13201
14225
|
try {
|
|
13202
14226
|
await remove(staging);
|
|
13203
14227
|
} catch (error) {
|
|
@@ -13291,6 +14315,7 @@ var CpolarComponentManager = class {
|
|
|
13291
14315
|
stateRoot;
|
|
13292
14316
|
logRoot;
|
|
13293
14317
|
stagingRoot;
|
|
14318
|
+
stateDirectory;
|
|
13294
14319
|
platform;
|
|
13295
14320
|
arch;
|
|
13296
14321
|
release;
|
|
@@ -13301,8 +14326,9 @@ var CpolarComponentManager = class {
|
|
|
13301
14326
|
errorCode;
|
|
13302
14327
|
queue = Promise.resolve();
|
|
13303
14328
|
constructor(options) {
|
|
14329
|
+
if (!isAbsolute(options.stateDirectory)) throw new Error("cpolar state directory must be absolute");
|
|
13304
14330
|
const stateDirectory = resolve(options.stateDirectory);
|
|
13305
|
-
|
|
14331
|
+
this.stateDirectory = stateDirectory;
|
|
13306
14332
|
this.platform = options.platform ?? process.platform;
|
|
13307
14333
|
this.arch = options.arch ?? process.arch;
|
|
13308
14334
|
this.release = CPOLAR_COMPONENT_RELEASES[`${this.platform}-${this.arch}`];
|
|
@@ -13330,13 +14356,24 @@ var CpolarComponentManager = class {
|
|
|
13330
14356
|
/** Inspect the managed binary and configuration without using global cpolar state. */
|
|
13331
14357
|
async initialize() {
|
|
13332
14358
|
const release = this.release;
|
|
13333
|
-
this.installed =
|
|
13334
|
-
|
|
14359
|
+
this.installed = false;
|
|
14360
|
+
this.configured = false;
|
|
14361
|
+
this.errorCode = void 0;
|
|
14362
|
+
try {
|
|
14363
|
+
await assertManagedParents(this.stateDirectory, this.executable, "cpolar");
|
|
14364
|
+
await assertManagedParents(this.stateDirectory, this.configFile, "cpolar");
|
|
14365
|
+
this.installed = release !== void 0 && await regularFile(this.executable, release.executableBytes);
|
|
14366
|
+
if (this.installed && release !== void 0 && await sha256(this.executable) !== release.executableSha256) {
|
|
14367
|
+
this.installed = false;
|
|
14368
|
+
this.errorCode = "cpolar_component_invalid";
|
|
14369
|
+
}
|
|
14370
|
+
this.configured = await regularFile(this.configFile);
|
|
14371
|
+
if (this.configured) await restrictPrivateFile(this.configFile);
|
|
14372
|
+
} catch (_error) {
|
|
13335
14373
|
this.installed = false;
|
|
14374
|
+
this.configured = false;
|
|
13336
14375
|
this.errorCode = "cpolar_component_invalid";
|
|
13337
14376
|
}
|
|
13338
|
-
this.configured = await regularFile(this.configFile);
|
|
13339
|
-
if (this.configured) await restrictPrivateFile(this.configFile);
|
|
13340
14377
|
}
|
|
13341
14378
|
/** Return a safe status that never includes the account token. */
|
|
13342
14379
|
status() {
|
|
@@ -13362,11 +14399,9 @@ var CpolarComponentManager = class {
|
|
|
13362
14399
|
return this.enqueue(async () => {
|
|
13363
14400
|
const release = this.release;
|
|
13364
14401
|
if (release === void 0) throw new Error("cpolar_component_unsupported");
|
|
14402
|
+
await assertManagedParents(this.stateDirectory, this.executable, "cpolar");
|
|
13365
14403
|
const staging = await installStep("cpolar_storage_failed", async () => {
|
|
13366
|
-
await
|
|
13367
|
-
recursive: true,
|
|
13368
|
-
mode: 448
|
|
13369
|
-
});
|
|
14404
|
+
await ensureManagedDirectory(this.stateDirectory, this.stagingRoot, "cpolar");
|
|
13370
14405
|
return mkdtemp(join(this.stagingRoot, "install-"));
|
|
13371
14406
|
});
|
|
13372
14407
|
let installFailed = false;
|
|
@@ -13394,19 +14429,24 @@ var CpolarComponentManager = class {
|
|
|
13394
14429
|
await installStep("cpolar_extract_failed", () => this.extractArtifact(archive, staging));
|
|
13395
14430
|
const extracted = join(staging, release.executableName);
|
|
13396
14431
|
if (!await installStep("cpolar_extract_failed", async () => await regularFile(extracted, release.executableBytes) && await sha256(extracted) === release.executableSha256)) throw new Error("cpolar_executable_hash_mismatch");
|
|
13397
|
-
await installStep("cpolar_storage_failed", () =>
|
|
14432
|
+
await installStep("cpolar_storage_failed", async () => {
|
|
14433
|
+
await ensureManagedDirectory(this.stateDirectory, this.componentRoot, "cpolar");
|
|
14434
|
+
await assertManagedParents(this.stateDirectory, this.executable, "cpolar");
|
|
14435
|
+
await publishVerifiedCpolarExecutable(extracted, this.componentRoot, this.componentStorage, release.executableName);
|
|
14436
|
+
});
|
|
13398
14437
|
this.installed = true;
|
|
13399
14438
|
this.errorCode = void 0;
|
|
13400
14439
|
} catch (error) {
|
|
13401
14440
|
installFailed = true;
|
|
13402
14441
|
try {
|
|
14442
|
+
await assertManagedParents(this.stateDirectory, this.executable, "cpolar");
|
|
13403
14443
|
this.installed = await regularFile(this.executable, release.executableBytes) && await sha256(this.executable) === release.executableSha256;
|
|
13404
14444
|
} catch (_inspectionError) {
|
|
13405
14445
|
this.installed = false;
|
|
13406
14446
|
}
|
|
13407
14447
|
throw error;
|
|
13408
14448
|
} finally {
|
|
13409
|
-
await cleanupCpolarInstallStaging(staging, installFailed);
|
|
14449
|
+
await cleanupCpolarInstallStaging(staging, installFailed, (path) => removeManagedTree(this.stateDirectory, path, "cpolar"));
|
|
13410
14450
|
}
|
|
13411
14451
|
});
|
|
13412
14452
|
}
|
|
@@ -13414,24 +14454,8 @@ var CpolarComponentManager = class {
|
|
|
13414
14454
|
configure(authtoken) {
|
|
13415
14455
|
return this.enqueue(async () => {
|
|
13416
14456
|
const token = validateCpolarAuthtoken(authtoken);
|
|
13417
|
-
await mkdir(this.stateRoot, {
|
|
13418
|
-
recursive: true,
|
|
13419
|
-
mode: 448
|
|
13420
|
-
});
|
|
13421
|
-
const temporary = join(this.stateRoot, `.cpolar.${randomBytes(12).toString("hex")}.tmp`);
|
|
13422
14457
|
const body = `authtoken: ${JSON.stringify(token)}\nconsole_ui: false\nupdate: false\ninspect_db_size: -1\n`;
|
|
13423
|
-
|
|
13424
|
-
await writeFile(temporary, body, {
|
|
13425
|
-
encoding: "utf8",
|
|
13426
|
-
flag: "wx",
|
|
13427
|
-
mode: 384
|
|
13428
|
-
});
|
|
13429
|
-
await rename(temporary, this.configFile);
|
|
13430
|
-
await restrictPrivateFile(this.configFile);
|
|
13431
|
-
} catch (error) {
|
|
13432
|
-
await rm(temporary, { force: true });
|
|
13433
|
-
throw error;
|
|
13434
|
-
}
|
|
14458
|
+
await writeManagedPrivateFile(this.stateDirectory, this.configFile, body, "cpolar");
|
|
13435
14459
|
this.configured = true;
|
|
13436
14460
|
this.errorCode = void 0;
|
|
13437
14461
|
});
|
|
@@ -13439,24 +14463,12 @@ var CpolarComponentManager = class {
|
|
|
13439
14463
|
/** Remove every cpolar file owned by DSH Mobile without touching global state. */
|
|
13440
14464
|
purge() {
|
|
13441
14465
|
return this.enqueue(async () => {
|
|
13442
|
-
await
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
recursive: true,
|
|
13449
|
-
force: true
|
|
13450
|
-
}),
|
|
13451
|
-
rm(this.logRoot, {
|
|
13452
|
-
recursive: true,
|
|
13453
|
-
force: true
|
|
13454
|
-
}),
|
|
13455
|
-
rm(this.stagingRoot, {
|
|
13456
|
-
recursive: true,
|
|
13457
|
-
force: true
|
|
13458
|
-
})
|
|
13459
|
-
]);
|
|
14466
|
+
await removeManagedPaths(this.stateDirectory, [
|
|
14467
|
+
this.componentRoot,
|
|
14468
|
+
this.stateRoot,
|
|
14469
|
+
this.logRoot,
|
|
14470
|
+
this.stagingRoot
|
|
14471
|
+
], "cpolar");
|
|
13460
14472
|
this.installed = false;
|
|
13461
14473
|
this.configured = false;
|
|
13462
14474
|
this.errorCode = void 0;
|
|
@@ -13470,82 +14482,25 @@ var CpolarComponentManager = class {
|
|
|
13470
14482
|
};
|
|
13471
14483
|
//#endregion
|
|
13472
14484
|
//#region src/caddy-files.ts
|
|
13473
|
-
/** Refuse link-shaped ancestors within
|
|
14485
|
+
/** Refuse link-shaped ancestors within Caddy's owned state root. */
|
|
13474
14486
|
async function assertCaddyParents(root, target) {
|
|
13475
|
-
|
|
13476
|
-
const candidate = resolve(target);
|
|
13477
|
-
const offset = relative(parent, candidate);
|
|
13478
|
-
if (!isAbsolute(root) || offset !== "" && (offset.startsWith("..") || isAbsolute(offset))) throw new Error("caddy_path_invalid");
|
|
13479
|
-
const paths = [parent];
|
|
13480
|
-
let cursor = parent;
|
|
13481
|
-
for (const segment of relative(parent, dirname(candidate)).split(/[\\/]/u).filter(Boolean)) {
|
|
13482
|
-
cursor = join(cursor, segment);
|
|
13483
|
-
paths.push(cursor);
|
|
13484
|
-
}
|
|
13485
|
-
for (const path of paths) {
|
|
13486
|
-
let entry;
|
|
13487
|
-
try {
|
|
13488
|
-
entry = await lstat(path);
|
|
13489
|
-
} catch (error) {
|
|
13490
|
-
if (error.code === "ENOENT") continue;
|
|
13491
|
-
throw error;
|
|
13492
|
-
}
|
|
13493
|
-
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("caddy_path_invalid");
|
|
13494
|
-
}
|
|
14487
|
+
await assertManagedParents(root, target, "caddy");
|
|
13495
14488
|
}
|
|
13496
|
-
/**
|
|
14489
|
+
/** Move a Caddy-owned path, preserving bounded Windows retries and ownership checks. */
|
|
14490
|
+
async function renameCaddyPath(root, source, destination) {
|
|
14491
|
+
await renameManagedPath(root, source, destination, "caddy");
|
|
14492
|
+
}
|
|
14493
|
+
/** Create a real Caddy directory, securing fresh directories and preserving existing ACLs. */
|
|
13497
14494
|
async function ensureCaddyDirectory(root, directory) {
|
|
13498
|
-
await
|
|
13499
|
-
await mkdir(directory, {
|
|
13500
|
-
recursive: true,
|
|
13501
|
-
mode: 448
|
|
13502
|
-
});
|
|
13503
|
-
const entry = await lstat(directory);
|
|
13504
|
-
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("caddy_path_invalid");
|
|
13505
|
-
await restrictPrivateFile(directory, 448);
|
|
14495
|
+
await ensureManagedDirectory(root, directory, "caddy");
|
|
13506
14496
|
}
|
|
13507
|
-
/** Atomically replace one private configuration file
|
|
14497
|
+
/** Atomically replace one private Caddy configuration file. */
|
|
13508
14498
|
async function writeCaddyPrivateFile(root, file, body) {
|
|
13509
|
-
await
|
|
13510
|
-
try {
|
|
13511
|
-
const entry = await lstat(file);
|
|
13512
|
-
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("caddy_config_target_invalid");
|
|
13513
|
-
} catch (error) {
|
|
13514
|
-
if (error.code !== "ENOENT") throw error;
|
|
13515
|
-
}
|
|
13516
|
-
const temporary = join(dirname(file), "." + basename(file) + "." + randomBytes(12).toString("hex") + ".tmp");
|
|
13517
|
-
try {
|
|
13518
|
-
await writeFile(temporary, body, {
|
|
13519
|
-
encoding: "utf8",
|
|
13520
|
-
flag: "wx",
|
|
13521
|
-
mode: 384
|
|
13522
|
-
});
|
|
13523
|
-
await restrictPrivateFile(temporary);
|
|
13524
|
-
await rename(temporary, file);
|
|
13525
|
-
} finally {
|
|
13526
|
-
await rm(temporary, { force: true });
|
|
13527
|
-
}
|
|
14499
|
+
await writeManagedPrivateFile(root, file, body, "caddy");
|
|
13528
14500
|
}
|
|
13529
|
-
/** Delete only owned
|
|
14501
|
+
/** Delete only Caddy-owned children; unlink junctions and symlinks themselves. */
|
|
13530
14502
|
async function removeCaddyTree(root, target) {
|
|
13531
|
-
await
|
|
13532
|
-
let entry;
|
|
13533
|
-
try {
|
|
13534
|
-
entry = await lstat(target);
|
|
13535
|
-
} catch (error) {
|
|
13536
|
-
if (error.code === "ENOENT") return;
|
|
13537
|
-
throw error;
|
|
13538
|
-
}
|
|
13539
|
-
if (entry.isSymbolicLink()) {
|
|
13540
|
-
await unlink(target);
|
|
13541
|
-
return;
|
|
13542
|
-
}
|
|
13543
|
-
if (!entry.isDirectory()) {
|
|
13544
|
-
await unlink(target);
|
|
13545
|
-
return;
|
|
13546
|
-
}
|
|
13547
|
-
for (const name of await readdir(target)) await removeCaddyTree(root, join(target, name));
|
|
13548
|
-
await rmdir(target);
|
|
14503
|
+
await removeManagedTree(root, target, "caddy");
|
|
13549
14504
|
}
|
|
13550
14505
|
//#endregion
|
|
13551
14506
|
//#region src/caddy-component.ts
|
|
@@ -13622,7 +14577,7 @@ var CaddyComponentManager = class {
|
|
|
13622
14577
|
errorPrefix: "caddy"
|
|
13623
14578
|
}));
|
|
13624
14579
|
this.inspectExecutable = options.inspectExecutable ?? inspect;
|
|
13625
|
-
this.promote = options.promoteDirectory ??
|
|
14580
|
+
this.promote = options.promoteDirectory ?? ((source, destination) => renameCaddyPath(this.stateDirectory, source, destination));
|
|
13626
14581
|
}
|
|
13627
14582
|
/** Recheck a pinned executable before launching; no unverified local binary can run. */
|
|
13628
14583
|
async ensureExecutable() {
|
|
@@ -13673,6 +14628,7 @@ var CaddyComponentManager = class {
|
|
|
13673
14628
|
const backup = join(this.componentRoot, ".previous-" + randomBytes(12).toString("hex"));
|
|
13674
14629
|
let previous = false;
|
|
13675
14630
|
let promoted = false;
|
|
14631
|
+
let failed = false;
|
|
13676
14632
|
try {
|
|
13677
14633
|
const controller = new AbortController();
|
|
13678
14634
|
const timeout = setTimeout(() => {
|
|
@@ -13699,7 +14655,7 @@ var CaddyComponentManager = class {
|
|
|
13699
14655
|
try {
|
|
13700
14656
|
const entry = await lstat(this.componentStorage);
|
|
13701
14657
|
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new Error("caddy_path_invalid");
|
|
13702
|
-
await
|
|
14658
|
+
await renameCaddyPath(this.stateDirectory, this.componentStorage, backup);
|
|
13703
14659
|
previous = true;
|
|
13704
14660
|
} catch (error) {
|
|
13705
14661
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -13708,18 +14664,33 @@ var CaddyComponentManager = class {
|
|
|
13708
14664
|
await this.promote(candidate, this.componentStorage);
|
|
13709
14665
|
promoted = true;
|
|
13710
14666
|
} catch (error) {
|
|
13711
|
-
if (previous) {
|
|
13712
|
-
await
|
|
14667
|
+
if (previous) try {
|
|
14668
|
+
await renameCaddyPath(this.stateDirectory, backup, this.componentStorage);
|
|
13713
14669
|
previous = false;
|
|
14670
|
+
} catch (rollbackError) {
|
|
14671
|
+
this.installed = false;
|
|
14672
|
+
this.errorCode = "caddy_component_invalid";
|
|
14673
|
+
throw new AggregateError([error, rollbackError], "caddy_component_rollback_failed", { cause: error });
|
|
13714
14674
|
}
|
|
13715
14675
|
throw error;
|
|
13716
14676
|
}
|
|
13717
14677
|
this.installed = true;
|
|
13718
14678
|
this.errorCode = void 0;
|
|
14679
|
+
} catch (error) {
|
|
14680
|
+
failed = true;
|
|
14681
|
+
throw error;
|
|
13719
14682
|
} finally {
|
|
13720
|
-
|
|
13721
|
-
|
|
13722
|
-
|
|
14683
|
+
let cleanupError;
|
|
14684
|
+
for (const path of [
|
|
14685
|
+
staging,
|
|
14686
|
+
candidate,
|
|
14687
|
+
...previous && promoted ? [backup] : []
|
|
14688
|
+
]) try {
|
|
14689
|
+
await removeCaddyTree(this.stateDirectory, path);
|
|
14690
|
+
} catch (error) {
|
|
14691
|
+
cleanupError ??= error;
|
|
14692
|
+
}
|
|
14693
|
+
if (!failed && cleanupError !== void 0) throw cleanupError;
|
|
13723
14694
|
}
|
|
13724
14695
|
});
|
|
13725
14696
|
}
|
|
@@ -14832,24 +15803,69 @@ var PluginReleaseManager = class {
|
|
|
14832
15803
|
//#endregion
|
|
14833
15804
|
//#region src/file-logger.ts
|
|
14834
15805
|
const MAX_LOG_BYTES = 5242880;
|
|
14835
|
-
|
|
14836
|
-
|
|
15806
|
+
const MAX_QUEUED_BYTES = 262144;
|
|
15807
|
+
const PATH_PREFIX = "mobile_log";
|
|
15808
|
+
async function regularLogFile(file) {
|
|
15809
|
+
try {
|
|
15810
|
+
const entry = await lstat(file);
|
|
15811
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("mobile_log_target_invalid");
|
|
15812
|
+
return entry;
|
|
15813
|
+
} catch (error) {
|
|
15814
|
+
if (error.code === "ENOENT") return void 0;
|
|
15815
|
+
throw error;
|
|
15816
|
+
}
|
|
15817
|
+
}
|
|
15818
|
+
/** Open a private append-only file before registering a safely disposable Cordis exporter. */
|
|
15819
|
+
async function installMobileFileLogger(ctx, stateDirectory, options = {}) {
|
|
14837
15820
|
const directory = join(stateDirectory, "logs");
|
|
14838
15821
|
const file = join(directory, "dsh-mobile.log");
|
|
14839
15822
|
const previous = `${file}.1`;
|
|
14840
|
-
await
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
await
|
|
14847
|
-
await
|
|
15823
|
+
await ensureManagedDirectory(stateDirectory, directory, PATH_PREFIX);
|
|
15824
|
+
const entry = await regularLogFile(file);
|
|
15825
|
+
if (entry !== void 0 && entry.size >= MAX_LOG_BYTES) {
|
|
15826
|
+
await regularLogFile(previous);
|
|
15827
|
+
await removeManagedTree(stateDirectory, previous, PATH_PREFIX);
|
|
15828
|
+
await renameManagedPath(stateDirectory, file, previous, PATH_PREFIX);
|
|
15829
|
+
await regularLogFile(previous);
|
|
15830
|
+
await restrictPrivateFile(previous);
|
|
15831
|
+
}
|
|
15832
|
+
await assertManagedParents(stateDirectory, file, PATH_PREFIX);
|
|
15833
|
+
await regularLogFile(file);
|
|
15834
|
+
const handle = await open(file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0), 384);
|
|
15835
|
+
let stream;
|
|
15836
|
+
try {
|
|
15837
|
+
const opened = await handle.stat();
|
|
15838
|
+
const actual = await regularLogFile(file);
|
|
15839
|
+
if (!opened.isFile() || actual === void 0 || actual.dev !== opened.dev || actual.ino !== opened.ino) throw new Error("mobile_log_target_invalid");
|
|
15840
|
+
await assertManagedParents(stateDirectory, file, PATH_PREFIX);
|
|
15841
|
+
await handle.chmod(384);
|
|
15842
|
+
await restrictPrivateFile(file);
|
|
15843
|
+
stream = options.createStream?.(handle) ?? handle.createWriteStream({
|
|
15844
|
+
encoding: "utf8",
|
|
15845
|
+
autoClose: true
|
|
15846
|
+
});
|
|
15847
|
+
} catch (error) {
|
|
15848
|
+
await handle.close();
|
|
15849
|
+
throw error;
|
|
14848
15850
|
}
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
15851
|
+
let stopped = false;
|
|
15852
|
+
let removeExporter;
|
|
15853
|
+
const stopLogging = (error) => {
|
|
15854
|
+
if (stopped) return;
|
|
15855
|
+
stopped = true;
|
|
15856
|
+
try {
|
|
15857
|
+
Promise.resolve(removeExporter?.()).catch(() => void 0);
|
|
15858
|
+
} catch (disposeError) {}
|
|
15859
|
+
stream.destroy();
|
|
15860
|
+
const candidate = error;
|
|
15861
|
+
const code = candidate !== null && typeof candidate === "object" && typeof candidate.code === "string" ? candidate.code : "write_failed";
|
|
15862
|
+
try {
|
|
15863
|
+
ctx.logger("dsh-mobile").warn("file logging disabled (%s); other logging remains available", code);
|
|
15864
|
+
} catch (loggerError) {}
|
|
15865
|
+
};
|
|
15866
|
+
stream.on("error", stopLogging);
|
|
15867
|
+
const closed = finished$1(stream, { cleanup: true }).catch((error) => {
|
|
15868
|
+
stopLogging(error);
|
|
14853
15869
|
});
|
|
14854
15870
|
const exporter = {
|
|
14855
15871
|
colors: false,
|
|
@@ -14859,20 +15875,40 @@ async function installMobileFileLogger(ctx, stateDirectory) {
|
|
|
14859
15875
|
"dsh-mobile": 3
|
|
14860
15876
|
},
|
|
14861
15877
|
export(message) {
|
|
14862
|
-
if (message.name !== "dsh-mobile") return;
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
15878
|
+
if (stopped || message.name !== "dsh-mobile") return;
|
|
15879
|
+
try {
|
|
15880
|
+
const record = {
|
|
15881
|
+
timestamp: new Date(message.ts).toISOString(),
|
|
15882
|
+
level: message.type,
|
|
15883
|
+
logger: message.name,
|
|
15884
|
+
message: Logger.format(exporter, message)
|
|
15885
|
+
};
|
|
15886
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
15887
|
+
if (stream.writableLength + Buffer.byteLength(line) > MAX_QUEUED_BYTES) {
|
|
15888
|
+
stopLogging(Object.assign(/* @__PURE__ */ new Error("file log queue exhausted"), { code: "backpressure" }));
|
|
15889
|
+
return;
|
|
15890
|
+
}
|
|
15891
|
+
stream.write(line);
|
|
15892
|
+
} catch (error) {
|
|
15893
|
+
stopLogging(error);
|
|
15894
|
+
}
|
|
14870
15895
|
}
|
|
14871
15896
|
};
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
15897
|
+
try {
|
|
15898
|
+
removeExporter = ctx.logger.exporter(exporter);
|
|
15899
|
+
ctx.effect(() => async () => {
|
|
15900
|
+
stopped = true;
|
|
15901
|
+
await removeExporter?.();
|
|
15902
|
+
stream.end();
|
|
15903
|
+
await closed;
|
|
15904
|
+
}, "dsh-mobile file logger");
|
|
15905
|
+
} catch (error) {
|
|
15906
|
+
stopped = true;
|
|
15907
|
+
stream.destroy();
|
|
15908
|
+
await closed;
|
|
15909
|
+
await removeExporter?.();
|
|
15910
|
+
throw error;
|
|
15911
|
+
}
|
|
14876
15912
|
return file;
|
|
14877
15913
|
}
|
|
14878
15914
|
//#endregion
|
|
@@ -15034,6 +16070,7 @@ function installedDshVersion() {
|
|
|
15034
16070
|
}
|
|
15035
16071
|
function mapAdminError(error) {
|
|
15036
16072
|
if (error instanceof HttpError) return error;
|
|
16073
|
+
if (error instanceof MobileExtensionError) return new HttpError(error.status, error.code);
|
|
15037
16074
|
const code = error.code;
|
|
15038
16075
|
if (error instanceof Error && error.message.includes("spawn UNKNOWN")) return new HttpError(409, "frp_component_launch_failed");
|
|
15039
16076
|
if (code === "EADDRNOTAVAIL") return new HttpError(409, "network_address_changed");
|
|
@@ -15210,10 +16247,14 @@ function frpIngressGatewayConfig(template, settings, stateFile, ingress, listenP
|
|
|
15210
16247
|
* An unreadable self-signed CA fails explicitly: falling back to the system
|
|
15211
16248
|
* trust store would hide a broken pairing identity behind a start timeout.
|
|
15212
16249
|
*/
|
|
15213
|
-
async function readFrpIngressTrustAnchor(settings, stateFile) {
|
|
16250
|
+
async function readFrpIngressTrustAnchor(settings, stateFile, ownedRoot = dirname(stateFile)) {
|
|
15214
16251
|
if (!isFrpSelfSignedIngress(settings)) return void 0;
|
|
15215
16252
|
try {
|
|
15216
|
-
|
|
16253
|
+
const file = frpIngressPaths(stateFile).caCertFile;
|
|
16254
|
+
await assertManagedParents(ownedRoot, file, "frp_ingress");
|
|
16255
|
+
const entry = await lstat(file);
|
|
16256
|
+
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("frp_ingress_file_invalid");
|
|
16257
|
+
return await readFile(file, "utf8");
|
|
15217
16258
|
} catch (error) {
|
|
15218
16259
|
throw new Error("frp_ingress_ca_invalid", { cause: error });
|
|
15219
16260
|
}
|
|
@@ -15306,15 +16347,15 @@ async function apply(ctx, config) {
|
|
|
15306
16347
|
const frpComponent = new FrpComponentManager({ stateDirectory });
|
|
15307
16348
|
await frpComponent.initialize();
|
|
15308
16349
|
const frpIdentity = await loadOrCreateInstallationId(join(stateDirectory, "installation-id"));
|
|
15309
|
-
const frpConfig = new FrpConfigStore(join(remoteDirectory, "frp", "config"), frpProxyName(frpIdentity));
|
|
16350
|
+
const frpConfig = new FrpConfigStore(join(remoteDirectory, "frp", "config"), frpProxyName(frpIdentity), stateDirectory);
|
|
15310
16351
|
await frpConfig.initialize();
|
|
15311
|
-
const originConfig = new OriginConfigStore(join(remoteDirectory, "origin", "config"));
|
|
16352
|
+
const originConfig = new OriginConfigStore(join(remoteDirectory, "origin", "config"), stateDirectory);
|
|
15312
16353
|
await originConfig.initialize();
|
|
15313
16354
|
const caddyComponent = new CaddyComponentManager({ stateDirectory });
|
|
15314
16355
|
await caddyComponent.initialize();
|
|
15315
16356
|
const caddyConfig = new CaddyConfigStore(stateDirectory);
|
|
15316
16357
|
await caddyConfig.initialize();
|
|
15317
|
-
const cloudflaredTunnel = new CloudflaredTunnelStore(join(remoteDirectory, "cloudflared"));
|
|
16358
|
+
const cloudflaredTunnel = new CloudflaredTunnelStore(join(remoteDirectory, "cloudflared"), stateDirectory);
|
|
15318
16359
|
await cloudflaredTunnel.initialize();
|
|
15319
16360
|
const unregisterBuiltin = mobileAccess.registerExtension({
|
|
15320
16361
|
schemaVersion: 1,
|
|
@@ -15433,7 +16474,7 @@ async function apply(ctx, config) {
|
|
|
15433
16474
|
*/
|
|
15434
16475
|
const createFrpGateway = async (publicOrigin, settings) => {
|
|
15435
16476
|
if (!isFrpSelfSignedIngress(settings)) return createRemoteGateway(publicOrigin);
|
|
15436
|
-
const ingress = await ensureFrpIngressCertificate(settings, remoteDeviceFile);
|
|
16477
|
+
const ingress = await ensureFrpIngressCertificate(settings, remoteDeviceFile, Date.now(), void 0, stateDirectory);
|
|
15437
16478
|
const resolved = frpIngressGatewayConfig(template, settings, remoteDeviceFile, ingress);
|
|
15438
16479
|
const candidate = new MobileAccessGateway(resolved, new JsonDeviceStore(resolved.stateFile, resolved.maxDevices), mobileAccess, upstreamLoginUrl, webSocketPaths, blockedUpgradePaths, void 0, clientModulePreferences);
|
|
15439
16480
|
try {
|
|
@@ -15482,6 +16523,7 @@ async function apply(ctx, config) {
|
|
|
15482
16523
|
store: tailscaleStore,
|
|
15483
16524
|
executable: funnelExecutable(import.meta.url),
|
|
15484
16525
|
stateDirectory: join(remoteDirectory, "tailscale"),
|
|
16526
|
+
ownedRoot: stateDirectory,
|
|
15485
16527
|
hostname: `dsh-${instanceId.slice(0, 12)}`,
|
|
15486
16528
|
createGateway: createRemoteGateway
|
|
15487
16529
|
}),
|
|
@@ -15509,9 +16551,9 @@ async function apply(ctx, config) {
|
|
|
15509
16551
|
* Only this module knows where the ingress material lives, so the anchor is
|
|
15510
16552
|
* read here from the ingress directory instead of being guessed downstream.
|
|
15511
16553
|
*/
|
|
15512
|
-
resolveDiscoveryTrustAnchor: (settings) => readFrpIngressTrustAnchor(settings, remoteDeviceFile),
|
|
16554
|
+
resolveDiscoveryTrustAnchor: (settings) => readFrpIngressTrustAnchor(settings, remoteDeviceFile, stateDirectory),
|
|
15513
16555
|
maintainIngressCertificate: async (settings, gateway) => {
|
|
15514
|
-
await ensureFrpIngressCertificate(settings, remoteDeviceFile, Date.now(), gateway.config.instanceId);
|
|
16556
|
+
await ensureFrpIngressCertificate(settings, remoteDeviceFile, Date.now(), gateway.config.instanceId, stateDirectory);
|
|
15515
16557
|
await gateway.refreshProvidedTls();
|
|
15516
16558
|
}
|
|
15517
16559
|
}),
|
|
@@ -15590,7 +16632,7 @@ async function apply(ctx, config) {
|
|
|
15590
16632
|
const frpGateway = remoteControllers.frp.gateway();
|
|
15591
16633
|
const pinnedFrpProbe = frpSettings !== void 0 && isFrpSelfSignedIngress(frpSettings) && frpGateway !== void 0 && frpOrigin !== void 0 ? async () => {
|
|
15592
16634
|
try {
|
|
15593
|
-
const trustAnchorPem = await readFrpIngressTrustAnchor(frpSettings, remoteDeviceFile);
|
|
16635
|
+
const trustAnchorPem = await readFrpIngressTrustAnchor(frpSettings, remoteDeviceFile, stateDirectory);
|
|
15594
16636
|
if (trustAnchorPem === void 0) return { state: "unreachable" };
|
|
15595
16637
|
const started = performance.now();
|
|
15596
16638
|
return await defaultProbeDiscovery({
|
|
@@ -15713,6 +16755,17 @@ async function apply(ctx, config) {
|
|
|
15713
16755
|
sendJson(response, 200, await releaseManager.status(), false);
|
|
15714
16756
|
return;
|
|
15715
16757
|
}
|
|
16758
|
+
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/extensions/hosts`) {
|
|
16759
|
+
sendJson(response, 200, mobileAccess.hostStatus(), false);
|
|
16760
|
+
return;
|
|
16761
|
+
}
|
|
16762
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/extensions/recover`) {
|
|
16763
|
+
const body = await readJsonObject(request, 4096);
|
|
16764
|
+
if (typeof body.id !== "string" || body.confirm !== true) throw new HttpError(400, "bad_request");
|
|
16765
|
+
await mobileAccess.recoverExtension(body.id);
|
|
16766
|
+
sendJson(response, 200, mobileAccess.hostStatus(), false);
|
|
16767
|
+
return;
|
|
16768
|
+
}
|
|
15716
16769
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/release/update`) {
|
|
15717
16770
|
await readJsonObject(request, 4096);
|
|
15718
16771
|
sendJson(response, 200, await releaseManager.update(), false);
|
|
@@ -15946,7 +16999,7 @@ async function apply(ctx, config) {
|
|
|
15946
16999
|
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/remote/frp/self-check`) {
|
|
15947
17000
|
const settings = frpConfig.settings();
|
|
15948
17001
|
if (settings === void 0) throw new HttpError(409, "frp_config_missing");
|
|
15949
|
-
const check = await frpIngressSelfCheck(settings, remoteDeviceFile);
|
|
17002
|
+
const check = await frpIngressSelfCheck(settings, remoteDeviceFile, stateDirectory);
|
|
15950
17003
|
const entry = resolveFrpEntryProbe(settings);
|
|
15951
17004
|
const [frpsReachable, entryReachable] = await Promise.all([probeTcpReachable(settings.serverAddress, settings.serverPort), probeTcpReachable(entry.host, entry.port)]);
|
|
15952
17005
|
sendJson(response, 200, {
|
|
@@ -16061,10 +17114,10 @@ async function apply(ctx, config) {
|
|
|
16061
17114
|
if ((await readJsonObject(request, 4096)).confirm !== true) throw new HttpError(400, "bad_request");
|
|
16062
17115
|
await remoteProviders.mutate(async () => {
|
|
16063
17116
|
await remoteControllers.frp.setEnabled(false);
|
|
16064
|
-
await
|
|
16065
|
-
frpComponent.purge(),
|
|
16066
|
-
frpConfig.purge(),
|
|
16067
|
-
purgeFrpIngressCertificates(remoteDeviceFile)
|
|
17117
|
+
await settleCleanupSteps([
|
|
17118
|
+
() => frpComponent.purge().then(() => void 0),
|
|
17119
|
+
() => frpConfig.purge().then(() => void 0),
|
|
17120
|
+
() => purgeFrpIngressCertificates(remoteDeviceFile, stateDirectory)
|
|
16068
17121
|
]);
|
|
16069
17122
|
});
|
|
16070
17123
|
sendJson(response, 200, remotePayload(), false);
|
|
@@ -16130,15 +17183,23 @@ async function apply(ctx, config) {
|
|
|
16130
17183
|
kind: "error",
|
|
16131
17184
|
text: "请带上需求,例如:/mobile 把手机端改成深色主题"
|
|
16132
17185
|
};
|
|
17186
|
+
const hostStates = new Map(mobileAccess.hostStatus().hosts.map((host) => [host.id, host]));
|
|
16133
17187
|
const guide = buildMobileGuide({
|
|
16134
17188
|
directory: stateDirectory,
|
|
16135
17189
|
hasCustomCss: await existsRegularFile(template.customCssFile),
|
|
16136
17190
|
hasCustomJs: await existsRegularFile(template.customScriptFile),
|
|
16137
|
-
extensions: mobileAccess.manifest().map((entry) =>
|
|
16138
|
-
|
|
16139
|
-
|
|
16140
|
-
|
|
16141
|
-
|
|
17191
|
+
extensions: mobileAccess.manifest().map((entry) => {
|
|
17192
|
+
const host = hostStates.get(entry.id);
|
|
17193
|
+
return {
|
|
17194
|
+
id: entry.id,
|
|
17195
|
+
name: entry.name,
|
|
17196
|
+
version: entry.version,
|
|
17197
|
+
...host === void 0 ? {} : {
|
|
17198
|
+
executionMode: host.mode,
|
|
17199
|
+
executionState: host.state
|
|
17200
|
+
}
|
|
17201
|
+
};
|
|
17202
|
+
}),
|
|
16142
17203
|
failedExtensionCount: mobileAccess.status().failed
|
|
16143
17204
|
});
|
|
16144
17205
|
agent.steer(createUserMessage({
|
|
@@ -16159,7 +17220,7 @@ async function apply(ctx, config) {
|
|
|
16159
17220
|
}
|
|
16160
17221
|
});
|
|
16161
17222
|
try {
|
|
16162
|
-
await mobileAccess.startLocal(template.extensionsDir, ctx);
|
|
17223
|
+
await mobileAccess.startLocal(template.extensionsDir, ctx, { hostExecution: template.hostExecution });
|
|
16163
17224
|
await lanController.initialize();
|
|
16164
17225
|
const stores = {
|
|
16165
17226
|
tailscale: tailscaleStore,
|
|
@@ -16211,6 +17272,6 @@ async function apply(ctx, config) {
|
|
|
16211
17272
|
}, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
|
|
16212
17273
|
}
|
|
16213
17274
|
//#endregion
|
|
16214
|
-
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, ClientModuleConflictError, ClientModulePreferenceStore, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
|
|
17275
|
+
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, ClientModuleConflictError, ClientModulePreferenceStore, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, DEFAULT_ORIGIN_LISTEN_PORT, FRP_VHOST_HTTP_PORT as DEFAULT_VHOST_HTTP_PORT, FRP_VHOST_HTTP_PORT, DEVICE_COOKIE, EXTENSION_LIMITS, FRP_ATTACH_DISCOVERY_PATH, FRP_ATTACH_LOCAL_DECLARATION, FRP_ATTACH_SELF_SIGNED_DECLARATION, FRP_ATTACH_VPS_DECLARATION, FRP_CADDY_IMPORT_LINE, FRP_CADDY_SNIPPET_MARKER, FRP_CADDY_SNIPPET_PATH, FRP_COMPONENT_RELEASES, FRP_DEFAULT_PROXY_NAME, FRP_DEFAULT_PUBLIC_PORT, FRP_RESERVED_PORTS, FrpComponentManager, FrpConfigStore, FrpController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, REMOTE_PROVIDERS, RequestTrustPolicy, SESSION_COOKIE, TASK_EVENT_DEBOUNCE_MS, TaskEventHub, WS_PATHS, WebSocketPathStore, addressAllowed, apply, assertExtensionId, configuredRemoteProvider, createCaddySite, createFrpAttachPlan, createFrpAttachTemplate, createFrpAttachTemplateParts, createFrpServerTemplate, createFrpcToml, createMobileAccessService, createRestrictedFrpServerTemplate, ensureFrpIngressCertificate, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, isCloudflaredRegistration, isFrpSelfSignedIngress, isGloballyRoutableIpv4, isLoopbackAddress, issueServerCertificate, mergeSavedCloudflaredTunnelSettings, mergeSavedFrpSettings, mergeSavedFrpTarget, name, normalizeWebSocketPaths, originGatewayConfig, parseAuthority, parseCidr, parseCloudflaredOrigin, parseCloudflaredTunnelSettings, parseControlFile, parseDeviceSnapshot, parseExtensionManifest, parseFrpSettings, parseGatewayConfig, parseMobileAccessControlState, parseOriginSettings, parseRemoteProviderState, probeOriginCertificate, readCertificateRenewal, resolveAuthority, resolveFrpEntryTls, resolveFrpMode, resolveFrpPublicPort, resolveFrpVhostHttpPort, resolveHostExecution, rewriteMobileIndex, validateAttachSettings, validateCloudflaredTunnelHostname, validateCloudflaredTunnelPort, validateCloudflaredTunnelToken, validateFrpEntryTls, validateFrpMode, validateFrpProxyName, validateFrpPublicOrigin, validateFrpPublicPort, validateFrpServerAddress, validateFrpServerPort, validateFrpToken, validateFrpVhostHttpPort, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validateWebSocketPath, watchTaskCompletions };
|
|
16215
17276
|
|
|
16216
17277
|
//# sourceMappingURL=index.mjs.map
|