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,3 @@
|
||||
export function daemonPairingOfferQueryKey(serverId: string) {
|
||||
return ["daemon-pairing-offer", serverId] as const;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { MutableDaemonConfig, SessionOutboundMessage } from "@getpaseo/prot
|
||||
import { checkoutDiffQueryKey } from "@/git/query-keys";
|
||||
import { buildTerminalsQueryKey } from "@/screens/workspace/terminals/state";
|
||||
import { daemonConfigQueryKey } from "@/data/daemon-config";
|
||||
import { daemonPairingOfferQueryKey } from "@/data/daemon-pairing";
|
||||
import { providersSnapshotQueryKey } from "@/data/providers-snapshot";
|
||||
import {
|
||||
checkoutDiffPushRoute,
|
||||
@@ -34,6 +35,7 @@ type RouterHandler = (message: RouterMessage) => void;
|
||||
type RouterClient = Parameters<typeof mountServerDataPushRouter>[0]["client"];
|
||||
|
||||
const daemonConfig: MutableDaemonConfig = {
|
||||
relay: { enabled: false },
|
||||
mcp: { injectIntoAgents: true },
|
||||
browserTools: { enabled: false },
|
||||
providers: {},
|
||||
@@ -145,6 +147,8 @@ describe("server data push router", () => {
|
||||
const queryClient = new QueryClient();
|
||||
const fake = createFakeClient();
|
||||
const serverId = "server-1";
|
||||
const pairingOfferKey = daemonPairingOfferQueryKey(serverId);
|
||||
queryClient.setQueryData(pairingOfferKey, { relayEnabled: true, url: "https://pairing" });
|
||||
const unmount = mountServerDataPushRouter({ client: fake.client, queryClient, serverId });
|
||||
|
||||
fake.emit(providerUpdate("2026-01-01T00:00:00.000Z"));
|
||||
@@ -159,6 +163,7 @@ describe("server data push router", () => {
|
||||
requestId: "providers_snapshot_update",
|
||||
});
|
||||
expect(queryClient.getQueryData(daemonConfigQueryKey(serverId))).toEqual(daemonConfig);
|
||||
expect(queryClient.getQueryState(pairingOfferKey)?.isInvalidated).toBe(true);
|
||||
|
||||
unmount();
|
||||
fake.emit(providerUpdate("2026-01-01T00:00:01.000Z"));
|
||||
@@ -502,12 +507,14 @@ describe("server data push router", () => {
|
||||
const otherServerId = "server-2";
|
||||
const providerKey = providersSnapshotQueryKey(serverId);
|
||||
const daemonConfigKey = daemonConfigQueryKey(serverId);
|
||||
const pairingOfferKey = daemonPairingOfferQueryKey(serverId);
|
||||
const diffKey = checkoutDiffQueryKey(serverId, "/repo", "uncommitted", undefined, false);
|
||||
const terminalKey = buildTerminalsQueryKey(serverId, "/repo", "workspace-a");
|
||||
const otherProviderKey = providersSnapshotQueryKey(otherServerId);
|
||||
|
||||
queryClient.setQueryData(providerKey, { entries: [], generatedAt: "now", requestId: "p" });
|
||||
queryClient.setQueryData(daemonConfigKey, daemonConfig);
|
||||
queryClient.setQueryData(pairingOfferKey, { relayEnabled: false, url: "" });
|
||||
queryClient.setQueryData(diffKey, { cwd: "/repo", files: [], error: null, requestId: "d" });
|
||||
queryClient.setQueryData(terminalKey, { cwd: "/repo", terminals: [], requestId: "t" });
|
||||
queryClient.setQueryData(otherProviderKey, {
|
||||
@@ -520,6 +527,7 @@ describe("server data push router", () => {
|
||||
|
||||
expect(queryClient.getQueryState(providerKey)?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(daemonConfigKey)?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(pairingOfferKey)?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(diffKey)?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(terminalKey)?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(otherProviderKey)?.isInvalidated).toBe(false);
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import { agentCommandsQueryRoot } from "@/hooks/agent-commands-query";
|
||||
import { orderCheckoutDiffFiles } from "@/git/diff-order";
|
||||
import { daemonConfigQueryKey } from "@/data/daemon-config";
|
||||
import { daemonPairingOfferQueryKey } from "@/data/daemon-pairing";
|
||||
import { providersSnapshotQueryKey, providersSnapshotQueryRoot } from "@/data/providers-snapshot";
|
||||
|
||||
type ProvidersSnapshotUpdateMessage = Extract<
|
||||
@@ -105,6 +106,12 @@ const RECONNECT_REPAIR_POLICIES: ReconnectRepairPolicy[] = [
|
||||
void queryClient.invalidateQueries({ queryKey: daemonConfigQueryKey(serverId) });
|
||||
},
|
||||
},
|
||||
{
|
||||
domain: "daemonPairingOffer",
|
||||
invalidate: ({ queryClient, serverId }) => {
|
||||
void queryClient.invalidateQueries({ queryKey: daemonPairingOfferQueryKey(serverId) });
|
||||
},
|
||||
},
|
||||
{
|
||||
domain: "checkoutDiff",
|
||||
invalidate: ({ queryClient, serverId }) => {
|
||||
@@ -403,6 +410,9 @@ function applyDaemonConfigStatus(input: {
|
||||
daemonConfigQueryKey(input.serverId),
|
||||
payload.config,
|
||||
);
|
||||
void input.queryClient.invalidateQueries({
|
||||
queryKey: daemonPairingOfferQueryKey(input.serverId),
|
||||
});
|
||||
}
|
||||
|
||||
function applyCheckoutDiffUpdate(input: {
|
||||
|
||||
Reference in New Issue
Block a user