Compare commits
1 commit
dev
...
kit/pty-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da5e29b320 |
5 changed files with 65 additions and 50 deletions
|
|
@ -2,7 +2,7 @@ export * as ServerAuth from "./auth"
|
||||||
|
|
||||||
import { ConfigService } from "@/effect/config-service"
|
import { ConfigService } from "@/effect/config-service"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Config as EffectConfig, Context, Option, Redacted } from "effect"
|
import { Config as EffectConfig, Context, Layer, Option, Redacted } from "effect"
|
||||||
|
|
||||||
export type Credentials = {
|
export type Credentials = {
|
||||||
password?: string
|
password?: string
|
||||||
|
|
@ -14,10 +14,31 @@ export type DecodedCredentials = {
|
||||||
readonly password: Redacted.Redacted
|
readonly password: Redacted.Redacted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read auth config from `Flag.*` instead of via Effect's `Config` system.
|
||||||
|
// Effect's generated `defaultLayer` reads `Config.string(...)` once and is
|
||||||
|
// memoized by `Layer` identity, so subsequent runtime mutation of
|
||||||
|
// `process.env` is never observed by the resolved layer. Tests and dynamic
|
||||||
|
// deploys mutate `Flag.OPENCODE_SERVER_*` at runtime; matching Hono's
|
||||||
|
// behavior requires re-reading `Flag.*` whenever a fresh listener (i.e. a
|
||||||
|
// fresh `memoMap`) is built. `Layer.sync` defers the read until layer-build
|
||||||
|
// time, so each new listener picks up the current `Flag.*` values.
|
||||||
|
//
|
||||||
|
// Note: this is per-listener, not per-request. Hono's `AuthMiddleware` reads
|
||||||
|
// `Flag.*` on every request; if exact per-request parity is ever required,
|
||||||
|
// the middleware itself must read `Flag.*` rather than yielding `Config`.
|
||||||
export class Config extends ConfigService.Service<Config>()("@opencode/ServerAuthConfig", {
|
export class Config extends ConfigService.Service<Config>()("@opencode/ServerAuthConfig", {
|
||||||
password: EffectConfig.string("OPENCODE_SERVER_PASSWORD").pipe(EffectConfig.option),
|
password: EffectConfig.string("OPENCODE_SERVER_PASSWORD").pipe(EffectConfig.option),
|
||||||
username: EffectConfig.string("OPENCODE_SERVER_USERNAME").pipe(EffectConfig.withDefault("opencode")),
|
username: EffectConfig.string("OPENCODE_SERVER_USERNAME").pipe(EffectConfig.withDefault("opencode")),
|
||||||
}) {}
|
}) {
|
||||||
|
static override get defaultLayer() {
|
||||||
|
return Layer.sync(this, () =>
|
||||||
|
this.of({
|
||||||
|
password: Flag.OPENCODE_SERVER_PASSWORD ? Option.some(Flag.OPENCODE_SERVER_PASSWORD) : Option.none(),
|
||||||
|
username: Flag.OPENCODE_SERVER_USERNAME ?? "opencode",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type Info = Context.Service.Shape<typeof Config>
|
export type Info = Context.Service.Shape<typeof Config>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ import { lazy } from "@/util/lazy"
|
||||||
import { Vcs } from "@/project/vcs"
|
import { Vcs } from "@/project/vcs"
|
||||||
import { Worktree } from "@/worktree"
|
import { Worktree } from "@/worktree"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
|
import { ServerAuth } from "@/server/auth"
|
||||||
import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@/server/cors"
|
import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@/server/cors"
|
||||||
import { serveUIEffect } from "@/server/shared/ui"
|
import { serveUIEffect } from "@/server/shared/ui"
|
||||||
import { ServerAuth } from "@/server/auth"
|
|
||||||
import { InstanceHttpApi, RootHttpApi } from "./api"
|
import { InstanceHttpApi, RootHttpApi } from "./api"
|
||||||
import { authorizationLayer, authorizationRouterMiddleware } from "./middleware/authorization"
|
import { authorizationLayer, authorizationRouterMiddleware } from "./middleware/authorization"
|
||||||
import { EventApi, eventHandlers } from "./event"
|
import { EventApi, eventHandlers } from "./event"
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ async function startListener(backend: "effect-httpapi" | "hono" = "effect-httpap
|
||||||
return Server.listen({ hostname: "127.0.0.1", port: 0 })
|
return Server.listen({ hostname: "127.0.0.1", port: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startNoAuthListener() {
|
async function startNoAuthListener(backend: "effect-httpapi" | "hono" = "effect-httpapi") {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "effect-httpapi"
|
||||||
Flag.OPENCODE_SERVER_PASSWORD = undefined
|
Flag.OPENCODE_SERVER_PASSWORD = undefined
|
||||||
Flag.OPENCODE_SERVER_USERNAME = auth.username
|
Flag.OPENCODE_SERVER_USERNAME = auth.username
|
||||||
delete process.env.OPENCODE_SERVER_PASSWORD
|
delete process.env.OPENCODE_SERVER_PASSWORD
|
||||||
|
|
@ -257,18 +257,20 @@ describe("HttpApi Server.listen", () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => {
|
for (const backend of ["effect-httpapi", "hono"] as const) {
|
||||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
testPty(`keeps PTY websocket tickets optional when server auth is disabled (${backend})`, async () => {
|
||||||
const listener = await startNoAuthListener()
|
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||||
try {
|
const listener = await startNoAuthListener(backend)
|
||||||
const info = await createCat(listener, tmp.path)
|
try {
|
||||||
const ws = await openSocket(socketURL(listener, info.id, tmp.path))
|
const info = await createCat(listener, tmp.path)
|
||||||
const message = waitForMessage(ws, (message) => message.includes("ping-no-auth"))
|
const ws = await openSocket(socketURL(listener, info.id, tmp.path))
|
||||||
ws.send("ping-no-auth\n")
|
const message = waitForMessage(ws, (message) => message.includes(`ping-no-auth-${backend}`))
|
||||||
expect(await message).toContain("ping-no-auth")
|
ws.send(`ping-no-auth-${backend}\n`)
|
||||||
ws.close(1000)
|
expect(await message).toContain(`ping-no-auth-${backend}`)
|
||||||
} finally {
|
ws.close(1000)
|
||||||
await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined)
|
} finally {
|
||||||
}
|
await stop(listener, "timed out cleaning up no-auth listener").catch(() => undefined)
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { afterEach, describe, expect, test } from "bun:test"
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
import { ConfigProvider, Layer } from "effect"
|
|
||||||
import { HttpRouter } from "effect/unstable/http"
|
import { HttpRouter } from "effect/unstable/http"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { Instance } from "../../src/project/instance"
|
|
||||||
import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
|
import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
|
||||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
||||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
||||||
|
|
@ -13,23 +11,19 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||||
|
|
||||||
void Log.init({ print: false })
|
void Log.init({ print: false })
|
||||||
|
|
||||||
const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
const original = {
|
||||||
|
OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
|
||||||
|
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
|
||||||
|
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
|
||||||
|
}
|
||||||
|
|
||||||
function app(input: { password?: string; username?: string }) {
|
function app(input: { password?: string; username?: string }) {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||||
const handler = HttpRouter.toWebHandler(
|
Flag.OPENCODE_SERVER_PASSWORD = input.password
|
||||||
ExperimentalHttpApiServer.routes.pipe(
|
Flag.OPENCODE_SERVER_USERNAME = input.username
|
||||||
Layer.provide(
|
const handler = HttpRouter.toWebHandler(ExperimentalHttpApiServer.routes, {
|
||||||
ConfigProvider.layer(
|
disableLogger: true,
|
||||||
ConfigProvider.fromUnknown({
|
}).handler
|
||||||
OPENCODE_SERVER_PASSWORD: input.password,
|
|
||||||
OPENCODE_SERVER_USERNAME: input.username,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
{ disableLogger: true },
|
|
||||||
).handler
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
|
fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
|
||||||
|
|
@ -48,7 +42,9 @@ async function cancelBody(response: Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||||
|
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
|
||||||
|
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
|
||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
await resetDatabase()
|
await resetDatabase()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterEach, describe, expect, test } from "bun:test"
|
import { afterEach, describe, expect, test } from "bun:test"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { ConfigProvider, Effect, Layer } from "effect"
|
import { Effect, Layer, Option } from "effect"
|
||||||
import {
|
import {
|
||||||
HttpClient,
|
HttpClient,
|
||||||
HttpClientRequest,
|
HttpClientRequest,
|
||||||
|
|
@ -50,12 +50,10 @@ function app(input?: { password?: string; username?: string }) {
|
||||||
const handler = HttpRouter.toWebHandler(
|
const handler = HttpRouter.toWebHandler(
|
||||||
ExperimentalHttpApiServer.routes.pipe(
|
ExperimentalHttpApiServer.routes.pipe(
|
||||||
Layer.provide(
|
Layer.provide(
|
||||||
ConfigProvider.layer(
|
ServerAuth.Config.layer({
|
||||||
ConfigProvider.fromUnknown({
|
password: input?.password ? Option.some(input.password) : Option.none(),
|
||||||
OPENCODE_SERVER_PASSWORD: input?.password,
|
username: input?.username ?? "opencode",
|
||||||
OPENCODE_SERVER_USERNAME: input?.username,
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
{ disableLogger: true },
|
{ disableLogger: true },
|
||||||
|
|
@ -71,6 +69,10 @@ function app(input?: { password?: string; username?: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer<HttpClient.HttpClient> }) {
|
function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer<HttpClient.HttpClient> }) {
|
||||||
|
const authConfigLayer = ServerAuth.Config.layer({
|
||||||
|
password: input?.password ? Option.some(input.password) : Option.none(),
|
||||||
|
username: input?.username ?? "opencode",
|
||||||
|
})
|
||||||
const handler = HttpRouter.toWebHandler(
|
const handler = HttpRouter.toWebHandler(
|
||||||
HttpRouter.use((router) =>
|
HttpRouter.use((router) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -79,17 +81,11 @@ function uiApp(input?: { password?: string; username?: string; client?: Layer.La
|
||||||
yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
|
yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
|
||||||
}),
|
}),
|
||||||
).pipe(
|
).pipe(
|
||||||
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))),
|
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(authConfigLayer))),
|
||||||
Layer.provide([
|
Layer.provide([
|
||||||
AppFileSystem.defaultLayer,
|
AppFileSystem.defaultLayer,
|
||||||
input?.client ?? httpClient(new Response("ui")),
|
input?.client ?? httpClient(new Response("ui")),
|
||||||
HttpServer.layerServices,
|
HttpServer.layerServices,
|
||||||
ConfigProvider.layer(
|
|
||||||
ConfigProvider.fromUnknown({
|
|
||||||
OPENCODE_SERVER_PASSWORD: input?.password,
|
|
||||||
OPENCODE_SERVER_USERNAME: input?.username,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
{ disableLogger: true },
|
{ disableLogger: true },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue