Compare commits

...

1 Commits

Author SHA1 Message Date
Dax Raad 251ce4a2ab fix(opencode): validate upgrade requests 2026-08-12 13:14:58 +00:00
3 changed files with 51 additions and 5 deletions
@@ -5,7 +5,8 @@ import { InstanceDisposed } from "@/server/event"
import "@opencode-ai/core/account"
import "@/server/event"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import semver from "semver"
import { described } from "./metadata"
const GlobalHealth = Schema.Struct({
@@ -48,7 +49,9 @@ const GlobalEventSchema = Schema.Struct({
}).annotate({ identifier: "GlobalEvent" })
export const GlobalUpgradeInput = Schema.Struct({
target: Schema.optional(Schema.String),
target: Schema.optional(
Schema.String.check(Schema.makeFilter((value) => semver.valid(value) !== null || "Expected a semantic version")),
),
})
const GlobalUpgradeResult = Schema.Union([
@@ -121,7 +124,7 @@ export const GlobalApi = HttpApi.make("global").add(
}),
),
HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, {
payload: [HttpApiSchema.NoContent, GlobalUpgradeInput],
payload: GlobalUpgradeInput,
success: described(GlobalUpgradeResult, "Upgrade result"),
error: HttpApiError.BadRequest,
}).annotateMerge(
@@ -129,6 +129,10 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
const upgradeRaw = Effect.fn("GlobalHttpApi.upgradeRaw")(function* (ctx: {
request: HttpServerRequest.HttpServerRequest
}) {
const contentType = ctx.request.headers["content-type"]?.split(";", 1)[0].trim().toLowerCase()
if (contentType !== "application/json") {
return HttpServerResponse.jsonUnsafe({ success: false, error: "Expected application/json" }, { status: 415 })
}
const body = yield* Effect.orDie(ctx.request.text)
const json = parseBody(body)
if (json === undefined) {
@@ -43,9 +43,12 @@ const apiLayer = HttpRouter.serve(
const it = testEffect(apiLayer)
describe("global HttpApi", () => {
it.live("upgrades to latest when the request body is omitted", () =>
it.live("upgrades to latest with an empty JSON payload", () =>
Effect.gen(function* () {
const response = yield* HttpClient.post(GlobalPaths.upgrade)
const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe(
HttpClientRequest.bodyJson({}),
Effect.flatMap(HttpClient.execute),
)
expect(response.status).toBe(200)
expect(yield* response.json).toEqual({ success: true, version: "9.9.9" })
@@ -63,4 +66,40 @@ describe("global HttpApi", () => {
expect(yield* response.json).toEqual({ success: false, error: "Invalid request body" })
}),
)
it.live("rejects non-JSON upgrade payloads", () =>
Effect.gen(function* () {
const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe(
HttpClientRequest.setBody(HttpBody.text('{"target":"1.2.3"}', "text/plain")),
HttpClient.execute,
)
expect(response.status).toBe(415)
expect(yield* response.json).toEqual({ success: false, error: "Expected application/json" })
}),
)
it.live("rejects non-semver upgrade targets", () =>
Effect.gen(function* () {
const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe(
HttpClientRequest.bodyJson({ target: "latest" }),
Effect.flatMap(HttpClient.execute),
)
expect(response.status).toBe(400)
expect(yield* response.json).toEqual({ success: false, error: "Invalid request body" })
}),
)
it.live("accepts semantic version upgrade targets", () =>
Effect.gen(function* () {
const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe(
HttpClientRequest.bodyJson({ target: "1.2.3-beta.1" }),
Effect.flatMap(HttpClient.execute),
)
expect(response.status).toBe(200)
expect(yield* response.json).toEqual({ success: true, version: "1.2.3-beta.1" })
}),
)
})