authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-06-22 19:15:43+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 10:15:43-07:00
log8919ae7c53973048827481ecc3a96d4d778955f7
treeb54e84c5ac2a9734f8c79ac649565835afdf288a
parentc1e2408e1f317bba2fbb48a27498b4ae4bef47d8
signature Signed by PGP key B5690EEEBB952194

[codex] Reject unsupported remote pairing protocols (#3498)

Co-authored-by: Codex <codex@openai.com>

2 files changed, 68 insertions(+), 2 deletions(-)

packages/shared/src/remote.test.ts+47
......@@ -72,6 +72,53 @@ describe("remote", () => {
7272 });
7373 });
7474
75 it("rejects unsupported direct pairing URL protocols", () => {
76 let pairingUrlError: unknown;
77 try {
78 resolveRemotePairingTarget({
79 pairingUrl: "ftp://remote.example.com/pair#token=pairing-token",
80 });
81 } catch (cause) {
82 pairingUrlError = cause;
83 }
84
85 expect(pairingUrlError).toBeInstanceOf(RemotePairingUrlInvalidError);
86 expect(pairingUrlError).toMatchObject({ protocol: "ftp:" });
87 expect((pairingUrlError as RemotePairingUrlInvalidError).cause).toBeUndefined();
88 });
89
90 it("rejects unsupported hosted pairing backend protocols", () => {
91 let hostError: unknown;
92 try {
93 resolveRemotePairingTarget({
94 pairingUrl:
95 "https://app.t3.codes/pair?host=ftp%3A%2F%2Fremote.example.com#token=pairing-token",
96 });
97 } catch (cause) {
98 hostError = cause;
99 }
100
101 expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError);
102 expect(hostError).toMatchObject({ source: "hosted-pairing-host", protocol: "ftp:" });
103 expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined();
104 });
105
106 it("rejects unsupported direct host protocols", () => {
107 let hostError: unknown;
108 try {
109 resolveRemotePairingTarget({
110 host: "ftp://remote.example.com",
111 pairingCode: "pairing-token",
112 });
113 } catch (cause) {
114 hostError = cause;
115 }
116
117 expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError);
118 expect(hostError).toMatchObject({ source: "direct-host", protocol: "ftp:" });
119 expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined();
120 });
121
75122 it("uses distinct structural errors for missing pairing inputs", () => {
76123 expect(() => resolveRemotePairingTarget({})).toThrowError(RemoteBackendUrlMissingError);
77124 expect(() =>
packages/shared/src/remote.ts+21-2
......@@ -3,6 +3,7 @@ import * as Schema from "effect/Schema";
33const PAIRING_TOKEN_PARAM = "token";
44const HOSTED_PAIRING_HOST_PARAM = "host";
55const HOSTED_PAIRING_LABEL_PARAM = "label";
6const SUPPORTED_REMOTE_BACKEND_PROTOCOLS = new Set(["http:", "https:", "ws:", "wss:"]);
67
78const readHashParams = (url: URL): URLSearchParams =>
89 new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
......@@ -18,7 +19,10 @@ export class RemoteBackendUrlMissingError extends Schema.TaggedErrorClass<Remote
1819
1920export class RemotePairingUrlInvalidError extends Schema.TaggedErrorClass<RemotePairingUrlInvalidError>()(
2021 "RemotePairingUrlInvalidError",
21 { cause: Schema.Defect() },
22 {
23 cause: Schema.optional(Schema.Defect()),
24 protocol: Schema.optional(Schema.String),
25 },
2226) {
2327 override get message(): string {
2428 return "Pairing URL is invalid.";
......@@ -29,7 +33,8 @@ export class RemoteBackendUrlInvalidError extends Schema.TaggedErrorClass<Remote
2933 "RemoteBackendUrlInvalidError",
3034 {
3135 source: Schema.Literals(["direct-host", "hosted-pairing-host"]),
32 cause: Schema.Defect(),
36 cause: Schema.optional(Schema.Defect()),
37 protocol: Schema.optional(Schema.String),
3338 },
3439) {
3540 override get message(): string {
......@@ -64,6 +69,9 @@ export const RemotePairingTargetError = Schema.Union([
6469]);
6570export type RemotePairingTargetError = typeof RemotePairingTargetError.Type;
6671
72const hasSupportedRemoteBackendProtocol = (url: URL): boolean =>
73 SUPPORTED_REMOTE_BACKEND_PROTOCOLS.has(url.protocol);
74
6775const normalizeRemoteBaseUrl = (
6876 rawValue: string,
6977 source: RemoteBackendUrlInvalidError["source"],
......@@ -83,6 +91,12 @@ const normalizeRemoteBaseUrl = (
8391 } catch (cause) {
8492 throw new RemoteBackendUrlInvalidError({ source, cause });
8593 }
94 if (!hasSupportedRemoteBackendProtocol(url)) {
95 throw new RemoteBackendUrlInvalidError({
96 source,
97 protocol: url.protocol,
98 });
99 }
86100 url.pathname = "/";
87101 url.search = "";
88102 url.hash = "";
......@@ -184,6 +198,11 @@ export const resolveRemotePairingTarget = (input: {
184198 } catch (cause) {
185199 throw new RemotePairingUrlInvalidError({ cause });
186200 }
201 if (!hasSupportedRemoteBackendProtocol(url)) {
202 throw new RemotePairingUrlInvalidError({
203 protocol: url.protocol,
204 });
205 }
187206 const hostedPairingRequest = readHostedPairingRequest(url);
188207 if (hostedPairingRequest) {
189208 const hostedBackendUrl = normalizeRemoteBaseUrl(