Skip to content

Commit 435becb

Browse files
authored
Refactor HttpApi auth middleware wiring (#24168)
1 parent 0405bc7 commit 435becb

10 files changed

Lines changed: 102 additions & 80 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Effect, Encoding, Layer, Redacted, Schema } from "effect"
2+
import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
3+
import { Flag } from "@/flag/flag"
4+
5+
class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
6+
"Unauthorized",
7+
{ message: Schema.String },
8+
{ httpApiStatus: 401 },
9+
) {}
10+
11+
export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/ExperimentalHttpApiAuthorization", {
12+
error: Unauthorized,
13+
security: {
14+
basic: HttpApiSecurity.basic,
15+
authToken: HttpApiSecurity.apiKey({ in: "query", key: "auth_token" }),
16+
},
17+
}) {}
18+
19+
const emptyCredential = {
20+
username: "",
21+
password: Redacted.make(""),
22+
}
23+
24+
function validateCredential<A, E, R>(
25+
effect: Effect.Effect<A, E, R>,
26+
credential: { readonly username: string; readonly password: typeof emptyCredential.password },
27+
) {
28+
return Effect.gen(function* () {
29+
if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect
30+
31+
if (credential.username !== (Flag.OPENCODE_SERVER_USERNAME ?? "opencode")) {
32+
return yield* new Unauthorized({ message: "Unauthorized" })
33+
}
34+
if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) {
35+
return yield* new Unauthorized({ message: "Unauthorized" })
36+
}
37+
return yield* effect
38+
})
39+
}
40+
41+
function decodeCredential(input: string) {
42+
return Encoding.decodeBase64String(input).asEffect().pipe(
43+
Effect.match({
44+
onFailure: () => emptyCredential,
45+
onSuccess: (header) => {
46+
const parts = header.split(":")
47+
if (parts.length !== 2) return emptyCredential
48+
return {
49+
username: parts[0],
50+
password: Redacted.make(parts[1]),
51+
}
52+
},
53+
}),
54+
)
55+
}
56+
57+
export const authorizationLayer = Layer.succeed(
58+
Authorization,
59+
Authorization.of({
60+
basic: (effect, { credential }) => validateCredential(effect, credential),
61+
authToken: (effect, { credential }) =>
62+
Effect.gen(function* () {
63+
return yield* validateCredential(effect, yield* decodeCredential(Redacted.value(credential)))
64+
}),
65+
}),
66+
)

packages/opencode/src/server/routes/instance/httpapi/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Config } from "@/config"
22
import { Provider } from "@/provider"
33
import { Effect, Layer } from "effect"
44
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
5+
import { Authorization } from "./auth"
56

67
const root = "/config"
78

@@ -33,7 +34,8 @@ export const ConfigApi = HttpApi.make("config")
3334
title: "config",
3435
description: "Experimental HttpApi config routes.",
3536
}),
36-
),
37+
)
38+
.middleware(Authorization),
3739
)
3840
.annotateMerge(
3941
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/file.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { File } from "@/file"
22
import { Effect, Layer, Schema } from "effect"
33
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
4+
import { Authorization } from "./auth"
45

56
const FileQuery = Schema.Struct({
67
path: Schema.String,
@@ -51,7 +52,8 @@ export const FileApi = HttpApi.make("file")
5152
title: "file",
5253
description: "Experimental HttpApi file routes.",
5354
}),
54-
),
55+
)
56+
.middleware(Authorization),
5557
)
5658
.annotateMerge(
5759
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/mcp.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MCP } from "@/mcp"
22
import { Effect, Layer, Schema } from "effect"
33
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
4+
import { Authorization } from "./auth"
45

56
export const McpPaths = {
67
status: "/mcp",
@@ -25,7 +26,8 @@ export const McpApi = HttpApi.make("mcp")
2526
title: "mcp",
2627
description: "Experimental HttpApi MCP routes.",
2728
}),
28-
),
29+
)
30+
.middleware(Authorization),
2931
)
3032
.annotateMerge(
3133
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/permission.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Permission } from "@/permission"
22
import { PermissionID } from "@/permission/schema"
33
import { Effect, Layer, Schema } from "effect"
44
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
5+
import { Authorization } from "./auth"
56

