Make relay opt-in when pairing devices (#2706)

* feat: make relay opt-in for device pairing

* fix(relay): keep pairing state authoritative

* fix(relay): refresh pairing state across transports

* fix(onboard): honor explicit relay opt-out

* fix(relay): surface transport startup failures

* fix(pairing): gate offers for older daemons

* test(cli): rely on real pairing transport coverage

* fix(cli): report live relay without a pid file

* fix(relay): synchronize config during daemon startup

* fix(relay): preserve mutability for ignored overrides

* fix(cli): use reachable daemon for pairing

* fix(app): redirect invalid pair device routes

* fix(pairing): support remote host device pairing

Keep Pair Device host-relative through the existing settings and daemon RPC flow. Route CLI output through an injected adapter so workflow tests do not mutate process globals.

* fix(pairing): validate pairing destinations

Expose the host-relative Pair Device flow on every supported app platform. Reject a reachable daemon whose identity does not match the requested Paseo home before reading or mutating its pairing state.

* fix(pairing): render QR codes across platforms

Generate SVG text without DOM or canvas and render it through the existing cross-platform SVG stack.

* fix(cli): pair from persisted relay state

Allow explicit relay pairing while the daemon is stopped when relay is already enabled in persisted configuration.
This commit is contained in:
Mohamed Boudra
2026-08-01 04:35:06 +08:00
committed by GitHub
parent 3778d0ad7b
commit d2de309fc3
73 changed files with 2163 additions and 543 deletions
@@ -78,12 +78,6 @@ interface DesktopDaemonLogs {
contents: string;
}
interface DesktopPairingOffer {
relayEnabled: boolean;
url: string | null;
qr: string | null;
}
function parseReleaseChannel(
args: Record<string, unknown> | undefined,
): AppReleaseChannel | undefined {
@@ -219,18 +213,6 @@ function logDesktopDaemonLifecycle(message: string, details?: Record<string, unk
});
}
function toTrimmedString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function resolveDesktopAppVersion(): string {
if (app.isPackaged) {
return app.getVersion();
@@ -509,36 +491,6 @@ async function getCliDaemonStatus(): Promise<string> {
return await runExternalCliTextCommand(["daemon", "status"]);
}
async function getDaemonPairing(): Promise<DesktopPairingOffer> {
const status = await resolveDesktopDaemonStatus();
if (status.status !== "running") {
return {
relayEnabled: false,
url: null,
qr: null,
};
}
try {
const payload = await runExternalCliJsonCommand(["daemon", "pair", "--json"]);
if (!isRecord(payload)) {
throw new Error("Daemon pairing response was not an object.");
}
return {
relayEnabled: payload.relayEnabled === true,
url: toTrimmedString(payload.url),
qr: toTrimmedString(payload.qr),
};
} catch {
return {
relayEnabled: false,
url: null,
qr: null,
};
}
}
async function getLocalDaemonVersion(): Promise<{ version: string | null; error: string | null }> {
const status = await resolveDesktopDaemonStatus();
if (status.status !== "running") {
@@ -573,7 +525,6 @@ export function createDaemonCommandHandlers(): Record<string, DesktopCommandHand
restart_desktop_daemon: () => restartDaemon(),
desktop_daemon_logs: () => getDaemonLogs(),
desktop_app_logs: () => getDesktopAppLogs(),
desktop_daemon_pairing: () => getDaemonPairing(),
desktop_get_system_idle_time: () => powerMonitor.getSystemIdleTime() * 1000,
cli_daemon_status: () => getCliDaemonStatus(),
write_attachment_base64: (args) => writeAttachmentBase64(args ?? {}),