From bb57cfbe6be6a977c23e26819199d859a9099f88 Mon Sep 17 00:00:00 2001 From: Kriday Dave Date: Tue, 14 Jul 2026 23:21:03 +0530 Subject: [PATCH] Normalize protocol-relative remote host input as https normalizeRemoteBaseUrl passed a protocol-relative input like //host verbatim to new URL, which throws Invalid URL because there is no base. Such input is now collapsed by stripping all leading slashes and treating it as https, matching how a bare host is handled. If the remainder already carries a scheme it is kept as-is, so //https://host does not double-prepend. Added tests for the direct-host and hosted-pairing paths, a port case, and the extra-slash and already-schemed edge cases. --- packages/shared/src/remote.test.ts | 64 ++++++++++++++++++++++++++++++ packages/shared/src/remote.ts | 8 ++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/remote.test.ts b/packages/shared/src/remote.test.ts index 24e78757009..5b7a9973652 100644 --- a/packages/shared/src/remote.test.ts +++ b/packages/shared/src/remote.test.ts @@ -59,6 +59,70 @@ describe("remote", () => { }); }); + it("treats a protocol-relative host as https", () => { + expect( + resolveRemotePairingTarget({ + host: "//remote.example.com", + pairingCode: "pairing-token", + }), + ).toEqual({ + credential: "pairing-token", + httpBaseUrl: "https://remote.example.com/", + wsBaseUrl: "wss://remote.example.com/", + }); + }); + + it("preserves the port when normalizing a protocol-relative host", () => { + expect( + resolveRemotePairingTarget({ + host: "//remote.example.com:3000", + pairingCode: "pairing-token", + }), + ).toEqual({ + credential: "pairing-token", + httpBaseUrl: "https://remote.example.com:3000/", + wsBaseUrl: "wss://remote.example.com:3000/", + }); + }); + + it("normalizes a protocol-relative host from a hosted pairing link", () => { + expect( + resolveRemotePairingTarget({ + pairingUrl: "https://app.t3.codes/pair?host=%2F%2Fremote.example.com#token=pairing-token", + }), + ).toEqual({ + credential: "pairing-token", + httpBaseUrl: "https://remote.example.com/", + wsBaseUrl: "wss://remote.example.com/", + }); + }); + + it("collapses extra leading slashes instead of producing an empty host", () => { + expect( + resolveRemotePairingTarget({ + host: "///example.com", + pairingCode: "pairing-token", + }), + ).toEqual({ + credential: "pairing-token", + httpBaseUrl: "https://example.com/", + wsBaseUrl: "wss://example.com/", + }); + }); + + it("does not double-prepend https when the host already carries a scheme", () => { + expect( + resolveRemotePairingTarget({ + host: "//https://example.com", + pairingCode: "pairing-token", + }), + ).toEqual({ + credential: "pairing-token", + httpBaseUrl: "https://example.com/", + wsBaseUrl: "wss://example.com/", + }); + }); + it("preserves host ports when normalizing a bare host input", () => { expect( resolveRemotePairingTarget({ diff --git a/packages/shared/src/remote.ts b/packages/shared/src/remote.ts index 7347dbc74a1..0adfc6fe90a 100644 --- a/packages/shared/src/remote.ts +++ b/packages/shared/src/remote.ts @@ -81,10 +81,10 @@ const normalizeRemoteBaseUrl = ( throw new RemoteBackendUrlMissingError(); } - const normalizedInput = - /^[a-zA-Z][a-zA-Z\d+-]*:\/\//.test(trimmed) || trimmed.startsWith("//") - ? trimmed - : `https://${trimmed}`; + const withoutLeadingSlashes = trimmed.replace(/^\/+/, ""); + const normalizedInput = /^[a-zA-Z][a-zA-Z\d+-]*:\/\//.test(withoutLeadingSlashes) + ? withoutLeadingSlashes + : `https://${withoutLeadingSlashes}`; let url: URL; try { url = new URL(normalizedInput);