dsh-mobile 0.8.4 → 0.8.6
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 +29 -0
- package/CONTRIBUTORS.md +16 -4
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/apps/mobile/README.md +2 -0
- package/apps/mobile/README.zh-CN.md +2 -0
- package/docs/CLIENT_MODULES.en.md +1 -1
- package/docs/CLIENT_MODULES.md +1 -1
- package/docs/CONNECTION_DIAGNOSTICS.en.md +2 -0
- package/docs/CONNECTION_DIAGNOSTICS.md +2 -0
- package/docs/IROH.en.md +1 -0
- package/docs/IROH.md +1 -0
- package/docs/README.md +1 -1
- package/docs/SLOW_CONNECTIONS.en.md +8 -0
- package/docs/SLOW_CONNECTIONS.md +8 -0
- package/docs/WEB_PUSH.en.md +20 -4
- package/docs/WEB_PUSH.md +20 -4
- package/lib/cli.js +156 -24
- package/lib/cli.js.map +1 -1
- package/lib/client.js +267 -18
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +59 -1
- package/lib/index.mjs +1134 -64
- package/lib/index.mjs.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,12 +1203,46 @@ 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)
|
|
1202
1210
|
});
|
|
1203
1211
|
}
|
|
1204
1212
|
// src/exec-file.ts
|
|
1213
|
+
/**
|
|
1214
|
+
* Longest stream text attached to a failed subprocess result.
|
|
1215
|
+
*
|
|
1216
|
+
* The text exists for diagnostics only: a caller decides which part is safe to
|
|
1217
|
+
* show. It is bounded here so a chatty child cannot turn one failure into an
|
|
1218
|
+
* unbounded buffer in every future consumer of that rejection.
|
|
1219
|
+
*/
|
|
1220
|
+
const MAX_SUBPROCESS_ERROR_STREAM_CHARS = 8192;
|
|
1221
|
+
function boundedStream(value) {
|
|
1222
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
1223
|
+
return value.length <= 8192 ? value : value.slice(0, MAX_SUBPROCESS_ERROR_STREAM_CHARS);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Attach the captured streams to a subprocess failure, best effort.
|
|
1227
|
+
*
|
|
1228
|
+
* `execFile` reports a non-zero exit as an Error whose only text is the command
|
|
1229
|
+
* line with stderr merged into `message`; the raw stream strings are handed to
|
|
1230
|
+
* the callback and otherwise discarded, so a caller cannot read them from the
|
|
1231
|
+
* rejection. They are copied onto it when it accepts properties. A frozen,
|
|
1232
|
+
* exotic or primitive rejection value must keep `message` as its diagnostic
|
|
1233
|
+
* rather than turn one failure into a different error, so every write is
|
|
1234
|
+
* guarded and an existing property is never overwritten.
|
|
1235
|
+
*/
|
|
1236
|
+
function attachCapturedStreams(error, stdout, stderr) {
|
|
1237
|
+
if (typeof error !== "object" || error === null) return;
|
|
1238
|
+
const target = error;
|
|
1239
|
+
try {
|
|
1240
|
+
const capturedStderr = boundedStream(stderr);
|
|
1241
|
+
const capturedStdout = boundedStream(stdout);
|
|
1242
|
+
if (target.stderr === void 0 && capturedStderr !== void 0) target.stderr = capturedStderr;
|
|
1243
|
+
if (target.stdout === void 0 && capturedStdout !== void 0) target.stdout = capturedStdout;
|
|
1244
|
+
} catch {}
|
|
1245
|
+
}
|
|
1205
1246
|
/** Capture both output streams even when a desktop host wraps execFile without Node's promisify metadata. */
|
|
1206
1247
|
function execFileText(file, args, options = {}) {
|
|
1207
1248
|
return new Promise((resolve, reject) => {
|
|
@@ -1211,6 +1252,7 @@ function execFileText(file, args, options = {}) {
|
|
|
1211
1252
|
encoding: "utf8"
|
|
1212
1253
|
}, (error, stdout, stderr) => {
|
|
1213
1254
|
if (error !== null) {
|
|
1255
|
+
attachCapturedStreams(error, stdout, stderr);
|
|
1214
1256
|
reject(error);
|
|
1215
1257
|
return;
|
|
1216
1258
|
}
|
|
@@ -2469,6 +2511,305 @@ var ApiLatencyRecorder = class {
|
|
|
2469
2511
|
}
|
|
2470
2512
|
}
|
|
2471
2513
|
};
|
|
2514
|
+
// src/catalog-cache.ts
|
|
2515
|
+
/** Host RPCs whose successful completion can change catalog results. */
|
|
2516
|
+
const CATALOG_MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
2517
|
+
"account/signOut",
|
|
2518
|
+
"account/startSignIn",
|
|
2519
|
+
"credentials/set",
|
|
2520
|
+
"credentials/unset",
|
|
2521
|
+
"pluginManager/installBundle",
|
|
2522
|
+
"pluginManager/removeBundle",
|
|
2523
|
+
"pluginManager/setBundleEnabled",
|
|
2524
|
+
"pluginManager/setPluginEnabled",
|
|
2525
|
+
"pluginManager/setVersionExemption",
|
|
2526
|
+
"pluginManager/updateBundle",
|
|
2527
|
+
"session/initializeDefaultModel",
|
|
2528
|
+
"session/selectModel",
|
|
2529
|
+
"settings/mutate",
|
|
2530
|
+
"settings/replace",
|
|
2531
|
+
"settings/update"
|
|
2532
|
+
]);
|
|
2533
|
+
const CATALOG_CACHE_METHODS = /* @__PURE__ */ new Set(["llm/listProviders", "session/modelCatalog"]);
|
|
2534
|
+
const DEFAULT_CATALOG_CACHE_LIMITS = Object.freeze({
|
|
2535
|
+
freshMs: 5e3,
|
|
2536
|
+
staleMs: 6e4,
|
|
2537
|
+
maxEntries: 4,
|
|
2538
|
+
maxEntryBytes: 2097152,
|
|
2539
|
+
maxTotalBytes: 8388608,
|
|
2540
|
+
maxInFlightFills: 2
|
|
2541
|
+
});
|
|
2542
|
+
const REQUEST_KEYS = /* @__PURE__ */ new Set([
|
|
2543
|
+
"type",
|
|
2544
|
+
"rpcId",
|
|
2545
|
+
"method",
|
|
2546
|
+
"payload"
|
|
2547
|
+
]);
|
|
2548
|
+
const RESPONSE_KEYS = /* @__PURE__ */ new Set([
|
|
2549
|
+
"type",
|
|
2550
|
+
"rpcId",
|
|
2551
|
+
"result"
|
|
2552
|
+
]);
|
|
2553
|
+
const encoder = new TextEncoder();
|
|
2554
|
+
const isObject$1 = (value) => typeof value === "object" && value !== null;
|
|
2555
|
+
const hasExactKeys = (value, keys) => {
|
|
2556
|
+
const present = Object.keys(value);
|
|
2557
|
+
if (present.length !== keys.size) return false;
|
|
2558
|
+
for (const key of present) if (!keys.has(key)) return false;
|
|
2559
|
+
return true;
|
|
2560
|
+
};
|
|
2561
|
+
/**
|
|
2562
|
+
* Validate one catalog request against the captured wire contract.
|
|
2563
|
+
* @param routeMethod - The `/api/<method>` route segment.
|
|
2564
|
+
* @param bodyJson - The exact buffered request body.
|
|
2565
|
+
* @returns The eligible method and its live rpcId, or undefined for anything else.
|
|
2566
|
+
*/
|
|
2567
|
+
function checkCatalogRequest(routeMethod, bodyJson) {
|
|
2568
|
+
if (!CATALOG_CACHE_METHODS.has(routeMethod)) return void 0;
|
|
2569
|
+
let parsed;
|
|
2570
|
+
try {
|
|
2571
|
+
parsed = JSON.parse(bodyJson);
|
|
2572
|
+
} catch {
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (!isObject$1(parsed) || !hasExactKeys(parsed, REQUEST_KEYS)) return void 0;
|
|
2576
|
+
if (parsed.type !== "client-request") return void 0;
|
|
2577
|
+
if (parsed.method !== routeMethod) return void 0;
|
|
2578
|
+
if (typeof parsed.rpcId !== "string" || parsed.rpcId.length === 0 || parsed.rpcId.length > 512) return void 0;
|
|
2579
|
+
const payload = parsed.payload;
|
|
2580
|
+
if (!isObject$1(payload) || !hasExactKeys(payload, /* @__PURE__ */ new Set(["args"]))) return void 0;
|
|
2581
|
+
const args = payload.args;
|
|
2582
|
+
if (!isObject$1(args) || Array.isArray(args) || Object.keys(args).length !== 0) return void 0;
|
|
2583
|
+
return {
|
|
2584
|
+
method: routeMethod,
|
|
2585
|
+
rpcId: parsed.rpcId
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Validate a fill response against the captured success schema.
|
|
2590
|
+
* @param envelopeJson - The buffered upstream response body.
|
|
2591
|
+
* @param expectedRpcId - The gateway-generated rpcId the fill sent.
|
|
2592
|
+
* @returns Whether the envelope is the shareable success variant for that id.
|
|
2593
|
+
*/
|
|
2594
|
+
function validateCatalogEnvelope(envelopeJson, expectedRpcId) {
|
|
2595
|
+
let parsed;
|
|
2596
|
+
try {
|
|
2597
|
+
parsed = JSON.parse(envelopeJson);
|
|
2598
|
+
} catch {
|
|
2599
|
+
return false;
|
|
2600
|
+
}
|
|
2601
|
+
if (!isObject$1(parsed) || !hasExactKeys(parsed, RESPONSE_KEYS)) return false;
|
|
2602
|
+
if (parsed.type !== "server-response") return false;
|
|
2603
|
+
if (parsed.rpcId !== expectedRpcId) return false;
|
|
2604
|
+
const result = parsed.result;
|
|
2605
|
+
if (!isObject$1(result)) return false;
|
|
2606
|
+
return result.ok === true && "value" in result;
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Re-stamp a validated envelope for one caller.
|
|
2610
|
+
* @param envelopeJson - The cached upstream envelope.
|
|
2611
|
+
* @param expectedRpcId - The fill rpcId the envelope must carry.
|
|
2612
|
+
* @param liveRpcId - The caller's rpcId to serialize into the response.
|
|
2613
|
+
* @returns The per-caller JSON body, or undefined when validation fails.
|
|
2614
|
+
*/
|
|
2615
|
+
function restampEnvelope(envelopeJson, expectedRpcId, liveRpcId) {
|
|
2616
|
+
let parsed;
|
|
2617
|
+
try {
|
|
2618
|
+
parsed = JSON.parse(envelopeJson);
|
|
2619
|
+
} catch {
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
if (!validateCatalogEnvelope(envelopeJson, expectedRpcId)) return void 0;
|
|
2623
|
+
const body = parsed;
|
|
2624
|
+
body.rpcId = liveRpcId;
|
|
2625
|
+
return JSON.stringify(body);
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* Generation-aware SWR cache over the injected fill transport.
|
|
2629
|
+
* @typeParam TUpstreamSnapshot - Opaque immutable upstream identity snapshot the gateway captures per request.
|
|
2630
|
+
*/
|
|
2631
|
+
var CatalogCache = class {
|
|
2632
|
+
#limits;
|
|
2633
|
+
#clock;
|
|
2634
|
+
#fetchFill;
|
|
2635
|
+
#entries = /* @__PURE__ */ new Map();
|
|
2636
|
+
#flights = /* @__PURE__ */ new Map();
|
|
2637
|
+
#identityDigest;
|
|
2638
|
+
#epoch = 0;
|
|
2639
|
+
#disposed = false;
|
|
2640
|
+
constructor(deps) {
|
|
2641
|
+
this.#fetchFill = deps.fetchFill;
|
|
2642
|
+
this.#clock = deps.clock;
|
|
2643
|
+
this.#limits = {
|
|
2644
|
+
...DEFAULT_CATALOG_CACHE_LIMITS,
|
|
2645
|
+
...deps.limits
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
get entryCount() {
|
|
2649
|
+
return this.#entries.size;
|
|
2650
|
+
}
|
|
2651
|
+
get residentBytes() {
|
|
2652
|
+
let total = 0;
|
|
2653
|
+
for (const entry of this.#entries.values()) total += entry.bytes;
|
|
2654
|
+
return total;
|
|
2655
|
+
}
|
|
2656
|
+
get inFlightFills() {
|
|
2657
|
+
let active = 0;
|
|
2658
|
+
for (const flight of this.#flights.values()) if (!flight.settled) active += 1;
|
|
2659
|
+
return active;
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Serve one eligible request from the current generation.
|
|
2663
|
+
* @param input - Method, caller rpcId, current identity digest and its snapshot.
|
|
2664
|
+
* @returns A per-caller replay, or passthrough when the ordinary path must run.
|
|
2665
|
+
*/
|
|
2666
|
+
async serve(input) {
|
|
2667
|
+
if (this.#disposed) return { kind: "passthrough" };
|
|
2668
|
+
if (!CATALOG_CACHE_METHODS.has(input.method)) return { kind: "passthrough" };
|
|
2669
|
+
const method = input.method;
|
|
2670
|
+
this.#syncIdentity(input.identityDigest);
|
|
2671
|
+
const generation = this.#currentGeneration();
|
|
2672
|
+
const key = `${input.identityDigest}\u0000${method}`;
|
|
2673
|
+
const entry = this.#entries.get(key);
|
|
2674
|
+
if (entry !== void 0) {
|
|
2675
|
+
const age = this.#clock() - entry.completedAt;
|
|
2676
|
+
if (age < this.#limits.freshMs) {
|
|
2677
|
+
entry.lastServedAt = this.#clock();
|
|
2678
|
+
const body = restampEnvelope(entry.envelopeJson, entry.upstreamRpcId, input.rpcId);
|
|
2679
|
+
if (body !== void 0) return {
|
|
2680
|
+
kind: "hit",
|
|
2681
|
+
body,
|
|
2682
|
+
generation,
|
|
2683
|
+
validUntil: entry.validUntil
|
|
2684
|
+
};
|
|
2685
|
+
} else if (age < this.#limits.staleMs) {
|
|
2686
|
+
entry.lastServedAt = this.#clock();
|
|
2687
|
+
const body = restampEnvelope(entry.envelopeJson, entry.upstreamRpcId, input.rpcId);
|
|
2688
|
+
if (body !== void 0) {
|
|
2689
|
+
this.#startBackgroundFill(method, input.identityDigest, input.upstream);
|
|
2690
|
+
return {
|
|
2691
|
+
kind: "stale",
|
|
2692
|
+
body,
|
|
2693
|
+
generation,
|
|
2694
|
+
validUntil: entry.validUntil
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
const flightKey = `${input.identityDigest}|${generation.epoch}|${method}`;
|
|
2700
|
+
const existing = this.#flights.get(flightKey);
|
|
2701
|
+
if (existing === void 0 && this.inFlightFills >= this.#limits.maxInFlightFills) return { kind: "passthrough" };
|
|
2702
|
+
const outcome = await (existing ?? this.#createFlight(method, generation, input.upstream)).promise;
|
|
2703
|
+
if (!outcome.shareable) return { kind: "passthrough" };
|
|
2704
|
+
if (!this.#isCurrent(generation)) return { kind: "passthrough" };
|
|
2705
|
+
if (!validateCatalogEnvelope(outcome.envelopeJson, outcome.upstreamRpcId)) return { kind: "passthrough" };
|
|
2706
|
+
const body = restampEnvelope(outcome.envelopeJson, outcome.upstreamRpcId, input.rpcId);
|
|
2707
|
+
if (body === void 0) return { kind: "passthrough" };
|
|
2708
|
+
return {
|
|
2709
|
+
kind: "replay",
|
|
2710
|
+
body,
|
|
2711
|
+
generation,
|
|
2712
|
+
validUntil: this.#clock() + this.#limits.staleMs
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
/** Record the current upstream identity; a change clears entries and bumps the epoch. */
|
|
2716
|
+
noteIdentity(identityDigest) {
|
|
2717
|
+
this.#syncIdentity(identityDigest);
|
|
2718
|
+
}
|
|
2719
|
+
/** Invalidate every entry after a catalog-affecting mutation. */
|
|
2720
|
+
noteCatalogMutation() {
|
|
2721
|
+
this.#epoch += 1;
|
|
2722
|
+
this.#entries.clear();
|
|
2723
|
+
}
|
|
2724
|
+
/** Whether a generation is still the current one. */
|
|
2725
|
+
isCurrent(generation) {
|
|
2726
|
+
return this.#isCurrent(generation);
|
|
2727
|
+
}
|
|
2728
|
+
/** Final pre-commit check for a replay result: generation, disposal and age. */
|
|
2729
|
+
canCommit(result) {
|
|
2730
|
+
if (this.#disposed) return false;
|
|
2731
|
+
if (!this.#isCurrent(result.generation)) return false;
|
|
2732
|
+
return this.#clock() < result.validUntil;
|
|
2733
|
+
}
|
|
2734
|
+
/** Invalidate everything and refuse new fills or publication. */
|
|
2735
|
+
dispose() {
|
|
2736
|
+
this.#disposed = true;
|
|
2737
|
+
this.#entries.clear();
|
|
2738
|
+
}
|
|
2739
|
+
#isCurrent(generation) {
|
|
2740
|
+
if (this.#disposed) return false;
|
|
2741
|
+
return generation.identityDigest === this.#identityDigest && generation.epoch === this.#epoch;
|
|
2742
|
+
}
|
|
2743
|
+
#currentGeneration() {
|
|
2744
|
+
return {
|
|
2745
|
+
identityDigest: this.#identityDigest,
|
|
2746
|
+
epoch: this.#epoch
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
#syncIdentity(identityDigest) {
|
|
2750
|
+
if (identityDigest === this.#identityDigest) return;
|
|
2751
|
+
this.#identityDigest = identityDigest;
|
|
2752
|
+
this.#epoch += 1;
|
|
2753
|
+
this.#entries.clear();
|
|
2754
|
+
}
|
|
2755
|
+
#createFlight(method, generation, upstream) {
|
|
2756
|
+
const flightKey = `${generation.identityDigest}|${generation.epoch}|${method}`;
|
|
2757
|
+
const invoke = async () => this.#fetchFill({
|
|
2758
|
+
method,
|
|
2759
|
+
generation,
|
|
2760
|
+
upstream
|
|
2761
|
+
});
|
|
2762
|
+
const flight = {
|
|
2763
|
+
generation,
|
|
2764
|
+
method,
|
|
2765
|
+
settled: false,
|
|
2766
|
+
promise: invoke().catch(() => ({ shareable: false })).then((outcome) => {
|
|
2767
|
+
flight.settled = true;
|
|
2768
|
+
if (this.#flights.get(flightKey) === flight) this.#flights.delete(flightKey);
|
|
2769
|
+
if (outcome.shareable && this.#isCurrent(generation)) this.#retain(method, generation, outcome);
|
|
2770
|
+
return outcome;
|
|
2771
|
+
})
|
|
2772
|
+
};
|
|
2773
|
+
this.#flights.set(flightKey, flight);
|
|
2774
|
+
return flight;
|
|
2775
|
+
}
|
|
2776
|
+
#startBackgroundFill(method, identityDigest, upstream) {
|
|
2777
|
+
if (this.#disposed) return;
|
|
2778
|
+
const generation = this.#currentGeneration();
|
|
2779
|
+
const flightKey = `${identityDigest}|${generation.epoch}|${method}`;
|
|
2780
|
+
if (this.#flights.has(flightKey)) return;
|
|
2781
|
+
if (this.inFlightFills >= this.#limits.maxInFlightFills) return;
|
|
2782
|
+
this.#createFlight(method, generation, upstream).promise.catch(() => void 0);
|
|
2783
|
+
}
|
|
2784
|
+
#retain(method, generation, outcome) {
|
|
2785
|
+
if (!validateCatalogEnvelope(outcome.envelopeJson, outcome.upstreamRpcId)) return;
|
|
2786
|
+
const bytes = encoder.encode(outcome.envelopeJson).length;
|
|
2787
|
+
if (bytes > this.#limits.maxEntryBytes) return;
|
|
2788
|
+
const key = `${generation.identityDigest}\u0000${method}`;
|
|
2789
|
+
const now = this.#clock();
|
|
2790
|
+
if (this.#entries.has(key)) this.#entries.delete(key);
|
|
2791
|
+
const evict = () => {
|
|
2792
|
+
const candidates = [...this.#entries.entries()].sort(([, a], [, b]) => a.lastServedAt - b.lastServedAt);
|
|
2793
|
+
for (const [candidateKey] of candidates) {
|
|
2794
|
+
if (this.#entries.size < this.#limits.maxEntries && this.residentBytes + bytes <= this.#limits.maxTotalBytes) break;
|
|
2795
|
+
this.#entries.delete(candidateKey);
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
evict();
|
|
2799
|
+
if (this.#entries.size >= this.#limits.maxEntries) return;
|
|
2800
|
+
if (this.residentBytes + bytes > this.#limits.maxTotalBytes) return;
|
|
2801
|
+
this.#entries.set(key, {
|
|
2802
|
+
identityDigest: generation.identityDigest,
|
|
2803
|
+
method,
|
|
2804
|
+
envelopeJson: outcome.envelopeJson,
|
|
2805
|
+
upstreamRpcId: outcome.upstreamRpcId,
|
|
2806
|
+
completedAt: now,
|
|
2807
|
+
validUntil: now + this.#limits.staleMs,
|
|
2808
|
+
bytes,
|
|
2809
|
+
lastServedAt: now
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
};
|
|
2472
2813
|
// src/upstream-auth-log.ts
|
|
2473
2814
|
const LOG_INTERVAL_MS = 3e4;
|
|
2474
2815
|
/** Two fixed sampling slots keep a first failure visible even after a successful exchange. */
|
|
@@ -5387,12 +5728,16 @@ const MOBILE_LAYOUT_MODULE = "@deepseek-ai/dsh-client-ui-layout";
|
|
|
5387
5728
|
const MOBILE_LAYOUT_PATH = `${AUTH_PREFIX}/mobile-layout.js`;
|
|
5388
5729
|
const MOBILE_BOOT_BATCH_PREFIX = `${AUTH_PREFIX}/mobile-boot/`;
|
|
5389
5730
|
const MOBILE_DOCUMENT_VIEW_HEADER = "x-dsh-mobile-view";
|
|
5731
|
+
const CATALOG_CACHE_MARKER = "x-dsh-mobile-catalog";
|
|
5732
|
+
/** Eligible catalog envelopes are small fixed-shape JSON; larger declared bodies keep the streaming path. */
|
|
5733
|
+
const CATALOG_CANDIDATE_BODY_LIMIT = 65536;
|
|
5390
5734
|
const MOBILE_PUSH_CONFIG_PATH = `${AUTH_PREFIX}/push/config`;
|
|
5391
5735
|
const MOBILE_PUSH_SUBSCRIPTION_PATH = `${AUTH_PREFIX}/push/subscription`;
|
|
5392
5736
|
const MOBILE_PUSH_PREFERENCES_PATH = `${AUTH_PREFIX}/push/preferences`;
|
|
5393
5737
|
const MOBILE_PUSH_DISABLE_PATH = `${AUTH_PREFIX}/push/disable`;
|
|
5394
5738
|
const MOBILE_PUSH_TEST_PATH = `${AUTH_PREFIX}/push/test`;
|
|
5395
5739
|
const MOBILE_PUSH_RESOLVE_PATH = `${AUTH_PREFIX}/push/resolve`;
|
|
5740
|
+
const BACKGROUND_EVENT_PATH = `${AUTH_PREFIX}/background-events`;
|
|
5396
5741
|
const MOBILE_PUSH_PREFERENCE_KEYS = [
|
|
5397
5742
|
"question",
|
|
5398
5743
|
"plan",
|
|
@@ -5869,7 +6214,7 @@ function splitMobileBootBatch(entries, sizes, passThrough) {
|
|
|
5869
6214
|
};
|
|
5870
6215
|
for (const entry of entries) {
|
|
5871
6216
|
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) {
|
|
6217
|
+
if ((passThrough.has(entry.url) || size !== void 0 && (size <= 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES)) && entry.id !== MOBILE_LAYOUT_MODULE) {
|
|
5873
6218
|
rows.push({
|
|
5874
6219
|
phase: "application",
|
|
5875
6220
|
url: entry.url,
|
|
@@ -5878,7 +6223,7 @@ function splitMobileBootBatch(entries, sizes, passThrough) {
|
|
|
5878
6223
|
});
|
|
5879
6224
|
continue;
|
|
5880
6225
|
}
|
|
5881
|
-
const bytes = (size
|
|
6226
|
+
const bytes = (size !== void 0 && size > 0 ? size : 0) + MOBILE_BOOT_SEPARATOR_BYTES;
|
|
5882
6227
|
if (chunk !== void 0 && chunk.entries.length > 0 && chunk.bytes + bytes > MOBILE_BOOT_CHUNK_BYTES) flush();
|
|
5883
6228
|
chunk ??= {
|
|
5884
6229
|
ids: [],
|
|
@@ -6138,6 +6483,12 @@ function sanitizeRequestHeaders(request, upstream) {
|
|
|
6138
6483
|
const value = request.headers[name];
|
|
6139
6484
|
if (value !== void 0) headers[name] = value;
|
|
6140
6485
|
}
|
|
6486
|
+
for (const name of Object.keys(request.headers)) {
|
|
6487
|
+
const lower = name.toLowerCase();
|
|
6488
|
+
if (!lower.startsWith("x-dsh-") || lower.startsWith("x-dsh-mobile-") || lower === "x-dsh-auth-token") continue;
|
|
6489
|
+
const value = request.headers[name];
|
|
6490
|
+
if (value !== void 0) headers[name] = Array.isArray(value) ? value[0] : value;
|
|
6491
|
+
}
|
|
6141
6492
|
return headers;
|
|
6142
6493
|
}
|
|
6143
6494
|
const BLOCKED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -6482,6 +6833,18 @@ var MobileAccessGateway = class {
|
|
|
6482
6833
|
upstreamCookie;
|
|
6483
6834
|
upstreamCookieExpiresAt = 0;
|
|
6484
6835
|
upstreamCookieTask;
|
|
6836
|
+
#catalogCache;
|
|
6837
|
+
#upstreamIdentityRevision = 0;
|
|
6838
|
+
#catalogFills = /* @__PURE__ */ new Set();
|
|
6839
|
+
/** The catalog SWR cache, or undefined when `catalogCache` is disabled. */
|
|
6840
|
+
get catalogCache() {
|
|
6841
|
+
if (!this.config.catalogCache) return void 0;
|
|
6842
|
+
this.#catalogCache ??= new CatalogCache({
|
|
6843
|
+
fetchFill: (input) => this.catalogFill(input),
|
|
6844
|
+
clock: () => performance.now()
|
|
6845
|
+
});
|
|
6846
|
+
return this.#catalogCache;
|
|
6847
|
+
}
|
|
6485
6848
|
upstreamAuthRequest;
|
|
6486
6849
|
nextOperationId = 1;
|
|
6487
6850
|
closing = false;
|
|
@@ -7279,6 +7642,7 @@ var MobileAccessGateway = class {
|
|
|
7279
7642
|
const computerImage = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-image`;
|
|
7280
7643
|
const workspaceDirectories = (request.method === "GET" || request.method === "POST") && target.decodedPathname === `/mobile-access/workspace-directories`;
|
|
7281
7644
|
const clientModules = target.decodedPathname === `${AUTH_PREFIX}/client-modules`;
|
|
7645
|
+
const backgroundEvents = target.decodedPathname === BACKGROUND_EVENT_PATH;
|
|
7282
7646
|
const mobilePushRoute = [
|
|
7283
7647
|
MOBILE_PUSH_CONFIG_PATH,
|
|
7284
7648
|
MOBILE_PUSH_SUBSCRIPTION_PATH,
|
|
@@ -7312,7 +7676,7 @@ var MobileAccessGateway = class {
|
|
|
7312
7676
|
fallback: void 0,
|
|
7313
7677
|
digest: requestedCompatRevision
|
|
7314
7678
|
} : void 0;
|
|
7315
|
-
if (customAsset === void 0 && requestedMobileBootBatch === void 0 && !computerImages && !computerImage && !workspaceDirectories && !clientModules && !mobilePushRoute && extensionTarget(target.decodedPathname) === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
|
|
7679
|
+
if (customAsset === void 0 && requestedMobileBootBatch === void 0 && !computerImages && !computerImage && !workspaceDirectories && !clientModules && !backgroundEvents && !mobilePushRoute && extensionTarget(target.decodedPathname) === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
|
|
7316
7680
|
if (![
|
|
7317
7681
|
"GET",
|
|
7318
7682
|
"HEAD",
|
|
@@ -7353,6 +7717,11 @@ var MobileAccessGateway = class {
|
|
|
7353
7717
|
await this.handleMobilePushRequest(target, request, response, authorization);
|
|
7354
7718
|
return;
|
|
7355
7719
|
}
|
|
7720
|
+
if (backgroundEvents) {
|
|
7721
|
+
if (request.method !== "GET" || target.search !== "") throw new HttpError(request.method === "GET" ? 400 : 405, request.method === "GET" ? "bad_request" : "method_not_allowed");
|
|
7722
|
+
this.openBackgroundEventStream(request, response, authorization);
|
|
7723
|
+
return;
|
|
7724
|
+
}
|
|
7356
7725
|
if (clientModules) {
|
|
7357
7726
|
if (target.search !== "") throw new HttpError(400, "bad_request");
|
|
7358
7727
|
if (request.method !== "GET" && request.method !== "POST") throw new HttpError(405, "method_not_allowed");
|
|
@@ -7529,6 +7898,21 @@ var MobileAccessGateway = class {
|
|
|
7529
7898
|
return;
|
|
7530
7899
|
}
|
|
7531
7900
|
if (stockFrontend && target.decodedPathname === "/") request.url = "/";
|
|
7901
|
+
const candidateContentType = String(request.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase();
|
|
7902
|
+
const candidateDeclaredLength = request.headers["content-length"] === void 0 ? void 0 : Number(request.headers["content-length"]);
|
|
7903
|
+
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;
|
|
7904
|
+
if (catalogRouteMethod !== void 0) {
|
|
7905
|
+
const candidateBody = await this.readRequestBody(request, Math.min(CATALOG_CANDIDATE_BODY_LIMIT, this.config.maxBodyBytes));
|
|
7906
|
+
const eligible = checkCatalogRequest(catalogRouteMethod, candidateBody.toString("utf8"));
|
|
7907
|
+
if (eligible !== void 0 && this.catalogCache !== void 0) {
|
|
7908
|
+
if (await this.serveCatalogCache(eligible, request, response, authorization)) return;
|
|
7909
|
+
response.setHeader(CATALOG_CACHE_MARKER, "miss-passthrough");
|
|
7910
|
+
await this.proxyHttp(request, response, authorization, documentView, candidateBody);
|
|
7911
|
+
return;
|
|
7912
|
+
}
|
|
7913
|
+
await this.proxyHttp(request, response, authorization, documentView, candidateBody);
|
|
7914
|
+
return;
|
|
7915
|
+
}
|
|
7532
7916
|
await this.proxyHttp(request, response, authorization, documentView);
|
|
7533
7917
|
}
|
|
7534
7918
|
async handleExtensionRequest(targetInfo, target, request, response, authorization) {
|
|
@@ -7823,6 +8207,8 @@ var MobileAccessGateway = class {
|
|
|
7823
8207
|
if (pair === void 0 || pair.length > 4096 || !UPSTREAM_COOKIE_PAIR.test(pair) || !Number.isSafeInteger(expiresAt) || maxAgeSeconds <= 0) throw new HttpError(502, "upstream_unavailable");
|
|
7824
8208
|
this.upstreamCookie = pair;
|
|
7825
8209
|
this.upstreamCookieExpiresAt = expiresAt;
|
|
8210
|
+
this.#upstreamIdentityRevision += 1;
|
|
8211
|
+
this.catalogCache?.noteIdentity(createHash("sha256").update(pair).digest("hex"));
|
|
7826
8212
|
this.upstreamAuthDiagnostics.record(attempt, {
|
|
7827
8213
|
outcome: "accepted",
|
|
7828
8214
|
stage,
|
|
@@ -8346,7 +8732,7 @@ var MobileAccessGateway = class {
|
|
|
8346
8732
|
const index = cursor++;
|
|
8347
8733
|
const source = candidates[index].url;
|
|
8348
8734
|
const size = await this.upstreamBundleSize(source, signal);
|
|
8349
|
-
if (size === void 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES) passThrough.add(source);
|
|
8735
|
+
if (size === void 0 || size <= 0 || size >= MAX_MOBILE_BOOT_ENTRY_BYTES) passThrough.add(source);
|
|
8350
8736
|
else sizes.set(source, size);
|
|
8351
8737
|
}
|
|
8352
8738
|
};
|
|
@@ -8650,13 +9036,181 @@ var MobileAccessGateway = class {
|
|
|
8650
9036
|
}
|
|
8651
9037
|
};
|
|
8652
9038
|
}
|
|
9039
|
+
/** Read one bounded request body verbatim; the caller owns replaying it upstream. */
|
|
9040
|
+
readRequestBody(request, limitBytes) {
|
|
9041
|
+
const declared = request.headers["content-length"];
|
|
9042
|
+
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > limitBytes)) throw new HttpError(413, "payload_too_large");
|
|
9043
|
+
return new Promise((resolve, reject) => {
|
|
9044
|
+
const chunks = [];
|
|
9045
|
+
let received = 0;
|
|
9046
|
+
let failed = false;
|
|
9047
|
+
request.on("data", (chunk) => {
|
|
9048
|
+
if (failed) return;
|
|
9049
|
+
received += chunk.length;
|
|
9050
|
+
if (received > limitBytes) {
|
|
9051
|
+
failed = true;
|
|
9052
|
+
reject(new HttpError(413, "payload_too_large"));
|
|
9053
|
+
request.resume();
|
|
9054
|
+
return;
|
|
9055
|
+
}
|
|
9056
|
+
chunks.push(chunk);
|
|
9057
|
+
});
|
|
9058
|
+
request.once("end", () => {
|
|
9059
|
+
if (!failed) resolve(Buffer.concat(chunks));
|
|
9060
|
+
});
|
|
9061
|
+
request.once("error", (error) => {
|
|
9062
|
+
if (!failed) reject(error);
|
|
9063
|
+
});
|
|
9064
|
+
});
|
|
9065
|
+
}
|
|
9066
|
+
/** One gateway-owned catalog fill: its own socket, rpcId, deadline and byte cap. */
|
|
9067
|
+
async catalogFill(input) {
|
|
9068
|
+
const rpcId = `dsh-mobile-catalog-${randomUUID()}`;
|
|
9069
|
+
const body = JSON.stringify({
|
|
9070
|
+
type: "client-request",
|
|
9071
|
+
rpcId,
|
|
9072
|
+
method: input.method,
|
|
9073
|
+
payload: { args: {} }
|
|
9074
|
+
});
|
|
9075
|
+
return await new Promise((resolve) => {
|
|
9076
|
+
let settled = false;
|
|
9077
|
+
let upstreamRequest;
|
|
9078
|
+
const finish = (outcome) => {
|
|
9079
|
+
if (settled) return;
|
|
9080
|
+
settled = true;
|
|
9081
|
+
clearTimeout(absoluteDeadline);
|
|
9082
|
+
this.#catalogFills.delete(upstreamRequest);
|
|
9083
|
+
resolve(outcome);
|
|
9084
|
+
};
|
|
9085
|
+
upstreamRequest = request({
|
|
9086
|
+
protocol: "http:",
|
|
9087
|
+
hostname: stripIpv6Brackets(this.config.upstreamOrigin.hostname),
|
|
9088
|
+
port: Number(this.config.upstreamOrigin.port),
|
|
9089
|
+
method: "POST",
|
|
9090
|
+
path: `/api/${input.method}`,
|
|
9091
|
+
headers: {
|
|
9092
|
+
host: this.config.upstreamOrigin.host,
|
|
9093
|
+
"content-type": "application/json",
|
|
9094
|
+
accept: "application/json",
|
|
9095
|
+
"accept-encoding": "identity",
|
|
9096
|
+
"content-length": String(Buffer.byteLength(body)),
|
|
9097
|
+
...input.upstream.cookieHeader === void 0 ? {} : { cookie: input.upstream.cookieHeader }
|
|
9098
|
+
},
|
|
9099
|
+
agent: false
|
|
9100
|
+
}, (response) => {
|
|
9101
|
+
const status = response.statusCode ?? 0;
|
|
9102
|
+
const rawContentType = String(response.headers["content-type"] ?? "");
|
|
9103
|
+
const mediaType = rawContentType.split(";", 1)[0]?.trim().toLowerCase();
|
|
9104
|
+
const charset = /charset\s*=\s*([^;]+)/iu.exec(rawContentType)?.[1]?.trim().toLowerCase();
|
|
9105
|
+
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") {
|
|
9106
|
+
response.destroy();
|
|
9107
|
+
finish({ shareable: false });
|
|
9108
|
+
return;
|
|
9109
|
+
}
|
|
9110
|
+
const chunks = [];
|
|
9111
|
+
let received = 0;
|
|
9112
|
+
let overflowed = false;
|
|
9113
|
+
response.on("data", (chunk) => {
|
|
9114
|
+
if (overflowed) return;
|
|
9115
|
+
received += chunk.length;
|
|
9116
|
+
if (received > DEFAULT_CATALOG_CACHE_LIMITS.maxEntryBytes) {
|
|
9117
|
+
overflowed = true;
|
|
9118
|
+
response.destroy();
|
|
9119
|
+
return;
|
|
9120
|
+
}
|
|
9121
|
+
chunks.push(chunk);
|
|
9122
|
+
});
|
|
9123
|
+
response.once("end", () => {
|
|
9124
|
+
if (overflowed) {
|
|
9125
|
+
finish({ shareable: false });
|
|
9126
|
+
return;
|
|
9127
|
+
}
|
|
9128
|
+
try {
|
|
9129
|
+
const envelopeJson = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
|
|
9130
|
+
JSON.parse(envelopeJson);
|
|
9131
|
+
finish({
|
|
9132
|
+
shareable: true,
|
|
9133
|
+
envelopeJson,
|
|
9134
|
+
upstreamRpcId: rpcId
|
|
9135
|
+
});
|
|
9136
|
+
} catch {
|
|
9137
|
+
finish({ shareable: false });
|
|
9138
|
+
}
|
|
9139
|
+
});
|
|
9140
|
+
response.once("error", () => finish({ shareable: false }));
|
|
9141
|
+
response.once("close", () => finish({ shareable: false }));
|
|
9142
|
+
});
|
|
9143
|
+
const absoluteDeadline = setTimeout(() => {
|
|
9144
|
+
upstreamRequest.destroy(/* @__PURE__ */ new Error("catalog fill deadline"));
|
|
9145
|
+
}, this.config.upstreamTimeoutMs);
|
|
9146
|
+
upstreamRequest.setTimeout(this.config.upstreamTimeoutMs, () => {
|
|
9147
|
+
upstreamRequest.destroy(/* @__PURE__ */ new Error("catalog fill idle timeout"));
|
|
9148
|
+
});
|
|
9149
|
+
upstreamRequest.once("error", () => finish({ shareable: false }));
|
|
9150
|
+
this.#catalogFills.add(upstreamRequest);
|
|
9151
|
+
upstreamRequest.end(body);
|
|
9152
|
+
});
|
|
9153
|
+
}
|
|
9154
|
+
/**
|
|
9155
|
+
* Serve one eligible catalog request from the SWR cache.
|
|
9156
|
+
* @returns true when the response was committed here; false for the ordinary path.
|
|
9157
|
+
*/
|
|
9158
|
+
async serveCatalogCache(eligible, request, response, authorization) {
|
|
9159
|
+
const cache = this.catalogCache;
|
|
9160
|
+
if (cache === void 0) return false;
|
|
9161
|
+
const operation = this.allocateRequest(authorization, response, {});
|
|
9162
|
+
const onClose = () => {
|
|
9163
|
+
operation.abort();
|
|
9164
|
+
operation.release();
|
|
9165
|
+
};
|
|
9166
|
+
response.once("close", onClose);
|
|
9167
|
+
try {
|
|
9168
|
+
const snapshotRevision = this.#upstreamIdentityRevision;
|
|
9169
|
+
const hadIdentityBefore = this.upstreamCookie !== void 0;
|
|
9170
|
+
const cookieHeader = await waitForRequestTask(this.upstreamCookieHeader(), operation.signal);
|
|
9171
|
+
operation.signal.throwIfAborted();
|
|
9172
|
+
const superseded = hadIdentityBefore && snapshotRevision !== this.#upstreamIdentityRevision;
|
|
9173
|
+
if (response.destroyed || superseded) return false;
|
|
9174
|
+
const identityDigest = createHash("sha256").update(cookieHeader ?? "none").digest("hex");
|
|
9175
|
+
cache.noteIdentity(identityDigest);
|
|
9176
|
+
const result = await cache.serve({
|
|
9177
|
+
method: eligible.method,
|
|
9178
|
+
rpcId: eligible.rpcId,
|
|
9179
|
+
identityDigest,
|
|
9180
|
+
upstream: { cookieHeader }
|
|
9181
|
+
});
|
|
9182
|
+
operation.signal.throwIfAborted();
|
|
9183
|
+
if (result.kind === "passthrough" || !cache.canCommit(result) || response.destroyed) return false;
|
|
9184
|
+
const payload = Buffer.from(result.body, "utf8");
|
|
9185
|
+
const headers = {
|
|
9186
|
+
"content-type": "application/json",
|
|
9187
|
+
"cache-control": "no-store",
|
|
9188
|
+
[CATALOG_CACHE_MARKER]: result.kind
|
|
9189
|
+
};
|
|
9190
|
+
let body = payload;
|
|
9191
|
+
if (acceptsGzip(request.headers["accept-encoding"]) && payload.length >= MIN_COMPRESSIBLE_BYTES) {
|
|
9192
|
+
body = await gzipBuffer(payload);
|
|
9193
|
+
headers["content-encoding"] = "gzip";
|
|
9194
|
+
addVaryAcceptEncoding(headers);
|
|
9195
|
+
}
|
|
9196
|
+
if (!cache.canCommit(result) || response.destroyed) return false;
|
|
9197
|
+
headers["content-length"] = body.length;
|
|
9198
|
+
setSecurityHeaders(response, this.tlsEnabled, "proxied");
|
|
9199
|
+
response.writeHead(200, headers);
|
|
9200
|
+
response.end(body);
|
|
9201
|
+
return true;
|
|
9202
|
+
} finally {
|
|
9203
|
+
response.removeListener("close", onClose);
|
|
9204
|
+
operation.release();
|
|
9205
|
+
}
|
|
9206
|
+
}
|
|
8653
9207
|
/**
|
|
8654
9208
|
* Proxy one request, retrying transient failures for plugin and asset GETs.
|
|
8655
9209
|
* Only bodyless GETs can be replayed, before response headers reach the client.
|
|
8656
9210
|
* One request slot and cancellation signal cover attempts and backoff together.
|
|
8657
9211
|
* API requests and requests with bodies are forwarded once.
|
|
8658
9212
|
*/
|
|
8659
|
-
async proxyHttp(request, response, authorization, documentView) {
|
|
9213
|
+
async proxyHttp(request, response, authorization, documentView, bodyOverride) {
|
|
8660
9214
|
const declared = request.headers["content-length"];
|
|
8661
9215
|
if (declared !== void 0 && (!/^\d+$/u.test(declared) || Number(declared) > this.config.maxBodyBytes)) throw new HttpError(413, "payload_too_large");
|
|
8662
9216
|
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 +9238,7 @@ var MobileAccessGateway = class {
|
|
|
8684
9238
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
8685
9239
|
operation.signal.throwIfAborted();
|
|
8686
9240
|
try {
|
|
8687
|
-
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable, documentView);
|
|
9241
|
+
await this.proxyHttpOnce(request, response, holder, operation.signal, replayable, documentView, bodyOverride);
|
|
8688
9242
|
return;
|
|
8689
9243
|
} catch (error) {
|
|
8690
9244
|
if (operation.signal.aborted || response.destroyed) return;
|
|
@@ -8705,7 +9259,7 @@ var MobileAccessGateway = class {
|
|
|
8705
9259
|
operation.release();
|
|
8706
9260
|
}
|
|
8707
9261
|
}
|
|
8708
|
-
async proxyHttpOnce(request$2, response, holder, signal, replayable, documentView) {
|
|
9262
|
+
async proxyHttpOnce(request$2, response, holder, signal, replayable, documentView, bodyOverride) {
|
|
8709
9263
|
let bodyDone;
|
|
8710
9264
|
const projections = /* @__PURE__ */ new Set();
|
|
8711
9265
|
try {
|
|
@@ -8736,7 +9290,8 @@ var MobileAccessGateway = class {
|
|
|
8736
9290
|
});
|
|
8737
9291
|
upstreamRequest.once("response", resolve);
|
|
8738
9292
|
upstreamRequest.once("error", reject);
|
|
8739
|
-
if (
|
|
9293
|
+
if (bodyOverride !== void 0) upstreamRequest.end(bodyOverride);
|
|
9294
|
+
else if (replayable) upstreamRequest.end();
|
|
8740
9295
|
else {
|
|
8741
9296
|
bodyDone = pipeline(request$2, new ByteLimitTransform(this.config.maxBodyBytes), upstreamRequest);
|
|
8742
9297
|
bodyDone.catch(reject);
|
|
@@ -8760,6 +9315,7 @@ var MobileAccessGateway = class {
|
|
|
8760
9315
|
delete headers["accept-ranges"];
|
|
8761
9316
|
}
|
|
8762
9317
|
const statusCode = proxied.statusCode ?? 502;
|
|
9318
|
+
if (statusCode >= 200 && statusCode < 300 && this.catalogCache !== void 0 && pathname.startsWith("/api/") && CATALOG_MUTATION_METHODS.has(pathname.slice(5))) this.catalogCache.noteCatalogMutation();
|
|
8763
9319
|
this.httpFailures.recordUpstream(request$2.method, request$2.url?.split("?", 1)[0] ?? "", statusCode);
|
|
8764
9320
|
const cacheControl = graphEvents ? void 0 : revisionedStaticCacheControl(request$2, statusCode);
|
|
8765
9321
|
if (cacheControl !== void 0) headers["cache-control"] = cacheControl;
|
|
@@ -8906,6 +9462,54 @@ var MobileAccessGateway = class {
|
|
|
8906
9462
|
request.once("aborted", close);
|
|
8907
9463
|
response.once("close", close);
|
|
8908
9464
|
}
|
|
9465
|
+
/**
|
|
9466
|
+
* Native background notifications need only Host task and revocation events.
|
|
9467
|
+
* They use the paired session cookie but no mobile document view, because the
|
|
9468
|
+
* App has no live WebView while this stream is serving a suspended page.
|
|
9469
|
+
*/
|
|
9470
|
+
openBackgroundEventStream(request, response, authorization) {
|
|
9471
|
+
const operation = this.allocateRequest(authorization, response, {});
|
|
9472
|
+
let closed = false;
|
|
9473
|
+
let heartbeat;
|
|
9474
|
+
const close = () => {
|
|
9475
|
+
if (closed) return;
|
|
9476
|
+
closed = true;
|
|
9477
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
9478
|
+
this.taskEventListeners.delete(sendTask);
|
|
9479
|
+
const deviceListeners = this.deviceEventListeners.get(authorization.deviceId);
|
|
9480
|
+
deviceListeners?.delete(sendDevice);
|
|
9481
|
+
if (deviceListeners?.size === 0) this.deviceEventListeners.delete(authorization.deviceId);
|
|
9482
|
+
request.removeListener("aborted", close);
|
|
9483
|
+
response.removeListener("close", close);
|
|
9484
|
+
operation.release();
|
|
9485
|
+
};
|
|
9486
|
+
const sendTask = (payload) => {
|
|
9487
|
+
if (closed || response.destroyed || response.writableEnded) return;
|
|
9488
|
+
response.write(`event: task-notify\ndata: ${payload}\n\n`);
|
|
9489
|
+
};
|
|
9490
|
+
const sendDevice = (payload) => {
|
|
9491
|
+
if (closed || response.destroyed || response.writableEnded) return;
|
|
9492
|
+
response.write(`event: device-revoked\ndata: ${payload}\n\n`);
|
|
9493
|
+
};
|
|
9494
|
+
setSecurityHeaders(response, this.tlsEnabled);
|
|
9495
|
+
response.writeHead(200, {
|
|
9496
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
9497
|
+
"Cache-Control": "no-store",
|
|
9498
|
+
Connection: "keep-alive",
|
|
9499
|
+
"X-Accel-Buffering": "no"
|
|
9500
|
+
});
|
|
9501
|
+
response.write("retry: 2000\n: ready\n\n");
|
|
9502
|
+
this.taskEventListeners.add(sendTask);
|
|
9503
|
+
const deviceListeners = this.deviceEventListeners.get(authorization.deviceId) ?? /* @__PURE__ */ new Set();
|
|
9504
|
+
deviceListeners.add(sendDevice);
|
|
9505
|
+
this.deviceEventListeners.set(authorization.deviceId, deviceListeners);
|
|
9506
|
+
heartbeat = setInterval(() => {
|
|
9507
|
+
if (!closed && !response.destroyed && !response.writableEnded) response.write(": heartbeat\n\n");
|
|
9508
|
+
}, EXTENSION_EVENT_HEARTBEAT_MS);
|
|
9509
|
+
heartbeat.unref();
|
|
9510
|
+
request.once("aborted", close);
|
|
9511
|
+
response.once("close", close);
|
|
9512
|
+
}
|
|
8909
9513
|
async readUpgradeResponse(upstream, expectedAccept) {
|
|
8910
9514
|
return new Promise((resolve, reject) => {
|
|
8911
9515
|
let buffer = Buffer.alloc(0);
|
|
@@ -9186,6 +9790,9 @@ var MobileAccessGateway = class {
|
|
|
9186
9790
|
}
|
|
9187
9791
|
async performClose() {
|
|
9188
9792
|
this.closing = true;
|
|
9793
|
+
this.#catalogCache?.dispose();
|
|
9794
|
+
for (const fill of this.#catalogFills) fill.destroy(/* @__PURE__ */ new Error("gateway closing"));
|
|
9795
|
+
this.#catalogFills.clear();
|
|
9189
9796
|
const detachPushOrigin = this.detachPushOrigin;
|
|
9190
9797
|
this.detachPushOrigin = void 0;
|
|
9191
9798
|
detachPushOrigin?.();
|
|
@@ -9391,6 +9998,26 @@ var MemoryDeviceStore = class {
|
|
|
9391
9998
|
return structuredClone(this.snapshot);
|
|
9392
9999
|
}
|
|
9393
10000
|
};
|
|
10001
|
+
// src/mobile-push/service.ts
|
|
10002
|
+
/**
|
|
10003
|
+
* Small Cordis service exposed to trusted Host plugins. The provider owns the
|
|
10004
|
+
* runtime lifetime and all recipient, category, session, and expiry checks.
|
|
10005
|
+
*/
|
|
10006
|
+
var MobilePushService = class extends Service {
|
|
10007
|
+
send;
|
|
10008
|
+
constructor(ctx, send) {
|
|
10009
|
+
super(ctx, "mobilePush");
|
|
10010
|
+
this.send = send;
|
|
10011
|
+
}
|
|
10012
|
+
/**
|
|
10013
|
+
* Queue a notification for enabled web-push recipients.
|
|
10014
|
+
* @param input - Session, text, optional category and optional expiry.
|
|
10015
|
+
* @returns The number of newly queued recipient deliveries and the applied expiry.
|
|
10016
|
+
*/
|
|
10017
|
+
async notify(input) {
|
|
10018
|
+
return await this.send(input);
|
|
10019
|
+
}
|
|
10020
|
+
};
|
|
9394
10021
|
// src/managed-files.ts
|
|
9395
10022
|
/** Refuse link-shaped ancestors inside an owned state root before accessing a child. */
|
|
9396
10023
|
async function assertManagedParents(root, target, prefix) {
|
|
@@ -11907,7 +12534,7 @@ async function saveManagedSetup(setupFile, setup) {
|
|
|
11907
12534
|
}
|
|
11908
12535
|
function privateIpv4(value) {
|
|
11909
12536
|
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);
|
|
12537
|
+
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
12538
|
}
|
|
11912
12539
|
function networkCidr(address, cidr) {
|
|
11913
12540
|
const prefix = Number(cidr.slice(cidr.lastIndexOf("/") + 1));
|
|
@@ -15429,14 +16056,99 @@ const PAIRING_DESCRIPTOR_LOCAL_PORT = DEFAULT_IROH_LOCAL_PORT;
|
|
|
15429
16056
|
const READY_TIMEOUT_MS = 45e3;
|
|
15430
16057
|
const IDENTITY_TIMEOUT_MS = 15e3;
|
|
15431
16058
|
const IDENTITY_METADATA_MAX_BYTES = 8192;
|
|
16059
|
+
const IDENTITY_DIAGNOSTIC_MAX_BYTES = 4096;
|
|
15432
16060
|
const PAIRING_TIMEOUT_MS = 15e3;
|
|
15433
16061
|
const MAX_LINE_BYTES = 16384;
|
|
15434
16062
|
/** Gateway pairing windows default to five minutes; the descriptor caps at ten. */
|
|
15435
16063
|
const MAX_PAIRING_WINDOW_MS = 3e5;
|
|
15436
16064
|
const MIN_PAIRING_WINDOW_MS = 1e4;
|
|
16065
|
+
/** Safe classified failure from the short-lived endpoint identity probe. */
|
|
16066
|
+
var IrohIdentityProbeError = class extends Error {
|
|
16067
|
+
code;
|
|
16068
|
+
diagnostic;
|
|
16069
|
+
constructor(code, diagnostic) {
|
|
16070
|
+
super(code);
|
|
16071
|
+
this.code = code;
|
|
16072
|
+
this.diagnostic = diagnostic;
|
|
16073
|
+
this.name = "IrohIdentityProbeError";
|
|
16074
|
+
}
|
|
16075
|
+
};
|
|
15437
16076
|
function publicStatus$2(status) {
|
|
15438
16077
|
return Object.freeze({ ...status });
|
|
15439
16078
|
}
|
|
16079
|
+
function diagnosticText(value, dataDir) {
|
|
16080
|
+
const text = value.toString("utf8").replaceAll(dataDir, "<data-dir>").trim().replace(/\s+/gu, " ");
|
|
16081
|
+
return text === "" ? void 0 : text.slice(0, IDENTITY_DIAGNOSTIC_MAX_BYTES);
|
|
16082
|
+
}
|
|
16083
|
+
/** The wrapper `tls-prepare` prints when it discards the underlying native cause. */
|
|
16084
|
+
const HOST_TLS_PREPARATION_FAILURE = "Error: host TLS preparation failed";
|
|
16085
|
+
/**
|
|
16086
|
+
* Whether the wrapper is the only explanation stderr carries.
|
|
16087
|
+
*
|
|
16088
|
+
* Compare lines, never the whole buffer: the probe collapses whitespace for
|
|
16089
|
+
* reporting, and a preceding warning or a CRLF terminator must not hide the
|
|
16090
|
+
* fact that no cause chain was printed. A `Caused by:` section means the
|
|
16091
|
+
* native explanation already reached us, so the extra host run cannot add
|
|
16092
|
+
* anything the caller does not already have.
|
|
16093
|
+
*/
|
|
16094
|
+
function isBareHostTlsPreparationFailure(value, dataDir) {
|
|
16095
|
+
const lines = value.toString("utf8").replaceAll(dataDir, "<data-dir>").split(/\r?\n/gu).map((line) => line.trim()).filter((line) => line !== "");
|
|
16096
|
+
return lines.includes(HOST_TLS_PREPARATION_FAILURE) && !lines.some((line) => /^Caused by:/u.test(line));
|
|
16097
|
+
}
|
|
16098
|
+
/**
|
|
16099
|
+
* Ask the host entry point for the cause chain when `tls-prepare` reports only
|
|
16100
|
+
* its top-level wrapper. The command is local-only and bounded; it never
|
|
16101
|
+
* downloads a component or enables relay/discovery.
|
|
16102
|
+
*/
|
|
16103
|
+
async function hostIdentityDiagnostic(executable, dataDir, launch, onSpawn) {
|
|
16104
|
+
const args = [
|
|
16105
|
+
"--lang",
|
|
16106
|
+
"en",
|
|
16107
|
+
"host",
|
|
16108
|
+
"--data-dir",
|
|
16109
|
+
dataDir,
|
|
16110
|
+
"--local-only"
|
|
16111
|
+
];
|
|
16112
|
+
const child = launch === void 0 ? spawn(executable, args, {
|
|
16113
|
+
stdio: [
|
|
16114
|
+
"pipe",
|
|
16115
|
+
"pipe",
|
|
16116
|
+
"pipe"
|
|
16117
|
+
],
|
|
16118
|
+
windowsHide: true
|
|
16119
|
+
}) : launch(executable, args);
|
|
16120
|
+
onSpawn?.(child);
|
|
16121
|
+
let stderrOutput = Buffer.alloc(0);
|
|
16122
|
+
let settled = false;
|
|
16123
|
+
let deadline;
|
|
16124
|
+
return await new Promise((resolve) => {
|
|
16125
|
+
const finish = (value) => {
|
|
16126
|
+
if (settled) return;
|
|
16127
|
+
settled = true;
|
|
16128
|
+
if (deadline !== void 0) clearTimeout(deadline);
|
|
16129
|
+
resolve(value);
|
|
16130
|
+
};
|
|
16131
|
+
deadline = setTimeout(() => {
|
|
16132
|
+
terminateRemoteProcess(child).then(() => {
|
|
16133
|
+
finish(diagnosticText(stderrOutput, dataDir));
|
|
16134
|
+
}, () => {
|
|
16135
|
+
finish(diagnosticText(stderrOutput, dataDir));
|
|
16136
|
+
});
|
|
16137
|
+
}, 5e3);
|
|
16138
|
+
deadline.unref?.();
|
|
16139
|
+
child.stderr?.on("data", (chunk) => {
|
|
16140
|
+
const remaining = IDENTITY_DIAGNOSTIC_MAX_BYTES - stderrOutput.length;
|
|
16141
|
+
if (remaining > 0) stderrOutput = Buffer.concat([stderrOutput, chunk.subarray(0, remaining)]);
|
|
16142
|
+
});
|
|
16143
|
+
child.once("error", () => {
|
|
16144
|
+
finish(diagnosticText(stderrOutput, dataDir));
|
|
16145
|
+
});
|
|
16146
|
+
child.once("close", () => {
|
|
16147
|
+
finish(diagnosticText(stderrOutput, dataDir));
|
|
16148
|
+
});
|
|
16149
|
+
child.stdin?.end();
|
|
16150
|
+
});
|
|
16151
|
+
}
|
|
15440
16152
|
/**
|
|
15441
16153
|
* Prepare the sidecar's private identity/TLS state and resolve its endpoint.
|
|
15442
16154
|
*
|
|
@@ -15465,16 +16177,21 @@ async function defaultResolveIdentity(executable, dataDir, options = {}) {
|
|
|
15465
16177
|
}) : options.launch(executable, args);
|
|
15466
16178
|
options.onSpawn?.(child);
|
|
15467
16179
|
let output = Buffer.alloc(0);
|
|
16180
|
+
let stderrOutput = Buffer.alloc(0);
|
|
15468
16181
|
let settled = false;
|
|
16182
|
+
const diagnostic = () => {
|
|
16183
|
+
const text = stderrOutput.toString("utf8").replaceAll(dataDir, "<data-dir>").trim().replace(/\s+/gu, " ");
|
|
16184
|
+
return text === "" ? void 0 : text.slice(0, IDENTITY_DIAGNOSTIC_MAX_BYTES);
|
|
16185
|
+
};
|
|
15469
16186
|
return await new Promise((resolve, reject) => {
|
|
15470
16187
|
const fail = (code) => {
|
|
15471
16188
|
if (settled) return;
|
|
15472
16189
|
settled = true;
|
|
15473
16190
|
clearTimeout(deadline);
|
|
15474
16191
|
terminateRemoteProcess(child).then(() => {
|
|
15475
|
-
reject(new
|
|
16192
|
+
reject(new IrohIdentityProbeError(code, diagnostic()));
|
|
15476
16193
|
}, () => {
|
|
15477
|
-
reject(new AggregateError([new
|
|
16194
|
+
reject(new AggregateError([new IrohIdentityProbeError(code, diagnostic()), /* @__PURE__ */ new Error("iroh_probe_stop_failed")], "iroh_identity_cleanup_failed"));
|
|
15478
16195
|
});
|
|
15479
16196
|
};
|
|
15480
16197
|
const deadline = setTimeout(() => {
|
|
@@ -15489,7 +16206,10 @@ async function defaultResolveIdentity(executable, dataDir, options = {}) {
|
|
|
15489
16206
|
}
|
|
15490
16207
|
output = Buffer.concat([output, chunk]);
|
|
15491
16208
|
});
|
|
15492
|
-
child.stderr?.
|
|
16209
|
+
child.stderr?.on("data", (chunk) => {
|
|
16210
|
+
const remaining = IDENTITY_DIAGNOSTIC_MAX_BYTES - stderrOutput.length;
|
|
16211
|
+
if (remaining > 0) stderrOutput = Buffer.concat([stderrOutput, chunk.subarray(0, remaining)]);
|
|
16212
|
+
});
|
|
15493
16213
|
child.once("error", () => {
|
|
15494
16214
|
fail("iroh_identity_failed");
|
|
15495
16215
|
});
|
|
@@ -15497,14 +16217,28 @@ async function defaultResolveIdentity(executable, dataDir, options = {}) {
|
|
|
15497
16217
|
clearTimeout(deadline);
|
|
15498
16218
|
if (settled) return;
|
|
15499
16219
|
settled = true;
|
|
16220
|
+
const probeDiagnostic = diagnostic();
|
|
16221
|
+
if (code !== 0 && probeDiagnostic !== void 0 && isBareHostTlsPreparationFailure(stderrOutput, dataDir)) {
|
|
16222
|
+
hostIdentityDiagnostic(executable, dataDir, options.launch, options.onSpawn).then((detail) => {
|
|
16223
|
+
const resolvedDiagnostic = detail ?? probeDiagnostic;
|
|
16224
|
+
reject(new IrohIdentityProbeError(/private (?:Windows )?data directory is not secure|private path ACL grants/iu.test(resolvedDiagnostic) ? "iroh_data_directory_insecure" : "iroh_identity_invalid", resolvedDiagnostic));
|
|
16225
|
+
}, () => {
|
|
16226
|
+
reject(new IrohIdentityProbeError("iroh_identity_invalid", probeDiagnostic));
|
|
16227
|
+
});
|
|
16228
|
+
return;
|
|
16229
|
+
}
|
|
16230
|
+
if (code !== 0 && probeDiagnostic !== void 0 && /private (?:Windows )?data directory is not secure|private path ACL grants/iu.test(probeDiagnostic)) {
|
|
16231
|
+
reject(new IrohIdentityProbeError("iroh_data_directory_insecure", probeDiagnostic));
|
|
16232
|
+
return;
|
|
16233
|
+
}
|
|
15500
16234
|
let metadata;
|
|
15501
16235
|
try {
|
|
15502
16236
|
metadata = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(output));
|
|
15503
16237
|
} catch {
|
|
15504
|
-
reject(
|
|
16238
|
+
reject(new IrohIdentityProbeError("iroh_identity_invalid", probeDiagnostic));
|
|
15505
16239
|
return;
|
|
15506
16240
|
}
|
|
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(
|
|
16241
|
+
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
16242
|
else resolve(metadata.endpointId);
|
|
15509
16243
|
});
|
|
15510
16244
|
});
|
|
@@ -15691,9 +16425,15 @@ var IrohController = class {
|
|
|
15691
16425
|
this.options.onStatus?.(this.status());
|
|
15692
16426
|
} catch {}
|
|
15693
16427
|
}
|
|
15694
|
-
async failGeneration(generation, errorCode, origin) {
|
|
16428
|
+
async failGeneration(generation, errorCode, origin, diagnostic) {
|
|
15695
16429
|
if (generation !== this.generation) return;
|
|
15696
16430
|
this.generation += 1;
|
|
16431
|
+
try {
|
|
16432
|
+
this.options.onFailure?.({
|
|
16433
|
+
errorCode,
|
|
16434
|
+
...diagnostic === void 0 ? {} : { diagnostic }
|
|
16435
|
+
});
|
|
16436
|
+
} catch {}
|
|
15697
16437
|
this.publish({
|
|
15698
16438
|
enabled: this.enabled,
|
|
15699
16439
|
state: "error",
|
|
@@ -15778,7 +16518,9 @@ var IrohController = class {
|
|
|
15778
16518
|
});
|
|
15779
16519
|
} }) : await this.options.resolveIdentity(executable, this.options.dataDirectory);
|
|
15780
16520
|
} catch (error) {
|
|
15781
|
-
|
|
16521
|
+
const code = error instanceof AggregateError ? "iroh_identity_cleanup_failed" : error instanceof IrohIdentityProbeError ? error.code : "iroh_identity_failed";
|
|
16522
|
+
const diagnostic = error instanceof IrohIdentityProbeError ? error.diagnostic : error instanceof AggregateError && error.errors[0] instanceof IrohIdentityProbeError ? error.errors[0].diagnostic : void 0;
|
|
16523
|
+
await this.failGeneration(generation, code, void 0, diagnostic);
|
|
15782
16524
|
return;
|
|
15783
16525
|
}
|
|
15784
16526
|
if (generation !== this.generation || !this.enabled) return;
|
|
@@ -16394,23 +17136,36 @@ function remoteSuffix(origin) {
|
|
|
16394
17136
|
return "地址格式无效";
|
|
16395
17137
|
}
|
|
16396
17138
|
}
|
|
17139
|
+
/**
|
|
17140
|
+
* Read-only PowerShell that reports whether both managed LAN rules are usable.
|
|
17141
|
+
*
|
|
17142
|
+
* The verdict mirrors `windowsFirewallSetupScript`'s post-condition: enabled,
|
|
17143
|
+
* inbound, allow, and `Profile` exactly `Any`. A Domain/Private-scoped rule is
|
|
17144
|
+
* active only on those networks, so reporting it as ready would tell a user on
|
|
17145
|
+
* a Public network that nothing needs fixing. The script only reads: it never
|
|
17146
|
+
* enables, creates or deletes a rule, and the caller keeps its 3s budget.
|
|
17147
|
+
* @param port - managed LAN listener port the rules must cover.
|
|
17148
|
+
*/
|
|
17149
|
+
function windowsFirewallProbeScript(port) {
|
|
17150
|
+
return [
|
|
17151
|
+
"$specs = @(@{ Name = 'DSH Mobile HTTPS'; Protocol = 'TCP' }, @{ Name = 'DSH Mobile Discovery'; Protocol = 'UDP' })",
|
|
17152
|
+
"$ready = $true",
|
|
17153
|
+
"$specs | ForEach-Object {",
|
|
17154
|
+
" $spec = $_",
|
|
17155
|
+
" $rule = Get-NetFirewallRule -DisplayName $spec.Name -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' -and $_.Profile -eq 'Any' } | Select-Object -First 1",
|
|
17156
|
+
" if ($null -eq $rule) { $ready = $false; return }",
|
|
17157
|
+
" $filters = @($rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)",
|
|
17158
|
+
` $matching = @($filters | Where-Object { $_.Protocol -eq $spec.Protocol -and ($_.LocalPort -eq 'Any' -or $_.LocalPort -eq '${String(port)}') })`,
|
|
17159
|
+
" if ($matching.Count -eq 0) { $ready = $false }",
|
|
17160
|
+
"}",
|
|
17161
|
+
"if ($ready) { 'ready' } else { 'missing' }"
|
|
17162
|
+
].join("; ");
|
|
17163
|
+
}
|
|
16397
17164
|
function defaultFirewallProbe(platform = process.platform) {
|
|
16398
17165
|
return async (port) => {
|
|
16399
17166
|
if (platform !== "win32") return { state: "not-applicable" };
|
|
16400
17167
|
if (port === void 0) return { state: "unknown" };
|
|
16401
|
-
const script =
|
|
16402
|
-
"$specs = @(@{ Name = 'DSH Mobile HTTPS'; Protocol = 'TCP' }, @{ Name = 'DSH Mobile Discovery'; Protocol = 'UDP' })",
|
|
16403
|
-
"$ready = $true",
|
|
16404
|
-
"$specs | ForEach-Object {",
|
|
16405
|
-
" $spec = $_",
|
|
16406
|
-
" $rule = Get-NetFirewallRule -DisplayName $spec.Name -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' } | Select-Object -First 1",
|
|
16407
|
-
" if ($null -eq $rule) { $ready = $false; return }",
|
|
16408
|
-
" $filters = @($rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)",
|
|
16409
|
-
` $matching = @($filters | Where-Object { $_.Protocol -eq $spec.Protocol -and ($_.LocalPort -eq 'Any' -or $_.LocalPort -eq '${String(port)}') })`,
|
|
16410
|
-
" if ($matching.Count -eq 0) { $ready = $false }",
|
|
16411
|
-
"}",
|
|
16412
|
-
"if ($ready) { 'ready' } else { 'missing' }"
|
|
16413
|
-
].join("; ");
|
|
17168
|
+
const script = windowsFirewallProbeScript(port);
|
|
16414
17169
|
try {
|
|
16415
17170
|
return { state: (await execFileText("powershell.exe", [
|
|
16416
17171
|
"-NoProfile",
|
|
@@ -16787,6 +17542,116 @@ async function collectConnectionDiagnostics(snapshot, probes = {}) {
|
|
|
16787
17542
|
})
|
|
16788
17543
|
});
|
|
16789
17544
|
}
|
|
17545
|
+
// src/macos-file-access.ts
|
|
17546
|
+
/**
|
|
17547
|
+
* Reports whether this process already holds the macOS Full Disk Access grant.
|
|
17548
|
+
*
|
|
17549
|
+
* macOS shows a folder authorization dialog the first time a process reads a
|
|
17550
|
+
* protected location (Desktop, Documents, Downloads, external or network
|
|
17551
|
+
* volumes). While that dialog waits for a click, the calling thread stays
|
|
17552
|
+
* blocked, so an unattended machine can stall every request that needs the
|
|
17553
|
+
* file system. Granting Full Disk Access removes that class of dialog.
|
|
17554
|
+
*
|
|
17555
|
+
* The probe reads the per-user TCC database. That file is itself protected by
|
|
17556
|
+
* TCC, so opening it succeeds only for a process macOS already trusts with
|
|
17557
|
+
* Full Disk Access; a denied read fails immediately with EACCES/EPERM because
|
|
17558
|
+
* the grant can only be made in System Settings and can never be requested
|
|
17559
|
+
* through a dialog. The probe therefore never blocks on user input, unlike
|
|
17560
|
+
* reading one of the protected folders themselves.
|
|
17561
|
+
*/
|
|
17562
|
+
/** A final `ok` is sticky; only a non-ok result is retried, at most this often. */
|
|
17563
|
+
const DEFAULT_RETRY_MS = 2e3;
|
|
17564
|
+
/** A probe that never settles is treated as missing rather than awaited forever. */
|
|
17565
|
+
const DEFAULT_TIMEOUT_MS = 2e3;
|
|
17566
|
+
/**
|
|
17567
|
+
* Caches the Full Disk Access state so a status response can read it
|
|
17568
|
+
* synchronously while the file system check stays asynchronous.
|
|
17569
|
+
*/
|
|
17570
|
+
var MacFileAccessProbe = class {
|
|
17571
|
+
platform;
|
|
17572
|
+
home;
|
|
17573
|
+
readAccess;
|
|
17574
|
+
now;
|
|
17575
|
+
retryMs;
|
|
17576
|
+
timeoutMs;
|
|
17577
|
+
statusValue = "missing";
|
|
17578
|
+
checkedAt = Number.NEGATIVE_INFINITY;
|
|
17579
|
+
activeProbe;
|
|
17580
|
+
constructor(options = {}) {
|
|
17581
|
+
this.platform = options.platform ?? process.platform;
|
|
17582
|
+
this.home = options.home ?? homedir();
|
|
17583
|
+
this.readAccess = options.readAccess ?? ((path) => access(path, constants.R_OK));
|
|
17584
|
+
this.now = options.now ?? Date.now;
|
|
17585
|
+
this.retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
|
|
17586
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
17587
|
+
}
|
|
17588
|
+
/** Whether the probe applies to this platform at all. */
|
|
17589
|
+
get applicable() {
|
|
17590
|
+
return this.platform === "darwin";
|
|
17591
|
+
}
|
|
17592
|
+
/** Latest cached result. Never performs I/O. */
|
|
17593
|
+
status() {
|
|
17594
|
+
return this.applicable ? this.statusValue : "ok";
|
|
17595
|
+
}
|
|
17596
|
+
/**
|
|
17597
|
+
* Refresh the cached result when it is not final yet.
|
|
17598
|
+
* @returns The result after this call.
|
|
17599
|
+
*/
|
|
17600
|
+
async refresh() {
|
|
17601
|
+
if (!this.applicable) return "ok";
|
|
17602
|
+
if (this.statusValue === "ok") return "ok";
|
|
17603
|
+
if (this.activeProbe !== void 0) return this.activeProbe;
|
|
17604
|
+
const at = this.now();
|
|
17605
|
+
if (at - this.checkedAt < this.retryMs) return this.statusValue;
|
|
17606
|
+
this.checkedAt = at;
|
|
17607
|
+
const pending = this.probe();
|
|
17608
|
+
this.activeProbe = pending;
|
|
17609
|
+
try {
|
|
17610
|
+
this.statusValue = await pending;
|
|
17611
|
+
return this.statusValue;
|
|
17612
|
+
} finally {
|
|
17613
|
+
if (this.activeProbe === pending) this.activeProbe = void 0;
|
|
17614
|
+
}
|
|
17615
|
+
}
|
|
17616
|
+
/** Run one bounded access check and collapse every failure into `missing`. */
|
|
17617
|
+
async probe() {
|
|
17618
|
+
const database = join(this.home, "Library", "Application Support", "com.apple.TCC", "TCC.db");
|
|
17619
|
+
let timer;
|
|
17620
|
+
try {
|
|
17621
|
+
await Promise.race([this.readAccess(database), new Promise((_resolve, reject) => {
|
|
17622
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("macos_file_access_probe_timeout")), this.timeoutMs);
|
|
17623
|
+
timer.unref?.();
|
|
17624
|
+
})]);
|
|
17625
|
+
return "ok";
|
|
17626
|
+
} catch {
|
|
17627
|
+
return "missing";
|
|
17628
|
+
} finally {
|
|
17629
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
17630
|
+
}
|
|
17631
|
+
}
|
|
17632
|
+
};
|
|
17633
|
+
/** System Settings deep link for the Full Disk Access pane. */
|
|
17634
|
+
const MAC_FULL_DISK_ACCESS_PANE = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles";
|
|
17635
|
+
/** Absolute path so a GUI-launched process does not depend on PATH. */
|
|
17636
|
+
const OPEN_COMMAND = "/usr/bin/open";
|
|
17637
|
+
function runCommand(file, args) {
|
|
17638
|
+
return new Promise((resolve, reject) => {
|
|
17639
|
+
execFile(file, [...args], (error) => {
|
|
17640
|
+
if (error === null) resolve();
|
|
17641
|
+
else reject(error);
|
|
17642
|
+
});
|
|
17643
|
+
});
|
|
17644
|
+
}
|
|
17645
|
+
/**
|
|
17646
|
+
* Open the Full Disk Access pane from the host process.
|
|
17647
|
+
*
|
|
17648
|
+
* The desktop app's renderer refuses to navigate to a custom scheme, so the
|
|
17649
|
+
* button cannot use `window.open`; `open` runs on the Mac that owns the app.
|
|
17650
|
+
* @param run - Command runner, injectable for tests.
|
|
17651
|
+
*/
|
|
17652
|
+
async function openMacFullDiskAccessSettings(run = runCommand) {
|
|
17653
|
+
await run(OPEN_COMMAND, [MAC_FULL_DISK_ACCESS_PANE]);
|
|
17654
|
+
}
|
|
16790
17655
|
// src/mobile-guide.ts
|
|
16791
17656
|
/** Compose the guide with the current customization state injected. */
|
|
16792
17657
|
function buildMobileGuide(state) {
|
|
@@ -19926,11 +20791,33 @@ function launchedProfileName(argv) {
|
|
|
19926
20791
|
}
|
|
19927
20792
|
return "web";
|
|
19928
20793
|
}
|
|
20794
|
+
function profileInstallingModule(dshHome, moduleUrl) {
|
|
20795
|
+
let directory;
|
|
20796
|
+
try {
|
|
20797
|
+
directory = dirname(fileURLToPath(moduleUrl));
|
|
20798
|
+
} catch {
|
|
20799
|
+
return;
|
|
20800
|
+
}
|
|
20801
|
+
const profilesRoot = resolve(dshHome, "profiles");
|
|
20802
|
+
for (let depth = 0; depth < 16; depth += 1) {
|
|
20803
|
+
const candidate = resolve(directory);
|
|
20804
|
+
const fromProfiles = relative(profilesRoot, candidate);
|
|
20805
|
+
if (fromProfiles !== "" && !fromProfiles.startsWith("..") && !isAbsolute(fromProfiles)) try {
|
|
20806
|
+
const manifest = JSON.parse(readFileSync(join(candidate, "package.json"), "utf8"));
|
|
20807
|
+
if (typeof manifest.dependencies?.[PACKAGE_NAME] === "string" && typeof manifest.dsh?.profile === "object" && manifest.dsh.profile !== null) return candidate;
|
|
20808
|
+
} catch {}
|
|
20809
|
+
const parent = dirname(candidate);
|
|
20810
|
+
if (parent === candidate) break;
|
|
20811
|
+
directory = parent;
|
|
20812
|
+
}
|
|
20813
|
+
}
|
|
19929
20814
|
/** Resolve the launcher-owned Desktop profile, or the CLI profile outside Desktop. */
|
|
19930
|
-
function releaseProfileDirectory(ctx, dshHome, argv) {
|
|
20815
|
+
function releaseProfileDirectory(ctx, dshHome, argv, moduleUrl = import.meta.url) {
|
|
19931
20816
|
const desktopProfiles = ctx.get("desktopProfiles");
|
|
19932
20817
|
const desktopDirectory = desktopProfiles?.current?.dir;
|
|
19933
20818
|
if (typeof desktopDirectory === "string" && isAbsolute(desktopDirectory)) return desktopDirectory;
|
|
20819
|
+
const installedProfile = profileInstallingModule(dshHome, moduleUrl);
|
|
20820
|
+
if (installedProfile !== void 0) return installedProfile;
|
|
19934
20821
|
if (desktopProfiles !== void 0 || ctx.get("desktopRuntime") !== void 0) return void 0;
|
|
19935
20822
|
return join(dshHome, "profiles", launchedProfileName(argv));
|
|
19936
20823
|
}
|
|
@@ -21432,7 +22319,8 @@ function validSource(value) {
|
|
|
21432
22319
|
"lifetime",
|
|
21433
22320
|
"turn",
|
|
21434
22321
|
"callId",
|
|
21435
|
-
"goal"
|
|
22322
|
+
"goal",
|
|
22323
|
+
"custom"
|
|
21436
22324
|
])) return false;
|
|
21437
22325
|
return [
|
|
21438
22326
|
value.sourceId,
|
|
@@ -21449,7 +22337,7 @@ function validSource(value) {
|
|
|
21449
22337
|
"live",
|
|
21450
22338
|
"projected",
|
|
21451
22339
|
"committed"
|
|
21452
|
-
].includes(String(value.lifetime)) && (value.turn === void 0 || Number.isSafeInteger(value.turn)) && (value.callId === void 0 || typeof value.callId === "string") && (value.goal === void 0 || object(value.goal) && only(value.goal, ["id", "revision"]) && typeof value.goal.id === "string" && Number.isSafeInteger(value.goal.revision));
|
|
22340
|
+
].includes(String(value.lifetime)) && (value.turn === void 0 || Number.isSafeInteger(value.turn)) && (value.callId === void 0 || typeof value.callId === "string") && (value.goal === void 0 || object(value.goal) && only(value.goal, ["id", "revision"]) && typeof value.goal.id === "string" && Number.isSafeInteger(value.goal.revision)) && (value.custom === void 0 || object(value.custom) && only(value.custom, ["title", "body"]) && typeof value.custom.title === "string" && value.custom.title.length > 0 && value.custom.title.length <= 80 && !/[\u0000-\u001f\u007f]/u.test(value.custom.title) && typeof value.custom.body === "string" && value.custom.body.length > 0 && value.custom.body.length <= 200 && !/[\u0000-\u001f\u007f]/u.test(value.custom.body));
|
|
21453
22341
|
}
|
|
21454
22342
|
function scopedCounts(rows, scope, maximum = 1024) {
|
|
21455
22343
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -22065,10 +22953,11 @@ function validateNavigate(navigate, category) {
|
|
|
22065
22953
|
if (typeof navigate !== "string" || !/^\/\?mobile-push=[A-Za-z0-9_-]{1,256}$/u.test(navigate)) throw new TypeError("push_navigation_invalid");
|
|
22066
22954
|
}
|
|
22067
22955
|
/** Build Apple's declarative Web Push schema with generic, locale-owned text. */
|
|
22068
|
-
function buildDeclarativePayload(category, locale, navigate, tag) {
|
|
22956
|
+
function buildDeclarativePayload(category, locale, navigate, tag, custom) {
|
|
22069
22957
|
validateNavigate(navigate, category);
|
|
22070
22958
|
if (!(locale in messages) || !(category in messages[locale]) || typeof tag !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/u.test(tag)) throw new TypeError("push_payload_invalid");
|
|
22071
|
-
|
|
22959
|
+
if (custom !== void 0 && (typeof custom.title !== "string" || custom.title.length === 0 || custom.title.length > 80 || typeof custom.body !== "string" || custom.body.length === 0 || custom.body.length > 200)) throw new TypeError("push_payload_invalid");
|
|
22960
|
+
const [title, body] = custom === void 0 ? messages[locale][category] : [custom.title, custom.body];
|
|
22072
22961
|
const payload = JSON.stringify({
|
|
22073
22962
|
web_push: 8030,
|
|
22074
22963
|
notification: {
|
|
@@ -22408,6 +23297,12 @@ function sameKey(a, b) {
|
|
|
22408
23297
|
function isPreferenceEnabled(preferences, category) {
|
|
22409
23298
|
return preferences[category] === true;
|
|
22410
23299
|
}
|
|
23300
|
+
function notificationText(value, maximum, label) {
|
|
23301
|
+
if (typeof value !== "string") throw new TypeError(`push_${label}_invalid`);
|
|
23302
|
+
const text = value.trim().replace(/[\u0000-\u001f\u007f]+/gu, " ");
|
|
23303
|
+
if (text.length === 0 || text.length > maximum) throw new TypeError(`push_${label}_invalid`);
|
|
23304
|
+
return text;
|
|
23305
|
+
}
|
|
22411
23306
|
function pendingHas(snapshot, intent) {
|
|
22412
23307
|
return snapshot.pending.some((item) => item.id === intent.id && sameKey(item.key, intent.key));
|
|
22413
23308
|
}
|
|
@@ -22521,6 +23416,51 @@ var PushDelivery = class {
|
|
|
22521
23416
|
this.#kick();
|
|
22522
23417
|
});
|
|
22523
23418
|
}
|
|
23419
|
+
/**
|
|
23420
|
+
* Queue trusted Host-plugin text for every enabled web-push recipient that
|
|
23421
|
+
* opted into the selected notification category. The source keeps its own
|
|
23422
|
+
* short expiry; a restart may still retry it only inside that window.
|
|
23423
|
+
*/
|
|
23424
|
+
async notifyCustom(input) {
|
|
23425
|
+
if (!this.#started || this.#closed || !this.#admitting) throw new Error("push_unavailable");
|
|
23426
|
+
const sessionId = typeof input.sessionId === "string" ? input.sessionId.trim() : "";
|
|
23427
|
+
if (sessionId === "" || sessionId.length > 4096) throw new TypeError("push_session_id_invalid");
|
|
23428
|
+
const title = notificationText(input.title, 80, "title");
|
|
23429
|
+
const body = notificationText(input.body, 200, "body");
|
|
23430
|
+
const category = input.category ?? "response";
|
|
23431
|
+
if (![
|
|
23432
|
+
"question",
|
|
23433
|
+
"plan",
|
|
23434
|
+
"approval",
|
|
23435
|
+
"response",
|
|
23436
|
+
"goal",
|
|
23437
|
+
"failure"
|
|
23438
|
+
].includes(category)) throw new TypeError("push_category_invalid");
|
|
23439
|
+
const now = this.#now();
|
|
23440
|
+
const maximumExpiry = now + OUTCOME_TTL_MS;
|
|
23441
|
+
if (input.expiresAt !== void 0 && (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= now)) throw new TypeError("push_expiry_invalid");
|
|
23442
|
+
const expiresAt = Math.min(input.expiresAt ?? maximumExpiry, maximumExpiry);
|
|
23443
|
+
if (!await this.#host.sessionExists(sessionId)) throw new Error("push_session_unavailable");
|
|
23444
|
+
const source = {
|
|
23445
|
+
sourceId: `custom:${randomUUID()}`,
|
|
23446
|
+
sessionId,
|
|
23447
|
+
targetSessionId: sessionId,
|
|
23448
|
+
category,
|
|
23449
|
+
createdAt: now,
|
|
23450
|
+
expiresAt,
|
|
23451
|
+
lifetime: "committed",
|
|
23452
|
+
custom: {
|
|
23453
|
+
title,
|
|
23454
|
+
body
|
|
23455
|
+
}
|
|
23456
|
+
};
|
|
23457
|
+
const queued = await this.#admit(source);
|
|
23458
|
+
this.#kick();
|
|
23459
|
+
return {
|
|
23460
|
+
queued,
|
|
23461
|
+
expiresAt
|
|
23462
|
+
};
|
|
23463
|
+
}
|
|
22524
23464
|
async resolve(owner, reference) {
|
|
22525
23465
|
if (this.#closed || !reference) return void 0;
|
|
22526
23466
|
let snapshot;
|
|
@@ -22560,14 +23500,15 @@ var PushDelivery = class {
|
|
|
22560
23500
|
this.#origins.clear();
|
|
22561
23501
|
}
|
|
22562
23502
|
async #admit(source) {
|
|
22563
|
-
if (!this.#admitting || source.expiresAt <= this.#now()) return;
|
|
23503
|
+
if (!this.#admitting || source.expiresAt <= this.#now()) return 0;
|
|
22564
23504
|
let eligible = false;
|
|
22565
23505
|
try {
|
|
22566
|
-
eligible = this.#host.available && await this.#host.eligible(source);
|
|
23506
|
+
eligible = source.custom === void 0 ? this.#host.available && await this.#host.eligible(source) : this.#host.available && await this.#host.sessionExists(source.sessionId);
|
|
22567
23507
|
} catch {
|
|
22568
23508
|
eligible = false;
|
|
22569
23509
|
}
|
|
22570
|
-
if (!eligible || source.expiresAt <= this.#now()) return;
|
|
23510
|
+
if (!eligible || source.expiresAt <= this.#now()) return 0;
|
|
23511
|
+
let queued = 0;
|
|
22571
23512
|
for (const origin of this.#origins.values()) {
|
|
22572
23513
|
if (!origin.active || this.#closed) continue;
|
|
22573
23514
|
const devices = await this.#readAuthorized(origin);
|
|
@@ -22576,7 +23517,7 @@ var PushDelivery = class {
|
|
|
22576
23517
|
try {
|
|
22577
23518
|
snapshot = this.#registry.snapshot();
|
|
22578
23519
|
} catch {
|
|
22579
|
-
return;
|
|
23520
|
+
return queued;
|
|
22580
23521
|
}
|
|
22581
23522
|
const registrations = snapshot.registrations.filter((reg) => reg.scopeId === origin.scopeId && reg.origin === origin.origin && devices.has(reg.deviceId) && reg.enabled && reg.health === "ready" && isPreferenceEnabled(reg.preferences, source.category) && snapshot.keys?.fingerprint === reg.keyFingerprint);
|
|
22582
23523
|
if (registrations.length === 0) continue;
|
|
@@ -22605,6 +23546,7 @@ var PushDelivery = class {
|
|
|
22605
23546
|
attempts: 0,
|
|
22606
23547
|
nextAttemptAt: this.#now()
|
|
22607
23548
|
});
|
|
23549
|
+
queued += 1;
|
|
22608
23550
|
}
|
|
22609
23551
|
return {
|
|
22610
23552
|
...clean,
|
|
@@ -22613,6 +23555,7 @@ var PushDelivery = class {
|
|
|
22613
23555
|
});
|
|
22614
23556
|
}
|
|
22615
23557
|
this.#kick();
|
|
23558
|
+
return queued;
|
|
22616
23559
|
}
|
|
22617
23560
|
async #reconcileKnownScopes() {
|
|
22618
23561
|
let snapshot;
|
|
@@ -22797,7 +23740,7 @@ var PushDelivery = class {
|
|
|
22797
23740
|
await this.#removeIntent(intent);
|
|
22798
23741
|
return;
|
|
22799
23742
|
}
|
|
22800
|
-
const payload = buildDeclarativePayload(intent.source.category, registration.locale, `/?mobile-push=${reference}`, intent.source.sourceId.replace(/[^A-Za-z0-9._:-]/gu, "_").slice(0, 128) || "push");
|
|
23743
|
+
const payload = buildDeclarativePayload(intent.source.category, registration.locale, `/?mobile-push=${reference}`, intent.source.sourceId.replace(/[^A-Za-z0-9._:-]/gu, "_").slice(0, 128) || "push", intent.source.custom);
|
|
22801
23744
|
let result;
|
|
22802
23745
|
try {
|
|
22803
23746
|
result = await this.#sender.send({
|
|
@@ -22907,6 +23850,11 @@ var PushDelivery = class {
|
|
|
22907
23850
|
/** Maps the Host tri-state for durable-work decisions; boolean eligible() would collapse transient uncertainty into denial. */
|
|
22908
23851
|
async #sourceGuard(source) {
|
|
22909
23852
|
if (!this.#host.available) return "unverifiable";
|
|
23853
|
+
if (source.custom !== void 0) try {
|
|
23854
|
+
return await this.#host.sessionExists(source.sessionId) ? "allowed" : "denied";
|
|
23855
|
+
} catch {
|
|
23856
|
+
return "unverifiable";
|
|
23857
|
+
}
|
|
22910
23858
|
let result;
|
|
22911
23859
|
try {
|
|
22912
23860
|
result = await this.#host.reconcile(source);
|
|
@@ -23260,6 +24208,10 @@ var PushGatewayCoordinator = class {
|
|
|
23260
24208
|
installationId
|
|
23261
24209
|
});
|
|
23262
24210
|
}
|
|
24211
|
+
async notifyCustom(input) {
|
|
24212
|
+
if (!this.host.available) throw new Error("push_unavailable");
|
|
24213
|
+
return await this.delivery.notifyCustom(input);
|
|
24214
|
+
}
|
|
23263
24215
|
async resolve(owner, reference) {
|
|
23264
24216
|
if (!this.host.available || !await this.authorize(owner, true)) return void 0;
|
|
23265
24217
|
return this.delivery.resolve(owner, reference);
|
|
@@ -23377,34 +24329,132 @@ function withInstallationIdentity(config, installationId, explicitIdentity) {
|
|
|
23377
24329
|
// src/lan-setup.ts
|
|
23378
24330
|
const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
|
|
23379
24331
|
const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
|
|
24332
|
+
const MIN_MANAGED_PORT = 1024;
|
|
24333
|
+
const MAX_MANAGED_PORT = 65535;
|
|
24334
|
+
function assertManagedPort(port, name) {
|
|
24335
|
+
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)}`);
|
|
24336
|
+
}
|
|
24337
|
+
/** Longest failure detail carried in the thrown message. */
|
|
24338
|
+
const MAX_FAILURE_DETAIL_CHARS = 512;
|
|
24339
|
+
/**
|
|
24340
|
+
* Strip the elevated invocation and filesystem locations from PowerShell text.
|
|
24341
|
+
*
|
|
24342
|
+
* The launcher's stderr quotes the command it ran, including the base64
|
|
24343
|
+
* `-EncodedCommand` payload, and the elevated script quotes its own scratch
|
|
24344
|
+
* path. Neither helps an operator, and both can push the one useful line past
|
|
24345
|
+
* the length bound, so they are replaced before the detail is bounded.
|
|
24346
|
+
*/
|
|
24347
|
+
function redactPowerShellDetail(value) {
|
|
24348
|
+
return value.replace(/EncodedCommand\s+\S+/giu, "EncodedCommand <redacted>").replace(/\b[A-Za-z0-9+/]{64,}={0,2}\b/gu, "<redacted>").replace(/(?<![A-Za-z])[A-Za-z]:[\\/][^\s'"]*/gu, "<path>");
|
|
24349
|
+
}
|
|
24350
|
+
/** Bounded, redacted detail for a failed elevated Windows operation. */
|
|
24351
|
+
function powerShellFailureDetail(error, innerError = "") {
|
|
24352
|
+
if (!(error instanceof Error)) return "unknown failure";
|
|
24353
|
+
const code = "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? ` (${String(error.code)})` : "";
|
|
24354
|
+
const detail = redactPowerShellDetail(innerError.trim() === "" && "stderr" in error && typeof error.stderr === "string" ? error.stderr : innerError).trim().replace(/\s+/gu, " ").slice(0, MAX_FAILURE_DETAIL_CHARS);
|
|
24355
|
+
return `${code}${detail === "" ? "" : `: ${detail}`}`;
|
|
24356
|
+
}
|
|
24357
|
+
/** Default sink for a scratch directory that could not be removed. */
|
|
24358
|
+
function warnFirewallCleanupFailure(detail) {
|
|
24359
|
+
process.emitWarning(`Windows firewall temporary cleanup failed (${detail})`, { code: "DSH_MOBILE_FIREWALL_CLEANUP_FAILED" });
|
|
24360
|
+
}
|
|
24361
|
+
/**
|
|
24362
|
+
* Remove the elevated helper's scratch directory without touching the run's outcome.
|
|
24363
|
+
*
|
|
24364
|
+
* The directory is created before elevation and removed in a `finally`, so a
|
|
24365
|
+
* cleanup failure must not replace the firewall error that explains the
|
|
24366
|
+
* failure, and must not fail a run that already succeeded. It stays observable
|
|
24367
|
+
* instead: the removal error's code is reported to the warning sink, whose own
|
|
24368
|
+
* failure is ignored for the same reason.
|
|
24369
|
+
* @param directory - scratch path created for this run.
|
|
24370
|
+
* @param options - injectable removal and warning sink for tests.
|
|
24371
|
+
*/
|
|
24372
|
+
async function discardFirewallScratch(directory, options = {}) {
|
|
24373
|
+
const remove = options.remove ?? ((path) => rm(path, {
|
|
24374
|
+
recursive: true,
|
|
24375
|
+
force: true
|
|
24376
|
+
}));
|
|
24377
|
+
try {
|
|
24378
|
+
await remove(directory);
|
|
24379
|
+
} catch (error) {
|
|
24380
|
+
const code = error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "unknown";
|
|
24381
|
+
const warn = options.warn ?? warnFirewallCleanupFailure;
|
|
24382
|
+
try {
|
|
24383
|
+
warn(code);
|
|
24384
|
+
} catch {}
|
|
24385
|
+
}
|
|
24386
|
+
}
|
|
23380
24387
|
async function runElevatedPowerShell(script) {
|
|
23381
|
-
await
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
"
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
24388
|
+
const temporary = await mkdtemp(join(tmpdir(), "dsh-mobile-firewall-"));
|
|
24389
|
+
const errorFile = join(temporary, "error.log");
|
|
24390
|
+
const wrapped = [
|
|
24391
|
+
`$__dshMobileErrorFile = '${errorFile.replaceAll("'", "''")}'`,
|
|
24392
|
+
"try {",
|
|
24393
|
+
script,
|
|
24394
|
+
"} catch {",
|
|
24395
|
+
" try { [IO.File]::WriteAllText($__dshMobileErrorFile, ($_ | Out-String)) } catch {}",
|
|
24396
|
+
" throw",
|
|
24397
|
+
"}"
|
|
24398
|
+
].join("\n");
|
|
24399
|
+
const launch = [
|
|
24400
|
+
"$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
|
|
24401
|
+
` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(wrapped, "utf16le").toString("base64")}')`,
|
|
24402
|
+
"; exit $process.ExitCode"
|
|
24403
|
+
].join(" ");
|
|
24404
|
+
try {
|
|
24405
|
+
await execFileText("powershell.exe", [
|
|
24406
|
+
"-NoProfile",
|
|
24407
|
+
"-NonInteractive",
|
|
24408
|
+
"-Command",
|
|
24409
|
+
launch
|
|
24410
|
+
], { windowsHide: true });
|
|
24411
|
+
} catch (error) {
|
|
24412
|
+
let innerError = "";
|
|
24413
|
+
try {
|
|
24414
|
+
innerError = (await readFile(errorFile, "utf8")).replace(/^\uFEFF/u, "");
|
|
24415
|
+
} catch {}
|
|
24416
|
+
throw new Error(`Windows firewall operation failed${powerShellFailureDetail(error, innerError)}`, { cause: error });
|
|
24417
|
+
} finally {
|
|
24418
|
+
await discardFirewallScratch(temporary);
|
|
24419
|
+
}
|
|
24420
|
+
}
|
|
24421
|
+
function firewallRuleScript(name, protocol, port) {
|
|
24422
|
+
return [
|
|
24423
|
+
`$rules = @(Get-NetFirewallRule -DisplayName '${name}' -ErrorAction SilentlyContinue)`,
|
|
24424
|
+
"if ($rules.Count -eq 0) {",
|
|
24425
|
+
` New-NetFirewallRule -DisplayName '${name}' -Direction Inbound -Action Allow -Protocol ${protocol} -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
|
|
24426
|
+
"} else {",
|
|
24427
|
+
" foreach ($rule in $rules) {",
|
|
24428
|
+
" $rule | Set-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -Profile Any",
|
|
24429
|
+
` $rule | Get-NetFirewallPortFilter | Set-NetFirewallPortFilter -Protocol ${protocol} -LocalPort ${String(port)}`,
|
|
24430
|
+
" $rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter -RemoteAddress LocalSubnet",
|
|
24431
|
+
" }",
|
|
24432
|
+
"}",
|
|
24433
|
+
`$verified = @(Get-NetFirewallRule -DisplayName '${name}' -ErrorAction Stop | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' -and $_.Profile -eq 'Any' })`,
|
|
24434
|
+
`$matchingPort = @($verified | Get-NetFirewallPortFilter | Where-Object { $_.Protocol -eq '${protocol}' -and $_.LocalPort -contains '${String(port)}' })`,
|
|
24435
|
+
`$matchingAddress = @($verified | Get-NetFirewallAddressFilter | Where-Object { $_.RemoteAddress -contains 'LocalSubnet' })`,
|
|
24436
|
+
`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" }`
|
|
24437
|
+
];
|
|
24438
|
+
}
|
|
24439
|
+
/** Build the elevated Windows firewall script without removing a working rule first. */
|
|
24440
|
+
function windowsFirewallSetupScript(port) {
|
|
24441
|
+
assertManagedPort(port, "Windows firewall port");
|
|
24442
|
+
return [
|
|
24443
|
+
"$ErrorActionPreference = 'Stop'",
|
|
24444
|
+
...firewallRuleScript(FIREWALL_TCP_RULE, "TCP", port),
|
|
24445
|
+
...firewallRuleScript(FIREWALL_UDP_RULE, "UDP", port)
|
|
24446
|
+
].join("\n");
|
|
24447
|
+
}
|
|
24448
|
+
/** Install or update the two Windows inbound rules used by the managed LAN listener. */
|
|
23393
24449
|
async function configureWindowsFirewall(port) {
|
|
23394
24450
|
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("; "));
|
|
24451
|
+
await runElevatedPowerShell(windowsFirewallSetupScript(port));
|
|
23402
24452
|
}
|
|
23403
24453
|
/** Generate private TLS material and persist a managed setup selected by the user. */
|
|
23404
24454
|
async function prepareManagedLanSetup(options) {
|
|
23405
24455
|
if (!isAbsolute(options.setupFile) || !isAbsolute(options.controlFile)) throw new Error("managed LAN setup paths must be absolute");
|
|
23406
|
-
|
|
23407
|
-
|
|
24456
|
+
assertManagedPort(options.listenPort, "managed LAN listenPort");
|
|
24457
|
+
assertManagedPort(options.dshPort, "managed LAN dshPort");
|
|
23408
24458
|
const directory = dirname(options.setupFile);
|
|
23409
24459
|
const tlsDirectory = join(directory, "tls");
|
|
23410
24460
|
await mkdir(tlsDirectory, {
|
|
@@ -23876,6 +24926,7 @@ async function apply(ctx, config) {
|
|
|
23876
24926
|
if (service === void 0) throw new Error("push_unavailable");
|
|
23877
24927
|
return service;
|
|
23878
24928
|
};
|
|
24929
|
+
new MobilePushService(ctx, async (input) => (await requiredPushGatewayService()).notifyCustom(input));
|
|
23879
24930
|
const queueDeviceInvalidations = (scopeId, deviceIds) => {
|
|
23880
24931
|
let queued = pendingDeviceInvalidations.get(scopeId);
|
|
23881
24932
|
if (queued === void 0) pendingDeviceInvalidations.set(scopeId, queued = /* @__PURE__ */ new Set());
|
|
@@ -23950,6 +25001,11 @@ async function apply(ctx, config) {
|
|
|
23950
25001
|
async test(owner, installationId) {
|
|
23951
25002
|
await (await requiredPushGatewayService()).test(owner, installationId);
|
|
23952
25003
|
},
|
|
25004
|
+
async notifyCustom(input) {
|
|
25005
|
+
const service = await requiredPushGatewayService();
|
|
25006
|
+
if (service.notifyCustom === void 0) throw new Error("push_unavailable");
|
|
25007
|
+
return await service.notifyCustom(input);
|
|
25008
|
+
},
|
|
23953
25009
|
async resolve(owner, reference) {
|
|
23954
25010
|
return (await requiredPushGatewayService()).resolve(owner, reference);
|
|
23955
25011
|
}
|
|
@@ -24042,6 +25098,7 @@ async function apply(ctx, config) {
|
|
|
24042
25098
|
const cloudflaredComponent = new CloudflaredComponentManager({ stateDirectory });
|
|
24043
25099
|
await cloudflaredComponent.initialize();
|
|
24044
25100
|
const frpComponent = new FrpComponentManager({ stateDirectory });
|
|
25101
|
+
const macFileAccess = new MacFileAccessProbe();
|
|
24045
25102
|
await frpComponent.initialize();
|
|
24046
25103
|
const irohComponent = new IrohComponentManager({
|
|
24047
25104
|
stateDirectory,
|
|
@@ -24301,6 +25358,9 @@ async function apply(ctx, config) {
|
|
|
24301
25358
|
component: irohComponent,
|
|
24302
25359
|
dataDirectory: join(remoteDirectory, "iroh", "sidecar"),
|
|
24303
25360
|
createGateway: (endpointId) => createIrohGateway(endpointId),
|
|
25361
|
+
onFailure: ({ errorCode, diagnostic }) => {
|
|
25362
|
+
logger.warn("iroh startup failed: %s%s", errorCode, diagnostic === void 0 ? "" : `; ${diagnostic}`);
|
|
25363
|
+
},
|
|
24304
25364
|
/**
|
|
24305
25365
|
* Read the pairing CA the live gateway advertises. The fingerprint must
|
|
24306
25366
|
* match the gateway instance id, otherwise a rotated CA would mint QR
|
|
@@ -24389,7 +25449,10 @@ async function apply(ctx, config) {
|
|
|
24389
25449
|
originMode: originController.mode(),
|
|
24390
25450
|
caddyConfiguration: caddyConfig.status(),
|
|
24391
25451
|
caddyState: caddyController.status(),
|
|
24392
|
-
...
|
|
25452
|
+
...macFileAccess.applicable ? {
|
|
25453
|
+
macosHost: true,
|
|
25454
|
+
macFileAccess: macFileAccess.status()
|
|
25455
|
+
} : {}
|
|
24393
25456
|
});
|
|
24394
25457
|
const lanPayload = () => ({
|
|
24395
25458
|
...loaded.kind === "unconfigured" ? {
|
|
@@ -24713,9 +25776,16 @@ async function apply(ctx, config) {
|
|
|
24713
25776
|
return;
|
|
24714
25777
|
}
|
|
24715
25778
|
if (request.method === "GET" && target.decodedPathname === `/api/mobile-access/remote/control`) {
|
|
25779
|
+
await macFileAccess.refresh();
|
|
24716
25780
|
sendJson(response, 200, remotePayload(), false);
|
|
24717
25781
|
return;
|
|
24718
25782
|
}
|
|
25783
|
+
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/macos-file-access/open-settings`) {
|
|
25784
|
+
if (!macFileAccess.applicable) throw new HttpError(404, "not_found");
|
|
25785
|
+
await openMacFullDiskAccessSettings();
|
|
25786
|
+
sendJson(response, 200, { opened: true }, false);
|
|
25787
|
+
return;
|
|
25788
|
+
}
|
|
24719
25789
|
if (request.method === "POST" && target.decodedPathname === `/api/mobile-access/remote/provider`) {
|
|
24720
25790
|
const body = await readJsonObject(request, 4096);
|
|
24721
25791
|
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");
|
|
@@ -25271,6 +26341,6 @@ async function apply(ctx, config) {
|
|
|
25271
26341
|
};
|
|
25272
26342
|
}, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
|
|
25273
26343
|
}
|
|
25274
|
-
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CPOLAR_COMPONENT_RELEASE, CPOLAR_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, ClientModuleConflictError, ClientModulePreferenceStore, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, CpolarComponentManager, DEFAULT_IROH_LOCAL_PORT, 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, IROH_COMPONENT_RELEASES, IROH_LEAF_RENEW_BEFORE_DAYS, IROH_LEAF_VALIDITY_DAYS, IROH_VERSION, IrohComponentManager, IrohController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_PAIRING_DESCRIPTOR_BYTES, MAX_PAIRING_LIFETIME_MS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, OriginConfigStore, OriginController, PAIRING_DESCRIPTOR_LOCAL_PORT, PAIRING_DESCRIPTOR_TYPE, PAIRING_DESCRIPTOR_VERSION, 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, decodePairingDescriptor, encodePairingDescriptor, ensureFrpIngressCertificate, ensureIrohTls, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, irohGatewayConfig, irohHostname, irohOrigin, irohTlsPaths, 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, validateIrohOrigin, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validatePairingDescriptor, validateWebSocketPath, watchTaskCompletions };
|
|
26344
|
+
export { AUTH_PREFIX, AccessController, AccessError, BlockedUpgradePathLog, BoundedRateLimiter, CERT_EXPIRING_DAYS, CLOUDFLARED_COMPONENT_RELEASE, CLOUDFLARED_COMPONENT_RELEASES, CPOLAR_COMPONENT_RELEASE, CPOLAR_COMPONENT_RELEASES, CSRF_COOKIE, CSRF_HEADER, ClientModuleConflictError, ClientModulePreferenceStore, CloudflaredComponentManager, CloudflaredController, CloudflaredTunnelStore, Config, CpolarComponentManager, DEFAULT_IROH_LOCAL_PORT, 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, IROH_COMPONENT_RELEASES, IROH_LEAF_RENEW_BEFORE_DAYS, IROH_LEAF_VALIDITY_DAYS, IROH_VERSION, IrohComponentManager, IrohController, JsonDeviceStore, JsonMobileAccessControlStore, JsonRemoteProviderStore, LOCAL_ADMIN_PREFIX, MAX_BLOCKED_UPGRADE_PATHS, MAX_EXTRA_WEBSOCKET_PATHS, MAX_PAIRING_DESCRIPTOR_BYTES, MAX_PAIRING_LIFETIME_MS, MAX_WEBSOCKET_PATH_LENGTH, MemoryDeviceStore, MobileAccessGateway, MobileAccessGatewayController, MobileAccessService, MobileExtensionError, MobilePushService, OriginConfigStore, OriginController, PAIRING_DESCRIPTOR_LOCAL_PORT, PAIRING_DESCRIPTOR_TYPE, PAIRING_DESCRIPTOR_VERSION, 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, decodePairingDescriptor, encodePairingDescriptor, ensureFrpIngressCertificate, ensureIrohTls, evaluateCertificateLifetime, frpAttachVpsParts, frpIngressPaths, frpIngressSelfCheck, frpProxyName, inject, irohGatewayConfig, irohHostname, irohOrigin, irohTlsPaths, 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, validateIrohOrigin, validateOriginAllowedCidrs, validateOriginListenHost, validateOriginListenPort, validateOriginPublicOrigin, validatePairingDescriptor, validateWebSocketPath, watchTaskCompletions };
|
|
25275
26345
|
|
|
25276
26346
|
//# sourceMappingURL=index.mjs.map
|