summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-24 17:11:07 -0400
committerGitHub <[email protected]>2026-04-24 17:11:07 -0400
commit435becbea01183ab3dd5581067c64f885b93c2d8 (patch)
tree36737a414cd30fa64730594942d7224d86e41f23
parent0405bc74e9f0251115232bba62d14d152406675a (diff)
downloadopencode-435becbea01183ab3dd5581067c64f885b93c2d8.tar.gz
opencode-435becbea01183ab3dd5581067c64f885b93c2d8.zip
Refactor HttpApi auth middleware wiring (#24168)
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/auth.ts66
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/config.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/file.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/mcp.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/permission.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/project.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/provider.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/question.ts4
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/server.ts84
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/workspace.ts4
10 files changed, 102 insertions, 80 deletions
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 000000000..adddcc4be
--- /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>()(
+ "Unauthorized",
+ { message: Schema.String },
+ { httpApiStatus: 401 },
+) {}
+
+export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/ExperimentalHttpApiAuthorization", {
+ error: Unauthorized,
+ security: {
+ basic: HttpApiSecurity.basic,
+ authToken: HttpApiSecurity.apiKey({ in: "query", key: "auth_token" }),
+ },
+}) {}
+
+const emptyCredential = {
+ username: "",
+ password: Redacted.make(""),
+}
+
+function validateCredential<A, E, R>(
+ effect: Effect.Effect<A, E, R>,
+ 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 2dfdec172..fcdf6d1a3 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 eaf43862a..c55d0c2e7 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 91467b1e9..34d4e09e2 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 ed8cb4e27..85dbecd11 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 7d2d8462f..10cf25118 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 67831a1fa..dd1a21d2b 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 3192b530e..526a78ee0 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 b45ad942b..14c2550ed 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>()(
- "Unauthorized",
- { message: Schema.String },
- { httpApiStatus: 401 },
-) {}
-
-class Authorization extends HttpApiMiddleware.Service<Authorization>()("@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 596545073..2ab6b03d2 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({