From a65aaf3f62a61717e5f074007a91caa528511d81 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Mon, 22 Jun 2026 08:27:45 +0000 Subject: [PATCH 1/2] Reject unsupported remote pairing protocols Co-authored-by: Codex --- packages/shared/src/remote.test.ts | 46 ++++++++++++++++++++++++++++++ packages/shared/src/remote.ts | 18 ++++++++++++ 2 files changed, 64 insertions(+) diff --git a/packages/shared/src/remote.test.ts b/packages/shared/src/remote.test.ts index 54c78907421..dfbf9c34da6 100644 --- a/packages/shared/src/remote.test.ts +++ b/packages/shared/src/remote.test.ts @@ -72,6 +72,52 @@ describe("remote", () => { }); }); + it("rejects unsupported direct pairing URL protocols", () => { + let pairingUrlError: unknown; + try { + resolveRemotePairingTarget({ + pairingUrl: "ftp://remote.example.com/pair#token=pairing-token", + }); + } catch (cause) { + pairingUrlError = cause; + } + + expect(pairingUrlError).toBeInstanceOf(RemotePairingUrlInvalidError); + expect((pairingUrlError as RemotePairingUrlInvalidError).cause).toBeInstanceOf(Error); + }); + + it("rejects unsupported hosted pairing backend protocols", () => { + let hostError: unknown; + try { + resolveRemotePairingTarget({ + pairingUrl: + "https://app.t3.codes/pair?host=ftp%3A%2F%2Fremote.example.com#token=pairing-token", + }); + } catch (cause) { + hostError = cause; + } + + expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError); + expect(hostError).toMatchObject({ source: "hosted-pairing-host" }); + expect((hostError as RemoteBackendUrlInvalidError).cause).toBeInstanceOf(Error); + }); + + it("rejects unsupported direct host protocols", () => { + let hostError: unknown; + try { + resolveRemotePairingTarget({ + host: "ftp://remote.example.com", + pairingCode: "pairing-token", + }); + } catch (cause) { + hostError = cause; + } + + expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError); + expect(hostError).toMatchObject({ source: "direct-host" }); + expect((hostError as RemoteBackendUrlInvalidError).cause).toBeInstanceOf(Error); + }); + it("uses distinct structural errors for missing pairing inputs", () => { expect(() => resolveRemotePairingTarget({})).toThrowError(RemoteBackendUrlMissingError); expect(() => diff --git a/packages/shared/src/remote.ts b/packages/shared/src/remote.ts index 703811609b8..aad985005d7 100644 --- a/packages/shared/src/remote.ts +++ b/packages/shared/src/remote.ts @@ -3,6 +3,7 @@ import * as Schema from "effect/Schema"; const PAIRING_TOKEN_PARAM = "token"; const HOSTED_PAIRING_HOST_PARAM = "host"; const HOSTED_PAIRING_LABEL_PARAM = "label"; +const SUPPORTED_REMOTE_BACKEND_PROTOCOLS = new Set(["http:", "https:", "ws:", "wss:"]); const readHashParams = (url: URL): URLSearchParams => new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash); @@ -64,6 +65,12 @@ export const RemotePairingTargetError = Schema.Union([ ]); export type RemotePairingTargetError = typeof RemotePairingTargetError.Type; +const createUnsupportedRemoteBackendProtocolError = (url: URL): TypeError => + new TypeError(`Unsupported remote backend URL protocol: ${url.protocol}`); + +const hasSupportedRemoteBackendProtocol = (url: URL): boolean => + SUPPORTED_REMOTE_BACKEND_PROTOCOLS.has(url.protocol); + const normalizeRemoteBaseUrl = ( rawValue: string, source: RemoteBackendUrlInvalidError["source"], @@ -83,6 +90,12 @@ const normalizeRemoteBaseUrl = ( } catch (cause) { throw new RemoteBackendUrlInvalidError({ source, cause }); } + if (!hasSupportedRemoteBackendProtocol(url)) { + throw new RemoteBackendUrlInvalidError({ + source, + cause: createUnsupportedRemoteBackendProtocolError(url), + }); + } url.pathname = "/"; url.search = ""; url.hash = ""; @@ -184,6 +197,11 @@ export const resolveRemotePairingTarget = (input: { } catch (cause) { throw new RemotePairingUrlInvalidError({ cause }); } + if (!hasSupportedRemoteBackendProtocol(url)) { + throw new RemotePairingUrlInvalidError({ + cause: createUnsupportedRemoteBackendProtocolError(url), + }); + } const hostedPairingRequest = readHostedPairingRequest(url); if (hostedPairingRequest) { const hostedBackendUrl = normalizeRemoteBaseUrl( From 43dca40d6500fec917884da8459b5d88c060f44e Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Mon, 22 Jun 2026 08:54:25 +0000 Subject: [PATCH 2/2] Address remote protocol error diagnostics Co-authored-by: Codex --- packages/shared/src/remote.test.ts | 11 ++++++----- packages/shared/src/remote.ts | 15 ++++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/remote.test.ts b/packages/shared/src/remote.test.ts index dfbf9c34da6..24e78757009 100644 --- a/packages/shared/src/remote.test.ts +++ b/packages/shared/src/remote.test.ts @@ -83,7 +83,8 @@ describe("remote", () => { } expect(pairingUrlError).toBeInstanceOf(RemotePairingUrlInvalidError); - expect((pairingUrlError as RemotePairingUrlInvalidError).cause).toBeInstanceOf(Error); + expect(pairingUrlError).toMatchObject({ protocol: "ftp:" }); + expect((pairingUrlError as RemotePairingUrlInvalidError).cause).toBeUndefined(); }); it("rejects unsupported hosted pairing backend protocols", () => { @@ -98,8 +99,8 @@ describe("remote", () => { } expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError); - expect(hostError).toMatchObject({ source: "hosted-pairing-host" }); - expect((hostError as RemoteBackendUrlInvalidError).cause).toBeInstanceOf(Error); + expect(hostError).toMatchObject({ source: "hosted-pairing-host", protocol: "ftp:" }); + expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined(); }); it("rejects unsupported direct host protocols", () => { @@ -114,8 +115,8 @@ describe("remote", () => { } expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError); - expect(hostError).toMatchObject({ source: "direct-host" }); - expect((hostError as RemoteBackendUrlInvalidError).cause).toBeInstanceOf(Error); + expect(hostError).toMatchObject({ source: "direct-host", protocol: "ftp:" }); + expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined(); }); it("uses distinct structural errors for missing pairing inputs", () => { diff --git a/packages/shared/src/remote.ts b/packages/shared/src/remote.ts index aad985005d7..7347dbc74a1 100644 --- a/packages/shared/src/remote.ts +++ b/packages/shared/src/remote.ts @@ -19,7 +19,10 @@ export class RemoteBackendUrlMissingError extends Schema.TaggedErrorClass()( "RemotePairingUrlInvalidError", - { cause: Schema.Defect() }, + { + cause: Schema.optional(Schema.Defect()), + protocol: Schema.optional(Schema.String), + }, ) { override get message(): string { return "Pairing URL is invalid."; @@ -30,7 +33,8 @@ export class RemoteBackendUrlInvalidError extends Schema.TaggedErrorClass - new TypeError(`Unsupported remote backend URL protocol: ${url.protocol}`); - const hasSupportedRemoteBackendProtocol = (url: URL): boolean => SUPPORTED_REMOTE_BACKEND_PROTOCOLS.has(url.protocol); @@ -93,7 +94,7 @@ const normalizeRemoteBaseUrl = ( if (!hasSupportedRemoteBackendProtocol(url)) { throw new RemoteBackendUrlInvalidError({ source, - cause: createUnsupportedRemoteBackendProtocolError(url), + protocol: url.protocol, }); } url.pathname = "/"; @@ -199,7 +200,7 @@ export const resolveRemotePairingTarget = (input: { } if (!hasSupportedRemoteBackendProtocol(url)) { throw new RemotePairingUrlInvalidError({ - cause: createUnsupportedRemoteBackendProtocolError(url), + protocol: url.protocol, }); } const hostedPairingRequest = readHostedPairingRequest(url);