From 6b9878e89cbbd6d645d0c9c7648a72944bfd9269 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 24 Apr 2026 09:45:54 -0400 Subject: [PATCH] refactor(httpapi): attach auth middleware in route modules --- .../server/routes/instance/httpapi/auth.ts | 66 +++++++++++++++ .../server/routes/instance/httpapi/config.ts | 4 +- .../server/routes/instance/httpapi/file.ts | 4 +- .../src/server/routes/instance/httpapi/mcp.ts | 4 +- .../routes/instance/httpapi/permission.ts | 4 +- .../server/routes/instance/httpapi/project.ts | 4 +- .../routes/instance/httpapi/provider.ts | 4 +- .../routes/instance/httpapi/question.ts | 4 +- .../server/routes/instance/httpapi/server.ts | 84 +++---------------- .../routes/instance/httpapi/workspace.ts | 4 +- 10 files changed, 102 insertions(+), 80 deletions(-) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/auth.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/auth.ts b/packages/opencode/src/server/routes/instance/httpapi/auth.ts new file mode 100644 index 000000000000..adddcc4bed82 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/auth.ts @@ -0,0 +1,66 @@ +import { Effect, Encoding, Layer, Redacted, Schema } from "effect" +import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" +import { Flag } from "@/flag/flag" + +class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + { message: Schema.String }, + { httpApiStatus: 401 }, +) {} + +export class Authorization extends HttpApiMiddleware.Service()("@opencode/ExperimentalHttpApiAuthorization", { + error: Unauthorized, + security: { + basic: HttpApiSecurity.basic, + authToken: HttpApiSecurity.apiKey({ in: "query", key: "auth_token" }), + }, +}) {} + +const emptyCredential = { + username: "", + password: Redacted.make(""), +} + +function validateCredential( + effect: Effect.Effect, + credential: { readonly username: string; readonly password: typeof emptyCredential.password }, +) { + return Effect.gen(function* () { + if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect + + if (credential.username !== (Flag.OPENCODE_SERVER_USERNAME ?? "opencode")) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + return yield* effect + }) +} + +function decodeCredential(input: string) { + return Encoding.decodeBase64String(input).asEffect().pipe( + Effect.match({ + onFailure: () => emptyCredential, + onSuccess: (header) => { + const parts = header.split(":") + if (parts.length !== 2) return emptyCredential + return { + username: parts[0], + password: Redacted.make(parts[1]), + } + }, + }), + ) +} + +export const authorizationLayer = Layer.succeed( + Authorization, + Authorization.of({ + basic: (effect, { credential }) => validateCredential(effect, credential), + authToken: (effect, { credential }) => + Effect.gen(function* () { + return yield* validateCredential(effect, yield* decodeCredential(Redacted.value(credential))) + }), + }), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/config.ts b/packages/opencode/src/server/routes/instance/httpapi/config.ts index 2dfdec172a51..fcdf6d1a33af 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/config.ts @@ -2,6 +2,7 @@ import { Config } from "@/config" import { Provider } from "@/provider" import { Effect, Layer } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/config" @@ -33,7 +34,8 @@ export const ConfigApi = HttpApi.make("config") title: "config", description: "Experimental HttpApi config routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/file.ts b/packages/opencode/src/server/routes/instance/httpapi/file.ts index eaf43862ad9a..c55d0c2e711c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/file.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/file.ts @@ -1,6 +1,7 @@ import { File } from "@/file" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const FileQuery = Schema.Struct({ path: Schema.String, @@ -51,7 +52,8 @@ export const FileApi = HttpApi.make("file") title: "file", description: "Experimental HttpApi file routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts index 91467b1e90fe..34d4e09e2dfb 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts @@ -1,6 +1,7 @@ import { MCP } from "@/mcp" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" export const McpPaths = { status: "/mcp", @@ -25,7 +26,8 @@ export const McpApi = HttpApi.make("mcp") title: "mcp", description: "Experimental HttpApi MCP routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/permission.ts index ed8cb4e2777b..85dbecd11615 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/permission.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/permission.ts @@ -2,6 +2,7 @@ import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/permission" @@ -35,7 +36,8 @@ export const PermissionApi = HttpApi.make("permission") title: "permission", description: "Experimental HttpApi permission routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts index 7d2d8462f075..10cf25118f06 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/project.ts @@ -2,6 +2,7 @@ import { Instance } from "@/project/instance" import { Project } from "@/project" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/project" @@ -33,7 +34,8 @@ export const ProjectApi = HttpApi.make("project") title: "project", description: "Experimental HttpApi project routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/provider.ts index 67831a1fafb2..dd1a21d2b0d3 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/provider.ts @@ -6,6 +6,7 @@ import { ProviderID } from "@/provider/schema" import { mapValues } from "remeda" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/provider" @@ -59,7 +60,8 @@ export const ProviderApi = HttpApi.make("provider") title: "provider", description: "Experimental HttpApi provider routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/question.ts b/packages/opencode/src/server/routes/instance/httpapi/question.ts index 3192b530e944..526a78ee0ac6 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/question.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/question.ts @@ -2,6 +2,7 @@ import { Question } from "@/question" import { QuestionID } from "@/question/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/question" @@ -45,7 +46,8 @@ export const QuestionApi = HttpApi.make("question") title: "question", description: "Question routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index b45ad942b706..14c2550ed20a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -1,14 +1,14 @@ -import { Effect, Layer, Redacted, Schema } from "effect" -import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" +import { Effect, Layer, Schema } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" import { AppRuntime } from "@/effect/app-runtime" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" import { Observability } from "@/effect" -import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util" +import { authorizationLayer } from "./auth" import { ConfigApi, configHandlers } from "./config" import { FileApi, fileHandlers } from "./file" import { McpApi, mcpHandlers } from "./mcp" @@ -38,56 +38,6 @@ function decode(input: string) { } } -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) => @@ -110,27 +60,17 @@ const instance = HttpRouter.middleware()( }), ).layer -const QuestionSecured = QuestionApi.middleware(Authorization) -const PermissionSecured = PermissionApi.middleware(Authorization) -const ProjectSecured = ProjectApi.middleware(Authorization) -const ProviderSecured = ProviderApi.middleware(Authorization) -const ConfigSecured = ConfigApi.middleware(Authorization) -const WorkspaceSecured = WorkspaceApi.middleware(Authorization) -const FileSecured = FileApi.middleware(Authorization) -const McpSecured = McpApi.middleware(Authorization) - export const routes = Layer.mergeAll( - HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)), - HttpApiBuilder.layer(FileSecured).pipe(Layer.provide(fileHandlers)), - HttpApiBuilder.layer(McpSecured).pipe(Layer.provide(mcpHandlers)), - HttpApiBuilder.layer(ProjectSecured).pipe(Layer.provide(projectHandlers)), - HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)), - HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)), - HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)), - HttpApiBuilder.layer(WorkspaceSecured).pipe(Layer.provide(workspaceHandlers)), + HttpApiBuilder.layer(ConfigApi).pipe(Layer.provide(configHandlers)), + HttpApiBuilder.layer(FileApi).pipe(Layer.provide(fileHandlers)), + HttpApiBuilder.layer(McpApi).pipe(Layer.provide(mcpHandlers)), + HttpApiBuilder.layer(ProjectApi).pipe(Layer.provide(projectHandlers)), + HttpApiBuilder.layer(QuestionApi).pipe(Layer.provide(questionHandlers)), + HttpApiBuilder.layer(PermissionApi).pipe(Layer.provide(permissionHandlers)), + HttpApiBuilder.layer(ProviderApi).pipe(Layer.provide(providerHandlers)), + HttpApiBuilder.layer(WorkspaceApi).pipe(Layer.provide(workspaceHandlers)), ).pipe( - Layer.provide(auth), - Layer.provide(normalize), + Layer.provide(authorizationLayer), Layer.provide(instance), Layer.provide(HttpServer.layerServices), Layer.provideMerge(Observability.layer), diff --git a/packages/opencode/src/server/routes/instance/httpapi/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/workspace.ts index 596545073e12..2ab6b03d24e1 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/workspace.ts @@ -4,6 +4,7 @@ import { WorkspaceAdaptorEntry } from "@/control-plane/types" import * as InstanceState from "@/effect/instance-state" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/experimental/workspace" export const WorkspacePaths = { @@ -49,7 +50,8 @@ export const WorkspaceApi = HttpApi.make("workspace") title: "workspace", description: "Experimental HttpApi workspace routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({