refactor: use Effect config for HttpApi authorization (#25035)

This commit is contained in:
Kit Langton
2026-04-29 22:22:32 -04:00
committed by GitHub
parent 38adc13295
commit cee9610d26
6 changed files with 178 additions and 92 deletions
@@ -1,17 +1,11 @@
import { Effect, Encoding, Layer, Redacted, Schema } from "effect"
import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
import { Flag } from "@opencode-ai/core/flag/flag"
class Unauthorized extends Schema.TaggedErrorClass<Unauthorized>()(
"Unauthorized",
{ message: Schema.String },
{ httpApiStatus: 401 },
) {}
import { ConfigService } from "@/effect/config-service"
import { Config, Context, Effect, Encoding, Layer, Option, Redacted } from "effect"
import { HttpApiError, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi"
export class Authorization extends HttpApiMiddleware.Service<Authorization>()(
"@opencode/ExperimentalHttpApiAuthorization",
{
error: Unauthorized,
error: HttpApiError.UnauthorizedNoContent,
security: {
basic: HttpApiSecurity.basic,
authToken: HttpApiSecurity.apiKey({ in: "query", key: "auth_token" }),
@@ -19,29 +13,38 @@ export class Authorization extends HttpApiMiddleware.Service<Authorization>()(
},
) {}
const emptyCredential = {
username: "",
password: Redacted.make(""),
}
export class ServerAuthConfig extends ConfigService.Service<ServerAuthConfig>()(
"@opencode/ExperimentalHttpApiServerAuthConfig",
{
password: Config.string("OPENCODE_SERVER_PASSWORD").pipe(Config.option),
username: Config.string("OPENCODE_SERVER_USERNAME").pipe(Config.withDefault("opencode")),
},
) {}
function validateCredential<A, E, R>(
effect: Effect.Effect<A, E, R>,
credential: { readonly username: string; readonly password: typeof emptyCredential.password },
credential: { readonly username: string; readonly password: Redacted.Redacted },
config: Context.Service.Shape<typeof ServerAuthConfig>,
) {
return Effect.gen(function* () {
if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect
if (Option.isNone(config.password) || config.password.value === "") return yield* effect
if (credential.username !== (Flag.OPENCODE_SERVER_USERNAME ?? "opencode")) {
return yield* new Unauthorized({ message: "Unauthorized" })
if (credential.username !== config.username) {
return yield* new HttpApiError.Unauthorized({})
}
if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) {
return yield* new Unauthorized({ message: "Unauthorized" })
if (Redacted.value(credential.password) !== config.password.value) {
return yield* new HttpApiError.Unauthorized({})
}
return yield* effect
})
}
function decodeCredential(input: string) {
const emptyCredential = {
username: "",
password: Redacted.make(""),
}
return Encoding.decodeBase64String(input)
.asEffect()
.pipe(
@@ -59,13 +62,16 @@ function decodeCredential(input: string) {
)
}
export const authorizationLayer = Layer.succeed(
export const authorizationLayer = Layer.effect(
Authorization,
Authorization.of({
basic: (effect, { credential }) => validateCredential(effect, credential),
authToken: (effect, { credential }) =>
Effect.gen(function* () {
return yield* validateCredential(effect, yield* decodeCredential(Redacted.value(credential)))
}),
Effect.gen(function* () {
const config = yield* ServerAuthConfig
return Authorization.of({
basic: (effect, { credential }) => validateCredential(effect, credential, config),
authToken: (effect, { credential }) =>
decodeCredential(Redacted.value(credential)).pipe(
Effect.flatMap((decoded) => validateCredential(effect, decoded, config)),
),
})
}),
)