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
@@ -107,7 +107,7 @@ Enables remote access when the daemon is behind a firewall.
|
|||||||
- Relay server is zero-knowledge — it routes encrypted bytes, cannot read content
|
- Relay server is zero-knowledge — it routes encrypted bytes, cannot read content
|
||||||
- Client and daemon channels with identical API (`createClientChannel`, `createDaemonChannel`)
|
- Client and daemon channels with identical API (`createClientChannel`, `createDaemonChannel`)
|
||||||
- Pairing via QR code transfers the daemon's public key to the client
|
- Pairing via QR code transfers the daemon's public key to the client
|
||||||
- Self-hosted relays opt into TLS with `daemon.relay.useTls` or `PASEO_RELAY_USE_TLS=true`
|
- Self-hosted relays opt into TLS with `daemon.relay.useTls` or `PASEO_RELAY_USE_TLS=true`; the public (client-facing) TLS setting can be overridden independently via `daemon.relay.publicUseTls` or `PASEO_RELAY_PUBLIC_USE_TLS`
|
||||||
|
|
||||||
See [SECURITY.md](../SECURITY.md) for the full threat model.
|
See [SECURITY.md](../SECURITY.md) for the full threat model.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ Single file, validated with `PersistedConfigSchema`.
|
|||||||
hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load
|
hostnames: true | string[], // legacy alias `allowedHosts` is migrated on load
|
||||||
mcp: { enabled: boolean, injectIntoAgents: boolean },
|
mcp: { enabled: boolean, injectIntoAgents: boolean },
|
||||||
cors: { allowedOrigins: string[] },
|
cors: { allowedOrigins: string[] },
|
||||||
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean },
|
relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean },
|
||||||
auth: { password: string } // bcrypt hash, optional
|
auth: { password: string } // bcrypt hash, optional
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { EventEmitter } from "node:events";
|
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 {
|
import {
|
||||||
type DaemonLaunchRuntime,
|
type DaemonLaunchRuntime,
|
||||||
type DetachedDaemonProcess,
|
type DetachedDaemonProcess,
|
||||||
|
resolveLocalDaemonState,
|
||||||
startLocalDaemonDetached,
|
startLocalDaemonDetached,
|
||||||
startLocalDaemonForeground,
|
startLocalDaemonForeground,
|
||||||
} from "./local-daemon.js";
|
} 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 {
|
function expectSupervisorLaunch(argv: string[]): void {
|
||||||
const joined = argv.join(" ");
|
const joined = argv.join(" ");
|
||||||
expect(joined).toContain("supervisor-entrypoint");
|
expect(joined).toContain("supervisor-entrypoint");
|
||||||
@@ -78,6 +93,12 @@ describe("local daemon launch supervision", () => {
|
|||||||
vi.useRealTimers();
|
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 () => {
|
test("foreground start spawns supervisor-entrypoint instead of server/index", async () => {
|
||||||
const runtime = new FakeDaemonRuntime();
|
const runtime = new FakeDaemonRuntime();
|
||||||
|
|
||||||
@@ -131,4 +152,24 @@ describe("local daemon launch supervision", () => {
|
|||||||
expect(launch?.args).toContain("--relay-use-tls");
|
expect(launch?.args).toContain("--relay-use-tls");
|
||||||
expect(launch?.options?.env?.PASEO_RELAY_USE_TLS).toBe("true");
|
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;
|
relayEnabled: boolean;
|
||||||
relayEndpoint: string;
|
relayEndpoint: string;
|
||||||
relayUseTls: boolean;
|
relayUseTls: boolean;
|
||||||
|
relayPublicUseTls: boolean;
|
||||||
logPath: string;
|
logPath: string;
|
||||||
pidPath: string;
|
pidPath: string;
|
||||||
pidInfo: LocalDaemonPidInfo | null;
|
pidInfo: LocalDaemonPidInfo | null;
|
||||||
@@ -409,6 +410,7 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
|
|||||||
relayEnabled: config.relayEnabled ?? true,
|
relayEnabled: config.relayEnabled ?? true,
|
||||||
relayEndpoint: config.relayPublicEndpoint ?? config.relayEndpoint ?? "relay.paseo.sh:443",
|
relayEndpoint: config.relayPublicEndpoint ?? config.relayEndpoint ?? "relay.paseo.sh:443",
|
||||||
relayUseTls: config.relayUseTls ?? false,
|
relayUseTls: config.relayUseTls ?? false,
|
||||||
|
relayPublicUseTls: config.relayPublicUseTls ?? config.relayUseTls ?? false,
|
||||||
logPath,
|
logPath,
|
||||||
pidPath,
|
pidPath,
|
||||||
pidInfo,
|
pidInfo,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
|
|||||||
relayEndpoint: config.relayEndpoint,
|
relayEndpoint: config.relayEndpoint,
|
||||||
relayPublicEndpoint: config.relayPublicEndpoint,
|
relayPublicEndpoint: config.relayPublicEndpoint,
|
||||||
relayUseTls: config.relayUseTls,
|
relayUseTls: config.relayUseTls,
|
||||||
|
relayPublicUseTls: config.relayPublicUseTls,
|
||||||
appBaseUrl: config.appBaseUrl,
|
appBaseUrl: config.appBaseUrl,
|
||||||
includeQr: true,
|
includeQr: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ async function resolveDaemonNodeLabel(
|
|||||||
|
|
||||||
function formatRelayStatus(state: ReturnType<typeof resolveLocalDaemonState>): string {
|
function formatRelayStatus(state: ReturnType<typeof resolveLocalDaemonState>): string {
|
||||||
if (!state.relayEnabled) return "disabled";
|
if (!state.relayEnabled) return "disabled";
|
||||||
const scheme = state.relayUseTls ? "wss" : "ws";
|
const scheme = state.relayPublicUseTls ? "wss" : "ws";
|
||||||
return `${scheme}://${state.relayEndpoint}`;
|
return `${scheme}://${state.relayEndpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -492,6 +492,7 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
|
|||||||
relayEndpoint: config.relayEndpoint,
|
relayEndpoint: config.relayEndpoint,
|
||||||
relayPublicEndpoint: config.relayPublicEndpoint,
|
relayPublicEndpoint: config.relayPublicEndpoint,
|
||||||
relayUseTls: config.relayUseTls,
|
relayUseTls: config.relayUseTls,
|
||||||
|
relayPublicUseTls: config.relayPublicUseTls,
|
||||||
appBaseUrl: config.appBaseUrl,
|
appBaseUrl: config.appBaseUrl,
|
||||||
includeQr: true,
|
includeQr: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ export interface PaseoDaemonConfig {
|
|||||||
relayEndpoint?: string;
|
relayEndpoint?: string;
|
||||||
relayPublicEndpoint?: string;
|
relayPublicEndpoint?: string;
|
||||||
relayUseTls?: boolean;
|
relayUseTls?: boolean;
|
||||||
|
relayPublicUseTls?: boolean;
|
||||||
appBaseUrl?: string;
|
appBaseUrl?: string;
|
||||||
auth?: DaemonAuthConfig;
|
auth?: DaemonAuthConfig;
|
||||||
openai?: PaseoOpenAIConfig;
|
openai?: PaseoOpenAIConfig;
|
||||||
@@ -300,7 +301,9 @@ export async function createPaseoDaemon(
|
|||||||
const staticDir = config.staticDir;
|
const staticDir = config.staticDir;
|
||||||
const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
|
const downloadTokenTtlMs = config.downloadTokenTtlMs ?? 60000;
|
||||||
|
|
||||||
const downloadTokenStore = new DownloadTokenStore({ ttlMs: downloadTokenTtlMs });
|
const downloadTokenStore = new DownloadTokenStore({
|
||||||
|
ttlMs: downloadTokenTtlMs,
|
||||||
|
});
|
||||||
|
|
||||||
const listenTarget = parseListenString(config.listen);
|
const listenTarget = parseListenString(config.listen);
|
||||||
|
|
||||||
@@ -863,6 +866,7 @@ export async function createPaseoDaemon(
|
|||||||
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
const relayEndpoint = config.relayEndpoint ?? "relay.paseo.sh:443";
|
||||||
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
const relayPublicEndpoint = config.relayPublicEndpoint ?? relayEndpoint;
|
||||||
const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
const relayUseTls = config.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
||||||
|
const relayPublicUseTls = config.relayPublicUseTls ?? relayUseTls;
|
||||||
const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
|
const appBaseUrl = config.appBaseUrl ?? "https://app.paseo.sh";
|
||||||
|
|
||||||
if (boundListenTarget.type === "tcp") {
|
if (boundListenTarget.type === "tcp") {
|
||||||
@@ -938,7 +942,10 @@ export async function createPaseoDaemon(
|
|||||||
const offer = await createConnectionOfferV2({
|
const offer = await createConnectionOfferV2({
|
||||||
serverId,
|
serverId,
|
||||||
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
||||||
relay: { endpoint: relayPublicEndpoint, useTls: relayUseTls },
|
relay: {
|
||||||
|
endpoint: relayPublicEndpoint,
|
||||||
|
useTls: relayPublicUseTls,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
||||||
|
|||||||
@@ -44,7 +44,42 @@ describe("daemon relay config", () => {
|
|||||||
});
|
});
|
||||||
expect(loadConfig(envHome, { env: { PASEO_RELAY_USE_TLS: "true" } }).relayUseTls).toBe(true);
|
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);
|
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;
|
endpoint: string;
|
||||||
publicEndpoint: string;
|
publicEndpoint: string;
|
||||||
useTls: boolean;
|
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 {
|
function resolveRelayConfig(input: ResolveRelayInput): ResolvedRelay {
|
||||||
@@ -166,10 +178,17 @@ function resolveRelayConfig(input: ResolveRelayInput): ResolvedRelay {
|
|||||||
endpoint;
|
endpoint;
|
||||||
const useTls =
|
const useTls =
|
||||||
input.cliRelayUseTls ??
|
input.cliRelayUseTls ??
|
||||||
(input.env.PASEO_RELAY_USE_TLS !== undefined
|
resolveTlsFromEnv(
|
||||||
? (parseBooleanEnv(input.env.PASEO_RELAY_USE_TLS) ?? false)
|
input.env.PASEO_RELAY_USE_TLS,
|
||||||
: (input.persisted.daemon?.relay?.useTls ?? endpoint === DEFAULT_RELAY_ENDPOINT));
|
input.persisted.daemon?.relay?.useTls,
|
||||||
return { enabled, endpoint, publicEndpoint, 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 {
|
interface ResolvedVoiceLlm {
|
||||||
@@ -305,6 +324,7 @@ export function loadConfig(
|
|||||||
relayEndpoint: relay.endpoint,
|
relayEndpoint: relay.endpoint,
|
||||||
relayPublicEndpoint: relay.publicEndpoint,
|
relayPublicEndpoint: relay.publicEndpoint,
|
||||||
relayUseTls: relay.useTls,
|
relayUseTls: relay.useTls,
|
||||||
|
relayPublicUseTls: relay.publicUseTls,
|
||||||
appBaseUrl,
|
appBaseUrl,
|
||||||
auth: resolveAuthConfig(env, persisted),
|
auth: resolveAuthConfig(env, persisted),
|
||||||
openai,
|
openai,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export async function generateLocalPairingOffer(args: {
|
|||||||
relayEndpoint?: string;
|
relayEndpoint?: string;
|
||||||
relayPublicEndpoint?: string;
|
relayPublicEndpoint?: string;
|
||||||
relayUseTls?: boolean;
|
relayUseTls?: boolean;
|
||||||
|
relayPublicUseTls?: boolean;
|
||||||
appBaseUrl?: string;
|
appBaseUrl?: string;
|
||||||
includeQr?: boolean;
|
includeQr?: boolean;
|
||||||
logger?: Logger;
|
logger?: Logger;
|
||||||
@@ -33,13 +34,14 @@ export async function generateLocalPairingOffer(args: {
|
|||||||
const relayEndpoint = args.relayEndpoint ?? "relay.paseo.sh:443";
|
const relayEndpoint = args.relayEndpoint ?? "relay.paseo.sh:443";
|
||||||
const relayPublicEndpoint = args.relayPublicEndpoint ?? relayEndpoint;
|
const relayPublicEndpoint = args.relayPublicEndpoint ?? relayEndpoint;
|
||||||
const relayUseTls = args.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
const relayUseTls = args.relayUseTls ?? relayEndpoint === "relay.paseo.sh:443";
|
||||||
|
const relayPublicUseTls = args.relayPublicUseTls ?? relayUseTls;
|
||||||
const appBaseUrl = args.appBaseUrl ?? "https://app.paseo.sh";
|
const appBaseUrl = args.appBaseUrl ?? "https://app.paseo.sh";
|
||||||
const serverId = getOrCreateServerId(args.paseoHome, { logger: args.logger });
|
const serverId = getOrCreateServerId(args.paseoHome, { logger: args.logger });
|
||||||
const daemonKeyPair = await loadOrCreateDaemonKeyPair(args.paseoHome, args.logger);
|
const daemonKeyPair = await loadOrCreateDaemonKeyPair(args.paseoHome, args.logger);
|
||||||
const offer = await createConnectionOfferV2({
|
const offer = await createConnectionOfferV2({
|
||||||
serverId,
|
serverId,
|
||||||
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
daemonPublicKeyB64: daemonKeyPair.publicKeyB64,
|
||||||
relay: { endpoint: relayPublicEndpoint, useTls: relayUseTls },
|
relay: { endpoint: relayPublicEndpoint, useTls: relayPublicUseTls },
|
||||||
});
|
});
|
||||||
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
const url = encodeOfferToFragmentUrl({ offer, appBaseUrl });
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ export const PersistedConfigSchema = z
|
|||||||
endpoint: z.string().optional(),
|
endpoint: z.string().optional(),
|
||||||
publicEndpoint: z.string().optional(),
|
publicEndpoint: z.string().optional(),
|
||||||
useTls: z.boolean().optional(),
|
useTls: z.boolean().optional(),
|
||||||
|
publicUseTls: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -387,7 +388,9 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per
|
|||||||
raw = readFileSync(configPath, "utf-8");
|
raw = readFileSync(configPath, "utf-8");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(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;
|
let parsed: unknown;
|
||||||
@@ -395,7 +398,9 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per
|
|||||||
parsed = JSON.parse(raw);
|
parsed = JSON.parse(raw);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(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);
|
const migrated = stripDeprecatedLocalSpeechConfigFields(parsed);
|
||||||
@@ -432,6 +437,8 @@ export function savePersistedConfig(
|
|||||||
log?.info(`Saved to ${configPath}`);
|
log?.info(`Saved to ${configPath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(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