refactor(server): unify instance httpapi middleware routing

Unify declared instance HTTP API endpoints under typed middleware routing, including event streaming and PTY WebSocket connect handling.\n\nPreserve PTY connect compatibility by checking missing PTYs before parsing optional cursor and ticket query fields, with regression coverage.
This commit is contained in:
Kit Langton
2026-05-27 08:45:11 -04:00
committed by GitHub
parent 9941e70d29
commit 76d814e747
18 changed files with 340 additions and 195 deletions
@@ -27,6 +27,13 @@ export class V2Authorization extends HttpApiMiddleware.Service<V2Authorization>(
},
) {}
export class PtyConnectAuthorization extends HttpApiMiddleware.Service<PtyConnectAuthorization>()(
"@opencode/ExperimentalHttpApiPtyConnectAuthorization",
{
error: HttpApiError.UnauthorizedNoContent,
},
) {}
function emptyCredential() {
return {
username: "",
@@ -105,7 +112,6 @@ export const authorizationRouterMiddleware = HttpRouter.middleware()(
const request = yield* HttpServerRequest.HttpServerRequest
const url = new URL(request.url, "http://localhost")
if (isPublicUIPath(request.method, url.pathname)) return yield* effect
if (hasPtyConnectTicketURL(url)) return yield* effect
return yield* credentialFromURL(url, request).pipe(
Effect.flatMap((credential) => validateRawCredential(effect, credential, config)),
)
@@ -129,6 +135,24 @@ export const authorizationLayer = Layer.effect(
}),
)
export const ptyConnectAuthorizationLayer = Layer.effect(
PtyConnectAuthorization,
Effect.gen(function* () {
const config = yield* ServerAuth.Config
if (!ServerAuth.required(config)) return PtyConnectAuthorization.of((effect) => effect)
return PtyConnectAuthorization.of((effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const url = new URL(request.url, "http://localhost")
if (hasPtyConnectTicketURL(url)) return yield* effect
return yield* credentialFromURL(url, request).pipe(
Effect.flatMap((credential) => validateCredential(effect, credential, config)),
)
}),
)
}),
)
export const v2AuthorizationLayer = Layer.effect(
V2Authorization,
Effect.gen(function* () {
@@ -1,7 +1,7 @@
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { InstanceStore } from "@/project/instance-store"
import { Effect, Layer } from "effect"
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
import { HttpServerResponse } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import { WorkspaceRouteContext } from "./workspace-routing"
@@ -41,10 +41,3 @@ export const instanceContextLayer = Layer.effect(
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store))
}),
)
export const instanceRouterMiddleware = HttpRouter.middleware()(
Effect.gen(function* () {
const store = yield* InstanceStore.Service
return (effect) => provideInstanceContext(effect, store)
}),
)
@@ -9,7 +9,7 @@ import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL }
import { NotFoundError } from "@/storage/storage"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Context, Data, Effect, Layer, Option, Schema } from "effect"
import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import * as Socket from "effect/unstable/socket/Socket"
import { InvalidRequestError } from "../errors"
@@ -219,7 +219,10 @@ function routeHttpApiWorkspace<E>(
const sessionID = getWorkspaceRouteSessionID(requestURL(request))
const session = sessionID
? yield* Session.Service.use((svc) => svc.get(sessionID)).pipe(
Effect.catchIf(NotFoundError.isInstance, () => Effect.succeed(undefined)),
Effect.catchIf(
(error): error is NotFoundError => NotFoundError.isInstance(error),
() => Effect.succeed(undefined),
),
Effect.catchDefect(() => Effect.succeed(undefined)),
)
: undefined
@@ -242,20 +245,3 @@ export const workspaceRoutingLayer = Layer.effect(
)
}),
)
export const workspaceRouterMiddleware = HttpRouter.middleware<{ provides: WorkspaceRouteContext }>()(
Effect.gen(function* () {
const makeWebSocket = yield* Socket.WebSocketConstructor
const workspace = yield* Workspace.Service
const client = yield* HttpClient.HttpClient
return (effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const plan = yield* planRequest(request)
return yield* routeWorkspace(client, effect, plan)
}).pipe(
Effect.provideService(Socket.WebSocketConstructor, makeWebSocket),
Effect.provideService(Workspace.Service, workspace),
)
}),
)