feat: add independent TLS control for relay public endpoint (#1045)

* feat: add independent TLS control for relay public endpoint

Add PASEO_RELAY_PUBLIC_USE_TLS env var and publicUseTls persisted
config to separately control TLS for the client→relay pairing offer,
falling back to relayUseTls when unset. The daemon→relay connection
continues to use PASEO_RELAY_USE_TLS alone.

Enables self-hosting behind a TLS-terminating reverse proxy where
the internal path is plain ws:// but public clients need wss://.

* fix: use public relay TLS in daemon status

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Zexin Yuan
2026-05-17 23:00:42 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent ee44b536f5
commit 338a41991d
12 changed files with 131 additions and 15 deletions
@@ -1,9 +1,13 @@
import { EventEmitter } from "node:events";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import {
type DaemonLaunchRuntime,
type DetachedDaemonProcess,
resolveLocalDaemonState,
startLocalDaemonDetached,
startLocalDaemonForeground,
} from "./local-daemon.js";
@@ -64,6 +68,17 @@ class FakeDaemonRuntime implements DaemonLaunchRuntime {
}
}
const tempRoots: string[] = [];
async function createPaseoHome(config: unknown): Promise<string> {
const root = await mkdtemp(path.join(os.tmpdir(), "paseo-local-daemon-"));
tempRoots.push(root);
const paseoHome = path.join(root, ".paseo");
await mkdir(paseoHome, { recursive: true });
await writeFile(path.join(paseoHome, "config.json"), JSON.stringify(config, null, 2));
return paseoHome;
}
function expectSupervisorLaunch(argv: string[]): void {
const joined = argv.join(" ");
expect(joined).toContain("supervisor-entrypoint");
@@ -78,6 +93,12 @@ describe("local daemon launch supervision", () => {
vi.useRealTimers();
});
afterEach(async () => {
await Promise.all(
tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
);
});
test("foreground start spawns supervisor-entrypoint instead of server/index", async () => {
const runtime = new FakeDaemonRuntime();
@@ -131,4 +152,24 @@ describe("local daemon launch supervision", () => {
expect(launch?.args).toContain("--relay-use-tls");
expect(launch?.options?.env?.PASEO_RELAY_USE_TLS).toBe("true");
});
test("local daemon state keeps public relay TLS separate from daemon relay TLS", async () => {
const home = await createPaseoHome({
version: 1,
daemon: {
relay: {
endpoint: "10.0.0.5:51185",
publicEndpoint: "paseo.example.com",
useTls: false,
publicUseTls: true,
},
},
});
const state = resolveLocalDaemonState({ home });
expect(state.relayEndpoint).toBe("paseo.example.com");
expect(state.relayUseTls).toBe(false);
expect(state.relayPublicUseTls).toBe(true);
});
});
@@ -33,6 +33,7 @@ export interface LocalDaemonState {
relayEnabled: boolean;
relayEndpoint: string;
relayUseTls: boolean;
relayPublicUseTls: boolean;
logPath: string;
pidPath: string;
pidInfo: LocalDaemonPidInfo | null;
@@ -409,6 +410,7 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
relayEnabled: config.relayEnabled ?? true,
relayEndpoint: config.relayPublicEndpoint ?? config.relayEndpoint ?? "relay.paseo.sh:443",
relayUseTls: config.relayUseTls ?? false,
relayPublicUseTls: config.relayPublicUseTls ?? config.relayUseTls ?? false,
logPath,
pidPath,
pidInfo,
+1
View File
@@ -29,6 +29,7 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
relayEndpoint: config.relayEndpoint,
relayPublicEndpoint: config.relayPublicEndpoint,
relayUseTls: config.relayUseTls,
relayPublicUseTls: config.relayPublicUseTls,
appBaseUrl: config.appBaseUrl,
includeQr: true,
});
+1 -1
View File
@@ -316,7 +316,7 @@ async function resolveDaemonNodeLabel(
function formatRelayStatus(state: ReturnType<typeof resolveLocalDaemonState>): string {
if (!state.relayEnabled) return "disabled";
const scheme = state.relayUseTls ? "wss" : "ws";
const scheme = state.relayPublicUseTls ? "wss" : "ws";
return `${scheme}://${state.relayEndpoint}`;
}
+1
View File
@@ -492,6 +492,7 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
relayEndpoint: config.relayEndpoint,
relayPublicEndpoint: config.relayPublicEndpoint,
relayUseTls: config.relayUseTls,
relayPublicUseTls: config.relayPublicUseTls,
appBaseUrl: config.appBaseUrl,
includeQr: true,
});