67
const root = "/permission"
78

@@ -35,7 +36,8 @@ export const PermissionApi = HttpApi.make("permission")
3536
title: "permission",
3637
description: "Experimental HttpApi permission routes.",
3738
}),
38-
),
39+
)
40+
.middleware(Authorization),
3941
)
4042
.annotateMerge(
4143
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/project.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Instance } from "@/project/instance"
22
import { Project } from "@/project"
33
import { Effect, Layer, Schema } from "effect"
44
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
5+
import { Authorization } from "./auth"
56

67
const root = "/project"
78

@@ -33,7 +34,8 @@ export const ProjectApi = HttpApi.make("project")
3334
title: "project",
3435
description: "Experimental HttpApi project routes.",
3536
}),
36-
),
37+
)
38+
.middleware(Authorization),
3739
)
3840
.annotateMerge(
3941
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/provider.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ProviderID } from "@/provider/schema"
66
import { mapValues } from "remeda"
77
import { Effect, Layer, Schema } from "effect"
88
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
9+
import { Authorization } from "./auth"
910

1011
const root = "/provider"
1112

@@ -59,7 +60,8 @@ export const ProviderApi = HttpApi.make("provider")
5960
title: "provider",
6061
description: "Experimental HttpApi provider routes.",
6162
}),
62-
),
63+
)
64+
.middleware(Authorization),
6365
)
6466
.annotateMerge(
6567
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/question.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Question } from "@/question"
22
import { QuestionID } from "@/question/schema"
33
import { Effect, Layer, Schema } from "effect"
44
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
5+
import { Authorization } from "./auth"
56

67
const root = "/question"
78

@@ -45,7 +46,8 @@ export const QuestionApi = HttpApi.make("question")
4546
title: "question",
4647
description: "Question routes.",
4748
}),
48-
),
49+
)
50+
.middleware(Authorization),
4951
)
5052
.annotateMerge(
5153
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/server.ts

Lines changed: 12 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { Effect, Layer, Redacted, Schema } from "effect"
2-
import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
1+
import { Effect, Layer, Schema } from "effect"
2+
import { HttpApiBuilder } from "effect/unstable/httpapi"
33
import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"
44
import { AppRuntime } from "@/effect/app-runtime"
55
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
66
import { Observability } from "@/effect"
7-
import { Flag } from "@/flag/flag"
87
import { InstanceBootstrap } from "@/project/bootstrap"
98
import { Instance } from "@/project/instance"
109
import { lazy } from "@/util/lazy"
1110
import { Filesystem } from "@/util"
11+
import { authorizationLayer } from "./auth"
1212
import { ConfigApi, configHandlers } from "./config"
1313
import { FileApi, fileHandlers } from "./file"
1414
import { McpApi, mcpHandlers } from "./mcp"
@@ -38,56 +38,6 @@ function decode(input: string) {
3838
}
3939
}
4040

