fix(server): add cors to modular routes

This commit is contained in:
Brendan Allan
2026-07-17 07:26:09 +00:00
parent efaeda00a1
commit 56ea8a4a49
2 changed files with 56 additions and 3 deletions
+39 -3
View File
@@ -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<string> = () => []) {
export function createRoutes(
password?: string,
serviceURLs: () => ReadonlyArray<string> = () => [],
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<AuthError, AuthServices>(
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
serviceURLs: () => ReadonlyArray<string> = () => [],
corsOptions?: CorsOptions,
) {
const pluginRuntimeCell = PluginRuntime.makeCell()
const replacements: LayerNode.Replacements = [
@@ -95,7 +102,7 @@ function makeRoutes<AuthError, AuthServices>(
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<AuthError, AuthServices>(
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
}
+17
View File
@@ -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")
})
})