From 56ea8a4a49e7c9ba41a22f438bbab73f9f726bb3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 17 Jul 2026 07:26:09 +0000 Subject: [PATCH] fix(server): add cors to modular routes --- packages/server/src/routes.ts | 42 ++++++++++++++++++++++-- packages/server/test/routes-cors.test.ts | 17 ++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 packages/server/test/routes-cors.test.ts diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index 3f44ec3bf1..a5c6bcaf85 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -17,7 +17,7 @@ import { SessionRestart } from "@opencode-ai/core/session/execution/restart" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Context, Effect, Layer, Option } from "effect" import { Api } from "./api" @@ -30,6 +30,7 @@ import { layer } from "./location" import { formLocationLayer } from "./middleware/form-location" import { sessionLocationLayer } from "./middleware/session-location" import { ServerInfo } from "./server-info" +import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "./cors" const applicationServices = LayerNode.group([ Database.node, @@ -51,12 +52,17 @@ const applicationServices = LayerNode.group([ SessionRestart.node, ]) -export function createRoutes(password?: string, serviceURLs: () => ReadonlyArray = () => []) { +export function createRoutes( + password?: string, + serviceURLs: () => ReadonlyArray = () => [], + corsOptions?: CorsOptions, +) { return makeRoutes( password ? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(password) }) : ServerAuth.Config.layer, serviceURLs, + corsOptions, ) } @@ -67,6 +73,7 @@ export function createEmbeddedRoutes() { function makeRoutes( auth: Layer.Layer, serviceURLs: () => ReadonlyArray = () => [], + corsOptions?: CorsOptions, ) { const pluginRuntimeCell = PluginRuntime.makeCell() const replacements: LayerNode.Replacements = [ @@ -95,7 +102,7 @@ function makeRoutes( Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service)(context)), ServerInfo.layer(serviceURLs), ) - return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe( + const apiRoutes = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe( Layer.provide(handlers.pipe(Layer.provide(services))), Layer.provide(formLocationLayer), Layer.provide(sessionLocationLayer), @@ -108,10 +115,39 @@ function makeRoutes( Layer.provideMerge(services), Layer.provideMerge(HttpRouter.layer), ) + return Layer.merge(apiRoutes, apiNotFoundRoute(corsOptions)).pipe( + Layer.provide(cors(corsOptions)), + Layer.provide(Layer.succeed(CorsConfig)(corsOptions)), + ) }), ) } +const cors = (options?: CorsOptions) => + HttpRouter.middleware( + HttpMiddleware.cors({ + allowedOrigins: (origin) => isAllowedCorsOrigin(origin, options), + maxAge: 86_400, + }), + { global: true }, + ) + +const apiNotFoundRoute = (options?: CorsOptions) => + HttpRouter.use((router) => + router.add("*", "/api/*", (request) => { + const response = HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 }) + const origin = request.headers.origin + if (!origin || !isAllowedCorsOrigin(origin, options)) return Effect.succeed(response) + return Effect.succeed( + HttpServerResponse.setHeader( + HttpServerResponse.setHeader(response, "access-control-allow-origin", origin), + "vary", + "Origin", + ), + ) + }), + ) + function simulateEnabled() { return !!process.env.OPENCODE_SIMULATE } diff --git a/packages/server/test/routes-cors.test.ts b/packages/server/test/routes-cors.test.ts new file mode 100644 index 0000000000..3e4f901970 --- /dev/null +++ b/packages/server/test/routes-cors.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "bun:test" +import { webHandler } from "../src/routes" + +describe("server CORS", () => { + test("adds CORS headers to /api/health 404 responses", async () => { + const response = await webHandler().handler( + new Request("http://localhost/api/health", { + method: "POST", + headers: { origin: "https://app.opencode.ai" }, + }), + ) + + expect(response.status).toBe(404) + expect(response.headers.get("access-control-allow-origin")).toBe("https://app.opencode.ai") + expect(response.headers.get("vary")).toContain("Origin") + }) +})