From d8e31d846a1936ba70f575bb45e63e72d54ccd87 Mon Sep 17 00:00:00 2001 From: James Long Date: Tue, 13 Oct 2026 13:33:55 -0400 Subject: [PATCH] fix(server): align connect route checks (#55177) --- packages/protocol/src/groups/persistent-pty.ts | 8 +++++--- packages/protocol/src/groups/pty.ts | 9 ++++----- packages/protocol/src/groups/server.ts | 9 +++++---- .../server/src/middleware/authorization.ts | 18 ++++++++++++------ 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/protocol/src/groups/persistent-pty.ts b/packages/protocol/src/groups/persistent-pty.ts index 9974318772e..d77ef6c8789 100644 --- a/packages/protocol/src/groups/persistent-pty.ts +++ b/packages/protocol/src/groups/persistent-pty.ts @@ -9,10 +9,12 @@ import { PTY_CONNECT_TICKET_QUERY, PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_H export { PTY_CONNECT_TICKET_QUERY, PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_HEADER_VALUE } -const CONNECT_PATH = /^\/api\/experimental\/persistent-pty\/[^/]+\/connect$/ +export const PERSISTENT_PTY_CONNECT_PATH = "/api/experimental/persistent-pty/:ptyID/connect" + +const PERSISTENT_PTY_CONNECT_URL = /^\/api\/experimental\/persistent-pty\/[^/]+\/connect$/ export function hasPersistentPtyConnectTicketURL(url: URL) { - return CONNECT_PATH.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY) + return PERSISTENT_PTY_CONNECT_URL.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY) } const errors = [InvalidRequestError, ServiceUnavailableError] as const @@ -100,7 +102,7 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental") }), ) .add( - HttpApiEndpoint.get("persistentPty.connect", "/api/experimental/persistent-pty/:ptyID/connect", { + HttpApiEndpoint.get("persistentPty.connect", PERSISTENT_PTY_CONNECT_PATH, { params: { ptyID: Pty.ID }, success: Schema.Boolean, error: [ForbiddenError, PtyNotFoundError, ServiceUnavailableError], diff --git a/packages/protocol/src/groups/pty.ts b/packages/protocol/src/groups/pty.ts index dd0c465553b..0c17bb0cb1f 100644 --- a/packages/protocol/src/groups/pty.ts +++ b/packages/protocol/src/groups/pty.ts @@ -9,13 +9,12 @@ import { LocationQuery, locationQueryOpenApi } from "./location.js" export const PTY_CONNECT_TICKET_QUERY = "ticket" export const PTY_CONNECT_TOKEN_HEADER = "x-opencode-ticket" export const PTY_CONNECT_TOKEN_HEADER_VALUE = "1" +export const PTY_CONNECT_PATH = "/api/pty/:ptyID/connect" -const PTY_CONNECT_PATH = /^\/api\/pty\/[^/]+\/connect$/ +const PTY_CONNECT_URL = /^\/api\/pty\/[^/]+\/connect$/ -// Authorization middleware skips credential checks when this matches; the PTY connect handler -// is then responsible for consuming and validating the ticket. export function hasPtyConnectTicketURL(url: URL) { - return PTY_CONNECT_PATH.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY) + return PTY_CONNECT_URL.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY) } export const makePtyGroup = ( @@ -120,7 +119,7 @@ export const makePtyGroup = Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest + const route = yield* HttpRouter.RouteContext // Ticketed PTY connects (browsers cannot set headers on WebSocket upgrades) and pairing links - // skip credential checks here; their handlers consume and validate the ticket or code. + // skip credential checks here; their handlers consume and validate the ticket or code. Use the + // route selected by Effect so authorization and dispatch cannot disagree about the request path. const url = new URL(request.url, "http://localhost") - if (hasPtyConnectTicketURL(url) || hasPersistentPtyConnectTicketURL(url) || isPairingConnectURL(url)) + if ( + ((route.route.path === PTY_CONNECT_PATH || route.route.path === PERSISTENT_PTY_CONNECT_PATH) && + !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)) || + route.route.path === PAIRING_CONNECT_PATH + ) return yield* effect if (yield* authorizedRequest(request, config)) return yield* effect if (challengeRequest(request))