dsh-mobile 0.8.5 → 0.8.7

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
@@ -1210,6 +1210,39 @@ function parseGatewayConfig(raw) {
1210
1210
  });
1211
1211
  }
1212
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
+ }
1213
1246
  /** Capture both output streams even when a desktop host wraps execFile without Node's promisify metadata. */
1214
1247
  function execFileText(file, args, options = {}) {
1215
1248
  return new Promise((resolve, reject) => {
@@ -1219,6 +1252,7 @@ function execFileText(file, args, options = {}) {
1219
1252
  encoding: "utf8"
1220
1253
  }, (error, stdout, stderr) => {
1221
1254
  if (error !== null) {
1255
+ attachCapturedStreams(error, stdout, stderr);
1222
1256
  reject(error);
1223
1257
  return;
1224
1258
  }
@@ -2122,6 +2156,8 @@ function requestPathCategory(path) {
2122
2156
  case "/mobile-access/events": return path;
2123
2157
  default:
2124
2158
  if (path.startsWith("/api/")) return "/api/*";
2159
+ if (path.startsWith("/plugins/")) return "/plugins/*";
2160
+ if (path.startsWith("/assets/")) return "/assets/*";
2125
2161
  if (path.startsWith("/mobile-access/extensions/")) return "/mobile-access/extensions/*";
2126
2162
  if (/^\/mobile-access\/compat-[a-f\d]{64}\.js$/u.test(path)) return "/mobile-access/compat.js";
2127
2163
  if (path.startsWith("/mobile-access/")) return "/mobile-access/*";
@@ -3244,7 +3280,7 @@ function forwardableClose(code) {
3244
3280
  * @param upstreamCookie - gateway's signed upstream cookie; browser cookies are discarded.
3245
3281
  * @param options - resolved message, queue, and compression limits.
3246
3282
  * @param handshakeTimeoutMs - maximum handshake and close-handshake duration.
3247
- * @returns fulfillment after both HTTP upgrades are accepted, or rejection before acceptance.
3283
+ * @returns the close handle after both HTTP upgrades are accepted, or rejection before acceptance.
3248
3284
  */
3249
3285
  function bridgeCompressedWebSocket(request, client, upstream, head, target, upstreamCookie, options, handshakeTimeoutMs) {
3250
3286
  return new Promise((resolve, reject) => {
@@ -3433,7 +3469,7 @@ function bridgeCompressedWebSocket(request, client, upstream, head, target, upst
3433
3469
  accepted = true;
3434
3470
  clearTimeout(handshakeTimer);
3435
3471
  incoming.resume();
3436
- resolve();
3472
+ resolve({ close: beginClose });
3437
3473
  });
3438
3474
  } catch (error) {
3439
3475
  stop(error instanceof Error ? error : /* @__PURE__ */ new Error("Client WebSocket upgrade failed"));
@@ -5703,6 +5739,7 @@ const MOBILE_PUSH_PREFERENCES_PATH = `${AUTH_PREFIX}/push/preferences`;
5703
5739
  const MOBILE_PUSH_DISABLE_PATH = `${AUTH_PREFIX}/push/disable`;
5704
5740
  const MOBILE_PUSH_TEST_PATH = `${AUTH_PREFIX}/push/test`;
5705
5741
  const MOBILE_PUSH_RESOLVE_PATH = `${AUTH_PREFIX}/push/resolve`;
5742
+ const BACKGROUND_EVENT_PATH = `${AUTH_PREFIX}/background-events`;
5706
5743
  const MOBILE_PUSH_PREFERENCE_KEYS = [
5707
5744
  "question",
5708
5745
  "plan",
@@ -6448,6 +6485,12 @@ function sanitizeRequestHeaders(request, upstream) {
6448
6485
  const value = request.headers[name];
6449
6486
  if (value !== void 0) headers[name] = value;
6450
6487
  }
6488
+ for (const name of Object.keys(request.headers)) {
6489
+ const lower = name.toLowerCase();
6490
+ if (!lower.startsWith("x-dsh-") || lower.startsWith("x-dsh-mobile-") || lower === "x-dsh-auth-token") continue;
6491
+ const value = request.headers[name];
6492
+ if (value !== void 0) headers[name] = Array.isArray(value) ? value[0] : value;
6493
+ }
6451
6494
  return headers;
6452
6495
  }
6453
6496
  const BLOCKED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
@@ -7601,6 +7644,7 @@ var MobileAccessGateway = class {
7601
7644
  const computerImage = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-image`;
7602
7645
  const workspaceDirectories = (request.method === "GET" || request.method === "POST") && target.decodedPathname === `/mobile-access/workspace-directories`;
7603
7646
  const clientModules = target.decodedPathname === `${AUTH_PREFIX}/client-modules`;
7647
+ const backgroundEvents = target.decodedPathname === BACKGROUND_EVENT_PATH;
7604
7648
  const mobilePushRoute = [
7605
7649
  MOBILE_PUSH_CONFIG_PATH,
7606
7650
  MOBILE_PUSH_SUBSCRIPTION_PATH,
@@ -7634,7 +7678,7 @@ var MobileAccessGateway = class {
7634
7678
  fallback: void 0,
7635
7679
  digest: requestedCompatRevision
7636
7680
  } : void 0;
7637
- 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");
7681
+ 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");
7638
7682
  if (![
7639
7683
  "GET",
7640
7684
  "HEAD",
@@ -7675,6 +7719,11 @@ var MobileAccessGateway = class {
7675
7719
  await this.handleMobilePushRequest(target, request, response, authorization);
7676
7720
  return;
7677
7721
  }
7722
+ if (backgroundEvents) {
7723
+ if (request.method !== "GET" || target.search !== "") throw new HttpError(request.method === "GET" ? 400 : 405, request.method === "GET" ? "bad_request" : "method_not_allowed");
7724
+ this.openBackgroundEventStream(request, response, authorization);
7725
+ return;
7726
+ }
7678
7727
  if (clientModules) {
7679
7728
  if (target.search !== "") throw new HttpError(400, "bad_request");
7680
7729
  if (request.method !== "GET" && request.method !== "POST") throw new HttpError(405, "method_not_allowed");
@@ -9415,6 +9464,54 @@ var MobileAccessGateway = class {
9415
9464
  request.once("aborted", close);
9416
9465
  response.once("close", close);
9417
9466
  }
9467
+ /**
9468
+ * Native background notifications need only Host task and revocation events.
9469
+ * They use the paired session cookie but no mobile document view, because the
9470
+ * App has no live WebView while this stream is serving a suspended page.
9471
+ */
9472
+ openBackgroundEventStream(request, response, authorization) {
9473
+ const operation = this.allocateRequest(authorization, response, {});
9474
+ let closed = false;
9475
+ let heartbeat;
9476
+ const close = () => {
9477
+ if (closed) return;
9478
+ closed = true;
9479
+ if (heartbeat !== void 0) clearInterval(heartbeat);
9480
+ this.taskEventListeners.delete(sendTask);
9481
+ const deviceListeners = this.deviceEventListeners.get(authorization.deviceId);
9482
+ deviceListeners?.delete(sendDevice);
9483
+ if (deviceListeners?.size === 0) this.deviceEventListeners.delete(authorization.deviceId);
9484
+ request.removeListener("aborted", close);
9485
+ response.removeListener("close", close);
9486
+ operation.release();
9487
+ };
9488
+ const sendTask = (payload) => {
9489
+ if (closed || response.destroyed || response.writableEnded) return;
9490
+ response.write(`event: task-notify\ndata: ${payload}\n\n`);
9491
+ };
9492
+ const sendDevice = (payload) => {
9493
+ if (closed || response.destroyed || response.writableEnded) return;
9494
+ response.write(`event: device-revoked\ndata: ${payload}\n\n`);
9495
+ };
9496
+ setSecurityHeaders(response, this.tlsEnabled);
9497
+ response.writeHead(200, {
9498
+ "Content-Type": "text/event-stream; charset=utf-8",
9499
+ "Cache-Control": "no-store",
9500
+ Connection: "keep-alive",
9501
+ "X-Accel-Buffering": "no"
9502
+ });
9503
+ response.write("retry: 2000\n: ready\n\n");
9504
+ this.taskEventListeners.add(sendTask);
9505
+ const deviceListeners = this.deviceEventListeners.get(authorization.deviceId) ?? /* @__PURE__ */ new Set();
9506
+ deviceListeners.add(sendDevice);
9507
+ this.deviceEventListeners.set(authorization.deviceId, deviceListeners);
9508
+ heartbeat = setInterval(() => {
9509
+ if (!closed && !response.destroyed && !response.writableEnded) response.write(": heartbeat\n\n");
9510
+ }, EXTENSION_EVENT_HEARTBEAT_MS);
9511
+ heartbeat.unref();
9512
+ request.once("aborted", close);
9513
+ response.once("close", close);
9514
+ }
9418
9515
  async readUpgradeResponse(upstream, expectedAccept) {
9419
9516
  return new Promise((resolve, reject) => {
9420
9517
  let buffer = Buffer.alloc(0);
@@ -9528,9 +9625,31 @@ var MobileAccessGateway = class {
9528
9625
  client.destroy();
9529
9626
  upstream.destroy();
9530
9627
  };
9628
+ let closeCompressedBridge;
9629
+ const finishGracefully = () => {
9630
+ if (closeCompressedBridge !== void 0) closeCompressedBridge(1e3, "session expired");
9631
+ else {
9632
+ upstream.unpipe(client);
9633
+ client.unpipe(upstream);
9634
+ upstream.pause();
9635
+ const sendCloseFrame = (socket) => {
9636
+ if (socket.destroyed || !socket.writable) return;
9637
+ try {
9638
+ socket.write(Buffer.from([
9639
+ 136,
9640
+ 2,
9641
+ 3,
9642
+ 232
9643
+ ]));
9644
+ } catch {}
9645
+ };
9646
+ sendCloseFrame(client);
9647
+ }
9648
+ setTimeout(closeBoth, 50).unref();
9649
+ };
9531
9650
  client.on("error", closeBoth);
9532
9651
  upstream.on("error", closeBoth);
9533
- const timer = setTimeout(closeBoth, Math.max(1, authorization.expiresAt - Date.now()));
9652
+ const timer = setTimeout(finishGracefully, Math.max(1, authorization.expiresAt - Date.now()));
9534
9653
  timer.unref();
9535
9654
  const record = Object.freeze({
9536
9655
  ...authorization,
@@ -9559,7 +9678,10 @@ var MobileAccessGateway = class {
9559
9678
  const compressionTarget = new URL(this.config.upstreamOrigin.href);
9560
9679
  compressionTarget.pathname = target.pathname;
9561
9680
  compressionTarget.search = target.search;
9562
- await bridgeCompressedWebSocket(request, client, upstream, head, compressionTarget, upstreamCookie, this.config.websocketCompression, this.config.upstreamTimeoutMs);
9681
+ const bridge = await bridgeCompressedWebSocket(request, client, upstream, head, compressionTarget, upstreamCookie, this.config.websocketCompression, this.config.upstreamTimeoutMs);
9682
+ closeCompressedBridge = (code, reason) => {
9683
+ bridge.close(code, reason);
9684
+ };
9563
9685
  return;
9564
9686
  }
9565
9687
  await new Promise((resolve, reject) => {
@@ -9903,6 +10025,26 @@ var MemoryDeviceStore = class {
9903
10025
  return structuredClone(this.snapshot);
9904
10026
  }
9905
10027
  };
10028
+ // src/mobile-push/service.ts
10029
+ /**
10030
+ * Small Cordis service exposed to trusted Host plugins. The provider owns the
10031
+ * runtime lifetime and all recipient, category, session, and expiry checks.
10032
+ */
10033
+ var MobilePushService = class extends Service {
10034
+ send;
10035
+ constructor(ctx, send) {
10036
+ super(ctx, "mobilePush");
10037
+ this.send = send;
10038
+ }
10039
+ /**
10040
+ * Queue a notification for enabled web-push recipients.
10041
+ * @param input - Session, text, optional category and optional expiry.
10042
+ * @returns The number of newly queued recipient deliveries and the applied expiry.
10043
+ */
10044
+ async notify(input) {
10045
+ return await this.send(input);
10046
+ }
10047
+ };
9906
10048
  // src/managed-files.ts
9907
10049
  /** Refuse link-shaped ancestors inside an owned state root before accessing a child. */
9908
10050
  async function assertManagedParents(root, target, prefix) {
@@ -15961,6 +16103,79 @@ var IrohIdentityProbeError = class extends Error {
15961
16103
  function publicStatus$2(status) {
15962
16104
  return Object.freeze({ ...status });
15963
16105
  }
16106
+ function diagnosticText(value, dataDir) {
16107
+ const text = value.toString("utf8").replaceAll(dataDir, "<data-dir>").trim().replace(/\s+/gu, " ");
16108
+ return text === "" ? void 0 : text.slice(0, IDENTITY_DIAGNOSTIC_MAX_BYTES);
16109
+ }
16110
+ /** The wrapper `tls-prepare` prints when it discards the underlying native cause. */
16111
+ const HOST_TLS_PREPARATION_FAILURE = "Error: host TLS preparation failed";
16112
+ /**
16113
+ * Whether the wrapper is the only explanation stderr carries.
16114
+ *
16115
+ * Compare lines, never the whole buffer: the probe collapses whitespace for
16116
+ * reporting, and a preceding warning or a CRLF terminator must not hide the
16117
+ * fact that no cause chain was printed. A `Caused by:` section means the
16118
+ * native explanation already reached us, so the extra host run cannot add
16119
+ * anything the caller does not already have.
16120
+ */
16121
+ function isBareHostTlsPreparationFailure(value, dataDir) {
16122
+ const lines = value.toString("utf8").replaceAll(dataDir, "<data-dir>").split(/\r?\n/gu).map((line) => line.trim()).filter((line) => line !== "");
16123
+ return lines.includes(HOST_TLS_PREPARATION_FAILURE) && !lines.some((line) => /^Caused by:/u.test(line));
16124
+ }
16125
+ /**
16126
+ * Ask the host entry point for the cause chain when `tls-prepare` reports only
16127
+ * its top-level wrapper. The command is local-only and bounded; it never
16128
+ * downloads a component or enables relay/discovery.
16129
+ */
16130
+ async function hostIdentityDiagnostic(executable, dataDir, launch, onSpawn) {
16131
+ const args = [
16132
+ "--lang",
16133
+ "en",
16134
+ "host",
16135
+ "--data-dir",
16136
+ dataDir,
16137
+ "--local-only"
16138
+ ];
16139
+ const child = launch === void 0 ? spawn(executable, args, {
16140
+ stdio: [
16141
+ "pipe",
16142
+ "pipe",
16143
+ "pipe"
16144
+ ],
16145
+ windowsHide: true
16146
+ }) : launch(executable, args);
16147
+ onSpawn?.(child);
16148
+ let stderrOutput = Buffer.alloc(0);
16149
+ let settled = false;
16150
+ let deadline;
16151
+ return await new Promise((resolve) => {
16152
+ const finish = (value) => {
16153
+ if (settled) return;
16154
+ settled = true;
16155
+ if (deadline !== void 0) clearTimeout(deadline);
16156
+ resolve(value);
16157
+ };
16158
+ deadline = setTimeout(() => {
16159
+ terminateRemoteProcess(child).then(() => {
16160
+ finish(diagnosticText(stderrOutput, dataDir));
16161
+ }, () => {
16162
+ finish(diagnosticText(stderrOutput, dataDir));
16163
+ });
16164
+ }, 5e3);
16165
+ deadline.unref?.();
16166
+ child.stderr?.on("data", (chunk) => {
16167
+ const remaining = IDENTITY_DIAGNOSTIC_MAX_BYTES - stderrOutput.length;
16168
+ if (remaining > 0) stderrOutput = Buffer.concat([stderrOutput, chunk.subarray(0, remaining)]);
16169
+ });
16170
+ child.once("error", () => {
16171
+ finish(diagnosticText(stderrOutput, dataDir));
16172
+ });
16173
+ child.once("close", () => {
16174
+ finish(diagnosticText(stderrOutput, dataDir));
16175
+ });
16176
+ child.stdin?.end();
16177
+ });
16178
+ }
15964
16179
  /**
15965
16180
  * Prepare the sidecar's private identity/TLS state and resolve its endpoint.
15966
16181
  *
@@ -16030,6 +16245,15 @@ async function defaultResolveIdentity(executable, dataDir, options = {}) {
16030
16245
  if (settled) return;
16031
16246
  settled = true;
16032
16247
  const probeDiagnostic = diagnostic();
16248
+ if (code !== 0 && probeDiagnostic !== void 0 && isBareHostTlsPreparationFailure(stderrOutput, dataDir)) {
16249
+ hostIdentityDiagnostic(executable, dataDir, options.launch, options.onSpawn).then((detail) => {
16250
+ const resolvedDiagnostic = detail ?? probeDiagnostic;
16251
+ 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));
16252
+ }, () => {
16253
+ reject(new IrohIdentityProbeError("iroh_identity_invalid", probeDiagnostic));
16254
+ });
16255
+ return;
16256
+ }
16033
16257
  if (code !== 0 && probeDiagnostic !== void 0 && /private (?:Windows )?data directory is not secure|private path ACL grants/iu.test(probeDiagnostic)) {
16034
16258
  reject(new IrohIdentityProbeError("iroh_data_directory_insecure", probeDiagnostic));
16035
16259
  return;
@@ -16939,23 +17163,36 @@ function remoteSuffix(origin) {
16939
17163
  return "地址格式无效";
16940
17164
  }
16941
17165
  }
17166
+ /**
17167
+ * Read-only PowerShell that reports whether both managed LAN rules are usable.
17168
+ *
17169
+ * The verdict mirrors `windowsFirewallSetupScript`'s post-condition: enabled,
17170
+ * inbound, allow, and `Profile` exactly `Any`. A Domain/Private-scoped rule is
17171
+ * active only on those networks, so reporting it as ready would tell a user on
17172
+ * a Public network that nothing needs fixing. The script only reads: it never
17173
+ * enables, creates or deletes a rule, and the caller keeps its 3s budget.
17174
+ * @param port - managed LAN listener port the rules must cover.
17175
+ */
17176
+ function windowsFirewallProbeScript(port) {
17177
+ return [
17178
+ "$specs = @(@{ Name = 'DSH Mobile HTTPS'; Protocol = 'TCP' }, @{ Name = 'DSH Mobile Discovery'; Protocol = 'UDP' })",
17179
+ "$ready = $true",
17180
+ "$specs | ForEach-Object {",
17181
+ " $spec = $_",
17182
+ " $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",
17183
+ " if ($null -eq $rule) { $ready = $false; return }",
17184
+ " $filters = @($rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)",
17185
+ ` $matching = @($filters | Where-Object { $_.Protocol -eq $spec.Protocol -and ($_.LocalPort -eq 'Any' -or $_.LocalPort -eq '${String(port)}') })`,
17186
+ " if ($matching.Count -eq 0) { $ready = $false }",
17187
+ "}",
17188
+ "if ($ready) { 'ready' } else { 'missing' }"
17189
+ ].join("; ");
17190
+ }
16942
17191
  function defaultFirewallProbe(platform = process.platform) {
16943
17192
  return async (port) => {
16944
17193
  if (platform !== "win32") return { state: "not-applicable" };
16945
17194
  if (port === void 0) return { state: "unknown" };
16946
- const script = [
16947
- "$specs = @(@{ Name = 'DSH Mobile HTTPS'; Protocol = 'TCP' }, @{ Name = 'DSH Mobile Discovery'; Protocol = 'UDP' })",
16948
- "$ready = $true",
16949
- "$specs | ForEach-Object {",
16950
- " $spec = $_",
16951
- " $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",
16952
- " if ($null -eq $rule) { $ready = $false; return }",
16953
- " $filters = @($rule | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue)",
16954
- ` $matching = @($filters | Where-Object { $_.Protocol -eq $spec.Protocol -and ($_.LocalPort -eq 'Any' -or $_.LocalPort -eq '${String(port)}') })`,
16955
- " if ($matching.Count -eq 0) { $ready = $false }",
16956
- "}",
16957
- "if ($ready) { 'ready' } else { 'missing' }"
16958
- ].join("; ");
17195
+ const script = windowsFirewallProbeScript(port);
16959
17196
  try {
16960
17197
  return { state: (await execFileText("powershell.exe", [
16961
17198
  "-NoProfile",
@@ -22109,7 +22346,8 @@ function validSource(value) {
22109
22346
  "lifetime",
22110
22347
  "turn",
22111
22348
  "callId",
22112
- "goal"
22349
+ "goal",
22350
+ "custom"
22113
22351
  ])) return false;
22114
22352
  return [
22115
22353
  value.sourceId,
@@ -22126,7 +22364,7 @@ function validSource(value) {
22126
22364
  "live",
22127
22365
  "projected",
22128
22366
  "committed"
22129
- ].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));
22367
+ ].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));
22130
22368
  }
22131
22369
  function scopedCounts(rows, scope, maximum = 1024) {
22132
22370
  const counts = /* @__PURE__ */ new Map();
@@ -22742,10 +22980,11 @@ function validateNavigate(navigate, category) {
22742
22980
  if (typeof navigate !== "string" || !/^\/\?mobile-push=[A-Za-z0-9_-]{1,256}$/u.test(navigate)) throw new TypeError("push_navigation_invalid");
22743
22981
  }
22744
22982
  /** Build Apple's declarative Web Push schema with generic, locale-owned text. */
22745
- function buildDeclarativePayload(category, locale, navigate, tag) {
22983
+ function buildDeclarativePayload(category, locale, navigate, tag, custom) {
22746
22984
  validateNavigate(navigate, category);
22747
22985
  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");
22748
- const [title, body] = messages[locale][category];
22986
+ 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");
22987
+ const [title, body] = custom === void 0 ? messages[locale][category] : [custom.title, custom.body];
22749
22988
  const payload = JSON.stringify({
22750
22989
  web_push: 8030,
22751
22990
  notification: {
@@ -23085,6 +23324,12 @@ function sameKey(a, b) {
23085
23324
  function isPreferenceEnabled(preferences, category) {
23086
23325
  return preferences[category] === true;
23087
23326
  }
23327
+ function notificationText(value, maximum, label) {
23328
+ if (typeof value !== "string") throw new TypeError(`push_${label}_invalid`);
23329
+ const text = value.trim().replace(/[\u0000-\u001f\u007f]+/gu, " ");
23330
+ if (text.length === 0 || text.length > maximum) throw new TypeError(`push_${label}_invalid`);
23331
+ return text;
23332
+ }
23088
23333
  function pendingHas(snapshot, intent) {
23089
23334
  return snapshot.pending.some((item) => item.id === intent.id && sameKey(item.key, intent.key));
23090
23335
  }
@@ -23198,6 +23443,51 @@ var PushDelivery = class {
23198
23443
  this.#kick();
23199
23444
  });
23200
23445
  }
23446
+ /**
23447
+ * Queue trusted Host-plugin text for every enabled web-push recipient that
23448
+ * opted into the selected notification category. The source keeps its own
23449
+ * short expiry; a restart may still retry it only inside that window.
23450
+ */
23451
+ async notifyCustom(input) {
23452
+ if (!this.#started || this.#closed || !this.#admitting) throw new Error("push_unavailable");
23453
+ const sessionId = typeof input.sessionId === "string" ? input.sessionId.trim() : "";
23454
+ if (sessionId === "" || sessionId.length > 4096) throw new TypeError("push_session_id_invalid");
23455
+ const title = notificationText(input.title, 80, "title");
23456
+ const body = notificationText(input.body, 200, "body");
23457
+ const category = input.category ?? "response";
23458
+ if (![
23459
+ "question",
23460
+ "plan",
23461
+ "approval",
23462
+ "response",
23463
+ "goal",
23464
+ "failure"
23465
+ ].includes(category)) throw new TypeError("push_category_invalid");
23466
+ const now = this.#now();
23467
+ const maximumExpiry = now + OUTCOME_TTL_MS;
23468
+ if (input.expiresAt !== void 0 && (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= now)) throw new TypeError("push_expiry_invalid");
23469
+ const expiresAt = Math.min(input.expiresAt ?? maximumExpiry, maximumExpiry);
23470
+ if (!await this.#host.sessionExists(sessionId)) throw new Error("push_session_unavailable");
23471
+ const source = {
23472
+ sourceId: `custom:${randomUUID()}`,
23473
+ sessionId,
23474
+ targetSessionId: sessionId,
23475
+ category,
23476
+ createdAt: now,
23477
+ expiresAt,
23478
+ lifetime: "committed",
23479
+ custom: {
23480
+ title,
23481
+ body
23482
+ }
23483
+ };
23484
+ const queued = await this.#admit(source);
23485
+ this.#kick();
23486
+ return {
23487
+ queued,
23488
+ expiresAt
23489
+ };
23490
+ }
23201
23491
  async resolve(owner, reference) {
23202
23492
  if (this.#closed || !reference) return void 0;
23203
23493
  let snapshot;
@@ -23237,14 +23527,15 @@ var PushDelivery = class {
23237
23527
  this.#origins.clear();
23238
23528
  }
23239
23529
  async #admit(source) {
23240
- if (!this.#admitting || source.expiresAt <= this.#now()) return;
23530
+ if (!this.#admitting || source.expiresAt <= this.#now()) return 0;
23241
23531
  let eligible = false;
23242
23532
  try {
23243
- eligible = this.#host.available && await this.#host.eligible(source);
23533
+ eligible = source.custom === void 0 ? this.#host.available && await this.#host.eligible(source) : this.#host.available && await this.#host.sessionExists(source.sessionId);
23244
23534
  } catch {
23245
23535
  eligible = false;
23246
23536
  }
23247
- if (!eligible || source.expiresAt <= this.#now()) return;
23537
+ if (!eligible || source.expiresAt <= this.#now()) return 0;
23538
+ let queued = 0;
23248
23539
  for (const origin of this.#origins.values()) {
23249
23540
  if (!origin.active || this.#closed) continue;
23250
23541
  const devices = await this.#readAuthorized(origin);
@@ -23253,7 +23544,7 @@ var PushDelivery = class {
23253
23544
  try {
23254
23545
  snapshot = this.#registry.snapshot();
23255
23546
  } catch {
23256
- return;
23547
+ return queued;
23257
23548
  }
23258
23549
  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);
23259
23550
  if (registrations.length === 0) continue;
@@ -23282,6 +23573,7 @@ var PushDelivery = class {
23282
23573
  attempts: 0,
23283
23574
  nextAttemptAt: this.#now()
23284
23575
  });
23576
+ queued += 1;
23285
23577
  }
23286
23578
  return {
23287
23579
  ...clean,
@@ -23290,6 +23582,7 @@ var PushDelivery = class {
23290
23582
  });
23291
23583
  }
23292
23584
  this.#kick();
23585
+ return queued;
23293
23586
  }
23294
23587
  async #reconcileKnownScopes() {
23295
23588
  let snapshot;
@@ -23474,7 +23767,7 @@ var PushDelivery = class {
23474
23767
  await this.#removeIntent(intent);
23475
23768
  return;
23476
23769
  }
23477
- const payload = buildDeclarativePayload(intent.source.category, registration.locale, `/?mobile-push=${reference}`, intent.source.sourceId.replace(/[^A-Za-z0-9._:-]/gu, "_").slice(0, 128) || "push");
23770
+ 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);
23478
23771
  let result;
23479
23772
  try {
23480
23773
  result = await this.#sender.send({
@@ -23584,6 +23877,11 @@ var PushDelivery = class {
23584
23877
  /** Maps the Host tri-state for durable-work decisions; boolean eligible() would collapse transient uncertainty into denial. */
23585
23878
  async #sourceGuard(source) {
23586
23879
  if (!this.#host.available) return "unverifiable";
23880
+ if (source.custom !== void 0) try {
23881
+ return await this.#host.sessionExists(source.sessionId) ? "allowed" : "denied";
23882
+ } catch {
23883
+ return "unverifiable";
23884
+ }
23587
23885
  let result;
23588
23886
  try {
23589
23887
  result = await this.#host.reconcile(source);
@@ -23937,6 +24235,10 @@ var PushGatewayCoordinator = class {
23937
24235
  installationId
23938
24236
  });
23939
24237
  }
24238
+ async notifyCustom(input) {
24239
+ if (!this.host.available) throw new Error("push_unavailable");
24240
+ return await this.delivery.notifyCustom(input);
24241
+ }
23940
24242
  async resolve(owner, reference) {
23941
24243
  if (!this.host.available || !await this.authorize(owner, true)) return void 0;
23942
24244
  return this.delivery.resolve(owner, reference);
@@ -24059,16 +24361,71 @@ const MAX_MANAGED_PORT = 65535;
24059
24361
  function assertManagedPort(port, name) {
24060
24362
  if (!Number.isSafeInteger(port) || port < MIN_MANAGED_PORT || port > MAX_MANAGED_PORT) throw new Error(`${name} must be from ${String(MIN_MANAGED_PORT)} through ${String(MAX_MANAGED_PORT)}`);
24061
24363
  }
24062
- function powerShellFailureDetail(error) {
24364
+ /** Longest failure detail carried in the thrown message. */
24365
+ const MAX_FAILURE_DETAIL_CHARS = 512;
24366
+ /**
24367
+ * Strip the elevated invocation and filesystem locations from PowerShell text.
24368
+ *
24369
+ * The launcher's stderr quotes the command it ran, including the base64
24370
+ * `-EncodedCommand` payload, and the elevated script quotes its own scratch
24371
+ * path. Neither helps an operator, and both can push the one useful line past
24372
+ * the length bound, so they are replaced before the detail is bounded.
24373
+ */
24374
+ function redactPowerShellDetail(value) {
24375
+ 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>");
24376
+ }
24377
+ /** Bounded, redacted detail for a failed elevated Windows operation. */
24378
+ function powerShellFailureDetail(error, innerError = "") {
24063
24379
  if (!(error instanceof Error)) return "unknown failure";
24064
- const code = "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
24065
- const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr.trim().replace(/\s+/gu, " ").slice(0, 512) : "";
24066
- return `${code}${stderr === "" ? "" : `: ${stderr}`}`;
24380
+ const code = "code" in error && (typeof error.code === "string" || typeof error.code === "number") ? ` (${String(error.code)})` : "";
24381
+ 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);
24382
+ return `${code}${detail === "" ? "" : `: ${detail}`}`;
24383
+ }
24384
+ /** Default sink for a scratch directory that could not be removed. */
24385
+ function warnFirewallCleanupFailure(detail) {
24386
+ process.emitWarning(`Windows firewall temporary cleanup failed (${detail})`, { code: "DSH_MOBILE_FIREWALL_CLEANUP_FAILED" });
24387
+ }
24388
+ /**
24389
+ * Remove the elevated helper's scratch directory without touching the run's outcome.
24390
+ *
24391
+ * The directory is created before elevation and removed in a `finally`, so a
24392
+ * cleanup failure must not replace the firewall error that explains the
24393
+ * failure, and must not fail a run that already succeeded. It stays observable
24394
+ * instead: the removal error's code is reported to the warning sink, whose own
24395
+ * failure is ignored for the same reason.
24396
+ * @param directory - scratch path created for this run.
24397
+ * @param options - injectable removal and warning sink for tests.
24398
+ */
24399
+ async function discardFirewallScratch(directory, options = {}) {
24400
+ const remove = options.remove ?? ((path) => rm(path, {
24401
+ recursive: true,
24402
+ force: true
24403
+ }));
24404
+ try {
24405
+ await remove(directory);
24406
+ } catch (error) {
24407
+ const code = error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "unknown";
24408
+ const warn = options.warn ?? warnFirewallCleanupFailure;
24409
+ try {
24410
+ warn(code);
24411
+ } catch {}
24412
+ }
24067
24413
  }
24068
24414
  async function runElevatedPowerShell(script) {
24415
+ const temporary = await mkdtemp(join(tmpdir(), "dsh-mobile-firewall-"));
24416
+ const errorFile = join(temporary, "error.log");
24417
+ const wrapped = [
24418
+ `$__dshMobileErrorFile = '${errorFile.replaceAll("'", "''")}'`,
24419
+ "try {",
24420
+ script,
24421
+ "} catch {",
24422
+ " try { [IO.File]::WriteAllText($__dshMobileErrorFile, ($_ | Out-String)) } catch {}",
24423
+ " throw",
24424
+ "}"
24425
+ ].join("\n");
24069
24426
  const launch = [
24070
24427
  "$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
24071
- ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
24428
+ ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(wrapped, "utf16le").toString("base64")}')`,
24072
24429
  "; exit $process.ExitCode"
24073
24430
  ].join(" ");
24074
24431
  try {
@@ -24079,7 +24436,13 @@ async function runElevatedPowerShell(script) {
24079
24436
  launch
24080
24437
  ], { windowsHide: true });
24081
24438
  } catch (error) {
24082
- throw new Error(`Windows firewall operation failed${powerShellFailureDetail(error)}`, { cause: error });
24439
+ let innerError = "";
24440
+ try {
24441
+ innerError = (await readFile(errorFile, "utf8")).replace(/^\uFEFF/u, "");
24442
+ } catch {}
24443
+ throw new Error(`Windows firewall operation failed${powerShellFailureDetail(error, innerError)}`, { cause: error });
24444
+ } finally {
24445
+ await discardFirewallScratch(temporary);
24083
24446
  }
24084
24447
  }
24085
24448
  function firewallRuleScript(name, protocol, port) {
@@ -24094,7 +24457,7 @@ function firewallRuleScript(name, protocol, port) {
24094
24457
  " $rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter -RemoteAddress LocalSubnet",
24095
24458
  " }",
24096
24459
  "}",
24097
- `$verified = @(Get-NetFirewallRule -DisplayName '${name}' -Enabled True -Direction Inbound -Action Allow -Profile Any -ErrorAction Stop)`,
24460
+ `$verified = @(Get-NetFirewallRule -DisplayName '${name}' -ErrorAction Stop | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Allow' -and $_.Profile -eq 'Any' })`,
24098
24461
  `$matchingPort = @($verified | Get-NetFirewallPortFilter | Where-Object { $_.Protocol -eq '${protocol}' -and $_.LocalPort -contains '${String(port)}' })`,
24099
24462
  `$matchingAddress = @($verified | Get-NetFirewallAddressFilter | Where-Object { $_.RemoteAddress -contains 'LocalSubnet' })`,
24100
24463
  `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" }`
@@ -24590,6 +24953,7 @@ async function apply(ctx, config) {
24590
24953
  if (service === void 0) throw new Error("push_unavailable");
24591
24954
  return service;
24592
24955
  };
24956
+ new MobilePushService(ctx, async (input) => (await requiredPushGatewayService()).notifyCustom(input));
24593
24957
  const queueDeviceInvalidations = (scopeId, deviceIds) => {
24594
24958
  let queued = pendingDeviceInvalidations.get(scopeId);
24595
24959
  if (queued === void 0) pendingDeviceInvalidations.set(scopeId, queued = /* @__PURE__ */ new Set());
@@ -24664,6 +25028,11 @@ async function apply(ctx, config) {
24664
25028
  async test(owner, installationId) {
24665
25029
  await (await requiredPushGatewayService()).test(owner, installationId);
24666
25030
  },
25031
+ async notifyCustom(input) {
25032
+ const service = await requiredPushGatewayService();
25033
+ if (service.notifyCustom === void 0) throw new Error("push_unavailable");
25034
+ return await service.notifyCustom(input);
25035
+ },
24667
25036
  async resolve(owner, reference) {
24668
25037
  return (await requiredPushGatewayService()).resolve(owner, reference);
24669
25038
  }
@@ -25999,6 +26368,6 @@ async function apply(ctx, config) {
25999
26368
  };
26000
26369
  }, "dsh-mobile: independent LAN and selectable remote providers with /mobile command");
26001
26370
  }
26002
- 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 };
26371
+ 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 };
26003
26372
 
26004
26373
  //# sourceMappingURL=index.mjs.map