summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-27 21:48:50 -0400
committerGitHub <[email protected]>2026-04-27 21:48:50 -0400
commit892fd85ba7446bc593a1e76af0a2bc2c2bfc4c49 (patch)
tree2556d05b3225faa3ae3868cf7b40da79fe08c548 /packages
parent0eaa47d857e7cb1050ea329bb4ba09b456ba9142 (diff)
downloadopencode-892fd85ba7446bc593a1e76af0a2bc2c2bfc4c49.tar.gz
opencode-892fd85ba7446bc593a1e76af0a2bc2c2bfc4c49.zip
fix(httpapi): preserve provider oauth authorize parity (#24703)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/server/routes/instance/experimental.ts14
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/provider.ts19
-rw-r--r--packages/opencode/src/server/routes/instance/session.ts12
-rw-r--r--packages/opencode/test/server/httpapi-provider.test.ts152
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts6
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts6
6 files changed, 195 insertions, 14 deletions
diff --git a/packages/opencode/src/server/routes/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts
index ec46298f5..7e09fb9ad 100644
--- a/packages/opencode/src/server/routes/instance/experimental.ts
+++ b/packages/opencode/src/server/routes/instance/experimental.ts
@@ -37,7 +37,15 @@ const ConsoleSwitchBody = z.object({
orgID: z.string(),
})
-const QueryBoolean = z.enum(["true", "false"]).transform((value) => value === "true")
+const QueryBoolean = z.union([
+ z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean()),
+ z.enum(["true", "false"]),
+])
+
+function queryBoolean(value: z.infer<typeof QueryBoolean> | undefined) {
+ if (value === undefined) return
+ return value === true || value === "true"
+}
export const ExperimentalRoutes = lazy(() =>
new Hono()
@@ -368,12 +376,12 @@ export const ExperimentalRoutes = lazy(() =>
const sessions: Session.GlobalInfo[] = []
for await (const session of Session.listGlobal({
directory: query.directory,
- roots: query.roots,
+ roots: queryBoolean(query.roots),
start: query.start,
cursor: query.cursor,
search: query.search,
limit: limit + 1,
- archived: query.archived,
+ archived: queryBoolean(query.archived),
})) {
sessions.push(session)
}
diff --git a/packages/opencode/src/server/routes/instance/httpapi/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/provider.ts
index 9f4be61ad..59a17cf0f 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/provider.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/provider.ts
@@ -5,6 +5,7 @@ import { Provider } from "@/provider/provider"
import { ProviderID } from "@/provider/schema"
import { mapValues } from "remeda"
import { Effect, Layer, Schema } from "effect"
+import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "./auth"
@@ -35,7 +36,7 @@ export const ProviderApi = HttpApi.make("provider")
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
params: { providerID: ProviderID },
payload: ProviderAuth.AuthorizeInput,
- success: ProviderAuth.Authorization,
+ success: Schema.UndefinedOr(ProviderAuth.Authorization),
}).annotateMerge(
OpenApi.annotations({
identifier: "provider.oauth.authorize",
@@ -115,10 +116,22 @@ export const providerHandlers = Layer.unwrap(
inputs: ctx.payload.inputs,
})
.pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
- if (!result) return yield* new HttpApiError.BadRequest({})
return result
})
+ const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: {
+ params: { providerID: ProviderID }
+ request: HttpServerRequest.HttpServerRequest
+ }) {
+ const body = yield* Effect.orDie(ctx.request.text)
+ const payload = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ProviderAuth.AuthorizeInput))(body).pipe(
+ Effect.mapError(() => new HttpApiError.BadRequest({})),
+ )
+ const result = yield* authorize({ params: ctx.params, payload })
+ if (result === undefined) return HttpServerResponse.empty({ status: 200 })
+ return HttpServerResponse.jsonUnsafe(result)
+ })
+
const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: {
params: { providerID: ProviderID }
payload: ProviderAuth.CallbackInput
@@ -134,7 +147,7 @@ export const providerHandlers = Layer.unwrap(
})
return HttpApiBuilder.group(ProviderApi, "provider", (handlers) =>
- handlers.handle("list", list).handle("auth", auth).handle("authorize", authorize).handle("callback", callback),
+ handlers.handle("list", list).handle("auth", auth).handleRaw("authorize", authorizeRaw).handle("callback", callback),
)
}),
).pipe(
diff --git a/packages/opencode/src/server/routes/instance/session.ts b/packages/opencode/src/server/routes/instance/session.ts
index a5958d37a..8a7752e34 100644
--- a/packages/opencode/src/server/routes/instance/session.ts
+++ b/packages/opencode/src/server/routes/instance/session.ts
@@ -30,7 +30,15 @@ import { jsonRequest, runRequest } from "./trace"
const log = Log.create({ service: "server" })
-const QueryBoolean = z.enum(["true", "false"]).transform((value) => value === "true")
+const QueryBoolean = z.union([
+ z.preprocess((value) => (value === "true" ? true : value === "false" ? false : value), z.boolean()),
+ z.enum(["true", "false"]),
+])
+
+function queryBoolean(value: z.infer<typeof QueryBoolean> | undefined) {
+ if (value === undefined) return
+ return value === true || value === "true"
+}
export const SessionRoutes = lazy(() =>
new Hono()
@@ -69,7 +77,7 @@ export const SessionRoutes = lazy(() =>
const sessions: Session.Info[] = []
for await (const session of Session.list({
directory: query.directory,
- roots: query.roots,
+ roots: queryBoolean(query.roots),
start: query.start,
search: query.search,
limit: query.limit,
diff --git a/packages/opencode/test/server/httpapi-provider.test.ts b/packages/opencode/test/server/httpapi-provider.test.ts
new file mode 100644
index 000000000..8a0793514
--- /dev/null
+++ b/packages/opencode/test/server/httpapi-provider.test.ts
@@ -0,0 +1,152 @@
+import { afterEach, describe, expect } from "bun:test"
+import type { UpgradeWebSocket } from "hono/ws"
+import { Effect, FileSystem, Layer, Path } from "effect"
+import { NodeFileSystem, NodePath } from "@effect/platform-node"
+import { Flag } from "@opencode-ai/core/flag/flag"
+import { Instance } from "../../src/project/instance"
+import { InstanceRoutes } from "../../src/server/routes/instance"
+import * as Log from "@opencode-ai/core/util/log"
+import { resetDatabase } from "../fixture/db"
+import { provideInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+void Log.init({ print: false })
+
+const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
+const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
+const it = testEffect(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
+const providerID = "test-oauth-parity"
+const oauthURL = "https://example.com/oauth"
+const oauthInstructions = "Finish OAuth"
+
+function app(experimental: boolean) {
+ Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
+ return InstanceRoutes(websocket)
+}
+
+function requestAuthorize(input: {
+ app: ReturnType<typeof InstanceRoutes>
+ providerID: string
+ method: number
+ headers: HeadersInit
+}) {
+ return Effect.promise(async () => {
+ const response = await input.app.request(`/provider/${input.providerID}/oauth/authorize`, {
+ method: "POST",
+ headers: input.headers,
+ body: JSON.stringify({ method: input.method }),
+ })
+ return {
+ status: response.status,
+ body: await response.text(),
+ }
+ })
+}
+
+function writeProviderAuthPlugin(dir: string) {
+ return Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem
+ const path = yield* Path.Path
+
+ yield* fs.makeDirectory(path.join(dir, ".opencode", "plugin"), { recursive: true })
+ yield* fs.writeFileString(
+ path.join(dir, ".opencode", "plugin", "provider-oauth-parity.ts"),
+ [
+ "export default {",
+ ' id: "test.provider-oauth-parity",',
+ " server: async () => ({",
+ " auth: {",
+ ` provider: "${providerID}",`,
+ " methods: [",
+ ' { type: "api", label: "API key" },',
+ " {",
+ ' type: "oauth",',
+ ' label: "OAuth",',
+ " authorize: async () => ({",
+ ` url: "${oauthURL}",`,
+ ' method: "code",',
+ ` instructions: "${oauthInstructions}",`,
+ " callback: async () => ({ type: 'success', key: 'token' }),",
+ " }),",
+ " },",
+ " ],",
+ " },",
+ " }),",
+ "}",
+ "",
+ ].join("\n"),
+ )
+ })
+}
+
+function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
+ return Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem
+ const path = yield* Path.Path
+ const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })
+
+ yield* fs.writeFileString(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }),
+ )
+ yield* writeProviderAuthPlugin(dir)
+ yield* Effect.addFinalizer(() =>
+ Effect.promise(() => Instance.provide({ directory: dir, fn: () => Instance.dispose() })).pipe(Effect.ignore),
+ )
+
+ return yield* self(dir).pipe(provideInstance(dir))
+ })
+}
+
+afterEach(async () => {
+ Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
+ await Instance.disposeAll()
+ await resetDatabase()
+})
+
+describe("provider HttpApi", () => {
+ it.live(
+ "matches legacy OAuth authorize response shapes",
+ withProviderProject((dir) =>
+ Effect.gen(function* () {
+ const headers = { "x-opencode-directory": dir, "content-type": "application/json" }
+ const legacy = app(false)
+ const httpapi = app(true)
+
+ const apiLegacy = yield* requestAuthorize({
+ app: legacy,
+ providerID,
+ method: 0,
+ headers,
+ })
+ const apiHttpApi = yield* requestAuthorize({
+ app: httpapi,
+ providerID,
+ method: 0,
+ headers,
+ })
+ expect(apiLegacy).toEqual({ status: 200, body: "" })
+ expect(apiHttpApi).toEqual(apiLegacy)
+
+ const oauthLegacy = yield* requestAuthorize({
+ app: legacy,
+ providerID,
+ method: 1,
+ headers,
+ })
+ const oauthHttpApi = yield* requestAuthorize({
+ app: httpapi,
+ providerID,
+ method: 1,
+ headers,
+ })
+ expect(oauthHttpApi).toEqual(oauthLegacy)
+ expect(JSON.parse(oauthHttpApi.body)).toEqual({
+ url: oauthURL,
+ method: "code",
+ instructions: oauthInstructions,
+ })
+ }),
+ ),
+ )
+})
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index b361a095e..1dafe88d1 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -848,12 +848,12 @@ export class Session extends HeyApiClient {
parameters?: {
directory?: string
workspace?: string
- roots?: "true" | "false"
+ roots?: boolean | "true" | "false"
start?: number
cursor?: number
search?: string
limit?: number
- archived?: "true" | "false"
+ archived?: boolean | "true" | "false"
},
options?: Options<never, ThrowOnError>,
) {
@@ -1647,7 +1647,7 @@ export class Session2 extends HeyApiClient {
parameters?: {
directory?: string
workspace?: string
- roots?: "true" | "false"
+ roots?: boolean | "true" | "false"
start?: number
search?: string
limit?: number
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index ab2326548..201bf226c 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -3217,7 +3217,7 @@ export type ExperimentalSessionListData = {
/**
* Only return root sessions (no parentID)
*/
- roots?: "true" | "false"
+ roots?: boolean | "true" | "false"
/**
* Filter sessions updated on or after this timestamp (milliseconds since epoch)
*/
@@ -3237,7 +3237,7 @@ export type ExperimentalSessionListData = {
/**
* Include archived sessions (default false)
*/
- archived?: "true" | "false"
+ archived?: boolean | "true" | "false"
}
url: "/experimental/session"
}
@@ -3285,7 +3285,7 @@ export type SessionListData = {
/**
* Only return root sessions (no parentID)
*/
- roots?: "true" | "false"
+ roots?: boolean | "true" | "false"
/**
* Filter sessions updated on or after this timestamp (milliseconds since epoch)
*/