diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index f60c4e6381..17efd053b9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -1,6 +1,6 @@ import { Context, Effect, Layer } from "effect" -import { HttpApiBuilder } from "effect/unstable/httpapi" -import { FetchHttpClient, HttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi" +import { FetchHttpClient, HttpClient, HttpMiddleware, HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http" import * as Socket from "effect/unstable/socket/Socket" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Account } from "@/account/account" @@ -49,6 +49,7 @@ import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@/server/cors import { serveUIEffect } from "@/server/shared/ui" import { ServerAuth } from "@/server/auth" import { InstanceHttpApi, RootHttpApi } from "./api" +import { PublicApi } from "./public" import { authorizationLayer, authorizationRouterMiddleware } from "./middleware/authorization" import { EventApi, eventHandlers } from "./event" import { configHandlers } from "./handlers/config" @@ -144,6 +145,17 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe ]), ) +const openApiDocument = OpenApi.fromApi(PublicApi) +const openApiDocumentJson = JSON.stringify(openApiDocument) + +const docRoute = HttpRouter.use((router) => + router.add( + "GET", + "/doc", + () => Effect.succeed(HttpServerResponse.text(openApiDocumentJson, { headers: { "content-type": "application/json" } })), + ), +).pipe(Layer.provide(authOnlyRouterLayer)) + const uiRoute = HttpRouter.use((router) => Effect.gen(function* () { const fs = yield* AppFileSystem.Service @@ -153,7 +165,7 @@ const uiRoute = HttpRouter.use((router) => ).pipe(Layer.provide(authOnlyRouterLayer)) export function createRoutes(corsOptions?: CorsOptions) { - return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, uiRoute).pipe( + return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, docRoute, uiRoute).pipe( Layer.provide([ errorLayer, cors(corsOptions), diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 61b1af6135..8adb21e463 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -48,6 +48,23 @@ const it = testEffect(Layer.mergeAll(testStateLayer, httpApiServerLayer)) const directoryHeader = (dir: string) => HttpClientRequest.setHeader("x-opencode-directory", dir) describe("instance HttpApi", () => { + it.live("serves the OpenAPI document", () => + Effect.gen(function* () { + const response = yield* HttpClient.get("/doc") + + expect(response.status).toBe(200) + expect(response.headers["content-type"]).toContain("application/json") + expect(yield* response.json).toMatchObject({ + openapi: expect.any(String), + info: expect.any(Object), + paths: expect.objectContaining({ + "/global/health": expect.any(Object), + "/session": expect.any(Object), + }), + }) + }), + ) + it.live("serves path and VCS read endpoints", () => Effect.gen(function* () { const dir = yield* tmpdirScoped({ git: true })