From a152419266d989b395f52a07c64eae087a4e738a Mon Sep 17 00:00:00 2001 From: jlongster <17031+jlongster@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:07:06 +0000 Subject: [PATCH 1/2] fix(server): align connect route checks --- packages/protocol/src/groups/pty.ts | 9 ++++----- packages/server/src/middleware/authorization.ts | 12 ++++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/protocol/src/groups/pty.ts b/packages/protocol/src/groups/pty.ts index a40b6c4b5332..7c5366715603 100644 --- a/packages/protocol/src/groups/pty.ts +++ b/packages/protocol/src/groups/pty.ts @@ -9,13 +9,12 @@ import { LocationQuery, locationQueryOpenApi } from "./location" 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 PtyGroup = HttpApiGroup.make("server.pty") @@ -116,7 +115,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty") .add( // Query fields are decoded in the raw handler after the existence check so a missing // session responds with an empty 404 before any upgrade work. - HttpApiEndpoint.get("pty.connect", "/api/pty/:ptyID/connect", { + HttpApiEndpoint.get("pty.connect", PTY_CONNECT_PATH, { params: { ptyID: Pty.ID }, success: Schema.Boolean, error: [ForbiddenError, PtyNotFoundError], diff --git a/packages/server/src/middleware/authorization.ts b/packages/server/src/middleware/authorization.ts index cc785ee2472a..bfa4c0a6322e 100644 --- a/packages/server/src/middleware/authorization.ts +++ b/packages/server/src/middleware/authorization.ts @@ -2,9 +2,9 @@ import { ServerAuth } from "../auth" import { UnauthorizedError } from "@opencode-ai/protocol/errors" import { Authorization } from "@opencode-ai/protocol/middleware/authorization" export { Authorization } from "@opencode-ai/protocol/middleware/authorization" -import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty" +import { PTY_CONNECT_PATH, PTY_CONNECT_TICKET_QUERY } from "@opencode-ai/protocol/groups/pty" import { Effect, Encoding, Layer, Redacted } from "effect" -import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" const AUTH_TOKEN_QUERY = "auth_token" const WWW_AUTHENTICATE = 'Basic realm="Secure Area"' @@ -43,9 +43,13 @@ export const authorizationLayer = Layer.effect( return Authorization.of((effect) => Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest + const route = yield* HttpRouter.RouteContext // Browsers cannot set headers on WebSocket upgrades, so a ticketed PTY connect skips - // credential checks here; the connect handler consumes and validates the ticket. - if (hasPtyConnectTicketURL(new URL(request.url, "http://localhost"))) return yield* effect + // credential checks here; the connect handler consumes and validates the ticket. 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 (route.route.path === PTY_CONNECT_PATH && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)) + return yield* effect const credential = yield* credentialFromRequest(request) if (ServerAuth.authorized(credential, config)) return yield* effect yield* HttpEffect.appendPreResponseHandler((_request, response) => From 8d26d54a3bf29fbb93a19c541f875bc265736b13 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 23 Oct 2026 10:25:51 -0400 Subject: [PATCH 2/2] fix(server): match legacy PTY connect by route --- .../server/routes/instance/httpapi/groups/pty.ts | 4 ++-- .../instance/httpapi/middleware/authorization.ts | 7 +++++-- packages/opencode/src/server/shared/pty-ticket.ts | 13 +------------ packages/protocol/src/groups/pty.ts | 6 ------ 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts index 9d49009a5c51..25884ede2c86 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts @@ -1,7 +1,7 @@ import { Pty } from "@opencode-ai/core/pty" import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { PtyID } from "@opencode-ai/core/pty/schema" -import { PTY_CONNECT_TICKET_QUERY } from "@/server/shared/pty-ticket" +import { PTY_CONNECT_PATH, PTY_CONNECT_TICKET_QUERY } from "@/server/shared/pty-ticket" import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization, PtyConnectAuthorization } from "../middleware/authorization" @@ -34,7 +34,7 @@ export const PtyPaths = { update: `${root}/:ptyID`, remove: `${root}/:ptyID`, connectToken: `${root}/:ptyID/connect-token`, - connect: `${root}/:ptyID/connect`, + connect: PTY_CONNECT_PATH, } as const export const PtyApi = HttpApi.make("pty") diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts index 61ce39ad39e0..10f50a4bd29b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts @@ -2,7 +2,7 @@ import { ServerAuth } from "@/server/auth" import { Effect, Encoding, Layer, Redacted } from "effect" import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpApiError, HttpApiMiddleware } from "effect/unstable/httpapi" -import { hasPtyConnectTicketURL } from "@/server/shared/pty-ticket" +import { PTY_CONNECT_PATH, PTY_CONNECT_TICKET_QUERY } from "@/server/shared/pty-ticket" import { isPublicUIPath } from "@/server/shared/public-ui" export { Authorization as ServerAuthorization, @@ -139,8 +139,11 @@ export const ptyConnectAuthorizationLayer = Layer.effect( return PtyConnectAuthorization.of((effect) => Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest + const route = yield* HttpRouter.RouteContext + // 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)) return yield* effect + if (route.route.path === PTY_CONNECT_PATH && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)) + return yield* effect return yield* credentialFromURL(url, request).pipe( Effect.flatMap((credential) => validateCredential(effect, credential, config)), ) diff --git a/packages/opencode/src/server/shared/pty-ticket.ts b/packages/opencode/src/server/shared/pty-ticket.ts index 0efd06e6a7ab..53008f24443d 100644 --- a/packages/opencode/src/server/shared/pty-ticket.ts +++ b/packages/opencode/src/server/shared/pty-ticket.ts @@ -1,15 +1,4 @@ export const PTY_CONNECT_TICKET_QUERY = "ticket" export const PTY_CONNECT_TOKEN_HEADER = "x-opencode-ticket" export const PTY_CONNECT_TOKEN_HEADER_VALUE = "1" - -const PTY_CONNECT_PATH = /^\/pty\/[^/]+\/connect$/ - -// Auth middleware skips Basic Auth when this matches; the PTY connect handler -// is then responsible for validating the ticket. -export function isPtyConnectPath(pathname: string) { - return PTY_CONNECT_PATH.test(pathname) -} - -export function hasPtyConnectTicketURL(url: URL) { - return isPtyConnectPath(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY) -} +export const PTY_CONNECT_PATH = "/pty/:ptyID/connect" diff --git a/packages/protocol/src/groups/pty.ts b/packages/protocol/src/groups/pty.ts index 7c5366715603..501baaae4839 100644 --- a/packages/protocol/src/groups/pty.ts +++ b/packages/protocol/src/groups/pty.ts @@ -11,12 +11,6 @@ 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_URL = /^\/api\/pty\/[^/]+\/connect$/ - -export function hasPtyConnectTicketURL(url: URL) { - return PTY_CONNECT_URL.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY) -} - export const PtyGroup = HttpApiGroup.make("server.pty") .add( HttpApiEndpoint.get("pty.list", "/api/pty", {