41-
class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
42-
"Unauthorized",
43-
{ message: Schema.String },
44-
{ httpApiStatus: 401 },
45-
) {}
46-
47-
class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/ExperimentalHttpApiAuthorization", {
48-
error: Unauthorized,
49-
security: {
50-
basic: HttpApiSecurity.basic,
51-
},
52-
}) {}
53-
54-
const normalize = HttpRouter.middleware()(
55-
Effect.gen(function* () {
56-
return (effect) =>
57-
Effect.gen(function* () {
58-
const query = yield* HttpServerRequest.schemaSearchParams(Query)
59-
if (!query.auth_token) return yield* effect
60-
const req = yield* HttpServerRequest.HttpServerRequest
61-
const next = req.modify({
62-
headers: {
63-
...req.headers,
64-
authorization: `Basic ${query.auth_token}`,
65-
},
66-
})
67-
return yield* effect.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, next))
68-
})
69-
}),
70-
).layer
71-
72-
const auth = Layer.succeed(
73-
Authorization,
74-
Authorization.of({
75-
basic: (effect, { credential }) =>
76-
Effect.gen(function* () {
77-
if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect
78-
79-
const user = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
80-
if (credential.username !== user) {
81-
return yield* new Unauthorized({ message: "Unauthorized" })
82-
}
83-
if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) {
84-
return yield* new Unauthorized({ message: "Unauthorized" })
85-
}
86-
return yield* effect
87-
}),
88-
}),
89-
)
90-
9141
const instance = HttpRouter.middleware()(
9242
Effect.gen(function* () {
9343
return (effect) =>
@@ -110,27 +60,17 @@ const instance = HttpRouter.middleware()(
11060
}),
11161
).layer
11262

113-
const QuestionSecured = QuestionApi.middleware(Authorization)
114-
const PermissionSecured = PermissionApi.middleware(Authorization)
115-
const ProjectSecured = ProjectApi.middleware(Authorization)
116-
const ProviderSecured = ProviderApi.middleware(Authorization)
117-
const ConfigSecured = ConfigApi.middleware(Authorization)
118-
const WorkspaceSecured = WorkspaceApi.middleware(Authorization)
119-
const FileSecured = FileApi.middleware(Authorization)
120-
const McpSecured = McpApi.middleware(Authorization)
121-
12263
export const routes = Layer.mergeAll(
123-
HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)),
124-
HttpApiBuilder.layer(FileSecured).pipe(Layer.provide(fileHandlers)),
125-
HttpApiBuilder.layer(McpSecured).pipe(Layer.provide(mcpHandlers)),
126-
HttpApiBuilder.layer(ProjectSecured).pipe(Layer.provide(projectHandlers)),
127-
HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)),
128-
HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)),
129-
HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)),
130-
HttpApiBuilder.layer(WorkspaceSecured).pipe(Layer.provide(workspaceHandlers)),
64+
HttpApiBuilder.layer(ConfigApi).pipe(Layer.provide(configHandlers)),
65+
HttpApiBuilder.layer(FileApi).pipe(Layer.provide(fileHandlers)),
66+
HttpApiBuilder.layer(McpApi).pipe(Layer.provide(mcpHandlers)),
67+
HttpApiBuilder.layer(ProjectApi).pipe(Layer.provide(projectHandlers)),
68+
HttpApiBuilder.layer(QuestionApi).pipe(Layer.provide(questionHandlers)),
69+
HttpApiBuilder.layer(PermissionApi).pipe(Layer.provide(permissionHandlers)),
70+
HttpApiBuilder.layer(ProviderApi).pipe(Layer.provide(providerHandlers)),
71+
HttpApiBuilder.layer(WorkspaceApi).pipe(Layer.provide(workspaceHandlers)),
13172
).pipe(
132-
Layer.provide(auth),
133-
Layer.provide(normalize),
73+
Layer.provide(authorizationLayer),
13474
Layer.provide(instance),
13575
Layer.provide(HttpServer.layerServices),
13676
Layer.provideMerge(Observability.layer),

packages/opencode/src/server/routes/instance/httpapi/workspace.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { WorkspaceAdaptorEntry } from "@/control-plane/types"
44
import * as InstanceState from "@/effect/instance-state"
55
import { Effect, Layer, Schema } from "effect"
66
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
7+
import { Authorization } from "./auth"
78

89
const root = "/experimental/workspace"
910
export const WorkspacePaths = {
@@ -49,7 +50,8 @@ export const WorkspaceApi = HttpApi.make("workspace")
4950
title: "workspace",
5051
description: "Experimental HttpApi workspace routes.",
5152
}),
52-
),
53+
)
54+
.middleware(Authorization),
5355
)
5456
.annotateMerge(
5557
OpenApi.annotations({

0 commit comments

Comments
 (0)