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
+12
View File
@@ -1422,9 +1422,21 @@ export const ar: TranslationResources = {
loadingOffer: "جارٍ تحميل عرض الإقران...",
failedToLoadOffer: "فشل تحميل عرض الاقتران.",
relayDisabled: "لم يتم تمكين التتابع. تمكين التتابع لإقران جهاز.",
enableTitle: "تمكين التتابع؟",
enableDescription:
"يتيح التتابع لهذا الجهاز الاتصال من أي مكان. حركة مرور الإقران مشفرة من طرف إلى طرف.",
relayDocs: "كيفية عمل التتابع",
relayDocsAccessibility: "اقرأ كيفية عمل تتابع Paseo",
enableRelay: "تمكين التتابع",
enablingRelay: "جارٍ التمكين...",
notNow: "ليس الآن",
directConnectionHint:
"بدون التتابع، اتصل مباشرة عبر TCP أو Tailscale أو شبكة VPN أخرى. لن يتم إنشاء رمز QR.",
updateRequired: "حدّث المضيف لتمكين التتابع من Paseo Desktop.",
unavailable: "عرض الاقتران غير متاح.",
hint: "قم بمسح رمز QR هذا باستخدام Paseo على هاتفك، أو انسخ الرابط أدناه.",
qrUnavailable: "رمز QR غير متاح.",
qrAccessibility: "رمز QR للإقران",
retry: "أعد المحاولة",
copy: "ينسخ",
copied: "منقول",
+12
View File
@@ -1433,9 +1433,21 @@ export const en = {
loadingOffer: "Loading pairing offer...",
failedToLoadOffer: "Failed to load pairing offer.",
relayDisabled: "Relay is not enabled. Enable relay to pair a device.",
enableTitle: "Enable relay?",
enableDescription:
"Relay lets this device connect from anywhere. Pairing traffic is end-to-end encrypted.",
relayDocs: "How relay works",
relayDocsAccessibility: "Read how Paseo relay works",
enableRelay: "Enable relay",
enablingRelay: "Enabling...",
notNow: "Not now",
directConnectionHint:
"Without relay, connect directly over TCP, Tailscale, or another VPN. No QR code is created.",
updateRequired: "Update the host to enable relay from Paseo Desktop.",
unavailable: "Pairing offer unavailable.",
hint: "Scan this QR code with Paseo on your phone, or copy the link below.",
qrUnavailable: "QR code unavailable.",
qrAccessibility: "Pairing QR code",
retry: "Retry",
copy: "Copy",
copied: "Copied",
+12
View File
@@ -1465,9 +1465,21 @@ export const es: TranslationResources = {
loadingOffer: "Cargando oferta de maridaje...",
failedToLoadOffer: "No se pudo cargar la oferta de emparejamiento.",
relayDisabled: "El relé no está habilitado. Habilite el relé para emparejar un dispositivo.",
enableTitle: "¿Habilitar el relé?",
enableDescription:
"El relé permite conectar este dispositivo desde cualquier lugar. El tráfico de emparejamiento está cifrado de extremo a extremo.",
relayDocs: "Cómo funciona el relé",
relayDocsAccessibility: "Leer cómo funciona el relé de Paseo",
enableRelay: "Habilitar relé",
enablingRelay: "Habilitando...",
notNow: "Ahora no",
directConnectionHint:
"Sin relé, conéctese directamente por TCP, Tailscale u otra VPN. No se crea ningún código QR.",
updateRequired: "Actualice el host para habilitar el relé desde Paseo Desktop.",
unavailable: "Oferta de maridaje no disponible.",
hint: "Escanee este códigoQRconPaseoen su teléfono o copie el enlace a continuación.",
qrUnavailable: "CódigoQRno disponible.",
qrAccessibility: "Código QR de emparejamiento",
retry: "Rever",
copy: "Copiar",
copied: "Copiado",
+12
View File
@@ -1469,9 +1469,21 @@ export const fr: TranslationResources = {
loadingOffer: "Chargement de l'offre d'association...",
failedToLoadOffer: "Échec du chargement de l'offre d'association.",
relayDisabled: "Le relais n'est pas activé. Activer le relais pour coupler un appareil.",
enableTitle: "Activer le relais ?",
enableDescription:
"Le relais permet à cet appareil de se connecter depuis n’importe où. Le trafic de couplage est chiffré de bout en bout.",
relayDocs: "Fonctionnement du relais",
relayDocsAccessibility: "Lire comment fonctionne le relais Paseo",
enableRelay: "Activer le relais",
enablingRelay: "Activation...",
notNow: "Pas maintenant",
directConnectionHint:
"Sans relais, connectez-vous directement via TCP, Tailscale ou un autre VPN. Aucun code QR n’est créé.",
updateRequired: "Mettez à jour l’hôte pour activer le relais depuis Paseo Desktop.",
unavailable: "Offre de jumelage indisponible.",
hint: "Scannez ce codeQRavecPaseosur votre téléphone ou copiez le lien ci-dessous.",
qrUnavailable: "CodeQRindisponible.",
qrAccessibility: "Code QR de couplage",
retry: "Réessayer",
copy: "Copie",
copied: "Copié",
+12
View File
@@ -1438,9 +1438,21 @@ export const ja: TranslationResources = {
failedToLoadOffer: "ペアリングオファーの読み込みに失敗しました。",
relayDisabled:
"リレーが有効になっていません。デバイスをペアリングするにはリレーを有効にしてください。",
enableTitle: "リレーを有効にしますか?",
enableDescription:
"リレーを使うと、このデバイスからどこでも接続できます。ペアリング通信はエンドツーエンドで暗号化されます。",
relayDocs: "リレーの仕組み",
relayDocsAccessibility: "Paseo リレーの仕組みを読む",
enableRelay: "リレーを有効にする",
enablingRelay: "有効化中...",
notNow: "今はしない",
directConnectionHint:
"リレーを使わない場合は、TCP、Tailscale、または別の VPN で直接接続してください。QR コードは作成されません。",
updateRequired: "Paseo Desktop からリレーを有効にするにはホストを更新してください。",
unavailable: "ペアリングオファーが利用できません。",
hint: "スマートフォンのPaseoでこのQRコードをスキャンするか、以下のリンクをコピーしてください。",
qrUnavailable: "QRコードが利用できません。",
qrAccessibility: "ペアリング QR コード",
retry: "再試行",
copy: "コピー",
copied: "コピーしました",
+12
View File
@@ -1451,9 +1451,21 @@ export const ptBR: TranslationResources = {
loadingOffer: "Carregando oferta de pareamento...",
failedToLoadOffer: "Falha ao carregar oferta de pareamento.",
relayDisabled: "Relay não está ativado. Ative o relay para parear um dispositivo.",
enableTitle: "Ativar o relay?",
enableDescription:
"O relay permite conectar este dispositivo de qualquer lugar. O tráfego de pareamento é criptografado de ponta a ponta.",
relayDocs: "Como o relay funciona",
relayDocsAccessibility: "Leia como o relay do Paseo funciona",
enableRelay: "Ativar relay",
enablingRelay: "Ativando...",
notNow: "Agora não",
directConnectionHint:
"Sem relay, conecte diretamente por TCP, Tailscale ou outra VPN. Nenhum código QR é criado.",
updateRequired: "Atualize o host para ativar o relay pelo Paseo Desktop.",
unavailable: "Oferta de pareamento indisponível.",
hint: "Escaneie este QR code com o Paseo no seu celular ou copie o link abaixo.",
qrUnavailable: "QR code indisponível.",
qrAccessibility: "Código QR de pareamento",
retry: "Tentar novamente",
copy: "Copiar",
copied: "Copiado",
+12
View File
@@ -1456,9 +1456,21 @@ export const ru: TranslationResources = {
loadingOffer: "Загрузка предложения по сопряжению...",
failedToLoadOffer: "Не удалось загрузить предложение сопряжения.",
relayDisabled: "Реле не включено. Включите реле для сопряжения устройства.",
enableTitle: "Включить реле?",
enableDescription:
"Реле позволяет подключаться с этого устройства откуда угодно. Трафик сопряжения защищён сквозным шифрованием.",
relayDocs: "Как работает реле",
relayDocsAccessibility: "Прочитать, как работает реле Paseo",
enableRelay: "Включить реле",
enablingRelay: "Включение...",
notNow: "Не сейчас",
directConnectionHint:
"Без реле подключайтесь напрямую через TCP, Tailscale или другую VPN. QR-код не создаётся.",
updateRequired: "Обновите хост, чтобы включить реле из Paseo Desktop.",
unavailable: "Предложение по сопряжению недоступно.",
hint: "Отсканируйте этот код QR с помощью Paseo на своем телефоне или скопируйте ссылку ниже.",
qrUnavailable: "Код QR недоступен.",
qrAccessibility: "QR-код сопряжения",
retry: "Повторить попытку",
copy: "Копировать",
copied: "Скопировано",
+11
View File
@@ -1403,9 +1403,20 @@ export const zhCN: TranslationResources = {
loadingOffer: "正在加载配对 offer...",
failedToLoadOffer: "加载配对 offer 失败。",
relayDisabled: "Relay 未启用。启用 relay 后才能配对设备。",
enableTitle: "启用中继?",
enableDescription: "中继让此设备可以从任何地方连接。配对流量采用端到端加密。",
relayDocs: "中继如何工作",
relayDocsAccessibility: "阅读 Paseo 中继的工作原理",
enableRelay: "启用中继",
enablingRelay: "正在启用...",
notNow: "暂不",
directConnectionHint:
"不使用中继时,请通过 TCP、Tailscale 或其他 VPN 直接连接。不会生成二维码。",
updateRequired: "请更新主机,以便从 Paseo Desktop 启用中继。",
unavailable: "配对 offer 不可用。",
hint: "用手机上的 Paseo 扫描此二维码,或复制下方链接。",
qrUnavailable: "二维码不可用。",
qrAccessibility: "配对二维码",
retry: "重试",
copy: "复制",
copied: "已复制",