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:
@@ -0,0 +1,192 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
import { startIsolatedHostDaemon, type IsolatedHostDaemon } from "./helpers/isolated-host-daemon";
|
||||
import { connectDaemonClient } from "./helpers/daemon-client-loader";
|
||||
import {
|
||||
closePairDeviceModal,
|
||||
declineRelay,
|
||||
enableRelayAndExpectFailure,
|
||||
enableRelayAndExpectOffer,
|
||||
expectDaemonPidUnchanged,
|
||||
expectPairingOffer,
|
||||
expectPairingDisconnected,
|
||||
expectRelayUpdateRequired,
|
||||
expectRelayConsent,
|
||||
openPairDeviceModal,
|
||||
openPairDeviceFromHome,
|
||||
openRelaySecurityDocs,
|
||||
observePairingOfferRequests,
|
||||
prepareLocalPairingHost,
|
||||
preparePairingHost,
|
||||
reloadAndOpenPairDevice,
|
||||
retryRelayAndExpectFailure,
|
||||
switchPairDeviceToHost,
|
||||
} from "./helpers/pair-device";
|
||||
|
||||
interface RelayConfigDaemonClient {
|
||||
close(): Promise<void>;
|
||||
connect(): Promise<void>;
|
||||
patchDaemonConfig(config: { relay: { enabled: boolean } }): Promise<unknown>;
|
||||
}
|
||||
|
||||
test.describe("local device relay pairing", () => {
|
||||
let relayOffDaemon: IsolatedHostDaemon;
|
||||
let relayEnableDaemon: IsolatedHostDaemon;
|
||||
let relayOverrideDaemon: IsolatedHostDaemon;
|
||||
let relayEnabledDaemon: IsolatedHostDaemon;
|
||||
let invalidRelayDaemon: IsolatedHostDaemon;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
[
|
||||
relayOffDaemon,
|
||||
relayEnableDaemon,
|
||||
relayOverrideDaemon,
|
||||
relayEnabledDaemon,
|
||||
invalidRelayDaemon,
|
||||
] = await Promise.all([
|
||||
startIsolatedHostDaemon("pair-device-relay-off", {
|
||||
mutableRelay: { enabled: false },
|
||||
}),
|
||||
startIsolatedHostDaemon("pair-device-relay-enable", {
|
||||
mutableRelay: { enabled: false },
|
||||
}),
|
||||
startIsolatedHostDaemon("pair-device-relay-override"),
|
||||
startIsolatedHostDaemon("pair-device-relay-enabled", {
|
||||
mutableRelay: { enabled: true },
|
||||
}),
|
||||
startIsolatedHostDaemon("pair-device-relay-invalid", {
|
||||
mutableRelay: { enabled: false, endpoint: "invalid-endpoint" },
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await Promise.all([
|
||||
relayOffDaemon.close(),
|
||||
relayEnableDaemon.close(),
|
||||
relayOverrideDaemon.close(),
|
||||
relayEnabledDaemon.close(),
|
||||
invalidRelayDaemon.close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test("asks for consent and creates no QR when declined", async ({ page }) => {
|
||||
await prepareLocalPairingHost(page, relayOffDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectRelayConsent(page);
|
||||
await declineRelay(page);
|
||||
await openPairDeviceModal(page);
|
||||
await expectRelayConsent(page);
|
||||
});
|
||||
|
||||
test("opens relay security documentation through the desktop opener", async ({ page }) => {
|
||||
await prepareLocalPairingHost(page, relayOffDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await openRelaySecurityDocs(page);
|
||||
});
|
||||
|
||||
test("opens the same relay consent dialog from the home screen", async ({ page }) => {
|
||||
await prepareLocalPairingHost(page, relayOffDaemon);
|
||||
await openPairDeviceFromHome(page);
|
||||
const modal = page.getByTestId("open-project-pair-device-modal");
|
||||
await expect(modal.getByText("Enable relay?", { exact: true })).toBeVisible();
|
||||
await expect(modal.getByRole("button", { name: "Enable relay", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("opens relay consent in browser web", async ({ page }) => {
|
||||
await preparePairingHost(page, relayOffDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectRelayConsent(page);
|
||||
});
|
||||
|
||||
test("shows an actionable error when the daemon disconnects", async ({ page }) => {
|
||||
const daemon = await startIsolatedHostDaemon("pair-device-disconnect", {
|
||||
mutableRelay: { enabled: false },
|
||||
});
|
||||
try {
|
||||
await prepareLocalPairingHost(page, daemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectRelayConsent(page);
|
||||
await daemon.close();
|
||||
await expectPairingDisconnected(page);
|
||||
} finally {
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("pairs a device through the selected remote host", async ({ page }) => {
|
||||
await prepareLocalPairingHost(page, relayOffDaemon, [
|
||||
{
|
||||
serverId: relayEnabledDaemon.serverId,
|
||||
label: "Remote pairing host",
|
||||
endpoint: `127.0.0.1:${relayEnabledDaemon.port}`,
|
||||
},
|
||||
]);
|
||||
await switchPairDeviceToHost(page, relayEnabledDaemon.serverId);
|
||||
await openPairDeviceModal(page);
|
||||
await expectPairingOffer(page);
|
||||
});
|
||||
|
||||
test("enables relay live and keeps the saved offer after reload", async ({ page }) => {
|
||||
const daemonPid = relayEnableDaemon.getPid();
|
||||
await prepareLocalPairingHost(page, relayEnableDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await enableRelayAndExpectOffer(page);
|
||||
expectDaemonPidUnchanged(daemonPid, relayEnableDaemon.getPid());
|
||||
await closePairDeviceModal(page);
|
||||
await reloadAndOpenPairDevice(page);
|
||||
await expectPairingOffer(page);
|
||||
});
|
||||
|
||||
test("keeps consent actionable when a launch override rejects enable", async ({ page }) => {
|
||||
await prepareLocalPairingHost(page, relayOverrideDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectRelayConsent(page);
|
||||
await enableRelayAndExpectFailure(page);
|
||||
await retryRelayAndExpectFailure(page);
|
||||
});
|
||||
|
||||
test("keeps consent actionable when relay transport startup fails", async ({ page }) => {
|
||||
await prepareLocalPairingHost(page, invalidRelayDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectRelayConsent(page);
|
||||
await enableRelayAndExpectFailure(page, "Invalid host:port");
|
||||
await retryRelayAndExpectFailure(page, "Invalid host:port");
|
||||
});
|
||||
|
||||
test("shows an offer immediately when relay is already enabled", async ({ page }) => {
|
||||
await prepareLocalPairingHost(page, relayEnabledDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectPairingOffer(page);
|
||||
});
|
||||
|
||||
test("refreshes the mounted offer when another client disables relay", async ({ page }) => {
|
||||
const daemon = await startIsolatedHostDaemon("pair-device-live-config-push", {
|
||||
mutableRelay: { enabled: true },
|
||||
});
|
||||
const client = await connectDaemonClient<RelayConfigDaemonClient>({
|
||||
clientIdPrefix: "pair-device-live-config-push",
|
||||
port: daemon.port,
|
||||
});
|
||||
try {
|
||||
await prepareLocalPairingHost(page, daemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectPairingOffer(page);
|
||||
await client.patchDaemonConfig({ relay: { enabled: false } });
|
||||
await expectRelayConsent(page);
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("asks for a host update when live relay config is unsupported", async ({
|
||||
page,
|
||||
relayConfigOutdatedDaemon,
|
||||
}) => {
|
||||
const expectNoUnsupportedRequest = observePairingOfferRequests(page, relayConfigOutdatedDaemon);
|
||||
await prepareLocalPairingHost(page, relayConfigOutdatedDaemon);
|
||||
await openPairDeviceModal(page);
|
||||
await expectRelayUpdateRequired(page);
|
||||
expectNoUnsupportedRequest();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user