refactor(server): clarify HttpApi route auth layers (#26372)

This commit is contained in:
Kit Langton
2026-05-08 13:06:00 -04:00
committed by GitHub
parent 13b3117ca9
commit 3052a79b32
3 changed files with 20 additions and 5 deletions

View File

@@ -20,13 +20,17 @@ import { SyncApi } from "./groups/sync"
import { TuiApi } from "./groups/tui"
import { WorkspaceApi } from "./groups/workspace"
import { V2Api } from "./groups/v2"
import { Authorization } from "./middleware/authorization"
// SSE event schemas built from the same BusEvent/SyncEvent registries that
// the Hono spec uses, so both specs emit identical Event/SyncEvent components.
const EventSchema = Schema.Union(BusEvent.effectPayloads()).annotate({ identifier: "Event" })
const SyncEventSchemas = SyncEvent.effectPayloads()
export const RootHttpApi = HttpApi.make("opencode-root").addHttpApi(ControlApi).addHttpApi(GlobalApi)
export const RootHttpApi = HttpApi.make("opencode-root")
.addHttpApi(ControlApi)
.addHttpApi(GlobalApi)
.middleware(Authorization)
export const InstanceHttpApi = HttpApi.make("opencode-instance")
.addHttpApi(ConfigApi)

View File

@@ -96,9 +96,16 @@ const cors = (corsOptions?: CorsOptions) =>
{ global: true },
)
// Route tree:
// - rootApiRoutes: typed /global/* and control routes; auth is declared by RootHttpApi.
// - eventApiRoutes/rawInstanceRoutes: raw instance routes; auth and workspace routing happen as router middleware.
// - instanceApiRoutes: schema routes; auth is declared on each group and workspace context is provided below.
// - uiRoute: raw catch-all fallback; auth is router middleware so public static assets can bypass it.
const authOnlyRouterLayer = authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))
const httpApiAuthLayer = authorizationLayer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))
const rootApiRoutes = HttpApiBuilder.layer(RootHttpApi).pipe(
Layer.provide([controlHandlers, globalHandlers]),
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))),
Layer.provide(httpApiAuthLayer),
)
const instanceRouterLayer = authorizationRouterMiddleware
.combine(instanceRouterMiddleware)
@@ -131,7 +138,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe(
const rawInstanceRoutes = Layer.mergeAll(ptyConnectRoute).pipe(Layer.provide(instanceRouterLayer))
const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe(
Layer.provide([
authorizationLayer.pipe(Layer.provide(ServerAuth.Config.defaultLayer)),
httpApiAuthLayer,
workspaceRoutingLayer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal)),
instanceContextLayer,
]),
@@ -143,7 +150,7 @@ const uiRoute = HttpRouter.use((router) =>
const client = yield* HttpClient.HttpClient
yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
}),
).pipe(Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))))
).pipe(Layer.provide(authOnlyRouterLayer))
export function createRoutes(corsOptions?: CorsOptions) {
return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, uiRoute).pipe(

View File

@@ -363,12 +363,16 @@ describe("HttpApi server", () => {
const auth = { authorization: authorization("opencode", "secret") }
const wrongAuth = { authorization: authorization("opencode", "wrong") }
const [missingConfig, wrongConfig, goodConfig] = await Promise.all([
const [missingHealth, goodHealth, missingConfig, wrongConfig, goodConfig] = await Promise.all([
server.request(GlobalPaths.health),
server.request(GlobalPaths.health, { headers: auth }),
server.request(GlobalPaths.config),
server.request(GlobalPaths.config, { headers: wrongAuth }),
server.request(GlobalPaths.config, { headers: auth }),
])
expect(missingHealth.status).toBe(401)
expect(goodHealth.status).toBe(200)
expect(missingConfig.status).toBe(401)
expect(wrongConfig.status).toBe(401)
expect(goodConfig.status).toBe(200)