Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
.add(
HttpApiEndpoint.get("connect", PtyPaths.connect, {
params: Params,
query: CursorQuery,
success: Schema.Boolean,
}).annotateMerge(
OpenApi.annotations({
Expand Down
51 changes: 51 additions & 0 deletions packages/opencode/src/server/routes/instance/httpapi/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,56 @@ import { SyncApi } from "./sync"
import { TuiApi } from "./tui"
import { WorkspaceApi } from "./workspace"

type OpenApiParameter = {
name: string
in: string
required?: boolean
schema?: unknown
}

type OpenApiOperation = {
parameters?: OpenApiParameter[]
}

type OpenApiPathItem = Partial<Record<"get" | "post" | "put" | "delete" | "patch", OpenApiOperation>>

type OpenApiSpec = {
paths?: Record<string, OpenApiPathItem>
}

const InstanceQueryParameters = [
{
name: "directory",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "workspace",
in: "query",
required: false,
schema: { type: "string" },
},
] satisfies OpenApiParameter[]

function documentInstanceQueryParameters(input: Record<string, unknown>) {
const spec = input as OpenApiSpec
for (const [path, item] of Object.entries(spec.paths ?? {})) {
if (path.startsWith("/global/") || path.startsWith("/auth/")) continue
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
const operation = item[method]
if (!operation) continue
operation.parameters = [
...InstanceQueryParameters,
...(operation.parameters ?? []).filter(
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
),
]
}
}
return input
}

export const PublicApi = HttpApi.make("opencode")
.addHttpApi(ControlApi)
.addHttpApi(GlobalApi)
Expand All @@ -41,5 +91,6 @@ export const PublicApi = HttpApi.make("opencode")
title: "opencode",
version: "1.0.0",
description: "opencode api",
transform: documentInstanceQueryParameters,
}),
)
37 changes: 37 additions & 0 deletions packages/opencode/test/server/httpapi-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof m
.sort()
}

function openApiParameters(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }) {
return Object.fromEntries(
Object.entries(spec.paths).flatMap(([path, item]) =>
methods
.filter((method) => item[method])
.map((method) => [
`${method.toUpperCase()} ${path}`,
(item[method]?.parameters ?? [])
.map(parameterKey)
.filter((param) => param !== undefined)
.sort(),
]),
),
)
}

type Operation = {
parameters?: unknown[]
}

function parameterKey(param: unknown) {
if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return
if (typeof param.in !== "string" || typeof param.name !== "string") return
return `${param.in}:${param.name}:${"required" in param && param.required === true}`
}

function authorization(username: string, password: string) {
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
}
Expand Down Expand Up @@ -63,6 +89,17 @@ describe("HttpApi server", () => {
expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([])
})

test("matches generated OpenAPI route parameters", async () => {
const hono = openApiParameters(await Server.openapi())
const effect = openApiParameters(OpenApi.fromApi(PublicApi))

expect(
Object.keys(hono)
.filter((route) => JSON.stringify(hono[route]) !== JSON.stringify(effect[route]))
.map((route) => ({ route, hono: hono[route], effect: effect[route] })),
).toEqual([])
})

test("allows requests when auth is disabled", async () => {
await using tmp = await tmpdir({ git: true })
await Bun.write(`${tmp.path}/hello.txt`, "hello")
Expand Down
Loading