import { createHash, X509Certificate } from "node:crypto";
import { readFile, writeFile, mkdir, mkdtemp, rename, rm, copyFile, stat } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import path from "node:path";
import { integrationAccessUrl } from "../packages/config/dist/index.js";
import { PRIVATE_NETWORK_TOOLS, privateNetworkRoot, privateGatewayPorts } from "../apps/agent/dist/private-gateway.js";
export async function provisionPrivateNetwork(config) {
if (!config.server.privateGateway) return;
if (process.platform !== "win32" || process.arch !== "x64") throw new Error("Private gateway provisioning currently supports Windows x64.");
const root = privateNetworkRoot();
await mkdir(root, { recursive: true });
for (const tool of PRIVATE_NETWORK_TOOLS) {
const target = path.join(root, `${tool.name}-${tool.version}.exe`);
try { if (!(await stat(target)).isFile()) throw new Error("Tool target must be a regular file"); continue; } catch (error) { if (error.code !== "ENOENT") throw error; }
const staging = await mkdtemp(path.join(root, "download-"));
try {
const response = await fetch(`https://api.github.com/repos/${tool.repository}/releases/assets/${tool.assetId}`, { headers: { Accept: "application/octet-stream", "User-Agent": "PalmTTY-setup" }, signal: AbortSignal.timeout(600_000) });
if (!response.ok) throw new Error(`Could not download official ${tool.name} archive: ${response.status}`);
const chunks = []; let size = 0;
for await (const chunk of response.body) {
size += chunk.length;
if (size > 64 * 1024 * 1024) throw new Error("Official tool archive exceeds 64 MiB limit.");
chunks.push(chunk);
}
const archive = Buffer.concat(chunks);
if (createHash("sha256").update(archive).digest("hex") !== tool.sha256) throw new Error(`${tool.name} archive integrity check failed.`);
const archivePath = path.join(staging, "tool.zip");
await writeFile(archivePath, archive);
const unpack = spawnSync("tar.exe", ["-xf", archivePath, "-C", staging, `${tool.name}.exe`], { windowsHide: true, stdio: "ignore", timeout: 30_000 });
if (unpack.status !== 0) throw new Error(`Could not unpack ${tool.name}.`);
await rename(path.join(staging, `${tool.name}.exe`), target);
console.log(`[PalmTTY] Verified ${tool.name} ${tool.version}.`);
} finally { await rm(staging, { recursive: true, force: true }); }
}
}
export async function exportPrivateNetworkSetup(config) {
const gateway = config.server.privateGateway;
if (!gateway) return;
const root = privateNetworkRoot();
const publicRoot = path.join(root, "public");
await mkdir(publicRoot, { recursive: true });
const codeEnabled = config.integrations.vscodeWeb?.mode === "managed";
let certificate;
if (codeEnabled) {
const caPath = path.join(root, "pki", "pki", "authorities", "local", "root.crt");
certificate = new X509Certificate(await readFile(caPath));
for (const destination of [root, publicRoot]) await copyFile(caPath, path.join(destination, "PalmTTY-root.crt"));
// Preserve the existing profile identity and CA; only the new IP leaf changes.
const profile = `
Wi-Fi 和 WireGuard 使用相同 IP 地址,无需私有 DNS。不要在此输入访问令牌。
WireGuard 路由需包含主机 IP。首次根信任需明确操作,iPhone 安装后需启用完全信任。
`,"utf8"); const caddy = path.join(root,`caddy-${PRIVATE_NETWORK_TOOLS[0].version}.exe`); const literal = value=>"'"+String(value).replaceAll("'","''")+"'"; const firewall = `# Requires an explicit administrator PowerShell. No automatic elevation. $ErrorActionPreference = 'Stop' if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this reviewed script in an administrator PowerShell.' } $privateSources = @('LocalSubnet','10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','100.64.0.0/10') foreach ($retired in @('PalmTTY-PrivateDNS-UDP','PalmTTY-PrivateDNS-TCP')) { if (Get-NetFirewallRule -Name $retired -ErrorAction SilentlyContinue) { Remove-NetFirewallRule -Name $retired } } $rules = @( @{ Name='PalmTTY-PrivateHTTPS'; Program=${literal(caddy)}; Ports=@(${privateGatewayPorts(config).join(",")}) }, @{ Name='PalmTTY-PrivateHTTP'; Program=${literal(process.execPath)}; Ports=@(${config.server.port}) } ) foreach ($rule in $rules) { if (Get-NetFirewallRule -Name $rule.Name -ErrorAction SilentlyContinue) { Remove-NetFirewallRule -Name $rule.Name } New-NetFirewallRule -Name $rule.Name -DisplayName $rule.Name -Direction Inbound -Action Allow -Profile Any -Program $rule.Program -Protocol TCP -LocalAddress ${gateway.listenHost} -LocalPort $rule.Ports -RemoteAddress $privateSources | Out-Null } Write-Output 'PalmTTY private-source HTTP/HTTPS firewall rules configured; retired DNS rules removed.' `; await writeFile(path.join(root,"configure-firewall.ps1"),firewall,"utf8"); console.log(`[PalmTTY] Private IP client setup: ${path.join(root,"连接说明.txt")}`); }