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/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 ?? 0) + MOBILE_BOOT_SEPARATOR_BYTES;
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 (replayable) upstreamRequest.end();
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 Error(code));
16192
+ reject(new IrohIdentityProbeError(code, diagnostic()));
15476
16193
  }, () => {
15477
- reject(new AggregateError([new Error(code), /* @__PURE__ */ new Error("iroh_probe_stop_failed")], "iroh_identity_cleanup_failed"));
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?.resume();
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(/* @__PURE__ */ new Error("iroh_identity_invalid"));
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(/* @__PURE__ */ new Error("iroh_identity_invalid"));
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
- await this.failGeneration(generation, error instanceof AggregateError ? "iroh_identity_cleanup_failed" : "iroh_identity_failed");
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
- const [title, body] = messages[locale][category];
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 execFileText("powershell.exe", [
23382
- "-NoProfile",
23383
- "-NonInteractive",
23384
- "-Command",
23385
- [
23386
- "$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
23387
- ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
23388
- "; exit $process.ExitCode"
23389
- ].join(" ")
23390
- ], { windowsHide: true });
23391
- }
23392
- /** Install the two Windows inbound rules used by the managed LAN listener. */
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
- if (!Number.isSafeInteger(options.listenPort) || options.listenPort < 1024 || options.listenPort > 65535) throw new Error("managed LAN listenPort must be from 1024 through 65535");
23407
- if (!Number.isSafeInteger(options.dshPort) || options.dshPort < 1024 || options.dshPort > 65535) throw new Error("managed LAN dshPort must be from 1024 through 65535");
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
- ...process.platform === "darwin" ? { macosHost: true } : {}
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