diff --git a/packages/server/src/process.ts b/packages/server/src/process.ts index c3b01b0140..d1d7020180 100644 --- a/packages/server/src/process.ts +++ b/packages/server/src/process.ts @@ -10,6 +10,7 @@ import { HttpMiddleware, HttpRouter, HttpServer, HttpServerRequest, HttpServerRe import { randomUUID } from "node:crypto" import { createServer } from "node:http" import { ServerAuth } from "./auth" +import { isAllowedCorsOrigin } from "./cors" import { authorizedRequest } from "./middleware/authorization" import { withoutParentSpan } from "./request-tracing" import { createRoutes } from "./routes" @@ -48,7 +49,12 @@ export const start = Effect.fn("ServerProcess.start")(function* ( const application = yield* Ref.make(Option.none()) // Request fibers may continue inbound trace context, but must not inherit the server startup parent. yield* bound.http - .serve(dispatch(password, status, application, shutdown), HttpMiddleware.logger) + .serve( + dispatch(password, status, application, shutdown).pipe( + HttpMiddleware.cors({ allowedOrigins: isAllowedCorsOrigin, maxAge: 86_400 }), + ), + HttpMiddleware.logger, + ) .pipe(withoutParentSpan) if (lifecycle) yield* lifecycle.onListen(bound.http.address, Deferred.succeed(shutdown, undefined).pipe(Effect.asVoid)).pipe( diff --git a/packages/server/test/process.test.ts b/packages/server/test/process.test.ts new file mode 100644 index 0000000000..eeec517e04 --- /dev/null +++ b/packages/server/test/process.test.ts @@ -0,0 +1,42 @@ +import { expect } from "bun:test" +import { Effect } from "effect" +import { HttpServer } from "effect/unstable/http" +import { it } from "../../core/test/lib/effect" +import { ServerProcess } from "../src/process" + +it.live("allows browser preflight requests without credentials", () => + Effect.gen(function* () { + const server = yield* ServerProcess.start({ + hostname: "127.0.0.1", + port: 0, + password: "secret", + database: { path: ":memory:" }, + }) + const response = yield* Effect.promise(() => + fetch(new URL("/api/health", HttpServer.formatAddress(server.address)), { + method: "OPTIONS", + headers: { + origin: "http://localhost:3000", + "access-control-request-method": "GET", + "access-control-request-headers": "authorization", + }, + }), + ) + + expect(response.status).toBe(204) + expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:3000") + expect(response.headers.get("access-control-allow-headers")).toBe("authorization") + + const health = yield* Effect.promise(() => + fetch(new URL("/api/health", HttpServer.formatAddress(server.address)), { + headers: { + authorization: `Basic ${btoa("opencode:secret")}`, + origin: "http://localhost:3000", + }, + }), + ) + + expect(health.status).toBe(200) + expect(health.headers.get("access-control-allow-origin")).toBe("http://localhost:3000") + }), +)