import { NodeHttpServer } from "@effect/platform-node" import { Effect, Layer, Redacted, Schema } from "effect" import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { createServer } from "node:http" import { AppRuntime } from "@/effect/app-runtime" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { Filesystem } from "@/util/filesystem" import { Permission } from "@/permission" import { Question } from "@/question" import { PermissionApi, PermissionLive } from "./permission" import { QuestionApi, QuestionLive } from "./question" const Query = Schema.Struct({ directory: Schema.optional(Schema.String), workspace: Schema.optional(Schema.String), auth_token: Schema.optional(Schema.String), }) const Headers = Schema.Struct({ authorization: Schema.optional(Schema.String), "x-opencode-directory": Schema.optional(Schema.String), }) export namespace ExperimentalHttpApiServer { function decode(input: string) { try { return decodeURIComponent(input) } catch { return input } } class Unauthorized extends Schema.TaggedErrorClass()( "Unauthorized", { message: Schema.String }, { httpApiStatus: 401 }, ) {} class Authorization extends HttpApiMiddleware.Service()("@opencode/ExperimentalHttpApiAuthorization", { error: Unauthorized, security: { basic: HttpApiSecurity.basic, }, }) {} const normalize = HttpRouter.middleware()( Effect.gen(function* () { return (effect) => Effect.gen(function* () { const query = yield* HttpServerRequest.schemaSearchParams(Query) if (!query.auth_token) return yield* effect const req = yield* HttpServerRequest.HttpServerRequest const next = req.modify({ headers: { ...req.headers, authorization: `Basic ${query.auth_token}`, }, }) return yield* effect.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, next)) }) }), ).layer const auth = Layer.succeed( Authorization, Authorization.of({ basic: (effect, { credential }) => Effect.gen(function* () { if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect const user = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" if (credential.username !== user) { return yield* new Unauthorized({ message: "Unauthorized" }) } if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) { return yield* new Unauthorized({ message: "Unauthorized" }) } return yield* effect }), }), ) const instance = HttpRouter.middleware()( Effect.gen(function* () { return (effect) => Effect.gen(function* () { const query = yield* HttpServerRequest.schemaSearchParams(Query) const headers = yield* HttpServerRequest.schemaHeaders(Headers) const raw = query.directory || headers["x-opencode-directory"] || process.cwd() const workspace = query.workspace || undefined const ctx = yield* Effect.promise(() => Instance.provide({ directory: Filesystem.resolve(decode(raw)), init: () => AppRuntime.runPromise(InstanceBootstrap), fn: () => Instance.current, }), ) const next = workspace ? effect.pipe(Effect.provideService(WorkspaceRef, workspace)) : effect return yield* next.pipe(Effect.provideService(InstanceRef, ctx)) }) }), ).layer const QuestionSecured = QuestionApi.middleware(Authorization) const PermissionSecured = PermissionApi.middleware(Authorization) export const routes = Layer.mergeAll( HttpApiBuilder.layer(QuestionSecured, { openapiPath: "/experimental/httpapi/question/doc" }).pipe( Layer.provide(QuestionLive), ), HttpApiBuilder.layer(PermissionSecured, { openapiPath: "/experimental/httpapi/permission/doc" }).pipe( Layer.provide(PermissionLive), ), ).pipe(Layer.provide(auth), Layer.provide(normalize), Layer.provide(instance)) export const layer = (opts: { hostname: string; port: number }) => HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( Layer.provideMerge(NodeHttpServer.layer(createServer, { port: opts.port, host: opts.hostname })), ) export const layerTest = HttpRouter.serve(routes, { disableListenLog: true, disableLogger: true }).pipe( Layer.provideMerge(NodeHttpServer.layerTest), Layer.provideMerge(Question.defaultLayer), Layer.provideMerge(Permission.defaultLayer), ) }