diff --git a/packages/app/e2e/browser/settings-navigation.spec.ts b/packages/app/e2e/browser/settings-navigation.spec.ts index 3657f2293..5a3624f06 100644 --- a/packages/app/e2e/browser/settings-navigation.spec.ts +++ b/packages/app/e2e/browser/settings-navigation.spec.ts @@ -114,16 +114,6 @@ test.describe("Settings sidebar navigation", () => { await expectDirectHostUriHidden(page); }); - test("browser web hides direct connection custom headers", async ({ page }) => { - await gotoAppShell(page); - await openSettings(page); - await openAddHostFlow(page); - await selectHostConnectionType(page, "direct"); - await toggleHostAdvanced(page); - - await expect(page.getByTestId("direct-header-add")).toHaveCount(0); - }); - test("sidebar shows a Back to workspace row that leaves /settings", async ({ page }) => { await gotoAppShell(page); await openSettings(page); diff --git a/packages/app/e2e/support/helpers/pair-device.ts b/packages/app/e2e/support/helpers/pair-device.ts index b1c15de82..a454cdf2d 100644 --- a/packages/app/e2e/support/helpers/pair-device.ts +++ b/packages/app/e2e/support/helpers/pair-device.ts @@ -17,6 +17,49 @@ interface PairingHostInput { endpoint: string; } +export async function prepareLocalPairingHost( + page: Page, + daemon: IsolatedHostDaemon | OutdatedDaemon, + additionalHosts: PairingHostInput[] = [], +): Promise { + await page.addInitScript((localServerId) => { + (window as unknown as { paseoDesktop: unknown }).paseoDesktop = { + platform: "darwin", + invoke: async (command: string) => { + if (command === "desktop_daemon_status") { + return { + serverId: localServerId, + status: "running", + listen: null, + hostname: null, + pid: null, + home: "", + version: null, + desktopManaged: true, + error: null, + }; + } + if (command === "get_desktop_settings") { + return { + releaseChannel: "stable", + daemon: { manageBuiltInDaemon: false, keepRunningAfterQuit: true }, + }; + } + return null; + }, + getPendingOpenProject: async () => null, + events: { on: async () => () => undefined }, + opener: { + openUrl: async (url: string) => { + localStorage.setItem("@paseo:e2e-opened-url", url); + }, + }, + }; + }, daemon.serverId); + + await preparePairingHost(page, daemon, additionalHosts); +} + export async function preparePairingHost( page: Page, daemon: IsolatedHostDaemon | OutdatedDaemon, diff --git a/packages/app/src/components/add-host-modal.tsx b/packages/app/src/components/add-host-modal.tsx index dc7ef43d6..6bd72e2ad 100644 --- a/packages/app/src/components/add-host-modal.tsx +++ b/packages/app/src/components/add-host-modal.tsx @@ -1,10 +1,9 @@ -import { useCallback, useMemo, useReducer, useRef, useState } from "react"; +import { useCallback, useMemo, useReducer, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert, Pressable, Text, View } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor } from "@/constants/layout"; -import { getIsElectron, isNative } from "@/constants/platform"; -import { Check, ChevronDown, ChevronRight, Eye, EyeOff, Link2, Plus, X } from "lucide-react-native"; +import { Check, ChevronDown, ChevronRight, Eye, EyeOff, Link2 } from "lucide-react-native"; import type { HostProfile } from "@/types/host-connection"; import { useHosts, useHostMutations } from "@/runtime/host-runtime"; import { @@ -13,11 +12,6 @@ import { serializeConnectionUriForStorage, } from "@/utils/daemon-endpoints"; import { DaemonConnectionTestError } from "@/utils/test-daemon-connection"; -import { - prepareConnectionHeaders, - type ConnectionHeaderDraft, - type ConnectionHeaderValidationIssue, -} from "@/utils/connection-headers"; import { AdaptiveModalSheet, AdaptiveTextInput, type SheetHeader } from "./adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; @@ -129,30 +123,6 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.medium, }, - advancedContent: { - gap: theme.spacing[4], - }, - headerSection: { - gap: theme.spacing[2], - }, - headerTitleRow: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - gap: theme.spacing[3], - }, - headerList: { - gap: theme.spacing[2], - }, - headerRow: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - }, - headerInput: { - flex: 1, - minWidth: 0, - }, actions: { flexDirection: "row", gap: theme.spacing[3], @@ -211,94 +181,6 @@ function prepareDirectConnection( }; } -function headerIssueMessage( - issue: ConnectionHeaderValidationIssue, - t: ReturnType["t"], -): string { - if (issue.type === "missingName") return t("pairing.direct.headers.errors.missingName"); - if (issue.type === "invalidName") { - return t("pairing.direct.headers.errors.invalidName", { name: issue.name }); - } - if (issue.type === "invalidValue") { - return t("pairing.direct.headers.errors.invalidValue", { name: issue.name }); - } - return t("pairing.direct.headers.errors.duplicateName", { name: issue.name }); -} - -interface ConnectionHeaderRowProps { - draft: ConnectionHeaderDraft; - index: number; - disabled: boolean; - placeholderTextColor: string; - iconColor: string; - onUpdate: (id: number, field: "name" | "value", value: string) => void; - onRemove: (id: number) => void; -} - -function ConnectionHeaderRow({ - draft, - index, - disabled, - placeholderTextColor, - iconColor, - onUpdate, - onRemove, -}: ConnectionHeaderRowProps) { - const { t } = useTranslation(); - const handleNameChange = useCallback( - (value: string) => onUpdate(draft.id, "name", value), - [draft.id, onUpdate], - ); - const handleValueChange = useCallback( - (value: string) => onUpdate(draft.id, "value", value), - [draft.id, onUpdate], - ); - const handleRemove = useCallback(() => onRemove(draft.id), [draft.id, onRemove]); - - return ( - - - - - - - - ); -} - function draftFromConnectionUri(uri: string): DirectConnectionDraft { const parsed = parseConnectionUri(uri); return { @@ -423,8 +305,6 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const [advancedUri, setAdvancedUri] = useState(""); - const [headerDrafts, setHeaderDrafts] = useState([]); - const nextHeaderId = useRef(1); const [inputResetKey, bumpInputResetKey] = useReducer((key: number) => key + 1, 0); const clearInput = useCallback(() => { @@ -435,8 +315,6 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod setIsPasswordVisible(false); setIsAdvancedOpen(false); setAdvancedUri(""); - setHeaderDrafts([]); - nextHeaderId.current = 1; bumpInputResetKey(); }, []); @@ -491,18 +369,11 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod if (isSaving) return; let connection: PreparedDirectConnection; - let headers: Record | undefined; try { connection = prepareDirectConnection( { host, port, useTls, password }, directConnectionLabels, ); - const preparedHeaders = prepareConnectionHeaders(headerDrafts); - if (preparedHeaders.issue) { - setErrorMessage(headerIssueMessage(preparedHeaders.issue, t)); - return; - } - headers = preparedHeaders.headers; } catch (error) { const message = error instanceof Error ? error.message : directConnectionLabels.invalidConnection; @@ -518,7 +389,6 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod endpoint: connection.endpoint, useTls: connection.useTls, ...(connection.password ? { password: connection.password } : {}), - ...(headers ? { headers } : {}), }); const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId); @@ -555,7 +425,6 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod daemons, directConnectionLabels, handleClose, - headerDrafts, host, isMobile, isSaving, @@ -584,22 +453,6 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod setIsPasswordVisible((current) => !current); }, []); - const handleAddHeader = useCallback(() => { - const id = nextHeaderId.current; - nextHeaderId.current += 1; - setHeaderDrafts((current) => [...current, { id, name: "", value: "" }]); - }, []); - - const handleUpdateHeader = useCallback((id: number, field: "name" | "value", value: string) => { - setHeaderDrafts((current) => - current.map((draft) => (draft.id === id ? { ...draft, [field]: value } : draft)), - ); - }, []); - - const handleRemoveHeader = useCallback((id: number) => { - setHeaderDrafts((current) => current.filter((draft) => draft.id !== id)); - }, []); - const handleToggleAdvanced = useCallback(() => { if (!isAdvancedOpen) { try { @@ -756,58 +609,24 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod {t("pairing.direct.advanced.label")} {isAdvancedOpen ? ( - - - - {isNative || getIsElectron() ? ( - - - {t("pairing.direct.headers.title")} - - - - {headerDrafts.map((draft, index) => ( - - ))} - - - ) : null} - + ) : null} {errorMessage ? {errorMessage} : null} diff --git a/packages/app/src/desktop/daemon/desktop-daemon-transport.test.ts b/packages/app/src/desktop/daemon/desktop-daemon-transport.test.ts index d75c2e8da..a9fe2497c 100644 --- a/packages/app/src/desktop/daemon/desktop-daemon-transport.test.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon-transport.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { - createDesktopLocalDaemonTransportFactory, - createDesktopWebSocketTransportFactory, -} from "./desktop-daemon-transport"; +import { createDesktopLocalDaemonTransportFactory } from "./desktop-daemon-transport"; import { createFakeLocalDaemonTransportRpc } from "./test-local-daemon-transport-rpc"; const LOCAL_URL = "paseo+local://socket?path=%2Ftmp%2Fpaseo.sock"; @@ -46,25 +43,4 @@ describe("desktop-daemon-transport", () => { expect(rpc.closedSessions).toEqual(["local-session-2"]); expect(cleanup).toHaveBeenCalledTimes(1); }); - - it("forwards remote WebSocket headers and protocols through the desktop bridge", () => { - const rpc = createFakeLocalDaemonTransportRpc(); - const transportFactory = createDesktopWebSocketTransportFactory(rpc); - expect(transportFactory).not.toBeNull(); - - transportFactory!({ - url: "wss://daemon.example/ws", - headers: { "X-Tenant": "acme", Authorization: "Bearer secret" }, - protocols: ["paseo.bearer.secret"], - }); - - expect(rpc.openCalls).toEqual([ - { - transportType: "websocket", - url: "wss://daemon.example/ws", - headers: { "X-Tenant": "acme", Authorization: "Bearer secret" }, - protocols: ["paseo.bearer.secret"], - }, - ]); - }); }); diff --git a/packages/app/src/desktop/daemon/desktop-daemon-transport.ts b/packages/app/src/desktop/daemon/desktop-daemon-transport.ts index 1dd3d80b9..32f5ed6cf 100644 --- a/packages/app/src/desktop/daemon/desktop-daemon-transport.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon-transport.ts @@ -2,13 +2,11 @@ import type { DaemonTransport, DaemonTransportFactory, } from "@getpaseo/client/internal/daemon-client"; -import type { DesktopDaemonTransportTarget, LocalTransportTarget } from "./desktop-daemon"; +import type { LocalTransportTarget } from "./desktop-daemon"; import { defaultLocalDaemonTransportRpc, - defaultWebSocketDaemonTransportRpc, type LocalDaemonTransportRpc, } from "./local-daemon-transport-rpc"; -import { isElectronRuntime } from "@/desktop/host"; const LOCAL_TRANSPORT_SCHEME = "paseo+local:"; @@ -52,12 +50,11 @@ function parseLocalDaemonTransportUrl(url: string): LocalTransportTarget { }; } -function createDesktopDaemonTransportFactory( - resolveTarget: (options: Parameters[0]) => DesktopDaemonTransportTarget, - rpc: LocalDaemonTransportRpc, -): DaemonTransportFactory { - return (options) => { - const target = resolveTarget(options); +export function createDesktopLocalDaemonTransportFactory( + rpc: LocalDaemonTransportRpc = defaultLocalDaemonTransportRpc, +): DaemonTransportFactory | null { + return ({ url }) => { + const target = parseLocalDaemonTransportUrl(url); let sessionId: string | null = null; let unlisten: (() => void) | null = null; let disposed = false; @@ -190,26 +187,3 @@ function createDesktopDaemonTransportFactory( return transport; }; } - -export function createDesktopLocalDaemonTransportFactory( - rpc: LocalDaemonTransportRpc = defaultLocalDaemonTransportRpc, -): DaemonTransportFactory | null { - return createDesktopDaemonTransportFactory(({ url }) => parseLocalDaemonTransportUrl(url), rpc); -} - -export function createDesktopWebSocketTransportFactory( - rpc?: LocalDaemonTransportRpc, -): DaemonTransportFactory | null { - if (!rpc && !isElectronRuntime()) { - return null; - } - return createDesktopDaemonTransportFactory( - ({ url, headers, protocols }) => ({ - transportType: "websocket" as const, - url, - ...(headers && Object.keys(headers).length > 0 ? { headers } : {}), - ...(protocols && protocols.length > 0 ? { protocols } : {}), - }), - rpc ?? defaultWebSocketDaemonTransportRpc, - ); -} diff --git a/packages/app/src/desktop/daemon/desktop-daemon.ts b/packages/app/src/desktop/daemon/desktop-daemon.ts index c66588de2..af202a73a 100644 --- a/packages/app/src/desktop/daemon/desktop-daemon.ts +++ b/packages/app/src/desktop/daemon/desktop-daemon.ts @@ -46,16 +46,6 @@ export interface LocalTransportTarget { transportPath: string; } -export interface WebSocketTransportTarget { - [key: string]: unknown; - transportType: "websocket"; - url: string; - headers?: Record; - protocols?: string[]; -} - -export type DesktopDaemonTransportTarget = LocalTransportTarget | WebSocketTransportTarget; - interface LocalTransportEventPayload { sessionId: string; kind: "open" | "message" | "close" | "error"; @@ -203,16 +193,6 @@ export async function openLocalTransportSession(target: LocalTransportTarget): P return raw; } -export async function openWebSocketTransportSession( - target: WebSocketTransportTarget, -): Promise { - const raw = await invokeDesktopCommand("open_websocket_daemon_transport", target); - if (typeof raw !== "string" || raw.trim().length === 0) { - throw new Error("Unexpected WebSocket transport session response."); - } - return raw; -} - export async function sendLocalTransportMessage(input: { sessionId: string; text?: string; diff --git a/packages/app/src/desktop/daemon/local-daemon-transport-rpc.ts b/packages/app/src/desktop/daemon/local-daemon-transport-rpc.ts index 544db85a5..54961107e 100644 --- a/packages/app/src/desktop/daemon/local-daemon-transport-rpc.ts +++ b/packages/app/src/desktop/daemon/local-daemon-transport-rpc.ts @@ -2,10 +2,8 @@ import { closeLocalTransportSession, listenToLocalTransportEvents, openLocalTransportSession, - openWebSocketTransportSession, sendLocalTransportMessage, - type DesktopDaemonTransportTarget, - type WebSocketTransportTarget, + type LocalTransportTarget, } from "./desktop-daemon"; export interface LocalDaemonTransportEvent { @@ -19,7 +17,7 @@ export interface LocalDaemonTransportEvent { } export interface LocalDaemonTransportRpc { - openSession(target: DesktopDaemonTransportTarget): Promise; + openSession(target: LocalTransportTarget): Promise; listenToEvents(handler: (event: LocalDaemonTransportEvent) => void): Promise<() => void>; sendMessage(input: { sessionId: string; text?: string; binaryBase64?: string }): Promise; closeSession(sessionId: string): Promise; @@ -31,10 +29,3 @@ export const defaultLocalDaemonTransportRpc: LocalDaemonTransportRpc = { sendMessage: sendLocalTransportMessage, closeSession: closeLocalTransportSession, }; - -export const defaultWebSocketDaemonTransportRpc: LocalDaemonTransportRpc = { - openSession: (target) => openWebSocketTransportSession(target as WebSocketTransportTarget), - listenToEvents: listenToLocalTransportEvents, - sendMessage: sendLocalTransportMessage, - closeSession: closeLocalTransportSession, -}; diff --git a/packages/app/src/desktop/daemon/test-local-daemon-transport-rpc.ts b/packages/app/src/desktop/daemon/test-local-daemon-transport-rpc.ts index 26c55e6f8..8f2214b03 100644 --- a/packages/app/src/desktop/daemon/test-local-daemon-transport-rpc.ts +++ b/packages/app/src/desktop/daemon/test-local-daemon-transport-rpc.ts @@ -1,4 +1,4 @@ -import type { DesktopDaemonTransportTarget } from "./desktop-daemon"; +import type { LocalTransportTarget } from "./desktop-daemon"; import type { LocalDaemonTransportEvent, LocalDaemonTransportRpc, @@ -11,7 +11,7 @@ export interface RecordedSend { } export interface FakeLocalDaemonTransportRpc extends LocalDaemonTransportRpc { - readonly openCalls: DesktopDaemonTransportTarget[]; + readonly openCalls: LocalTransportTarget[]; readonly recordedSends: RecordedSend[]; readonly closedSessions: string[]; resolveOpen(sessionId: string): void; @@ -22,7 +22,7 @@ export interface FakeLocalDaemonTransportRpc extends LocalDaemonTransportRpc { } export function createFakeLocalDaemonTransportRpc(): FakeLocalDaemonTransportRpc { - const openCalls: DesktopDaemonTransportTarget[] = []; + const openCalls: LocalTransportTarget[] = []; const recordedSends: RecordedSend[] = []; const closedSessions: string[] = []; let eventHandler: ((event: LocalDaemonTransportEvent) => void) | null = null; diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index a231a7042..2329fea34 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -1426,19 +1426,6 @@ export const ar: TranslationResources = { show: "عرض متقدم", hide: "إخفاء المتقدمة", }, - headers: { - title: "رؤوس مخصصة", - add: "إضافة رأس", - name: "الاسم", - value: "القيمة", - remove: "إزالة الرأس", - errors: { - missingName: "أدخل اسمًا لكل رأس", - invalidName: "{{name}} ليس اسم رأس صالحًا", - invalidValue: "يحتوي {{name}} على فاصل أسطر غير صالح", - duplicateName: "تم إدخال {{name}} أكثر من مرة", - }, - }, passwordVisibility: { show: "إظهار كلمة المرور", hide: "إخفاء كلمة المرور", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 765dd2b6a..357ccef58 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -1437,19 +1437,6 @@ export const en = { show: "Show advanced", hide: "Hide advanced", }, - headers: { - title: "Custom headers", - add: "Add header", - name: "Name", - value: "Value", - remove: "Remove header", - errors: { - missingName: "Enter a name for each header", - invalidName: "{{name}} is not a valid header name", - invalidValue: "{{name}} contains an invalid line break", - duplicateName: "{{name}} is entered more than once", - }, - }, passwordVisibility: { show: "Show password", hide: "Hide password", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index dd56db59b..af64e4e76 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1467,19 +1467,6 @@ export const es: TranslationResources = { show: "Mostrar avanzado", hide: "Ocultar avanzado", }, - headers: { - title: "Encabezados personalizados", - add: "Añadir encabezado", - name: "Nombre", - value: "Valor", - remove: "Eliminar encabezado", - errors: { - missingName: "Introduce un nombre para cada encabezado", - invalidName: "{{name}} no es un nombre de encabezado válido", - invalidValue: "{{name}} contiene un salto de línea no válido", - duplicateName: "{{name}} se ha introducido más de una vez", - }, - }, passwordVisibility: { show: "Mostrar contraseña", hide: "Ocultar contraseña", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 00b061a88..3cd4fb551 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1471,19 +1471,6 @@ export const fr: TranslationResources = { show: "Afficher avancé", hide: "Masquer avancé", }, - headers: { - title: "En-têtes personnalisés", - add: "Ajouter un en-tête", - name: "Nom", - value: "Valeur", - remove: "Supprimer l'en-tête", - errors: { - missingName: "Saisissez un nom pour chaque en-tête", - invalidName: "{{name}} n'est pas un nom d'en-tête valide", - invalidValue: "{{name}} contient un saut de ligne non valide", - duplicateName: "{{name}} est saisi plusieurs fois", - }, - }, passwordVisibility: { show: "Afficher le mot de passe", hide: "Masquer le mot de passe", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 2ec9f2334..a4a5b139f 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -1440,19 +1440,6 @@ export const ja: TranslationResources = { show: "詳細設定を表示", hide: "詳細設定を非表示", }, - headers: { - title: "カスタムヘッダー", - add: "ヘッダーを追加", - name: "名前", - value: "値", - remove: "ヘッダーを削除", - errors: { - missingName: "各ヘッダーの名前を入力してください", - invalidName: "{{name}} は有効なヘッダー名ではありません", - invalidValue: "{{name}} に無効な改行が含まれています", - duplicateName: "{{name}} が複数回入力されています", - }, - }, passwordVisibility: { show: "パスワードを表示", hide: "パスワードを非表示", diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index 4aa353f60..9d43e6ebc 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -1436,19 +1436,6 @@ export const ko: TranslationResources = { show: "고급 표시", hide: "고급 숨기기", }, - headers: { - title: "사용자 지정 헤더", - add: "헤더 추가", - name: "이름", - value: "값", - remove: "헤더 삭제", - errors: { - missingName: "각 헤더의 이름을 입력하세요", - invalidName: "{{name}}은(는) 올바른 헤더 이름이 아닙니다", - invalidValue: "{{name}}에 잘못된 줄바꿈이 있습니다", - duplicateName: "{{name}}이(가) 두 번 이상 입력되었습니다", - }, - }, passwordVisibility: { show: "비밀번호 표시", hide: "비밀번호 숨기기", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index b5dfbb388..3659ae9bc 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -1453,19 +1453,6 @@ export const ptBR: TranslationResources = { show: "Mostrar avançado", hide: "Ocultar avançado", }, - headers: { - title: "Cabeçalhos personalizados", - add: "Adicionar cabeçalho", - name: "Nome", - value: "Valor", - remove: "Remover cabeçalho", - errors: { - missingName: "Digite um nome para cada cabeçalho", - invalidName: "{{name}} não é um nome de cabeçalho válido", - invalidValue: "{{name}} contém uma quebra de linha inválida", - duplicateName: "{{name}} foi inserido mais de uma vez", - }, - }, passwordVisibility: { show: "Mostrar senha", hide: "Ocultar senha", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 2e387d70d..c4053e568 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -1458,19 +1458,6 @@ export const ru: TranslationResources = { show: "Показать расширенные", hide: "Скрыть расширенные", }, - headers: { - title: "Пользовательские заголовки", - add: "Добавить заголовок", - name: "Имя", - value: "Значение", - remove: "Удалить заголовок", - errors: { - missingName: "Введите имя для каждого заголовка", - invalidName: "{{name}} — недопустимое имя заголовка", - invalidValue: "{{name}} содержит недопустимый перенос строки", - duplicateName: "{{name}} введён несколько раз", - }, - }, passwordVisibility: { show: "Показать пароль", hide: "Скрыть пароль", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index ab79c6826..e4c1fbddb 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -1409,19 +1409,6 @@ export const zhCN: TranslationResources = { show: "显示高级选项", hide: "隐藏高级选项", }, - headers: { - title: "自定义请求头", - add: "添加请求头", - name: "名称", - value: "值", - remove: "删除请求头", - errors: { - missingName: "请为每个请求头输入名称", - invalidName: "{{name}} 不是有效的请求头名称", - invalidValue: "{{name}} 包含无效换行符", - duplicateName: "{{name}} 输入了多次", - }, - }, passwordVisibility: { show: "显示密码", hide: "隐藏密码", diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index 17a50e4ed..697872f09 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -3042,7 +3042,7 @@ describe("HostRuntimeStore", () => { } }); - it("upsertDirectConnection stores SSL, password, and custom header settings", async () => { + it("upsertDirectConnection stores SSL and password settings", async () => { const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, @@ -3060,7 +3060,6 @@ describe("HostRuntimeStore", () => { endpoint: "example.paseo.test:7443", useTls: true, password: "shared-secret", - headers: { "X-Tenant": "acme" }, label: "tls host", }); @@ -3072,7 +3071,6 @@ describe("HostRuntimeStore", () => { endpoint: "example.paseo.test:7443", useTls: true, password: "shared-secret", - headers: { "X-Tenant": "acme" }, }, ]); diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 7e82f9e76..982d411e4 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -36,7 +36,6 @@ import { import { buildLocalDaemonTransportUrl, createDesktopLocalDaemonTransportFactory, - createDesktopWebSocketTransportFactory, } from "@/desktop/daemon/desktop-daemon-transport"; import { getDesktopHost } from "@/desktop/host"; import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities"; @@ -479,10 +478,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps { return { createClient: ({ host, connection, clientId, runtimeGeneration }) => { const localTransportFactory = createDesktopLocalDaemonTransportFactory(); - const webSocketTransportFactory = createDesktopWebSocketTransportFactory(); - const webSocketConfig = webSocketTransportFactory - ? { transportFactory: webSocketTransportFactory } - : { webSocketFactory: createAppWebSocketFactory() }; + const webSocketConfig = { webSocketFactory: createAppWebSocketFactory() }; const base = { suppressSendErrors: true, clientId, @@ -510,7 +506,6 @@ function createDefaultDeps(): HostRuntimeControllerDeps { useTls: connection.useTls ?? false, }), ...(connection.password ? { password: connection.password } : {}), - ...(connection.headers ? { headers: connection.headers } : {}), }); } return new DaemonClient({ @@ -1651,7 +1646,6 @@ export class HostRuntimeStore { endpoint: string; useTls?: boolean; password?: string; - headers?: Record; label?: string; existingClient?: DaemonClient; }): Promise { @@ -1666,7 +1660,6 @@ export class HostRuntimeStore { endpoint, useTls: input.useTls ?? false, ...(password ? { password } : {}), - ...(input.headers ? { headers: input.headers } : {}), }, existingClient: input.existingClient, }); @@ -1708,7 +1701,6 @@ export class HostRuntimeStore { endpoint: string; useTls?: boolean; password?: string; - headers?: Record; label?: string; }): Promise<{ profile: HostProfile; serverId: string; hostname: string | null }> { const endpoint = normalizeHostPort(input.endpoint); @@ -1721,7 +1713,6 @@ export class HostRuntimeStore { endpoint, useTls: input.useTls ?? false, ...(password ? { password } : {}), - ...(input.headers ? { headers: input.headers } : {}), }, }); } @@ -2463,14 +2454,12 @@ export interface HostMutations { endpoint: string; useTls?: boolean; password?: string; - headers?: Record; label?: string; }) => Promise; probeAndUpsertDirectConnection: (input: { endpoint: string; useTls?: boolean; password?: string; - headers?: Record; label?: string; }) => Promise<{ profile: HostProfile; serverId: string; hostname: string | null }>; upsertRelayConnection: (input: { diff --git a/packages/app/src/types/host-connection.test.ts b/packages/app/src/types/host-connection.test.ts index dcb92ba54..88194dc3b 100644 --- a/packages/app/src/types/host-connection.test.ts +++ b/packages/app/src/types/host-connection.test.ts @@ -75,49 +75,6 @@ describe("normalizeStoredHostProfile", () => { expect(profile?.connections[0]).not.toHaveProperty("password"); }); - it("preserves custom headers on stored direct TCP connections", () => { - const profile = normalizeStoredHostProfile({ - serverId: "srv_headers", - connections: [ - { - id: "direct:example.test:6767", - type: "directTcp", - endpoint: "example.test:6767", - headers: { "X-Tenant": "acme" }, - }, - ], - }); - - expect(profile?.connections[0]).toEqual({ - id: "direct:example.test:6767", - type: "directTcp", - endpoint: "example.test:6767", - useTls: false, - headers: { "X-Tenant": "acme" }, - }); - }); - - it("keeps the connection but discards malformed stored custom headers", () => { - const profile = normalizeStoredHostProfile({ - serverId: "srv_malformed_headers", - connections: [ - { - id: "direct:example.test:6767", - type: "directTcp", - endpoint: "example.test:6767", - headers: { "X-Tenant": 42 }, - }, - ], - }); - - expect(profile?.connections[0]).toEqual({ - id: "direct:example.test:6767", - type: "directTcp", - endpoint: "example.test:6767", - useTls: false, - }); - }); - it("preserves legacy relay ids when TLS is absent", () => { const profile = normalizeStoredHostProfile({ serverId: "srv_relay", @@ -219,59 +176,33 @@ describe("upsertHostConnectionInProfiles", () => { expect(profile.appearance).toEqual({ color: "amber", badgeDisplay: "hidden" }); }); - it("deduplicates matching custom headers regardless of key order", () => { - const firstConnection: HostConnection = { - id: "direct:example.test:6767", - type: "directTcp", - endpoint: "example.test:6767", - headers: { "X-One": "1", "X-Two": "2" }, - }; - const existing = { - ...makeHost("srv_known"), - connections: [firstConnection], - }; - - const [profile] = upsertHostConnectionInProfiles({ - profiles: [existing], - serverId: "srv_known", - connection: { - ...firstConnection, - headers: { "X-Two": "2", "X-One": "1" }, - }, - }); - - expect(profile.connections).toHaveLength(1); - }); - - it("replaces a direct connection when its custom headers change", () => { + it("replaces a direct connection when its settings change", () => { const existingConnection: HostConnection = { id: "direct:example.test:6767", type: "directTcp", endpoint: "example.test:6767", - headers: { "X-Tenant": "old" }, + useTls: false, + password: "old-secret", }; - const existing = { + const existing: HostProfile = { ...makeHost("srv_known"), connections: [existingConnection], preferredConnectionId: existingConnection.id, }; + const replacement: HostConnection = { + ...existingConnection, + useTls: true, + password: "new-secret", + }; const [profile] = upsertHostConnectionInProfiles({ profiles: [existing], serverId: "srv_known", - connection: { - ...existingConnection, - headers: { "X-Tenant": "new" }, - }, + connection: replacement, }); - expect(profile.connections).toEqual([ - { - ...existingConnection, - headers: { "X-Tenant": "new" }, - }, - ]); - expect(profile.preferredConnectionId).toBe(existingConnection.id); + expect(profile.connections).toEqual([replacement]); + expect(profile.preferredConnectionId).toBe(replacement.id); }); }); diff --git a/packages/app/src/types/host-connection.ts b/packages/app/src/types/host-connection.ts index a6312f2d3..421f5868c 100644 --- a/packages/app/src/types/host-connection.ts +++ b/packages/app/src/types/host-connection.ts @@ -11,7 +11,6 @@ import { defaultHostAppearance, normalizeStoredHostAppearance, } from "@/hosts/appearance"; -import { normalizeConnectionHeadersRecord } from "@/utils/connection-headers"; export { DirectTcpHostConnectionSchema, type DirectTcpHostConnection }; @@ -105,18 +104,6 @@ export function resolveActiveHostServerId(params: { ); } -function stringRecordEquals( - left: Record | undefined, - right: Record | undefined, -): boolean { - const leftEntries = Object.entries(left ?? {}); - const rightRecord = right ?? {}; - return ( - leftEntries.length === Object.keys(rightRecord).length && - leftEntries.every(([key, value]) => rightRecord[key] === value) - ); -} - function hostConnectionEquals(left: HostConnection, right: HostConnection): boolean { if (left.type !== right.type || left.id !== right.id) { return false; @@ -126,8 +113,7 @@ function hostConnectionEquals(left: HostConnection, right: HostConnection): bool return ( left.endpoint === right.endpoint && (left.useTls ?? false) === (right.useTls ?? false) && - left.password === right.password && - stringRecordEquals(left.headers, right.headers) + left.password === right.password ); } if (left.type === "directSocket" && right.type === "directSocket") { @@ -315,11 +301,6 @@ function toObjectRecord(value: unknown): Record | undefined { return isPlainRecord(value) ? value : undefined; } -function normalizeStoredHeaders(value: unknown): { headers?: Record } { - const headers = normalizeConnectionHeadersRecord(value); - return headers ? { headers } : {}; -} - function normalizeStoredConnection(connection: unknown): HostConnection | null { const record = toObjectRecord(connection); if (!record) { @@ -337,7 +318,6 @@ function normalizeStoredConnection(connection: unknown): HostConnection | null { endpoint, useTls: record.useTls, ...(typeof record.password === "string" ? { password: record.password } : {}), - ...normalizeStoredHeaders(record.headers), }); } catch { return null; diff --git a/packages/app/src/utils/connection-headers.test.ts b/packages/app/src/utils/connection-headers.test.ts deleted file mode 100644 index ca9a80783..000000000 --- a/packages/app/src/utils/connection-headers.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { normalizeConnectionHeadersRecord, prepareConnectionHeaders } from "./connection-headers"; - -describe("prepareConnectionHeaders", () => { - it("trims and returns custom headers", () => { - expect( - prepareConnectionHeaders([ - { id: 1, name: " X-Tenant ", value: " acme " }, - { id: 2, name: "X-Empty", value: "" }, - ]), - ).toEqual({ headers: { "X-Tenant": "acme", "X-Empty": "" } }); - }); - - it("ignores completely empty draft rows", () => { - expect(prepareConnectionHeaders([{ id: 1, name: "", value: "" }])).toEqual({}); - }); - - it("requires a name when a value is entered", () => { - expect(prepareConnectionHeaders([{ id: 1, name: "", value: "acme" }])).toEqual({ - issue: { type: "missingName" }, - }); - }); - - it("rejects invalid and duplicate names case-insensitively", () => { - expect(prepareConnectionHeaders([{ id: 1, name: "Bad Header", value: "value" }])).toEqual({ - issue: { type: "invalidName", name: "Bad Header" }, - }); - expect( - prepareConnectionHeaders([ - { id: 1, name: "X-Tenant", value: "one" }, - { id: 2, name: "x-tenant", value: "two" }, - ]), - ).toEqual({ issue: { type: "duplicateName", name: "x-tenant" } }); - }); - - it("rejects line breaks in values", () => { - expect(prepareConnectionHeaders([{ id: 1, name: "X-Test", value: "one\ntwo" }])).toEqual({ - issue: { type: "invalidValue", name: "X-Test" }, - }); - }); -}); - -describe("normalizeConnectionHeadersRecord", () => { - it("accepts a valid persisted string record", () => { - expect(normalizeConnectionHeadersRecord({ "X-Tenant": "acme" })).toEqual({ - "X-Tenant": "acme", - }); - }); - - it("rejects malformed persisted header values", () => { - expect(normalizeConnectionHeadersRecord({ "X-Tenant": 42 })).toBeUndefined(); - expect( - normalizeConnectionHeadersRecord({ "X-Tenant": "acme\r\nX-Injected: true" }), - ).toBeUndefined(); - }); -}); diff --git a/packages/app/src/utils/connection-headers.ts b/packages/app/src/utils/connection-headers.ts deleted file mode 100644 index c63ca6904..000000000 --- a/packages/app/src/utils/connection-headers.ts +++ /dev/null @@ -1,65 +0,0 @@ -export interface ConnectionHeaderDraft { - id: number; - name: string; - value: string; -} - -export type ConnectionHeaderValidationIssue = - | { type: "missingName" } - | { type: "invalidName"; name: string } - | { type: "invalidValue"; name: string } - | { type: "duplicateName"; name: string }; - -const HTTP_HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; - -function isPlainRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - -export function prepareConnectionHeaders(drafts: readonly ConnectionHeaderDraft[]): { - headers?: Record; - issue?: ConnectionHeaderValidationIssue; -} { - const headers: Record = {}; - const normalizedNames = new Set(); - - for (const draft of drafts) { - const name = draft.name.trim(); - const value = draft.value.trim(); - if (!name && !value) continue; - if (!name) return { issue: { type: "missingName" } }; - if (!HTTP_HEADER_NAME_PATTERN.test(name)) { - return { issue: { type: "invalidName", name } }; - } - if (/\r|\n/.test(value)) { - return { issue: { type: "invalidValue", name } }; - } - - const normalizedName = name.toLowerCase(); - if (normalizedNames.has(normalizedName)) { - return { issue: { type: "duplicateName", name } }; - } - normalizedNames.add(normalizedName); - headers[name] = value; - } - - return Object.keys(headers).length > 0 ? { headers } : {}; -} - -export function normalizeConnectionHeadersRecord( - value: unknown, -): Record | undefined { - if (!isPlainRecord(value)) { - return undefined; - } - - const drafts: ConnectionHeaderDraft[] = []; - for (const [name, headerValue] of Object.entries(value)) { - if (typeof headerValue !== "string") { - return undefined; - } - drafts.push({ id: drafts.length, name, value: headerValue }); - } - const result = prepareConnectionHeaders(drafts); - return result.issue ? undefined : result.headers; -} diff --git a/packages/app/src/utils/test-daemon-connection.test.ts b/packages/app/src/utils/test-daemon-connection.test.ts index cae1762d5..5ccee7e75 100644 --- a/packages/app/src/utils/test-daemon-connection.test.ts +++ b/packages/app/src/utils/test-daemon-connection.test.ts @@ -101,6 +101,29 @@ describe("test-daemon-connection connectToDaemon", () => { expect(probe.clientIdsRequested).toBe(2); }); + it("keeps direct TCP probes on the renderer WebSocket", async () => { + const { connectToDaemon } = await import("./test-daemon-connection"); + const deps = { + ...probe.deps, + createWebSocketTransportFactory: () => { + throw new Error("Direct TCP must not use the desktop WebSocket bridge"); + }, + }; + + const result = await connectToDaemon( + { + id: "direct:lan:6767", + type: "directTcp", + endpoint: "lan:6767", + }, + undefined, + deps, + ); + await result.client.close(); + + expect(probe.createdConfigs()[0]?.transportFactory).toBeUndefined(); + }); + it("encodes the local socket target into the client config", async () => { const { connectToDaemon } = await import("./test-daemon-connection"); const result = await connectToDaemon( @@ -134,46 +157,6 @@ describe("test-daemon-connection connectToDaemon", () => { expect(probe.createdConfigs()[0]?.password).toBe("shared-secret"); }); - it("passes direct TCP custom headers into the initial probe client config", async () => { - const { connectToDaemon } = await import("./test-daemon-connection"); - const result = await connectToDaemon( - { - id: "direct:lan:6767", - type: "directTcp", - endpoint: "lan:6767", - headers: { "X-Tenant": "acme" }, - }, - undefined, - probe.deps, - ); - await result.client.close(); - - expect(probe.createdConfigs()[0]?.headers).toEqual({ "X-Tenant": "acme" }); - }); - - it("uses the Electron WebSocket transport for the initial probe", async () => { - const { connectToDaemon } = await import("./test-daemon-connection"); - const transportFactory: NonNullable = () => { - throw new Error("The fake probe client should not open the transport."); - }; - const result = await connectToDaemon( - { - id: "direct:lan:6767", - type: "directTcp", - endpoint: "lan:6767", - headers: { "X-Tenant": "acme" }, - }, - undefined, - { - ...probe.deps, - createWebSocketTransportFactory: () => transportFactory, - }, - ); - await result.client.close(); - - expect(probe.createdConfigs()[0]?.transportFactory).toBe(transportFactory); - }); - it("passes performance tracing into the connected client", async () => { const { connectToDaemon } = await import("./test-daemon-connection"); const trace = { diff --git a/packages/app/src/utils/test-daemon-connection.ts b/packages/app/src/utils/test-daemon-connection.ts index abd19d5c1..381cb1afb 100644 --- a/packages/app/src/utils/test-daemon-connection.ts +++ b/packages/app/src/utils/test-daemon-connection.ts @@ -11,7 +11,6 @@ import { import { buildLocalDaemonTransportUrl, createDesktopLocalDaemonTransportFactory, - createDesktopWebSocketTransportFactory, } from "@/desktop/daemon/desktop-daemon-transport"; export interface DaemonProbeClient { @@ -30,7 +29,6 @@ export interface DaemonConnectionDependencies getClientId(): Promise; resolveAppVersion(): string | null; createLocalTransportFactory(): DaemonClientConfig["transportFactory"] | null; - createWebSocketTransportFactory?(): DaemonClientConfig["transportFactory"] | null; buildLocalTransportUrl(input: LocalTransportUrlInput): string; createClient(config: DaemonClientConfig): TClient; } @@ -39,17 +37,10 @@ const defaultDaemonConnectionDependencies: DaemonConnectionDependencies new DaemonClient(config), }; -function resolveWebSocketTransportFactory( - deps: Pick, "createWebSocketTransportFactory">, -): DaemonClientConfig["transportFactory"] { - return deps.createWebSocketTransportFactory?.() ?? undefined; -} - function normalizeNonEmptyString(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); @@ -112,11 +103,7 @@ export async function buildClientConfig( }, deps: Pick< DaemonConnectionDependencies, - | "getClientId" - | "resolveAppVersion" - | "createLocalTransportFactory" - | "createWebSocketTransportFactory" - | "buildLocalTransportUrl" + "getClientId" | "resolveAppVersion" | "createLocalTransportFactory" | "buildLocalTransportUrl" > = defaultDaemonConnectionDependencies, ): Promise { const clientId = await deps.getClientId(); @@ -148,10 +135,8 @@ export async function buildClientConfig( if (connection.type === "directTcp") { return { ...base, - transportFactory: resolveWebSocketTransportFactory(deps), url: buildDaemonWebSocketUrl(connection.endpoint, { useTls: connection.useTls ?? false }), ...(connection.password ? { password: connection.password } : {}), - ...(connection.headers ? { headers: connection.headers } : {}), }; } diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 86388cbd2..1c9b7faf6 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -707,33 +707,6 @@ test("passes password as HTTP bearer header and WebSocket subprotocol", async () }); }); -test("passes custom headers and keeps password authorization authoritative", async () => { - const logger = createMockLogger(); - const mock = createMockTransport(); - const transportFactory = vi.fn(() => mock.transport); - - const client = new DaemonClient({ - url: "ws://test", - clientId: "clsk_unit_test", - password: "shared-secret", - headers: { "X-Tenant": "acme", Authorization: "Bearer ignored" }, - logger, - reconnect: { enabled: false }, - transportFactory, - }); - clients.push(client); - - const connectPromise = client.connect(); - mock.triggerOpen(); - await connectPromise; - - expect(transportFactory).toHaveBeenCalledWith({ - url: "ws://test", - headers: { "X-Tenant": "acme", Authorization: "Bearer shared-secret" }, - protocols: ["paseo.bearer.shared-secret"], - }); -}); - test("advertises client capabilities in hello", async () => { const logger = createMockLogger(); const mock = createMockTransport(); diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 89251bdc5..745212900 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -301,7 +301,6 @@ export interface DaemonClientConfig { runtimeGeneration?: number | null; password?: string; authHeader?: string; - headers?: Record; suppressSendErrors?: boolean; transportFactory?: DaemonTransportFactory; webSocketFactory?: WebSocketFactory; @@ -1246,7 +1245,7 @@ export class DaemonClient { return; } - const headers: Record = { ...this.config.headers }; + const headers: Record = {}; const password = normalizePassword(this.config.password); if (password) { headers.Authorization = `Bearer ${password}`; diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index d75fbc74e..a0fdbd5e7 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -57,7 +57,6 @@ export interface PaseoClientConfig { runtimeGeneration?: number | null; password?: string; authHeader?: string; - headers?: Record; suppressSendErrors?: boolean; logger?: PaseoLogger; connectTimeoutMs?: number; diff --git a/packages/desktop/e2e/direct-connection-headers.spec.ts b/packages/desktop/e2e/direct-connection-headers.spec.ts deleted file mode 100644 index 78cc5c20d..000000000 --- a/packages/desktop/e2e/direct-connection-headers.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expect, test } from "../../app/e2e/support/fixtures"; -import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app"; -import { getServerId } from "../../app/e2e/support/helpers/server-id"; -import { - openAddHostFlow, - selectHostConnectionType, - toggleHostAdvanced, -} from "../../app/e2e/support/helpers/settings"; -import { installDesktopRuntime } from "./support/runtime"; - -test("Electron users can add and remove direct connection custom headers", async ({ page }) => { - await installDesktopRuntime(page, { serverId: getServerId() }); - await gotoAppShell(page); - await openSettings(page); - await openAddHostFlow(page); - await selectHostConnectionType(page, "direct"); - await toggleHostAdvanced(page); - - const addHeader = page.getByTestId("direct-header-add"); - await expect(addHeader).toBeVisible(); - await addHeader.click(); - - await page.getByTestId("direct-header-name-0").fill("X-Tenant"); - await page.getByTestId("direct-header-value-0").fill("acme"); - await expect(page.getByTestId("direct-header-name-0")).toHaveValue("X-Tenant"); - await expect(page.getByTestId("direct-header-value-0")).toHaveValue("acme"); - - await page.getByTestId("direct-header-remove-0").click(); - await expect(page.getByTestId("direct-header-name-0")).toHaveCount(0); - await expect(page.getByTestId("direct-header-value-0")).toHaveCount(0); -}); diff --git a/packages/desktop/e2e/pair-device-relay.spec.ts b/packages/desktop/e2e/pair-device-relay.spec.ts index c678b2a95..2f23852c2 100644 --- a/packages/desktop/e2e/pair-device-relay.spec.ts +++ b/packages/desktop/e2e/pair-device-relay.spec.ts @@ -1,5 +1,4 @@ import { expect, test } from "../../app/e2e/support/fixtures"; -import type { Page } from "@playwright/test"; import { startIsolatedHostDaemon, type IsolatedHostDaemon, @@ -19,12 +18,11 @@ import { openPairDeviceFromHome, openRelaySecurityDocs, observePairingOfferRequests, - preparePairingHost, + prepareLocalPairingHost, reloadAndOpenPairDevice, retryRelayAndExpectFailure, switchPairDeviceToHost, } from "../../app/e2e/support/helpers/pair-device"; -import { installDesktopRuntime } from "./support/runtime"; interface RelayConfigDaemonClient { close(): Promise; @@ -32,20 +30,6 @@ interface RelayConfigDaemonClient { patchDaemonConfig(config: { relay: { enabled: boolean } }): Promise; } -async function prepareLocalPairingHost( - page: Page, - daemon: Parameters[1], - additionalHosts: Parameters[2] = [], -): Promise { - const endpoint = "port" in daemon ? `127.0.0.1:${daemon.port}` : daemon.endpoint; - await installDesktopRuntime(page, { - serverId: daemon.serverId, - daemonListen: endpoint, - manageBuiltInDaemon: false, - }); - await preparePairingHost(page, daemon, additionalHosts); -} - test.describe("local device relay pairing", () => { let relayOffDaemon: IsolatedHostDaemon; let relayEnableDaemon: IsolatedHostDaemon; diff --git a/packages/desktop/e2e/settings-sidebar-scroll.spec.ts b/packages/desktop/e2e/settings-sidebar-scroll.spec.ts index 0c34d6071..942fcdb42 100644 --- a/packages/desktop/e2e/settings-sidebar-scroll.spec.ts +++ b/packages/desktop/e2e/settings-sidebar-scroll.spec.ts @@ -1,15 +1,24 @@ import { test, expect } from "../../app/e2e/support/fixtures"; import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app"; -import { getServerId } from "../../app/e2e/support/helpers/server-id"; -import { installDesktopRuntime } from "./support/runtime"; test.describe("Settings sidebar scrolling", () => { test.use({ viewport: { width: 900, height: 260 } }); test("desktop drag region does not cover the scroll body", async ({ page }) => { - await installDesktopRuntime(page, { - serverId: getServerId(), - manageBuiltInDaemon: true, + await page.addInitScript(() => { + window.paseoDesktop = { + platform: "darwin", + events: { on: () => () => {} }, + invoke: async (command: string) => { + if (command === "get_desktop_settings") { + return { + releaseChannel: "stable", + daemon: { manageBuiltInDaemon: true, keepRunningAfterQuit: true }, + }; + } + return null; + }, + }; }); await gotoAppShell(page); diff --git a/packages/desktop/e2e/support/runtime.ts b/packages/desktop/e2e/support/runtime.ts index 7b0869af2..fdffe1f31 100644 --- a/packages/desktop/e2e/support/runtime.ts +++ b/packages/desktop/e2e/support/runtime.ts @@ -139,123 +139,8 @@ export async function installDesktopRuntime( let daemonRunning = true; let currentPid: number | null = cfg.daemonPid ?? null; let startCount = 0; - let nextTransportSessionId = 0; - const transportSessions = new Map(); - const desktopEventHandlers = new Map void>>(); window.__desktopDaemonStartRequested = false; - function emitDesktopEvent(event: string, payload: unknown) { - for (const handler of desktopEventHandlers.get(event) ?? []) { - handler(payload); - } - } - - function bytesToBase64(bytes: Uint8Array): string { - let binary = ""; - for (const byte of bytes) { - binary += String.fromCharCode(byte); - } - return btoa(binary); - } - - function base64ToBytes(base64: string): Uint8Array { - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let index = 0; index < binary.length; index += 1) { - bytes[index] = binary.charCodeAt(index); - } - return bytes; - } - - function parseWebSocketTransportTarget(args?: Record): { - url: string; - protocols: string[]; - } { - const url = typeof args?.url === "string" ? args.url : ""; - const protocols = Array.isArray(args?.protocols) - ? args.protocols.filter((protocol): protocol is string => typeof protocol === "string") - : []; - const headers = - args?.headers && typeof args.headers === "object" - ? Object.entries(args.headers as Record) - : []; - const passwordProtocol = protocols.find((protocol) => protocol.startsWith("paseo.bearer.")); - const supportsHeaders = - headers.length === 0 || - (headers.length === 1 && - headers[0][0].toLowerCase() === "authorization" && - typeof headers[0][1] === "string" && - passwordProtocol !== undefined && - headers[0][1] === `Bearer ${passwordProtocol.slice("paseo.bearer.".length)}`); - - if (!url || !supportsHeaders) { - throw new Error("The browser-backed desktop E2E transport cannot attach custom headers."); - } - return { url, protocols }; - } - - async function openWebSocketTransport(args?: Record): Promise { - const target = parseWebSocketTransportTarget(args); - const sessionId = `e2e-websocket-session-${++nextTransportSessionId}`; - const socket = new WebSocket(target.url, target.protocols); - socket.binaryType = "arraybuffer"; - transportSessions.set(sessionId, socket); - - return await new Promise((resolve, reject) => { - let opened = false; - - socket.addEventListener("open", () => { - opened = true; - resolve(sessionId); - emitDesktopEvent("local-daemon-transport-event", { sessionId, kind: "open" }); - }); - socket.addEventListener("message", async (event) => { - if (typeof event.data === "string") { - emitDesktopEvent("local-daemon-transport-event", { - sessionId, - kind: "message", - text: event.data, - }); - return; - } - const buffer = - event.data instanceof ArrayBuffer - ? event.data - : await (event.data as Blob).arrayBuffer(); - emitDesktopEvent("local-daemon-transport-event", { - sessionId, - kind: "message", - binaryBase64: bytesToBase64(new Uint8Array(buffer)), - }); - }); - socket.addEventListener("close", (event) => { - transportSessions.delete(sessionId); - if (!opened) { - reject(new Error("Desktop E2E WebSocket closed before opening.")); - return; - } - emitDesktopEvent("local-daemon-transport-event", { - sessionId, - kind: "close", - code: event.code, - reason: event.reason, - }); - }); - socket.addEventListener("error", () => { - if (!opened) { - transportSessions.delete(sessionId); - reject(new Error("Desktop E2E WebSocket failed to open.")); - return; - } - emitDesktopEvent("local-daemon-transport-event", { - sessionId, - kind: "error", - error: "Desktop E2E WebSocket transport error.", - }); - }); - }); - } - function buildDaemonStatus() { return { serverId: cfg.serverId, @@ -290,55 +175,6 @@ export async function installDesktopRuntime( } } - function patchDesktopSettings(args?: Record) { - const daemon = args?.daemon; - if ( - daemon !== null && - typeof daemon === "object" && - "manageBuiltInDaemon" in daemon && - typeof daemon.manageBuiltInDaemon === "boolean" - ) { - manageDaemon = daemon.manageBuiltInDaemon; - } - return { - releaseChannel: "stable", - daemon: { manageBuiltInDaemon: manageDaemon, keepRunningAfterQuit: true }, - }; - } - - function sendWebSocketTransportMessage(args?: Record): null { - const sessionId = typeof args?.sessionId === "string" ? args.sessionId : ""; - const socket = transportSessions.get(sessionId); - if (!socket || socket.readyState !== WebSocket.OPEN) { - throw new Error(`Desktop E2E WebSocket session is not open: ${sessionId}`); - } - if (typeof args?.text === "string") { - socket.send(args.text); - return null; - } - if (typeof args?.binaryBase64 === "string") { - socket.send(base64ToBytes(args.binaryBase64)); - return null; - } - throw new Error("Desktop E2E WebSocket send requires a payload."); - } - - function closeWebSocketTransport(args?: Record): null { - const sessionId = typeof args?.sessionId === "string" ? args.sessionId : ""; - transportSessions.get(sessionId)?.close(); - transportSessions.delete(sessionId); - return null; - } - - const webSocketTransportCommandHandlers: Record< - string, - (args?: Record) => unknown - > = { - open_websocket_daemon_transport: openWebSocketTransport, - send_local_daemon_transport_message: sendWebSocketTransportMessage, - close_local_daemon_transport: closeWebSocketTransport, - }; - const desktopBridge: { platform: string; invoke: (command: string, args?: Record) => Promise; @@ -347,10 +183,7 @@ export async function installDesktopRuntime( open: (options?: Record) => Promise; }; getPendingOpenProject: () => Promise; - opener: { openUrl: (url: string) => Promise }; - events: { - on: (event: string, handler: (payload: unknown) => void) => Promise<() => void>; - }; + events: { on: () => Promise<() => void> }; editor?: { listTargets: () => Promise; openTarget: (input: DesktopEditorOpenRecord) => Promise; @@ -406,7 +239,19 @@ export async function installDesktopRuntime( } if (command === "patch_desktop_settings") { - return patchDesktopSettings(args); + const daemon = args?.daemon; + if ( + daemon !== null && + typeof daemon === "object" && + "manageBuiltInDaemon" in daemon && + typeof daemon.manageBuiltInDaemon === "boolean" + ) { + manageDaemon = daemon.manageBuiltInDaemon; + } + return { + releaseChannel: "stable", + daemon: { manageBuiltInDaemon: manageDaemon, keepRunningAfterQuit: true }, + }; } if (command === "stop_desktop_daemon") { @@ -419,11 +264,6 @@ export async function installDesktopRuntime( return startDesktopDaemon(); } - const transportCommandHandler = webSocketTransportCommandHandlers[command]; - if (transportCommandHandler) { - return await transportCommandHandler(args); - } - return null; }, dialog: { @@ -440,24 +280,7 @@ export async function installDesktopRuntime( }, }, getPendingOpenProject: async () => null, - opener: { - openUrl: async (url) => { - localStorage.setItem("@paseo:e2e-opened-url", url); - }, - }, - events: { - on: async (event, handler) => { - const handlers = desktopEventHandlers.get(event) ?? new Set(); - handlers.add(handler); - desktopEventHandlers.set(event, handlers); - return () => { - handlers.delete(handler); - if (handlers.size === 0) { - desktopEventHandlers.delete(event); - } - }; - }, - }, + events: { on: async () => () => undefined }, }; if (cfg.editorTargets) { diff --git a/packages/desktop/src/daemon/daemon-manager.ts b/packages/desktop/src/daemon/daemon-manager.ts index bae550ef6..faf4a7a10 100644 --- a/packages/desktop/src/daemon/daemon-manager.ts +++ b/packages/desktop/src/daemon/daemon-manager.ts @@ -26,10 +26,8 @@ import { import { createSkillsCommandHandlers, getSkillsController } from "../integrations/skills/index.js"; import { openLocalTransportSession, - openWebSocketTransportSession, sendLocalTransportMessage, closeLocalTransportSession, - type WebSocketTransportTarget, } from "./local-transport.js"; import { createNodeEntrypointInvocation, resolveDaemonRunnerEntrypoint } from "./runtime-paths.js"; import { runExternalCliJsonCommand, runExternalCliTextCommand } from "./cli/external.js"; @@ -539,9 +537,6 @@ export function createDaemonCommandHandlers(): Record { - return await openWebSocketTransportSession(args as unknown as WebSocketTransportTarget); - }, send_local_daemon_transport_message: async (args) => { await sendLocalTransportMessage( args as { sessionId: string; text?: string; binaryBase64?: string }, diff --git a/packages/desktop/src/daemon/local-transport.test.ts b/packages/desktop/src/daemon/local-transport.test.ts deleted file mode 100644 index d356d5d11..000000000 --- a/packages/desktop/src/daemon/local-transport.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { createServer } from "node:http"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { WebSocketServer } from "ws"; - -const send = vi.fn(); - -vi.mock("electron", () => ({ - BrowserWindow: { - getAllWindows: () => [{ webContents: { send } }], - }, -})); - -import { closeAllTransportSessions, openWebSocketTransportSession } from "./local-transport"; - -afterEach(() => { - closeAllTransportSessions(); - send.mockReset(); -}); - -describe("desktop WebSocket transport", () => { - it("attaches custom headers when opening a remote daemon session", async () => { - const server = createServer(); - const webSocketServer = new WebSocketServer({ server }); - const requestHeaders = new Promise>((resolve) => { - webSocketServer.once("connection", (_socket, request) => resolve(request.headers)); - }); - - await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); - const address = server.address(); - if (!address || typeof address === "string") { - throw new Error("Expected TCP test server address."); - } - - try { - const sessionId = await openWebSocketTransportSession({ - transportType: "websocket", - url: `ws://127.0.0.1:${address.port}/ws`, - headers: { "X-Tenant": "acme" }, - }); - - expect((await requestHeaders)["x-tenant"]).toBe("acme"); - expect(sessionId).toMatch(/^local-session-/); - } finally { - closeAllTransportSessions(); - await new Promise((resolve, reject) => { - webSocketServer.close((error) => (error ? reject(error) : resolve())); - }); - await new Promise((resolve, reject) => { - server.close((error) => { - if (error) { - reject(error); - return; - } - resolve(); - }); - }); - } - }); - - it("rejects non-WebSocket URLs", () => { - expect(() => - openWebSocketTransportSession({ - transportType: "websocket", - url: "https://daemon.example/ws", - }), - ).toThrow("Unsupported WebSocket transport URL"); - }); - - it("rejects malformed custom headers before opening a session", () => { - expect(() => - openWebSocketTransportSession({ - transportType: "websocket", - url: "ws://daemon.example/ws", - headers: { "Bad Header": "value" }, - }), - ).toThrow("Invalid WebSocket transport header"); - - expect(() => - openWebSocketTransportSession({ - transportType: "websocket", - url: "ws://daemon.example/ws", - headers: { "X-Tenant": "acme\r\nX-Injected: true" }, - }), - ).toThrow("Invalid WebSocket transport header"); - }); -}); diff --git a/packages/desktop/src/daemon/local-transport.ts b/packages/desktop/src/daemon/local-transport.ts index 65d45ace9..c3d5227f4 100644 --- a/packages/desktop/src/daemon/local-transport.ts +++ b/packages/desktop/src/daemon/local-transport.ts @@ -6,13 +6,6 @@ interface TransportTarget { transportPath: string; } -export interface WebSocketTransportTarget { - transportType: "websocket"; - url: string; - headers?: Record; - protocols?: string[]; -} - interface TransportEventPayload { sessionId: string; kind: "open" | "message" | "close" | "error"; @@ -30,7 +23,6 @@ interface Session { } const WS_ENDPOINT_PATH = "/ws"; -const HTTP_HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; let nextSessionId = 0; const sessions = new Map(); @@ -60,28 +52,6 @@ function describeTransportTarget(target: TransportTarget): string { return target.transportType === "pipe" ? "local daemon pipe" : "local daemon socket"; } -function normalizeWebSocketTarget(target: WebSocketTransportTarget): WebSocketTransportTarget { - const parsed = new URL(target.url); - if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") { - throw new Error(`Unsupported WebSocket transport URL: ${target.url}`); - } - - const headers: Record = {}; - for (const [name, value] of Object.entries(target.headers ?? {})) { - if (!HTTP_HEADER_NAME_PATTERN.test(name) || typeof value !== "string" || /[\r\n]/.test(value)) { - throw new Error("Invalid WebSocket transport header."); - } - headers[name] = value; - } - - return { - transportType: "websocket", - url: parsed.toString(), - ...(Object.keys(headers).length > 0 ? { headers } : {}), - ...(target.protocols?.length ? { protocols: [...target.protocols] } : {}), - }; -} - function decodeTransportMessage(input: { text?: string; binaryBase64?: string }): string | Buffer { if (typeof input.text === "string") { return input.text; @@ -95,28 +65,11 @@ function decodeTransportMessage(input: { text?: string; binaryBase64?: string }) } export function openLocalTransportSession(target: TransportTarget): Promise { - return openTransportSession(buildLocalWebSocketUrl(target), describeTransportTarget(target)); -} - -export function openWebSocketTransportSession(target: WebSocketTransportTarget): Promise { - const normalized = normalizeWebSocketTarget(target); - return openTransportSession(normalized.url, "remote daemon", { - headers: normalized.headers, - protocols: normalized.protocols, - }); -} - -function openTransportSession( - url: string, - targetDescription: string, - options?: { headers?: Record; protocols?: string[] }, -): Promise { const sessionId = `local-session-${++nextSessionId}`; + const url = buildLocalWebSocketUrl(target); return new Promise((resolve, reject) => { - const ws = options?.protocols - ? new WebSocket(url, options.protocols, { headers: options.headers }) - : new WebSocket(url, { headers: options?.headers }); + const ws = new WebSocket(url); const session: Session = { id: sessionId, ws, @@ -168,7 +121,9 @@ function openTransportSession( sessions.delete(sessionId); if (!openSettled) { - finalizeOpenFailure(`${targetDescription} closed before the session became ready.`); + finalizeOpenFailure( + `${describeTransportTarget(target)} closed before the session became ready.`, + ); return; } @@ -184,7 +139,9 @@ function openTransportSession( ws.on("error", (err: Error) => { if (!openSettled) { - finalizeOpenFailure(`Failed to connect to ${targetDescription}: ${err.message}`); + finalizeOpenFailure( + `Failed to connect to ${describeTransportTarget(target)}: ${err.message}`, + ); return; } diff --git a/packages/protocol/src/host-connection-schema.ts b/packages/protocol/src/host-connection-schema.ts index 2d568c1c8..86a98b0e4 100644 --- a/packages/protocol/src/host-connection-schema.ts +++ b/packages/protocol/src/host-connection-schema.ts @@ -6,7 +6,6 @@ export const DirectTcpHostConnectionSchema = z.object({ endpoint: z.string(), useTls: z.boolean().optional().default(false), password: z.string().optional(), - headers: z.record(z.string(), z.string()).optional(), }); export type DirectTcpHostConnection = z.input;