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:
co-authored by
Mohamed Boudra
parent
ee44b536f5
commit
338a41991d
@@ -239,6 +239,7 @@ export interface PaseoDaemonConfig {
|
||||
relayEndpoint?: string;
|
||||
relayPublicEndpoint?: string;
|
||||
relayUseTls?: boolean;
|
||||
relayPublicUseTls?: boolean;
|
||||
appBaseUrl?: string;
|
||||
auth?: DaemonAuthConfig;
|
||||
openai?: PaseoOpenAIConfig;
|
||||
@@ -300,7 +301,9 @@ export async function createPaseoDaemon(
|
||||
const staticDir = config.staticDir;
|
||||
const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
|
||||
|
||||
const downloadTokenStore = new DownloadTokenStore({ ttlMs: downloadTokenTtlMs });
|
||||
const downloadTokenStore = new DownloadTokenStore({
|
||||
ttlMs: downloadTokenTtlMs,
|
||||
});
|
||||
|
||||
const listenTarget = parseListenString(config.listen);
|
||||
|
||||
@@ -863,6 +866,7 @@ export async function createPaseoDaemon(
|
||||
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
||||
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
||||
const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
||||
const relayPublicUseTls = config.relayPublicUseTls ?? relayUseTls;
|
||||
const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
|
||||
|
||||
if (boundListenTarget.type === "tcp") {
|
||||
@@ -938,7 +942,10 @@ export async function createPaseoDaemon(
|
||||
const offer = await createConnectionOfferV2({
|
||||
serverId,
|
||||
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
||||
relay: { endpoint: relayPublicEndpoint, useTls: relayUseTls },
|
||||
relay: {
|
||||
endpoint: relayPublicEndpoint,
|
||||
useTls: relayPublicUseTls,
|
||||
},
|
||||
});
|
||||
|
||||
encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
||||
|
||||
@@ -44,7 +44,42 @@ describe("daemon relay config", () => {
|
||||
});
|
||||
expect(loadConfig(envHome, { env: { PASEO_RELAY_USE_TLS: "true" } }).relayUseTls).toBe(true);
|
||||
|
||||
const hostedHome = await createPaseoHome({ version: 1, daemon: { relay: {} } });
|
||||
const hostedHome = await createPaseoHome({
|
||||
version: 1,
|
||||
daemon: { relay: {} },
|
||||
});
|
||||
expect(loadConfig(hostedHome, { env: {} }).relayUseTls).toBe(true);
|
||||
});
|
||||
|
||||
test("relayPublicUseTls falls back to relayUseTls when unset", async () => {
|
||||
const home = await createPaseoHome({ version: 1, daemon: { relay: {} } });
|
||||
// Default: both true (hosted relay)
|
||||
expect(loadConfig(home, { env: {} }).relayPublicUseTls).toBe(true);
|
||||
});
|
||||
|
||||
test("PASEO_RELAY_PUBLIC_USE_TLS overrides relayUseTls for public side", async () => {
|
||||
const home = await createPaseoHome({ version: 1, daemon: { relay: {} } });
|
||||
const config = loadConfig(home, {
|
||||
env: { PASEO_RELAY_USE_TLS: "false", PASEO_RELAY_PUBLIC_USE_TLS: "true" },
|
||||
});
|
||||
expect(config.relayUseTls).toBe(false);
|
||||
expect(config.relayPublicUseTls).toBe(true);
|
||||
});
|
||||
|
||||
test("relayPublicUseTls falls back to relayUseTls when only PASEO_RELAY_USE_TLS is set", async () => {
|
||||
const home = await createPaseoHome({ version: 1, daemon: { relay: {} } });
|
||||
const config = loadConfig(home, { env: { PASEO_RELAY_USE_TLS: "false" } });
|
||||
expect(config.relayUseTls).toBe(false);
|
||||
expect(config.relayPublicUseTls).toBe(false);
|
||||
});
|
||||
|
||||
test("persisted publicUseTls overrides relayUseTls fallback", async () => {
|
||||
const home = await createPaseoHome({
|
||||
version: 1,
|
||||
daemon: { relay: { useTls: false, publicUseTls: true } },
|
||||
});
|
||||
const config = loadConfig(home, { env: {} });
|
||||
expect(config.relayUseTls).toBe(false);
|
||||
expect(config.relayPublicUseTls).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,6 +148,18 @@ interface ResolvedRelay {
|
||||
endpoint: string;
|
||||
publicEndpoint: string;
|
||||
useTls: boolean;
|
||||
publicUseTls: boolean;
|
||||
}
|
||||
|
||||
function resolveTlsFromEnv(
|
||||
envValue: string | undefined,
|
||||
persistedValue: boolean | undefined,
|
||||
fallback: boolean,
|
||||
): boolean {
|
||||
if (envValue !== undefined) {
|
||||
return parseBooleanEnv(envValue) ?? false;
|
||||
}
|
||||
return persistedValue ?? fallback;
|
||||
}
|
||||
|
||||
function resolveRelayConfig(input: ResolveRelayInput): ResolvedRelay {
|
||||
@@ -166,10 +178,17 @@ function resolveRelayConfig(input: ResolveRelayInput): ResolvedRelay {
|
||||
endpoint;
|
||||
const useTls =
|
||||
input.cliRelayUseTls ??
|
||||
(input.env.PASEO_RELAY_USE_TLS !== undefined
|
||||
? (parseBooleanEnv(input.env.PASEO_RELAY_USE_TLS) ?? false)
|
||||
: (input.persisted.daemon?.relay?.useTls ?? endpoint === DEFAULT_RELAY_ENDPOINT));
|
||||
return { enabled, endpoint, publicEndpoint, useTls };
|
||||
resolveTlsFromEnv(
|
||||
input.env.PASEO_RELAY_USE_TLS,
|
||||
input.persisted.daemon?.relay?.useTls,
|
||||
endpoint === DEFAULT_RELAY_ENDPOINT,
|
||||
);
|
||||
const publicUseTls = resolveTlsFromEnv(
|
||||
input.env.PASEO_RELAY_PUBLIC_USE_TLS,
|
||||
input.persisted.daemon?.relay?.publicUseTls,
|
||||
useTls,
|
||||
);
|
||||
return { enabled, endpoint, publicEndpoint, useTls, publicUseTls };
|
||||
}
|
||||
|
||||
interface ResolvedVoiceLlm {
|
||||
@@ -305,6 +324,7 @@ export function loadConfig(
|
||||
relayEndpoint: relay.endpoint,
|
||||
relayPublicEndpoint: relay.publicEndpoint,
|
||||
relayUseTls: relay.useTls,
|
||||
relayPublicUseTls: relay.publicUseTls,
|
||||
appBaseUrl,
|
||||
auth: resolveAuthConfig(env, persisted),
|
||||
openai,
|
||||
|
||||
@@ -17,6 +17,7 @@ export async function generateLocalPairingOffer(args: {
|
||||
relayEndpoint?: string;
|
||||
relayPublicEndpoint?: string;
|
||||
relayUseTls?: boolean;
|
||||
relayPublicUseTls?: boolean;
|
||||
appBaseUrl?: string;
|
||||
includeQr?: boolean;
|
||||
logger?: Logger;
|
||||
@@ -33,13 +34,14 @@ export async function generateLocalPairingOffer(args: {
|
||||
const relayEndpoint = args.relayEndpoint ?? "relay.paseo.sh:443";
|
||||
const relayPublicEndpoint = args.relayPublicEndpoint ?? relayEndpoint;
|
||||
const relayUseTls = args.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
||||
const relayPublicUseTls = args.relayPublicUseTls ?? relayUseTls;
|
||||
const appBaseUrl = args.appBaseUrl ?? "https://app.paseo.sh";
|
||||
const serverId = getOrCreateServerId(args.paseoHome, { logger: args.logger });
|
||||
const daemonKeyPair = await loadOrCreateDaemonKeyPair(args.paseoHome, args.logger);
|
||||
const offer = await createConnectionOfferV2({
|
||||
serverId,
|
||||
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
||||
relay: { endpoint: relayPublicEndpoint, useTls: relayUseTls },
|
||||
relay: { endpoint: relayPublicEndpoint, useTls: relayPublicUseTls },
|
||||
});
|
||||
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
||||
|
||||
|
||||
@@ -263,6 +263,7 @@ export const PersistedConfigSchema = z
|
||||
endpoint: z.string().optional(),
|
||||
publicEndpoint: z.string().optional(),
|
||||
useTls: z.boolean().optional(),
|
||||
publicUseTls: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
@@ -387,7 +388,9 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per
|
||||
raw = readFileSync(configPath, "utf-8");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`[Config] Failed to read ${configPath}: ${message}`, { cause: err });
|
||||
throw new Error(`[Config] Failed to read ${configPath}: ${message}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
@@ -395,7 +398,9 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`[Config] Invalid JSON in ${configPath}: ${message}`, { cause: err });
|
||||
throw new Error(`[Config] Invalid JSON in ${configPath}: ${message}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
const migrated = stripDeprecatedLocalSpeechConfigFields(parsed);
|
||||
@@ -432,6 +437,8 @@ export function savePersistedConfig(
|
||||
log?.info(`Saved to ${configPath}`);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`[Config] Failed to write ${configPath}: ${message}`, { cause: err });
|
||||
throw new Error(`[Config] Failed to write ${configPath}: ${message}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user