Skip to content

Commit ea3c6c3

Browse files
authored
fix(httpapi): document instance query parameters (#24809)
1 parent 9b68b71 commit ea3c6c3

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

packages/opencode/src/server/routes/instance/httpapi/pty.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
118118
.add(
119119
HttpApiEndpoint.get("connect", PtyPaths.connect, {
120120
params: Params,
121-
query: CursorQuery,
122121
success: Schema.Boolean,
123122
}).annotateMerge(
124123
OpenApi.annotations({

packages/opencode/src/server/routes/instance/httpapi/public.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,56 @@ import { SyncApi } from "./sync"
1717
import { TuiApi } from "./tui"
1818
import { WorkspaceApi } from "./workspace"
1919

20+
type OpenApiParameter = {
21+
name: string
22+
in: string
23+
required?: boolean
24+
schema?: unknown
25+
}
26+
27+
type OpenApiOperation = {
28+
parameters?: OpenApiParameter[]
29+
}
30+
31+
type OpenApiPathItem = Partial<Record<"get" | "post" | "put" | "delete" | "patch", OpenApiOperation>>
32+
33+
type OpenApiSpec = {
34+
paths?: Record<string, OpenApiPathItem>
35+
}
36+
37+
const InstanceQueryParameters = [
38+
{
39+
name: "directory",
40+
in: "query",
41+
required: false,
42+
schema: { type: "string" },
43+
},
44+
{
45+
name: "workspace",
46+
in: "query",
47+
required: false,
48+
schema: { type: "string" },
49+
},
50+
] satisfies OpenApiParameter[]
51+
52+
function documentInstanceQueryParameters(input: Record<string, unknown>) {
53+
const spec = input as OpenApiSpec
54+
for (const [path, item] of Object.entries(spec.paths ?? {})) {
55+
if (path.startsWith("/global/") || path.startsWith("/auth/")) continue
56+
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
57+
const operation = item[method]
58+
if (!operation) continue
59+
operation.parameters = [
60+
...InstanceQueryParameters,
61+
...(operation.parameters ?? []).filter(
62+
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
63+
),
64+
]
65+
}
66+
}
67+
return input
68+
}
69+
2070
export const PublicApi = HttpApi.make("opencode")
2171
.addHttpApi(ControlApi)
2272
.addHttpApi(GlobalApi)
@@ -41,5 +91,6 @@ export const PublicApi = HttpApi.make("opencode")
4191
title: "opencode",
4292
version: "1.0.0",
4393
description: "opencode api",
94+
transform: documentInstanceQueryParameters,
4495
}),
4596
)

packages/opencode/test/server/httpapi-bridge.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,32 @@ function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof m
3434
.sort()
3535
}
3636

37+
function openApiParameters(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], Operation>>> }) {
38+
return Object.fromEntries(
39+
Object.entries(spec.paths).flatMap(([path, item]) =>
40+
methods
41+
.filter((method) => item[method])
42+
.map((method) => [
43+
`${method.toUpperCase()} ${path}`,
44+
(item[method]?.parameters ?? [])
45+
.map(parameterKey)
46+
.filter((param) => param !== undefined)
47+
.sort(),
48+
]),
49+
),
50+
)
51+
}
52+
53+
type Operation = {
54+
parameters?: unknown[]
55+
}
56+
57+
function parameterKey(param: unknown) {
58+
if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return
59+
if (typeof param.in !== "string" || typeof param.name !== "string") return
60+
return `${param.in}:${param.name}:${"required" in param && param.required === true}`
61+
}
62+
3763
function authorization(username: string, password: string) {
3864
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
3965
}
@@ -63,6 +89,17 @@ describe("HttpApi server", () => {
6389
expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([])
6490
})
6591

92+
test("matches generated OpenAPI route parameters", async () => {
93+
const hono = openApiParameters(await Server.openapi())
94+
const effect = openApiParameters(OpenApi.fromApi(PublicApi))
95+
96+
expect(
97+
Object.keys(hono)
98+
.filter((route) => JSON.stringify(hono[route]) !== JSON.stringify(effect[route]))
99+
.map((route) => ({ route, hono: hono[route], effect: effect[route] })),
100+
).toEqual([])
101+
})
102+
66103
test("allows requests when auth is disabled", async () => {
67104
await using tmp = await tmpdir({ git: true })
68105
await Bun.write(`${tmp.path}/hello.txt`, "hello")

0 commit comments

Comments
 (0)