dsh-mobile 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/CONTRIBUTORS.md +12 -0
- package/README.en.md +8 -6
- package/README.md +8 -6
- package/docs/CONNECTION_DIAGNOSTICS.en.md +4 -0
- package/docs/CONNECTION_DIAGNOSTICS.md +4 -0
- package/docs/IROH.en.md +1 -0
- package/docs/IROH.md +1 -0
- package/docs/SLOW_CONNECTIONS.en.md +8 -0
- package/docs/SLOW_CONNECTIONS.md +8 -0
- package/docs/TRUSTED_NETWORKS.en.md +6 -0
- package/docs/TRUSTED_NETWORKS.md +6 -0
- package/lib/cli.js +59 -22
- package/lib/cli.js.map +1 -1
- package/lib/client.js +60 -4
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +19 -0
- package/lib/index.mjs +770 -42
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +3 -2
- package/lib/mobile-layout.js.map +1 -1
- package/package.json +1 -1
- package/packages/question-fixes/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve,
|
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
6
|
import { connect, createServer, isIP } from "node:net";
|
|
7
|
-
import { chmod, copyFile, link, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { access, chmod, copyFile, link, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
8
8
|
import { performance as performance$1 } from "node:perf_hooks";
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
10
|
import { createSocket } from "node:dgram";
|
|
@@ -25,7 +25,7 @@ import { connect as connect$1 } from "node:tls";
|
|
|
25
25
|
import { generate } from "selfsigned";
|
|
26
26
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm/message";
|
|
27
27
|
import { lookup } from "node:dns/promises";
|
|
28
|
-
import { constants } from "node:fs";
|
|
28
|
+
import { constants, readFileSync } from "node:fs";
|
|
29
29
|
import { z as z$1 } from "zod";
|
|
30
30
|
import webPush from "web-push";
|
|
31
31
|
// src/access.ts
|
|
@@ -1036,10 +1036,17 @@ const Config = z.object({
|
|
|
1036
1036
|
maxBodyBytes: z.natural(),
|
|
1037
1037
|
upstreamTimeoutMs: z.natural(),
|
|
1038
1038
|
upstreamApiTimeoutMs: z.natural(),
|
|
1039
|
+
catalogCache: z.boolean(),
|
|
1039
1040
|
rateLimitWindowMs: z.natural(),
|
|
1040
1041
|
maxPairingAttempts: z.natural(),
|
|
1041
1042
|
maxRateLimitKeys: z.natural()
|
|
1042
1043
|
});
|
|
1044
|
+
/** Resolve one optional boolean with an explicit default; reject other types. */
|
|
1045
|
+
function booleanValue(value, name, fallback) {
|
|
1046
|
+
if (value === void 0) return fallback;
|
|
1047
|
+
if (typeof value !== "boolean") throw new Error(`${name} must be a boolean`);
|
|
1048
|
+
return value;
|
|
1049
|
+
}
|
|
1043
1050
|
function integer(value, name, fallback, minimum, maximum) {
|
|
1044
1051
|
const resolved = value ?? fallback;
|
|
1045
1052
|
if (typeof resolved !== "number" || !Number.isSafeInteger(resolved) || resolved < minimum || resolved > maximum) throw new Error(`${name} must be an integer from ${String(minimum)} through ${String(maximum)}`);
|
|
@@ -1196,6 +1203,7 @@ function parseGatewayConfig(raw) {
|
|
|
1196
1203
|
maxBodyBytes: integer(value.maxBodyBytes, "maxBodyBytes", 167772160, 1024, 268435456),
|
|
1197
1204
|
upstreamTimeoutMs: integer(value.upstreamTimeoutMs, "upstreamTimeoutMs", 3e4, 1e3, 3e5),
|
|
1198
1205
|
upstreamApiTimeoutMs: integer(value.upstreamApiTimeoutMs, "upstreamApiTimeoutMs", 0, 0, 2147483647),
|
|
1206
|
+
catalogCache: booleanValue(value.catalogCache, "catalogCache", true),
|
|
1199
1207
|
rateLimitWindowMs: integer(value.rateLimitWindowMs, "rateLimitWindowMs", 6e4, 1e3, 36e5),
|
|
1200
1208
|
maxPairingAttempts: integer(value.maxPairingAttempts, "maxPairingAttempts", 8, 1, 100),
|
|
1201
1209
|
maxRateLimitKeys: integer(value.maxRateLimitKeys, "maxRateLimitKeys", 256, 1, 4096)
|
|
@@ -2469,6 +2477,305 @@ var ApiLatencyRecorder = class {
|
|
|
2469
2477
|
}
|
|
2470
2478
|
}
|
|
2471
2479
|
};
|
|
2480
|
+
// src/catalog-cache.ts
|
|
2481
|
+
/** Host RPCs whose successful completion can change catalog results. */
|
|
2482
|
+
const CATALOG_MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
2483
|
+
"account/signOut",
|
|
2484
|
+
"account/startSignIn",
|
|
2485
|
+
"credentials/set",
|
|
2486
|
+
"credentials/unset",
|
|
2487
|
+
"pluginManager/installBundle",
|
|
2488
|
+
"pluginManager/removeBundle",
|
|
2489
|
+
"pluginManager/setBundleEnabled",
|
|
2490
|
+
"pluginManager/setPluginEnabled",
|
|
2491
|
+
"pluginManager/setVersionExemption",
|
|
2492
|
+
"pluginManager/updateBundle",
|
|
2493
|
+
"session/initializeDefaultModel",
|
|
2494
|
+
"session/selectModel",
|
|
2495
|
+
"settings/mutate",
|
|
2496
|
+
"settings/replace",
|
|
2497
|
+
"settings/update"
|
|
2498
|
+
]);
|
|
2499
|
+
const CATALOG_CACHE_METHODS = /* @__PURE__ */ new Set(["llm/listProviders", "session/modelCatalog"]);
|
|
2500
|
+
const DEFAULT_CATALOG_CACHE_LIMITS = Object.freeze({
|
|
2501
|
+
freshMs: 5e3,
|
|
2502
|
+
staleMs: 6e4,
|
|
2503
|
+
maxEntries: 4,
|
|
2504
|
+
maxEntryBytes: 2097152,
|
|
2505
|
+
maxTotalBytes: 8388608,
|
|
2506
|
+
maxInFlightFills: 2
|
|
2507
|
+
});
|
|
2508
|
+
const REQUEST_KEYS = /* @__PURE__ */ new Set([
|
|
2509
|
+
"type",
|
|
2510
|
+
"rpcId",
|
|
2511
|
+
"method",
|
|
2512
|
+
"payload"
|
|
2513
|
+
]);
|
|
2514
|
+
const RESPONSE_KEYS = /* @__PURE__ */ new Set([
|
|
2515
|
+
"type",
|
|
2516
|
+
"rpcId",
|
|
2517
|
+
"result"
|
|
2518
|
+
]);
|
|
2519
|
+
const encoder = new TextEncoder();
|
|
2520
|
+
const isObject$1 = (value) => typeof value === "object" && value !== null;
|
|
2521
|
+
const hasExactKeys = (value, keys) => {
|
|
2522
|
+
const present = Object.keys(value);
|
|
2523
|
+
if (present.length !== keys.size) return false;
|
|
2524
|
+
for (const key of present) if (!keys.has(key)) return false;
|
|
2525
|
+
return true;
|
|
2526
|
+
};
|
|
2527
|
+
/**
|
|
2528
|
+
* Validate one catalog request against the captured wire contract.
|
|
2529
|
+
* @param routeMethod - The `/api/<method>` route segment.
|
|
2530
|
+
* @param bodyJson - The exact buffered request body.
|
|
2531
|
+
* @returns The eligible method and its live rpcId, or undefined for anything else.
|
|
2532
|
+
*/
|
|
2533
|
+
function checkCatalogRequest(routeMethod, bodyJson) {
|
|
2534
|
+
if (!CATALOG_CACHE_METHODS.has(routeMethod)) return void 0;
|
|
2535
|
+
let parsed;
|
|
2536
|
+
try {
|
|
2537
|
+
parsed = JSON.parse(bodyJson);
|
|
2538
|
+
} catch {
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
if (!isObject$1(parsed) || !hasExactKeys(parsed, REQUEST_KEYS)) return void 0;
|
|
2542
|
+
if (parsed.type !== "client-request") return void 0;
|
|
2543
|
+
if (parsed.method !== routeMethod) return void 0;
|
|
2544
|
+
if (typeof parsed.rpcId !== "string" || parsed.rpcId.length === 0 || parsed.rpcId.length > 512) return void 0;
|
|
2545
|
+
const payload = parsed.payload;
|
|
2546
|
+
if (!isObject$1(payload) || !hasExactKeys(payload, /* @__PURE__ */ new Set(["args"]))) return void 0;
|
|
2547
|
+
const args = payload.args;
|
|
2548
|
+
if (!isObject$1(args) || Array.isArray(args) || Object.keys(args).length !== 0) return void 0;
|
|
2549
|
+
return {
|
|
2550
|
+
method: routeMethod,
|
|
2551
|
+
rpcId: parsed.rpcId
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Validate a fill response against the captured success schema.
|
|
2556
|
+
* @param envelopeJson - The buffered upstream response body.
|
|
2557
|
+
* @param expectedRpcId - The gateway-generated rpcId the fill sent.
|
|
2558
|
+
* @returns Whether the envelope is the shareable success variant for that id.
|
|
2559
|
+
*/
|
|
2560
|
+
function validateCatalogEnvelope(envelopeJson, expectedRpcId) {
|
|
2561
|
+
let parsed;
|
|
2562
|
+
try {
|
|
2563
|
+
parsed = JSON.parse(envelopeJson);
|
|
2564
|
+
} catch {
|
|
2565
|
+
return false;
|
|
2566
|
+
}
|
|
2567
|
+
if (!isObject$1(parsed) || !hasExactKeys(parsed, RESPONSE_KEYS)) return false;
|
|
2568
|
+
if (parsed.type !== "server-response") return false;
|
|
2569
|
+
if (parsed.rpcId !== expectedRpcId) return false;
|
|
2570
|
+
const result = parsed.result;
|
|
2571
|
+
if (!isObject$1(result)) return false;
|
|
2572
|
+
return result.ok === true && "value" in result;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Re-stamp a validated envelope for one caller.
|
|
2576
|
+
* @param envelopeJson - The cached upstream envelope.
|
|
2577
|
+
* @param expectedRpcId - The fill rpcId the envelope must carry.
|
|
2578
|
+
* @param liveRpcId - The caller's rpcId to serialize into the response.
|
|
2579
|
+
* @returns The per-caller JSON body, or undefined when validation fails.
|
|
2580
|
+
*/
|
|
2581
|
+
function restampEnvelope(envelopeJson, expectedRpcId, liveRpcId) {
|
|
2582
|
+
let parsed;
|
|
2583
|
+
try {
|
|
2584
|
+
parsed = JSON.parse(envelopeJson);
|
|
2585
|
+
} catch {
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (!validateCatalogEnvelope(envelopeJson, expectedRpcId)) return void 0;
|
|
2589
|
+
const body = parsed;
|
|
2590
|
+
body.rpcId = liveRpcId;
|
|
2591
|
+
return JSON.stringify(body);
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Generation-aware SWR cache over the injected fill transport.
|
|
2595
|
+
* @typeParam TUpstreamSnapshot - Opaque immutable upstream identity snapshot the gateway captures per request.
|
|
2596
|
+
*/
|
|
2597
|
+
var CatalogCache = class {
|
|
2598
|
+
#limits;
|
|
2599
|
+
#clock;
|
|
2600
|
+
#fetchFill;
|
|
2601
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2602
|
+
#flights = /* @__PURE__ */ new Map();
|
|
2603
|
+
#identityDigest;
|
|
2604
|
+
#epoch = 0;
|
|
2605
|
+
#disposed = false;
|
|
2606
|
+
constructor(deps) {
|
|
2607
|
+
this.#fetchFill = deps.fetchFill;
|
|
2608
|
+
this.#clock = deps.clock;
|
|
2609
|
+
this.#limits = {
|
|
2610
|
+
...DEFAULT_CATALOG_CACHE_LIMITS,
|
|
2611
|
+
...deps.limits
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
get entryCount() {
|
|
2615
|
+
return this.#entries.size;
|
|
2616
|
+
}
|
|
2617
|
+
get residentBytes() {
|
|
2618
|
+
let total = 0;
|
|
2619
|
+
for (const entry of this.#entries.values()) total += entry.bytes;
|
|
2620
|
+
return total;
|
|
2621
|
+
}
|
|
2622
|
+
get inFlightFills() {
|
|
2623
|
+
let active = 0;
|
|
2624
|
+
for (const flight of this.#flights.values()) if (!flight.settled) active += 1;
|
|
2625
|
+
return active;
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Serve one eligible request from the current generation.
|
|
2629
|
+
* @param input - Method, caller rpcId, current identity digest and its snapshot.
|
|
2630
|
+
* @returns A per-caller replay, or passthrough when the ordinary path must run.
|
|
2631
|
+
*/
|
|
2632
|
+
async serve(input) {
|
|
2633
|
+
if (this.#disposed) return { kind: "passthrough" };
|
|
2634
|
+
if (!CATALOG_CACHE_METHODS.has(input.method)) return { kind: "passthrough" };
|
|
2635
|
+
const method = input.method;
|
|
2636
|
+
this.#syncIdentity(input.identityDigest);
|
|
2637
|
+
const generation = this.#currentGeneration();
|
|
2638
|
+
const key = `${input.identityDigest}\u0000${method}`;
|
|
2639
|
+
const entry = this.#entries.get(key);
|
|
2640
|
+
if (entry !== void 0) {
|
|
2641
|
+
const age = this.#clock() - entry.completedAt;
|
|
2642
|
+
if (age < this.#limits.freshMs) {
|
|
2643
|
+
entry.lastServedAt = this.#clock();
|
|
2644
|
+
const body = restampEnvelope(entry.envelopeJson, entry.upstreamRpcId, input.rpcId);
|
|
2645
|
+
if (body !== void 0) return {
|
|
2646
|
+
kind: "hit",
|
|
2647
|
+
body,
|
|
2648
|
+
generation,
|
|
2649
|
+
validUntil: entry.validUntil
|
|
2650
|
+
};
|
|
2651
|
+
} else if (age < this.#limits.staleMs) {
|
|
2652
|
+
entry.lastServedAt = this.#clock();
|
|
2653
|
+
const body = restampEnvelope(entry.envelopeJson, entry.upstreamRpcId, input.rpcId);
|
|
2654
|
+
if (body !== void 0) {
|
|
2655
|
+
this.#startBackgroundFill(method, input.identityDigest, input.upstream);
|
|
2656
|
+
return {
|
|
2657
|
+
kind: "stale",
|
|
2658
|
+
body,
|
|
2659
|
+
generation,
|
|
2660
|
+
validUntil: entry.validUntil
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
const flightKey = `${input.identityDigest}|${generation.epoch}|${method}`;
|
|
2666
|
+
const existing = this.#flights.get(flightKey);
|
|
2667
|
+
if (existing === void 0 && this.inFlightFills >= this.#limits.maxInFlightFills) return { kind: "passthrough" };
|
|
2668
|
+
const outcome = await (existing ?? this.#createFlight(method, generation, input.upstream)).promise;
|
|
2669
|
+
if (!outcome.shareable) return { kind: "passthrough" };
|
|
2670
|
+
if (!this.#isCurrent(generation)) return { kind: "passthrough" };
|
|
2671
|
+
if (!validateCatalogEnvelope(outcome.envelopeJson, outcome.upstreamRpcId)) return { kind: "passthrough" };
|
|
2672
|
+
const body = restampEnvelope(outcome.envelopeJson, outcome.upstreamRpcId, input.rpcId);
|
|
2673
|
+
if (body === void 0) return { kind: "passthrough" };
|
|
2674
|
+
return {
|
|
2675
|
+
kind: "replay",
|
|
2676
|
+
body,
|
|
2677
|
+
generation,
|
|
2678
|
+
validUntil: this.#clock() + this.#limits.staleMs
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
/** Record the current upstream identity; a change clears entries and bumps the epoch. */
|
|
2682
|
+
noteIdentity(identityDigest) {
|
|
2683
|
+
this.#syncIdentity(identityDigest);
|
|
2684
|
+
}
|
|
2685
|
+
/** Invalidate every entry after a catalog-affecting mutation. */
|
|
2686
|
+
noteCatalogMutation() {
|
|
2687
|
+
this.#epoch += 1;
|
|
2688
|
+
this.#entries.clear();
|
|
2689
|
+
}
|
|
2690
|
+
/** Whether a generation is still the current one. */
|
|
2691
|
+
isCurrent(generation) {
|
|
2692
|
+
return this.#isCurrent(generation);
|
|
2693
|
+
}
|
|
2694
|
+
/** Final pre-commit check for a replay result: generation, disposal and age. */
|
|
2695
|
+
canCommit(result) {
|
|
2696
|
+
if (this.#disposed) return false;
|
|
2697
|
+
if (!this.#isCurrent(result.generation)) return false;
|
|
2698
|
+
return this.#clock() < result.validUntil;
|
|
2699
|
+
}
|
|
2700
|
+
/** Invalidate everything and refuse new fills or publication. */
|
|
2701
|
+
dispose() {
|
|
2702
|
+
this.#disposed = true;
|
|
2703
|
+
this.#entries.clear();
|
|
2704
|
+
}
|
|
2705
|
+
#isCurrent(generation) {
|
|
2706
|
+
if (this.#disposed) return false;
|
|
2707
|
+
return generation.identityDigest === this.#identityDigest && generation.epoch === this.#epoch;
|
|
2708
|
+
}
|
|
2709
|
+
#currentGeneration() {
|
|
2710
|
+
return {
|
|
2711
|
+
identityDigest: this.#identityDigest,
|
|
2712
|
+
epoch: this.#epoch
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
#syncIdentity(identityDigest) {
|
|
2716
|
+
if (identityDigest === this.#identityDigest) return;
|
|
2717
|
+
this.#identityDigest = identityDigest;
|
|
2718
|
+
this.#epoch += 1;
|
|
2719
|
+
this.#entries.clear();
|
|
2720
|
+
}
|
|
2721
|
+
#createFlight(method, generation, upstream) {
|
|
2722
|
+
const flightKey = `${generation.identityDigest}|${generation.epoch}|${method}`;
|
|
2723
|
+
const invoke = async () => this.#fetchFill({
|
|
2724
|
+
method,
|
|
2725
|
+
generation,
|
|
2726
|
+
upstream
|
|
2727
|
+
});
|
|
2728
|
+
const flight = {
|
|
2729
|
+
generation,
|
|
2730
|
+
method,
|
|
2731
|
+
settled: false,
|
|
2732
|
+
promise: invoke().catch(() => ({ shareable: false })).then((outcome) => {
|
|
2733
|
+
flight.settled = true;
|
|
2734
|
+
if (this.#flights.get(flightKey) === flight) this.#flights.delete(flightKey);
|
|
2735
|
+
if (outcome.shareable && this.#isCurrent(generation)) this.#retain(method, generation, outcome);
|
|
2736
|
+
return outcome;
|
|
2737
|
+
})
|
|
2738
|
+
};
|
|
2739
|
+
this.#flights.set(flightKey, flight);
|
|
2740
|
+
return flight;
|
|
2741
|
+
}
|
|
2742
|
+
#startBackgroundFill(method, identityDigest, upstream) {
|
|
2743
|
+
if (this.#disposed) return;
|
|
2744
|
+
const generation = this.#currentGeneration();
|
|
2745
|
+
const flightKey = `${identityDigest}|${generation.epoch}|${method}`;
|
|
2746
|
+
if (this.#flights.has(flightKey)) return;
|
|
2747
|
+
if (this.inFlightFills >= this.#limits.maxInFlightFills) return;
|
|
2748
|
+
this.#createFlight(method, generation, upstream).promise.catch(() => void 0);
|
|
2749
|
+
}
|
|
2750
|
+
#retain(method, generation, outcome) {
|
|
2751
|
+
if (!validateCatalogEnvelope(outcome.envelopeJson, outcome.upstreamRpcId)) return;
|
|
2752
|
+
const bytes = encoder.encode(outcome.envelopeJson).length;
|
|
2753
|
+
if (bytes > this.#limits.maxEntryBytes) return;
|
|
2754
|
+
const key = `${generation.identityDigest}\u0000${method}`;
|
|
2755
|
+
const now = this.#clock();
|
|
2756
|
+
if (this.#entries.has(key)) this.#entries.delete(key);
|
|
2757
|
+
const evict = () => {
|
|
2758
|
+
const candidates = [...this.#entries.entries()].sort(([, a], [, b]) => a.lastServedAt - b.lastServedAt);
|
|
2759
|
+
for (const [candidateKey] of candidates) {
|
|
2760
|
+
if (this.#entries.size < this.#limits.maxEntries && this.residentBytes + bytes <= this.#limits.maxTotalBytes) break;
|
|
2761
|
+
this.#entries.delete(candidateKey);
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
evict();
|
|
2765
|
+
if (this.#entries.size >= this.#limits.maxEntries) return;
|
|
2766
|
+
if (this.residentBytes + bytes > this.#limits.maxTotalBytes) return;
|
|
2767
|
+
this.#entries.set(key, {
|
|
2768
|
+
identityDigest: generation.identityDigest,
|
|
2769
|
+
method,
|
|
2770
|
+
envelopeJson: outcome.envelopeJson,
|
|
2771
|
+
upstreamRpcId: outcome.upstreamRpcId,
|
|
2772
|
+
completedAt: now,
|
|
2773
|
+
validUntil: now + this.#limits.staleMs,
|
|
2774
|
+
bytes,
|
|
2775
|
+
lastServedAt: now
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2472
2779
|
// src/upstream-auth-log.ts
|
|
2473
2780
|
const LOG_INTERVAL_MS = 3e4;
|
|
2474
2781
|
/** Two fixed sampling slots keep a first failure visible even after a successful exchange. */
|
|
@@ -5387,6 +5694,9 @@ const MOBILE_LAYOUT_MODULE = "@deepseek-ai/dsh-client-ui-layout";
|
|
|
5387
5694
|
const MOBILE_LAYOUT_PATH = `${AUTH_PREFIX}/mobile-layout.js`;
|
|
5388
5695
|
const MOBILE_BOOT_BATCH_PREFIX = `${AUTH_PREFIX}/mobile-boot/`;
|
|
5389
5696
|
const MOBILE_DOCUMENT_VIEW_HEADER = "x-dsh-mobile-view";
|
|
5697
|
+
const CATALOG_CACHE_MARKER = "x-dsh-mobile-catalog";
|
|
5698
|
+
/** Eligible catalog envelopes are small fixed-shape JSON; larger declared bodies keep the streaming path. */
|
|
5699
|
+
const CATALOG_CANDIDATE_BODY_LIMIT = 65536;
|
|
5390
5700
|
const MOBILE_PUSH_CONFIG_PATH = `${AUTH_PREFIX}/push/config`;
|
|
5391
5701
|
const MOBILE_PUSH_SUBSCRIPTION_PATH = `${AUTH_PREFIX}/push/subscription`;
|
|
5392
5702
|
const MOBILE_PUSH_PREFERENCES_PATH = `${AUTH_PREFIX}/push/preferences`;
|
|
@@ -5869,7 +6179,7 @@ function splitMobileBootBatch(entries, sizes, passThrough) {
|
|
|
5869
6179
|
};
|
|
5870
6180
|
for (const entry of entries) {
|
|
5871
6181
|
const size = sizes.get(entry.url);
|
|
5872
|
-
if ((passThrough.has(entry.url) || size !== void 0 && size >= MAX_MOBILE_BOOT_ENTRY_BYTES) && entry.id !== MOBILE_LAYOUT_MODULE) {
|
|
6182
|
+
if ((passThrough.has(entry.url) || size !== void 0 && (size <= 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES)) && entry.id !== MOBILE_LAYOUT_MODULE) {
|
|
5873
6183
|
rows.push({
|
|
5874
6184
|
phase: "application",
|
|
5875
6185
|
url: entry.url,
|
|
@@ -5878,7 +6188,7 @@ function splitMobileBootBatch(entries, sizes, passThrough) {
|
|
|
5878
6188
|
});
|
|
5879
6189
|
continue;
|
|
5880
6190
|
}
|
|
5881
|
-
const bytes = (size
|
|
6191
|
+
const bytes = (size !== void 0 && size > 0 ? size : 0) + MOBILE_BOOT_SEPARATOR_BYTES;
|
|
5882
6192
|
if (chunk !== void 0 && chunk.entries.length > 0 && chunk.bytes + bytes > MOBILE_BOOT_CHUNK_BYTES) flush();
|
|
5883
6193
|
chunk ??= {
|
|
5884
6194
|
ids: [],
|
|
@@ -6482,6 +6792,18 @@ var MobileAccessGateway = class {
|
|
|
6482
6792
|
upstreamCookie;
|
|
6483
6793
|
upstreamCookieExpiresAt = 0;
|
|
6484
6794
|
upstreamCookieTask;
|
|
6795
|
+
#catalogCache;
|
|
6796
|
+
#upstreamIdentityRevision = 0;
|
|
6797
|
+
#catalogFills = /* @__PURE__ */ new Set();
|
|
6798
|
+
/** The catalog SWR cache, or undefined when `catalogCache` is disabled. */
|
|
6799
|
+
get catalogCache() {
|
|
6800
|
+
if (!this.config.catalogCache) return void 0;
|
|
6801
|
+
this.#catalogCache ??= new CatalogCache({
|
|
6802
|
+
fetchFill: (input) => this.catalogFill(input),
|
|
6803
|
+
clock: () => performance.now()
|
|
6804
|
+
});
|
|
6805
|
+
return this.#catalogCache;
|
|
6806
|
+
}
|
|
6485
6807
|
upstreamAuthRequest;
|
|
6486
6808
|
nextOperationId = 1;
|
|
6487
6809
|
closing = false;
|
|
@@ -7529,6 +7851,21 @@ var MobileAccessGateway = class {
|
|
|
7529
7851
|
return;
|
|
7530
7852
|
}
|
|
7531
7853
|
if (stockFrontend && target.decodedPathname === "/") request.url = "/";
|
|
7854
|
+
const candidateContentType = String(request.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase();
|
|
7855
|
+
const candidateDeclaredLength = request.headers["content-length"] === void 0 ? void 0 : Number(request.headers["content-length"]);
|
|
7856
|
+
const catalogRouteMethod = this.config.catalogCache && documentView === void 0 && request.method === "POST" && target.search === "" && candidateContentType === "application/json" && candidateDeclaredLength !== void 0 && Number.isFinite(candidateDeclaredLength) && candidateDeclaredLength > 0 && candidateDeclaredLength <= CATALOG_CANDIDATE_BODY_LIMIT && (target.decodedPathname === "/api/llm/listProviders" || target.decodedPathname === "/api/session/modelCatalog") ? target.decodedPathname.slice(5) : void 0;
|
|
7857
|
+
if (catalogRouteMethod !== void 0) {
|
|
7858
|
+
const candidateBody = await this.readRequestBody(request, Math.min(CATALOG_CANDIDATE_BODY_LIMIT, this.config.maxBodyBytes));
|
|
7859
|
+
const eligible = checkCatalogRequest(catalogRouteMethod, candidateBody.toString("utf8"));
|
|
7860
|
+
if (eligible !== void 0 && this.catalogCache !== void 0) {
|
|
7861
|
+
if (await this.serveCatalogCache(eligible, request, response, authorization)) return;
|
|
7862
|
+
response.setHeader(CATALOG_CACHE_MARKER, "miss-passthrough");
|
|
7863
|
+
await this.proxyHttp(request, response, authorization, documentView, candidateBody);
|
|
7864
|
+
return;
|
|
7865
|
+
}
|
|
7866
|
+
await this.proxyHttp(request, response, authorization, documentView, candidateBody);
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7532
7869
|
await this.proxyHttp(request, response, authorization, documentView);
|
|
7533
7870
|
}
|
|
7534
7871
|
async handleExtensionRequest(targetInfo, target, request, response, authorization) {
|
|
@@ -7823,6 +8160,8 @@ var MobileAccessGateway = class {
|
|
|
7823
8160
|
if (pair === void 0 || pair.length > 4096 || !UPSTREAM_COOKIE_PAIR.test(pair) || !Number.isSafeInteger(expiresAt) || maxAgeSeconds <= 0) throw new HttpError(502, "upstream_unavailable");
|
|
7824
8161
|
this.upstreamCookie = pair;
|
|
7825
8162
|
this.upstreamCookieExpiresAt = expiresAt;
|
|
8163
|
+
this.#upstreamIdentityRevision += 1;
|
|
8164
|
+
this.catalogCache?.noteIdentity(createHash("sha256").update(pair).digest("hex"));
|
|
7826
8165
|
this.upstreamAuthDiagnostics.record(attempt, {
|
|
7827
8166
|
outcome: "accepted",
|
|
7828
8167
|
stage,
|
|
@@ -8244,7 +8583,7 @@ var MobileAccessGateway = class {
|
|
|
8244
8583
|
}
|
|
8245
8584
|
}
|
|
8246
8585
|
operation.signal.throwIfAborted();
|
|
8247
|
-
const headers = sanitizeResponseHeaders(index.headers, this.config.upstreamOrigin);
|
|
8586
|
+
const headers = sanitizeResponseHeaders(index.headers, new URL(request.url ?? "/", this.config.upstreamOrigin));
|
|
8248
8587
|
delete headers["content-length"];
|
|
8249
8588
|
delete headers["content-encoding"];
|
|
8250
8589
|
delete headers.etag;
|
|
@@ -8346,7 +8685,7 @@ var MobileAccessGateway = class {
|
|
|
8346
8685
|
const index = cursor++;
|
|
8347
8686
|
const source = candidates[index].url;
|
|
8348
8687
|
const size = await this.upstreamBundleSize(source, signal);
|
|
8349
|
-
if (size === void 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES) passThrough.add(source);
|
|
8688
|
+
if (size === void 0 || size <= 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES) passThrough.add(source);
|
|
8350
8689
|
else sizes.set(source, size);
|
|
8351
8690
|
}
|
|
8352
8691
|
};
|
|
@@ -8650,13 +8989,181 @@ var MobileAccessGateway = class {
|
|
|
8650
8989
|
}
|
|
8651
8990
|
};
|
|
8652
8991
|
}
|
|
8992
|
+
/** Read one bounded request body verbatim; the caller owns replaying it upstream. */
|
|
8993
|
+
readRequestBody(request, limitBytes) {
|
|
8994
|
+
const declared = request.headers["content-length"];
|
|
8995
|
+
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > limitBytes)) throw new HttpError(413, "payload_too_large");
|
|
8996
|
+
return new Promise((resolve, reject) => {
|
|
8997
|
+
const chunks = [];
|
|
8998
|
+
let received = 0;
|
|
8999
|
+
let failed = false;
|
|
9000
|
+
request.on("data", (chunk) => {
|
|
9001
|
+
if (failed) return;
|
|
9002
|
+
received += chunk.length;
|
|
9003
|
+
if (received > limitBytes) {
|
|
9004
|
+
failed = true;
|
|
9005
|
+
reject(new HttpError(413, "payload_too_large"));
|
|
9006
|
+
request.resume();
|
|
9007
|
+
return;
|
|
9008
|
+
}
|
|
9009
|
+
chunks.push(chunk);
|
|
9010
|
+
});
|
|
9011
|
+
request.once("end", () => {
|
|
9012
|
+
if (!failed) resolve(Buffer.concat(chunks));
|
|
9013
|
+
});
|
|
9014
|
+
request.once("error", (error) => {
|
|
9015
|
+
if (!failed) reject(error);
|
|
9016
|
+
});
|
|
9017
|
+
});
|
|
9018
|
+
}
|
|
9019
|
+
/** One gateway-owned catalog fill: its own socket, rpcId, deadline and byte cap. */
|
|
9020
|
+
async catalogFill(input) {
|
|
9021
|
+
const rpcId = `dsh-mobile-catalog-${randomUUID()}`;
|
|
9022
|
+
const body = JSON.stringify({
|
|
9023
|
+
type: "client-request",
|
|
9024
|
+
rpcId,
|
|
9025
|
+
method: input.method,
|
|
9026
|
+
payload: { args: {} }
|
|
9027
|
+
});
|
|
9028
|
+
return await new Promise((resolve) => {
|
|
9029
|
+
let settled = false;
|
|
9030
|
+
let upstreamRequest;
|
|
9031
|
+
const finish = (outcome) => {
|
|
9032
|
+
if (settled) return;
|
|
9033
|
+
settled = true;
|
|
9034
|
+
clearTimeout(absoluteDeadline);
|
|
9035
|
+
this.#catalogFills.delete(upstreamRequest);
|
|
9036
|
+
resolve(outcome);
|
|
9037
|
+
};
|
|
9038
|
+
upstreamRequest = request({
|
|
9039
|
+
protocol: "http:",
|
|
9040
|
+
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
9041
|
+
port: Number(this.config.upstreamOrigin.port),
|
|
9042
|
+
method: "POST",
|
|
9043
|
+
path: `/api/${input.method}`,
|
|
9044
|
+
headers: {
|
|
9045
|
+
host: this.config.upstreamOrigin.host,
|
|
9046
|
+
"content-type": "application/json",
|
|
9047
|
+
accept: "application/json",
|
|
9048
|
+
"accept-encoding": "identity",
|
|
9049
|
+
"content-length": String(Buffer.byteLength(body)),
|
|
9050
|
+
...input.upstream.cookieHeader === void 0 ? {} : { cookie: input.upstream.cookieHeader }
|
|
9051
|
+
},
|
|
9052
|
+
agent: false
|
|
9053
|
+
}, (response) => {
|
|
9054
|
+
const status = response.statusCode ?? 0;
|
|
9055
|
+
const rawContentType = String(response.headers["content-type"] ?? "");
|
|
9056
|
+
const mediaType = rawContentType.split(";", 1)[0]?.trim().toLowerCase();
|
|
9057
|
+
const charset = /charset\s*=\s*([^;]+)/iu.exec(rawContentType)?.[1]?.trim().toLowerCase();
|
|
9058
|
+
if (status !== 200 || mediaType !== "application/json" || charset !== void 0 && charset !== "utf-8" && charset !== "utf8" || response.headers["set-cookie"] !== void 0 || response.headers["content-encoding"] !== void 0 && response.headers["content-encoding"] !== "identity") {
|
|
9059
|
+
response.destroy();
|
|
9060
|
+
finish({ shareable: false });
|
|
9061
|
+
return;
|
|
9062
|
+
}
|
|
9063
|
+
const chunks = [];
|
|
9064
|
+
let received = 0;
|
|
9065
|
+
let overflowed = false;
|
|
9066
|
+
response.on("data", (chunk) => {
|
|
9067
|
+
if (overflowed) return;
|
|
9068
|
+
received += chunk.length;
|
|
9069
|
+
if (received > DEFAULT_CATALOG_CACHE_LIMITS.maxEntryBytes) {
|
|
9070
|
+
overflowed = true;
|
|
9071
|
+
response.destroy();
|
|
9072
|
+
return;
|
|
9073
|
+
}
|
|
9074
|
+
chunks.push(chunk);
|
|
9075
|
+
});
|
|
9076
|
+
response.once("end", () => {
|
|
9077
|
+
if (overflowed) {
|
|
9078
|
+
finish({ shareable: false });
|
|
9079
|
+
return;
|
|
9080
|
+
}
|
|
9081
|
+
try {
|
|
9082
|
+
const envelopeJson = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
|
|
9083
|
+
JSON.parse(envelopeJson);
|
|
9084
|
+
finish({
|
|
9085
|
+
shareable: true,
|
|
9086
|
+
envelopeJson,
|
|
9087
|
+
upstreamRpcId: rpcId
|
|
9088
|
+
});
|
|
9089
|
+
} catch {
|
|
9090
|
+
finish({ shareable: false });
|
|
9091
|
+
}
|
|
9092
|
+
});
|
|
9093
|
+
response.once("error", () => finish({ shareable: false }));
|
|
9094
|
+
response.once("close", () => finish({ shareable: false }));
|
|
9095
|
+
});
|
|
9096
|
+
const absoluteDeadline = setTimeout(() => {
|
|
9097
|
+
upstreamRequest.destroy(/* @__PURE__ */ new Error("catalog fill deadline"));
|
|
9098
|
+
}, this.config.upstreamTimeoutMs);
|
|
9099
|
+
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
9100
|
+
upstreamRequest.destroy(/* @__PURE__ */ new Error("catalog fill idle timeout"));
|
|
9101
|
+
});
|
|
9102
|
+
upstreamRequest.once("error", () => finish({ shareable: false }));
|
|
9103
|
+
this.#catalogFills.add(upstreamRequest);
|
|
9104
|
+
upstreamRequest.end(body);
|
|
9105
|
+
});
|
|
9106
|
+
}
|
|
9107
|
+
/**
|
|
9108
|
+
* Serve one eligible catalog request from the SWR cache.
|
|
9109
|
+
* @returns true when the response was committed here; false for the ordinary path.
|
|
9110
|
+
*/
|
|
9111
|
+
async serveCatalogCache(eligible, request, response, authorization) {
|
|
9112
|
+
const cache = this.catalogCache;
|
|
9113
|
+
if (cache === void 0) return false;
|
|
9114
|
+
const operation = this.allocateRequest(authorization, response, {});
|
|
9115
|
+
const onClose = () => {
|
|
9116
|
+
operation.abort();
|
|
9117
|
+
operation.release();
|
|
9118
|
+
};
|
|
9119
|
+
response.once("close", onClose);
|
|
9120
|
+
try {
|
|
9121
|
+
const snapshotRevision = this.#upstreamIdentityRevision;
|
|
9122
|
+
const hadIdentityBefore = this.upstreamCookie !== void 0;
|
|
9123
|
+
const cookieHeader = await waitForRequestTask(this.upstreamCookieHeader(), operation.signal);
|
|
9124
|
+
operation.signal.throwIfAborted();
|
|
9125
|
+
const superseded = hadIdentityBefore && snapshotRevision !== this.#upstreamIdentityRevision;
|
|
9126
|
+
if (response.destroyed || superseded) return false;
|
|
9127
|
+
const identityDigest = createHash("sha256").update(cookieHeader ?? "none").digest("hex");
|
|
9128
|
+
cache.noteIdentity(identityDigest);
|
|
9129
|
+
const result = await cache.serve({
|
|
9130
|
+
method: eligible.method,
|
|
9131
|
+
rpcId: eligible.rpcId,
|
|
9132
|
+
identityDigest,
|
|
9133
|
+
upstream: { cookieHeader }
|
|
9134
|
+
});
|
|
9135
|
+
operation.signal.throwIfAborted();
|
|
9136
|
+
if (result.kind === "passthrough" || !cache.canCommit(result) || response.destroyed) return false;
|
|
9137
|
+
const payload = Buffer.from(result.body, "utf8");
|
|
9138
|
+
const headers = {
|
|
9139
|
+
"content-type": "application/json",
|
|
9140
|
+
"cache-control": "no-store",
|
|
9141
|
+
[CATALOG_CACHE_MARKER]: result.kind
|
|
9142
|
+
};
|
|
9143
|
+
let body = payload;
|
|
9144
|
+
if (acceptsGzip(request.headers["accept-encoding"]) && payload.length >= MIN_COMPRESSIBLE_BYTES) {
|
|
9145
|
+
body = await gzipBuffer(payload);
|
|
9146
|
+
headers["content-encoding"] = "gzip";
|
|
9147
|
+
addVaryAcceptEncoding(headers);
|
|
9148
|
+
}
|
|
9149
|
+
if (!cache.canCommit(result) || response.destroyed) return false;
|
|
9150
|
+
headers["content-length"] = body.length;
|
|
9151
|
+
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
9152
|
+
response.writeHead(200, headers);
|
|
9153
|
+
response.end(body);
|
|
9154
|
+
return true;
|
|
9155
|
+
} finally {
|
|
9156
|
+
response.removeListener("close", onClose);
|
|
9157
|
+
operation.release();
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
8653
9160
|
/**
|
|
8654
9161
|
* Proxy one request, retrying transient failures for plugin and asset GETs.
|
|
8655
9162
|
* Only bodyless GETs can be replayed, before response headers reach the client.
|
|
8656
9163
|
* One request slot and cancellation signal cover attempts and backoff together.
|
|
8657
9164
|
* API requests and requests with bodies are forwarded once.
|
|
8658
9165
|
*/
|
|
8659
|
-
async proxyHttp(request, response, authorization, documentView) {
|
|
9166
|
+
async proxyHttp(request, response, authorization, documentView, bodyOverride) {
|
|
8660
9167
|
const declared = request.headers["content-length"];
|
|
8661
9168
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > this.config.maxBodyBytes)) throw new HttpError(413, "payload_too_large");
|
|
8662
9169
|
const replayable = request.method === "GET" && request.headers["transfer-encoding"] === void 0 && (declared === void 0 || Number(declared) === 0) && (request.url?.split("?", 1)[0]?.startsWith("/plugins/") === true || request.url?.split("?", 1)[0]?.startsWith("/assets/") === true);
|
|
@@ -8684,7 +9191,7 @@ var MobileAccessGateway = class {
|
|
|
8684
9191
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
8685
9192
|
operation.signal.throwIfAborted();
|
|
8686
9193
|
try {
|
|
8687
|
-
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable, documentView);
|
|
9194
|
+
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable, documentView, bodyOverride);
|
|
8688
9195
|
return;
|
|
8689
9196
|
} catch (error) {
|
|
8690
9197
|
if (operation.signal.aborted || response.destroyed) return;
|
|
@@ -8705,7 +9212,7 @@ var MobileAccessGateway = class {
|
|
|
8705
9212
|
operation.release();
|
|
8706
9213
|
}
|
|
8707
9214
|
}
|
|
8708
|
-
async proxyHttpOnce(request$2, response, holder, signal, replayable, documentView) {
|
|
9215
|
+
async proxyHttpOnce(request$2, response, holder, signal, replayable, documentView, bodyOverride) {
|
|
8709
9216
|
let bodyDone;
|
|
8710
9217
|
const projections = /* @__PURE__ */ new Set();
|
|
8711
9218
|
try {
|
|
@@ -8736,7 +9243,8 @@ var MobileAccessGateway = class {
|
|
|
8736
9243
|
});
|
|
8737
9244
|
upstreamRequest.once("response", resolve);
|
|
8738
9245
|
upstreamRequest.once("error", reject);
|
|
8739
|
-
if (
|
|
9246
|
+
if (bodyOverride !== void 0) upstreamRequest.end(bodyOverride);
|
|
9247
|
+
else if (replayable) upstreamRequest.end();
|
|
8740
9248
|
else {
|
|
8741
9249
|
bodyDone = pipeline(request$2, new ByteLimitTransform(this.config.maxBodyBytes), upstreamRequest);
|
|
8742
9250
|
bodyDone.catch(reject);
|
|
@@ -8749,7 +9257,7 @@ var MobileAccessGateway = class {
|
|
|
8749
9257
|
}
|
|
8750
9258
|
if (graphEvents) holder.request?.setTimeout(this.config.upstreamApiTimeoutMs);
|
|
8751
9259
|
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
8752
|
-
const headers = sanitizeResponseHeaders(proxied.headers, this.config.upstreamOrigin);
|
|
9260
|
+
const headers = sanitizeResponseHeaders(proxied.headers, new URL(request$2.url ?? "/", this.config.upstreamOrigin));
|
|
8753
9261
|
if (graphEvents) {
|
|
8754
9262
|
if (proxied.headers["content-encoding"] !== void 0 && proxied.headers["content-encoding"] !== "identity") throw new HttpError(502, "upstream_unavailable");
|
|
8755
9263
|
delete headers["content-length"];
|
|
@@ -8760,6 +9268,7 @@ var MobileAccessGateway = class {
|
|
|
8760
9268
|
delete headers["accept-ranges"];
|
|
8761
9269
|
}
|
|
8762
9270
|
const statusCode = proxied.statusCode ?? 502;
|
|
9271
|
+
if (statusCode >= 200 && statusCode < 300 && this.catalogCache !== void 0 && pathname.startsWith("/api/") && CATALOG_MUTATION_METHODS.has(pathname.slice(5))) this.catalogCache.noteCatalogMutation();
|
|
8763
9272
|
this.httpFailures.recordUpstream(request$2.method, request$2.url?.split("?", 1)[0] ?? "", statusCode);
|
|
8764
9273
|
const cacheControl = graphEvents ? void 0 : revisionedStaticCacheControl(request$2, statusCode);
|
|
8765
9274
|
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
@@ -9186,6 +9695,9 @@ var MobileAccessGateway = class {
|
|
|
9186
9695
|
}
|
|
9187
9696
|
async performClose() {
|
|
9188
9697
|
this.closing = true;
|
|
9698
|
+
this.#catalogCache?.dispose();
|
|
9699
|
+
for (const fill of this.#catalogFills) fill.destroy(/* @__PURE__ */ new Error("gateway closing"));
|
|
9700
|
+
this.#catalogFills.clear();
|
|
9189
9701
|
const detachPushOrigin = this.detachPushOrigin;
|
|
9190
9702
|
this.detachPushOrigin = void 0;
|
|
9191
9703
|
detachPushOrigin?.();
|
|
@@ -11907,7 +12419,7 @@ async function saveManagedSetup(setupFile, setup) {
|
|
|
11907
12419
|
}
|
|
11908
12420
|
function privateIpv4(value) {
|
|
11909
12421
|
const parts = value.split(".").map(Number);
|
|
11910
|
-
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
|
|
12422
|
+
return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
|
|
11911
12423
|
}
|
|
11912
12424
|
function networkCidr(address, cidr) {
|
|
11913
12425
|
const prefix = Number(cidr.slice(cidr.lastIndexOf("/") + 1));
|
|
@@ -15429,11 +15941,23 @@ const PAIRING_DESCRIPTOR_LOCAL_PORT = DEFAULT_IROH_LOCAL_PORT;
|
|
|
15429
15941
|
const READY_TIMEOUT_MS = 45e3;
|
|
15430
15942
|
const IDENTITY_TIMEOUT_MS = 15e3;
|
|
15431
15943
|
const IDENTITY_METADATA_MAX_BYTES = 8192;
|
|
15944
|
+
const IDENTITY_DIAGNOSTIC_MAX_BYTES = 4096;
|
|
15432
15945
|
const PAIRING_TIMEOUT_MS = 15e3;
|
|
15433
15946
|
const MAX_LINE_BYTES = 16384;
|
|
15434
15947
|
/** Gateway pairing windows default to five minutes; the descriptor caps at ten. */
|
|
15435
15948
|
const MAX_PAIRING_WINDOW_MS = 3e5;
|
|
15436
15949
|
const MIN_PAIRING_WINDOW_MS = 1e4;
|
|
15950
|
+
/** Safe classified failure from the short-lived endpoint identity probe. */
|
|
15951
|
+
var IrohIdentityProbeError = class extends Error {
|
|
15952
|
+
code;
|
|
15953
|
+
diagnostic;
|
|
15954
|
+
constructor(code, diagnostic) {
|
|
15955
|
+
super(code);
|
|
15956
|
+
this.code = code;
|
|
15957
|
+
this.diagnostic = diagnostic;
|
|
15958
|
+
this.name = "IrohIdentityProbeError";
|
|
15959
|
+
}
|
|
15960
|
+
};
|
|
15437
15961
|
function publicStatus$2(status) {
|
|
15438
15962
|
return Object.freeze({ ...status });
|
|
15439
15963
|
}
|
|
@@ -15465,16 +15989,21 @@ async function defaultResolveIdentity(executable, dataDir, options = {}) {
|
|
|
15465
15989
|
}) : options.launch(executable, args);
|
|
15466
15990
|
options.onSpawn?.(child);
|
|
15467
15991
|
let output = Buffer.alloc(0);
|
|
15992
|
+
let stderrOutput = Buffer.alloc(0);
|
|
15468
15993
|
let settled = false;
|
|
15994
|
+
const diagnostic = () => {
|
|
15995
|
+
const text = stderrOutput.toString("utf8").replaceAll(dataDir, "<data-dir>").trim().replace(/\s+/gu, " ");
|
|
15996
|
+
return text === "" ? void 0 : text.slice(0, IDENTITY_DIAGNOSTIC_MAX_BYTES);
|
|
15997
|
+
};
|
|
15469
15998
|
return await new Promise((resolve, reject) => {
|
|
15470
15999
|
const fail = (code) => {
|
|
15471
16000
|
if (settled) return;
|
|
15472
16001
|
settled = true;
|
|
15473
16002
|
clearTimeout(deadline);
|
|
15474
16003
|
terminateRemoteProcess(child).then(() => {
|
|
15475
|
-
reject(new
|
|
16004
|
+
reject(new IrohIdentityProbeError(code, diagnostic()));
|
|
15476
16005
|
}, () => {
|
|
15477
|
-
reject(new AggregateError([new
|
|
16006
|
+
reject(new AggregateError([new IrohIdentityProbeError(code, diagnostic()), /* @__PURE__ */ new Error("iroh_probe_stop_failed")], "iroh_identity_cleanup_failed"));
|
|
15478
16007
|
});
|
|
15479
16008
|
};
|
|
15480
16009
|
const deadline = setTimeout(() => {
|
|
@@ -15489,7 +16018,10 @@ async function defaultResolveIdentity(executable, dataDir, options = {}) {
|
|
|
15489
16018
|
}
|
|
15490
16019
|
output = Buffer.concat([output, chunk]);
|
|
15491
16020
|
});
|
|
15492
|
-
child.stderr?.
|
|
16021
|
+
child.stderr?.on("data", (chunk) => {
|
|
16022
|
+
const remaining = IDENTITY_DIAGNOSTIC_MAX_BYTES - stderrOutput.length;
|
|
16023
|
+
if (remaining > 0) stderrOutput = Buffer.concat([stderrOutput, chunk.subarray(0, remaining)]);
|
|
16024
|
+
});
|
|
15493
16025
|
child.once("error", () => {
|
|
15494
16026
|
fail("iroh_identity_failed");
|
|
15495
16027
|
});
|
|
@@ -15497,14 +16029,19 @@ async function defaultResolveIdentity(executable, dataDir, options = {}) {
|
|
|
15497
16029
|
clearTimeout(deadline);
|
|
15498
16030
|
if (settled) return;
|
|
15499
16031
|
settled = true;
|
|
16032
|
+
const probeDiagnostic = diagnostic();
|
|
16033
|
+
if (code !== 0 && probeDiagnostic !== void 0 && /private (?:Windows )?data directory is not secure|private path ACL grants/iu.test(probeDiagnostic)) {
|
|
16034
|
+
reject(new IrohIdentityProbeError("iroh_data_directory_insecure", probeDiagnostic));
|
|
16035
|
+
return;
|
|
16036
|
+
}
|
|
15500
16037
|
let metadata;
|
|
15501
16038
|
try {
|
|
15502
16039
|
metadata = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(output));
|
|
15503
16040
|
} catch {
|
|
15504
|
-
reject(
|
|
16041
|
+
reject(new IrohIdentityProbeError("iroh_identity_invalid", probeDiagnostic));
|
|
15505
16042
|
return;
|
|
15506
16043
|
}
|
|
15507
|
-
if (code !== 0 || typeof metadata !== "object" || metadata === null || !("version" in metadata) || metadata.version !== 1 || !("endpointId" in metadata) || typeof metadata.endpointId !== "string" || !/^[0-9a-f]{64}$/u.test(metadata.endpointId)) reject(
|
|
16044
|
+
if (code !== 0 || typeof metadata !== "object" || metadata === null || !("version" in metadata) || metadata.version !== 1 || !("endpointId" in metadata) || typeof metadata.endpointId !== "string" || !/^[0-9a-f]{64}$/u.test(metadata.endpointId)) reject(new IrohIdentityProbeError("iroh_identity_invalid", probeDiagnostic));
|
|
15508
16045
|
else resolve(metadata.endpointId);
|
|
15509
16046
|
});
|
|
15510
16047
|
});
|
|
@@ -15691,9 +16228,15 @@ var IrohController = class {
|
|
|
15691
16228
|
this.options.onStatus?.(this.status());
|
|
15692
16229
|
} catch {}
|
|
15693
16230
|
}
|
|
15694
|
-
async failGeneration(generation, errorCode, origin) {
|
|
16231
|
+
async failGeneration(generation, errorCode, origin, diagnostic) {
|
|
15695
16232
|
if (generation !== this.generation) return;
|
|
15696
16233
|
this.generation += 1;
|
|
16234
|
+
try {
|
|
16235
|
+
this.options.onFailure?.({
|
|
16236
|
+
errorCode,
|
|
16237
|
+
...diagnostic === void 0 ? {} : { diagnostic }
|
|
16238
|
+
});
|
|
16239
|
+
} catch {}
|
|
15697
16240
|
this.publish({
|
|
15698
16241
|
enabled: this.enabled,
|
|
15699
16242
|
state: "error",
|
|
@@ -15778,7 +16321,9 @@ var IrohController = class {
|
|
|
15778
16321
|
});
|
|
15779
16322
|
} }) : await this.options.resolveIdentity(executable, this.options.dataDirectory);
|
|
15780
16323
|
} catch (error) {
|
|
15781
|
-
|
|
16324
|
+
const code = error instanceof AggregateError ? "iroh_identity_cleanup_failed" : error instanceof IrohIdentityProbeError ? error.code : "iroh_identity_failed";
|
|
16325
|
+
const diagnostic = error instanceof IrohIdentityProbeError ? error.diagnostic : error instanceof AggregateError && error.errors[0] instanceof IrohIdentityProbeError ? error.errors[0].diagnostic : void 0;
|
|
16326
|
+
await this.failGeneration(generation, code, void 0, diagnostic);
|
|
15782
16327
|
return;
|
|
15783
16328
|
}
|
|
15784
16329
|
if (generation !== this.generation || !this.enabled) return;
|
|
@@ -16787,6 +17332,116 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
|
|
|
16787
17332
|
})
|
|
16788
17333
|
});
|
|
16789
17334
|
}
|
|
17335
|
+
// src/macos-file-access.ts
|
|
17336
|
+
/**
|
|
17337
|
+
* Reports whether this process already holds the macOS Full Disk Access grant.
|
|
17338
|
+
*
|
|
17339
|
+
* macOS shows a folder authorization dialog the first time a process reads a
|
|
17340
|
+
* protected location (Desktop, Documents, Downloads, external or network
|
|
17341
|
+
* volumes). While that dialog waits for a click, the calling thread stays
|
|
17342
|
+
* blocked, so an unattended machine can stall every request that needs the
|
|
17343
|
+
* file system. Granting Full Disk Access removes that class of dialog.
|
|
17344
|
+
*
|
|
17345
|
+
* The probe reads the per-user TCC database. That file is itself protected by
|
|
17346
|
+
* TCC, so opening it succeeds only for a process macOS already trusts with
|
|
17347
|
+
* Full Disk Access; a denied read fails immediately with EACCES/EPERM because
|
|
17348
|
+
* the grant can only be made in System Settings and can never be requested
|
|
17349
|
+
* through a dialog. The probe therefore never blocks on user input, unlike
|
|
17350
|
+
* reading one of the protected folders themselves.
|
|
17351
|
+
*/
|
|
17352
|
+
/** A final `ok` is sticky; only a non-ok result is retried, at most this often. */
|
|
17353
|
+
const DEFAULT_RETRY_MS = 2e3;
|
|
17354
|
+
/** A probe that never settles is treated as missing rather than awaited forever. */
|
|
17355
|
+
const DEFAULT_TIMEOUT_MS = 2e3;
|
|
17356
|
+
/**
|
|
17357
|
+
* Caches the Full Disk Access state so a status response can read it
|
|
17358
|
+
* synchronously while the file system check stays asynchronous.
|
|
17359
|
+
*/
|
|
17360
|
+
var MacFileAccessProbe = class {
|
|
17361
|
+
platform;
|
|
17362
|
+
home;
|
|
17363
|
+
readAccess;
|
|
17364
|
+
now;
|
|
17365
|
+
retryMs;
|
|
17366
|
+
timeoutMs;
|
|
17367
|
+
statusValue = "missing";
|
|
17368
|
+
checkedAt = Number.NEGATIVE_INFINITY;
|
|
17369
|
+
activeProbe;
|
|
17370
|
+
constructor(options = {}) {
|
|
17371
|
+
this.platform = options.platform ?? process.platform;
|
|
17372
|
+
this.home = options.home ?? homedir();
|
|
17373
|
+
this.readAccess = options.readAccess ?? ((path) => access(path, constants.R_OK));
|
|
17374
|
+
this.now = options.now ?? Date.now;
|
|
17375
|
+
this.retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
17376
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
17377
|
+
}
|
|
17378
|
+
/** Whether the probe applies to this platform at all. */
|
|
17379
|
+
get applicable() {
|
|
17380
|
+
return this.platform === "darwin";
|
|
17381
|
+
}
|
|
17382
|
+
/** Latest cached result. Never performs I/O. */
|
|
17383
|
+
status() {
|
|
17384
|
+
return this.applicable ? this.statusValue : "ok";
|
|
17385
|
+
}
|
|
17386
|
+
/**
|
|
17387
|
+
* Refresh the cached result when it is not final yet.
|
|
17388
|
+
* @returns The result after this call.
|
|
17389
|
+
*/
|
|
17390
|
+
async refresh() {
|
|
17391
|
+
if (!this.applicable) return "ok";
|
|
17392
|
+
if (this.statusValue === "ok") return "ok";
|
|
17393
|
+
if (this.activeProbe !== void 0) return this.activeProbe;
|
|
17394
|
+
const at = this.now();
|
|
17395
|
+
if (at - this.checkedAt < this.retryMs) return this.statusValue;
|
|
17396
|
+
this.checkedAt = at;
|
|
17397
|
+
const pending = this.probe();
|
|
17398
|
+
this.activeProbe = pending;
|
|
17399
|
+
try {
|
|
17400
|
+
this.statusValue = await pending;
|
|
17401
|
+
return this.statusValue;
|
|
17402
|
+
} finally {
|
|
17403
|
+
if (this.activeProbe === pending) this.activeProbe = void 0;
|
|
17404
|
+
}
|
|
17405
|
+
}
|
|
17406
|
+
/** Run one bounded access check and collapse every failure into `missing`. */
|
|
17407
|
+
async probe() {
|
|
17408
|
+
const database = join(this.home, "Library", "Application Support", "com.apple.TCC", "TCC.db");
|
|
17409
|
+
let timer;
|
|
17410
|
+
try {
|
|
17411
|
+
await Promise.race([this.readAccess(database), new Promise((_resolve, reject) => {
|
|
17412
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("macos_file_access_probe_timeout")), this.timeoutMs);
|
|
17413
|
+
timer.unref?.();
|
|
17414
|
+
})]);
|
|
17415
|
+
return "ok";
|
|
17416
|
+
} catch {
|
|
17417
|
+
return "missing";
|
|
17418
|
+
} finally {
|
|
17419
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
17420
|
+
}
|
|
17421
|
+
}
|
|
17422
|
+
};
|
|
17423
|
+
/** System Settings deep link for the Full Disk Access pane. */
|
|
17424
|
+
const MAC_FULL_DISK_ACCESS_PANE = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles";
|
|
17425
|
+
/** Absolute path so a GUI-launched process does not depend on PATH. */
|
|
17426
|
+
const OPEN_COMMAND = "/usr/bin/open";
|
|
17427
|
+
function runCommand(file, args) {
|
|
17428
|
+
return new Promise((resolve, reject) => {
|
|
17429
|
+
execFile(file, [...args], (error) => {
|
|
17430
|
+
if (error === null) resolve();
|
|
17431
|
+
else reject(error);
|
|
17432
|
+
});
|
|
17433
|
+
});
|
|
17434
|
+
}
|
|
17435
|
+
/**
|
|
17436
|
+
* Open the Full Disk Access pane from the host process.
|
|
17437
|
+
*
|
|
17438
|
+
* The desktop app's renderer refuses to navigate to a custom scheme, so the
|
|
17439
|
+
* button cannot use `window.open`; `open` runs on the Mac that owns the app.
|
|
17440
|
+
* @param run - Command runner, injectable for tests.
|
|
17441
|
+
*/
|
|
17442
|
+
async function openMacFullDiskAccessSettings(run = runCommand) {
|
|
17443
|
+
await run(OPEN_COMMAND, [MAC_FULL_DISK_ACCESS_PANE]);
|
|
17444
|
+
}
|
|
16790
17445
|
// src/mobile-guide.ts
|
|
16791
17446
|
/** Compose the guide with the current customization state injected. */
|
|
16792
17447
|
function buildMobileGuide(state) {
|
|
@@ -19926,11 +20581,33 @@ function launchedProfileName(argv) {
|
|
|
19926
20581
|
}
|
|
19927
20582
|
return "web";
|
|
19928
20583
|
}
|
|
20584
|
+
function profileInstallingModule(dshHome, moduleUrl) {
|
|
20585
|
+
let directory;
|
|
20586
|
+
try {
|
|
20587
|
+
directory = dirname(fileURLToPath(moduleUrl));
|
|
20588
|
+
} catch {
|
|
20589
|
+
return;
|
|
20590
|
+
}
|
|
20591
|
+
const profilesRoot = resolve(dshHome, "profiles");
|
|
20592
|
+
for (let depth = 0; depth < 16; depth += 1) {
|
|
20593
|
+
const candidate = resolve(directory);
|
|
20594
|
+
const fromProfiles = relative(profilesRoot, candidate);
|
|
20595
|
+
if (fromProfiles !== "" && !fromProfiles.startsWith("..") && !isAbsolute(fromProfiles)) try {
|
|
20596
|
+
const manifest = JSON.parse(readFileSync(join(candidate, "package.json"), "utf8"));
|
|
20597
|
+
if (typeof manifest.dependencies?.[PACKAGE_NAME] === "string" && typeof manifest.dsh?.profile === "object" && manifest.dsh.profile !== null) return candidate;
|
|
20598
|
+
} catch {}
|
|
20599
|
+
const parent = dirname(candidate);
|
|
20600
|
+
if (parent === candidate) break;
|
|
20601
|
+
directory = parent;
|
|
20602
|
+
}
|
|
20603
|
+
}
|
|
19929
20604
|
/** Resolve the launcher-owned Desktop profile, or the CLI profile outside Desktop. */
|
|
19930
|
-
function releaseProfileDirectory(ctx, dshHome, argv) {
|
|
20605
|
+
function releaseProfileDirectory(ctx, dshHome, argv, moduleUrl = import.meta.url) {
|
|
19931
20606
|
const desktopProfiles = ctx.get("desktopProfiles");
|
|
19932
20607
|
const desktopDirectory = desktopProfiles?.current?.dir;
|
|
19933
20608
|
if (typeof desktopDirectory === "string" && isAbsolute(desktopDirectory)) return desktopDirectory;
|
|
20609
|
+
const installedProfile = profileInstallingModule(dshHome, moduleUrl);
|
|
20610
|
+
if (installedProfile !== void 0) return installedProfile;
|
|
19934
20611
|
if (desktopProfiles !== void 0 || ctx.get("desktopRuntime") !== void 0) return void 0;
|
|
19935
20612
|
return join(dshHome, "profiles", launchedProfileName(argv));
|
|
19936
20613
|
}
|
|
@@ -23377,34 +24054,71 @@ function withInstallationIdentity(config, installationId, explicitIdentity) {
|
|
|
23377
24054
|
// src/lan-setup.ts
|
|
23378
24055
|
const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
|
|
23379
24056
|
const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
|
|
24057
|
+
const MIN_MANAGED_PORT = 1024;
|
|
24058
|
+
const MAX_MANAGED_PORT = 65535;
|
|
24059
|
+
function assertManagedPort(port, name) {
|
|
24060
|
+
if (!Number.isSafeInteger(port) || port < MIN_MANAGED_PORT || port > MAX_MANAGED_PORT) throw new Error(`${name} must be from ${String(MIN_MANAGED_PORT)} through ${String(MAX_MANAGED_PORT)}`);
|
|
24061
|
+
}
|
|
24062
|
+
function powerShellFailureDetail(error) {
|
|
24063
|
+
if (!(error instanceof Error)) return "unknown failure";
|
|
24064
|
+
const code = "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
|
|
24065
|
+
const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr.trim().replace(/\s+/gu, " ").slice(0, 512) : "";
|
|
24066
|
+
return `${code}${stderr === "" ? "" : `: ${stderr}`}`;
|
|
24067
|
+
}
|
|
23380
24068
|
async function runElevatedPowerShell(script) {
|
|
23381
|
-
|
|
23382
|
-
"-
|
|
23383
|
-
|
|
23384
|
-
"
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
"
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
24069
|
+
const launch = [
|
|
24070
|
+
"$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
|
|
24071
|
+
` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
|
|
24072
|
+
"; exit $process.ExitCode"
|
|
24073
|
+
].join(" ");
|
|
24074
|
+
try {
|
|
24075
|
+
await execFileText("powershell.exe", [
|
|
24076
|
+
"-NoProfile",
|
|
24077
|
+
"-NonInteractive",
|
|
24078
|
+
"-Command",
|
|
24079
|
+
launch
|
|
24080
|
+
], { windowsHide: true });
|
|
24081
|
+
} catch (error) {
|
|
24082
|
+
throw new Error(`Windows firewall operation failed${powerShellFailureDetail(error)}`, { cause: error });
|
|
24083
|
+
}
|
|
24084
|
+
}
|
|
24085
|
+
function firewallRuleScript(name, protocol, port) {
|
|
24086
|
+
return [
|
|
24087
|
+
`$rules = @(Get-NetFirewallRule -DisplayName '${name}' -ErrorAction SilentlyContinue)`,
|
|
24088
|
+
"if ($rules.Count -eq 0) {",
|
|
24089
|
+
` New-NetFirewallRule -DisplayName '${name}' -Direction Inbound -Action Allow -Protocol ${protocol} -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
|
|
24090
|
+
"} else {",
|
|
24091
|
+
" foreach ($rule in $rules) {",
|
|
24092
|
+
" $rule | Set-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -Profile Any",
|
|
24093
|
+
` $rule | Get-NetFirewallPortFilter | Set-NetFirewallPortFilter -Protocol ${protocol} -LocalPort ${String(port)}`,
|
|
24094
|
+
" $rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter -RemoteAddress LocalSubnet",
|
|
24095
|
+
" }",
|
|
24096
|
+
"}",
|
|
24097
|
+
`$verified = @(Get-NetFirewallRule -DisplayName '${name}' -Enabled True -Direction Inbound -Action Allow -Profile Any -ErrorAction Stop)`,
|
|
24098
|
+
`$matchingPort = @($verified | Get-NetFirewallPortFilter | Where-Object { $_.Protocol -eq '${protocol}' -and $_.LocalPort -contains '${String(port)}' })`,
|
|
24099
|
+
`$matchingAddress = @($verified | Get-NetFirewallAddressFilter | Where-Object { $_.RemoteAddress -contains 'LocalSubnet' })`,
|
|
24100
|
+
`if ($verified.Count -eq 0 -or $matchingPort.Count -eq 0 -or $matchingAddress.Count -eq 0) { throw "DSH Mobile firewall rule '${name}' is not active with the expected scope" }`
|
|
24101
|
+
];
|
|
24102
|
+
}
|
|
24103
|
+
/** Build the elevated Windows firewall script without removing a working rule first. */
|
|
24104
|
+
function windowsFirewallSetupScript(port) {
|
|
24105
|
+
assertManagedPort(port, "Windows firewall port");
|
|
24106
|
+
return [
|
|
24107
|
+
"$ErrorActionPreference = 'Stop'",
|
|
24108
|
+
...firewallRuleScript(FIREWALL_TCP_RULE, "TCP", port),
|
|
24109
|
+
...firewallRuleScript(FIREWALL_UDP_RULE, "UDP", port)
|
|
24110
|
+
].join("\n");
|
|
24111
|
+
}
|
|
24112
|
+
/** Install or update the two Windows inbound rules used by the managed LAN listener. */
|
|
23393
24113
|
async function configureWindowsFirewall(port) {
|
|
23394
24114
|
if (process.platform !== "win32") return;
|
|
23395
|
-
await runElevatedPowerShell(
|
|
23396
|
-
"$ErrorActionPreference = 'Stop'",
|
|
23397
|
-
`Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
|
|
23398
|
-
`Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
|
|
23399
|
-
`New-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
|
|
23400
|
-
`New-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -Direction Inbound -Action Allow -Protocol UDP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`
|
|
23401
|
-
].join("; "));
|
|
24115
|
+
await runElevatedPowerShell(windowsFirewallSetupScript(port));
|
|
23402
24116
|
}
|
|
23403
24117
|
/** Generate private TLS material and persist a managed setup selected by the user. */
|
|
23404
24118
|
async function prepareManagedLanSetup(options) {
|
|
23405
24119
|
if (!isAbsolute(options.setupFile) || !isAbsolute(options.controlFile)) throw new Error("managed LAN setup paths must be absolute");
|
|
23406
|
-
|
|
23407
|
-
|
|
24120
|
+
assertManagedPort(options.listenPort, "managed LAN listenPort");
|
|
24121
|
+
assertManagedPort(options.dshPort, "managed LAN dshPort");
|
|
23408
24122
|
const directory = dirname(options.setupFile);
|
|
23409
24123
|
const tlsDirectory = join(directory, "tls");
|
|
23410
24124
|
await mkdir(tlsDirectory, {
|
|
@@ -24042,6 +24756,7 @@ async function apply(ctx, config) {
|
|
|
24042
24756
|
const cloudflaredComponent = new CloudflaredComponentManager({ stateDirectory });
|
|
24043
24757
|
await cloudflaredComponent.initialize();
|
|
24044
24758
|
const frpComponent = new FrpComponentManager({ stateDirectory });
|
|
24759
|
+
const macFileAccess = new MacFileAccessProbe();
|
|
24045
24760
|
await frpComponent.initialize();
|
|
24046
24761
|
const irohComponent = new IrohComponentManager({
|
|
24047
24762
|
stateDirectory,
|
|
@@ -24301,6 +25016,9 @@ async function apply(ctx, config) {
|
|
|
24301
25016
|
component: irohComponent,
|
|
24302
25017
|
dataDirectory: join(remoteDirectory, "iroh", "sidecar"),
|
|
24303
25018
|
createGateway: (endpointId) => createIrohGateway(endpointId),
|
|
25019
|
+
onFailure: ({ errorCode, diagnostic }) => {
|
|
25020
|
+
logger.warn("iroh startup failed: %s%s", errorCode, diagnostic === void 0 ? "" : `; ${diagnostic}`);
|
|
25021
|
+
},
|
|
24304
25022
|
/**
|
|
24305
25023
|
* Read the pairing CA the live gateway advertises. The fingerprint must
|
|
24306
25024
|
* match the gateway instance id, otherwise a rotated CA would mint QR
|
|
@@ -24389,7 +25107,10 @@ async function apply(ctx, config) {
|
|
|
24389
25107
|
originMode: originController.mode(),
|
|
24390
25108
|
caddyConfiguration: caddyConfig.status(),
|
|
24391
25109
|
caddyState: caddyController.status(),
|
|
24392
|
-
...
|
|
25110
|
+
...macFileAccess.applicable ? {
|
|
25111
|
+
macosHost: true,
|
|
25112
|
+
macFileAccess: macFileAccess.status()
|
|
25113
|
+
} : {}
|
|
24393
25114
|
});
|
|
24394
25115
|
const lanPayload = () => ({
|
|
24395
25116
|
...loaded.kind === "unconfigured" ? {
|
|
@@ -24713,9 +25434,16 @@ async function apply(ctx, config) {
|
|
|
24713
25434
|
return;
|
|
24714
25435
|
}
|
|
24715
25436
|
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/remote/control`) {
|
|
25437
|
+
await macFileAccess.refresh();
|
|
24716
25438
|
sendJson(response, 200, remotePayload(), false);
|
|
24717
25439
|
return;
|
|
24718
25440
|
}
|
|
25441
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/macos-file-access/open-settings`) {
|
|
25442
|
+
if (!macFileAccess.applicable) throw new HttpError(404, "not_found");
|
|
25443
|
+
await openMacFullDiskAccessSettings();
|
|
25444
|
+
sendJson(response, 200, { opened: true }, false);
|
|
25445
|
+
return;
|
|
25446
|
+
}
|
|
24719
25447
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/provider`) {
|
|
24720
25448
|
const body = await readJsonObject(request, 4096);
|
|
24721
25449
|
if (body.provider !== "tailscale" && body.provider !== "cpolar" && body.provider !== "cloudflared" && body.provider !== "frp" && body.provider !== "origin" && body.provider !== "iroh") throw new HttpError(400, "bad_request");
|