diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index 1cf1584e3e..9b7af481cf 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -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) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 54b02669d9..f60c4e6381 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -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( diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts index de228d8b26..8476ece0e7 100644 --- a/packages/opencode/test/server/httpapi-bridge.test.ts +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -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)