Fix HttpApi raw route authorization (#25154)

This commit is contained in:
Kit Langton
2026-04-30 15:55:20 -04:00
committed by GitHub
parent 0e9d9282c6
commit 3250b814ce
5 changed files with 176 additions and 14 deletions

View File

@@ -0,0 +1,89 @@
import { afterEach, describe, expect, test } from "bun:test"
import { ConfigProvider, Layer } from "effect"
import { HttpRouter } from "effect/unstable/http"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Instance } from "../../src/project/instance"
import { EventPaths } from "../../src/server/routes/instance/httpapi/event"
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { PtyID } from "../../src/pty/schema"
import { resetDatabase } from "../fixture/db"
import { tmpdir } from "../fixture/fixture"
import * as Log from "@opencode-ai/core/util/log"
void Log.init({ print: false })
const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
function app(input: { password?: string; username?: string }) {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
const handler = HttpRouter.toWebHandler(
ExperimentalHttpApiServer.routes.pipe(
Layer.provide(
ConfigProvider.layer(
ConfigProvider.fromUnknown({
OPENCODE_SERVER_PASSWORD: input.password,
OPENCODE_SERVER_USERNAME: input.username,
}),
),
),
),
{ disableLogger: true },
).handler
return {
fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
request(input: string | URL | Request, init?: RequestInit) {
return this.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
},
}
}
function basic(username: string, password: string) {
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
}
async function cancelBody(response: Response) {
await response.body?.cancel().catch(() => {})
}
afterEach(async () => {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
await Instance.disposeAll()
await resetDatabase()
})
describe("HttpApi raw route authorization", () => {
test("requires configured auth before opening the raw instance event stream", async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const server = app({ password: "secret" })
const headers = { "x-opencode-directory": tmp.path }
const missing = await server.request(EventPaths.event, { headers })
await cancelBody(missing)
expect(missing.status).toBe(401)
const authed = await server.request(EventPaths.event, {
headers: { ...headers, authorization: basic("opencode", "secret") },
})
await cancelBody(authed)
expect(authed.status).toBe(200)
})
test("requires configured auth before resolving the raw PTY websocket route", async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const server = app({ password: "secret" })
const route = PtyPaths.connect.replace(":ptyID", PtyID.ascending())
const headers = { "x-opencode-directory": tmp.path }
const missing = await server.request(route, { headers })
await cancelBody(missing)
expect(missing.status).toBe(401)
const authed = await server.request(route, {
headers: { ...headers, authorization: basic("opencode", "secret") },
})
await cancelBody(authed)
expect(authed.status).toBe(404)
})
})

View File

@@ -1,4 +1,4 @@
import { afterEach, describe, expect, test } from "bun:test"
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Instance } from "../../src/project/instance"
@@ -24,6 +24,7 @@ function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
}
afterEach(async () => {
mock.restore()
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
await Instance.disposeAll()
@@ -35,6 +36,7 @@ describe("sync HttpApi", () => {
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
const info = spyOn(Log.create({ service: "server.sync" }), "info")
const session = await Instance.provide({
directory: tmp.path,
@@ -78,6 +80,8 @@ describe("sync HttpApi", () => {
})
expect(replayed.status).toBe(200)
expect(await replayed.json()).toEqual({ sessionID: session.id })
expect(info.mock.calls.some(([message]) => message === "sync replay requested")).toBe(true)
expect(info.mock.calls.some(([message]) => message === "sync replay complete")).toBe(true)
})
test("matches legacy seq validation", async () => {