87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
import { Provider } from "@/provider/provider"
|
|
import { NamedError } from "@opencode-ai/core/util/error"
|
|
import { NotFoundError } from "@/storage/storage"
|
|
import { Session } from "@/session/session"
|
|
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
|
import type { ErrorHandler, MiddlewareHandler } from "hono"
|
|
import { HTTPException } from "hono/http-exception"
|
|
import * as Log from "@opencode-ai/core/util/log"
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
import { basicAuth } from "hono/basic-auth"
|
|
import { cors } from "hono/cors"
|
|
import { compress } from "hono/compress"
|
|
import * as ServerBackend from "./backend"
|
|
import { isAllowedCorsOrigin, type CorsOptions } from "./cors"
|
|
|
|
const log = Log.create({ service: "server" })
|
|
|
|
export const ErrorMiddleware: ErrorHandler = (err, c) => {
|
|
log.error("failed", {
|
|
error: err,
|
|
})
|
|
if (err instanceof NamedError) {
|
|
let status: ContentfulStatusCode
|
|
if (err instanceof NotFoundError) status = 404
|
|
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
|
else if (err.name === "ProviderAuthValidationFailed") status = 400
|
|
else if (err.name.startsWith("Worktree")) status = 400
|
|
else status = 500
|
|
return c.json(err.toObject(), { status })
|
|
}
|
|
if (err instanceof Session.BusyError) {
|
|
return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
|
|
}
|
|
if (err instanceof HTTPException) return err.getResponse()
|
|
const message = err instanceof Error && err.stack ? err.stack : err.toString()
|
|
return c.json(new NamedError.Unknown({ message }).toObject(), {
|
|
status: 500,
|
|
})
|
|
}
|
|
|
|
export const AuthMiddleware: MiddlewareHandler = (c, next) => {
|
|
// Allow CORS preflight requests to succeed without auth.
|
|
// Browser clients sending Authorization headers will preflight with OPTIONS.
|
|
if (c.req.method === "OPTIONS") return next()
|
|
const password = Flag.OPENCODE_SERVER_PASSWORD
|
|
if (!password) return next()
|
|
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
|
|
|
|
if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
|
|
|
|
return basicAuth({ username, password })(c, next)
|
|
}
|
|
|
|
export function LoggerMiddleware(backendAttributes: ServerBackend.Attributes): MiddlewareHandler {
|
|
return async (c, next) => {
|
|
const skip = c.req.path === "/log"
|
|
if (skip) return next()
|
|
const attributes = {
|
|
method: c.req.method,
|
|
path: c.req.path,
|
|
...backendAttributes,
|
|
}
|
|
log.info("request", attributes)
|
|
const timer = log.time("request", attributes)
|
|
await next()
|
|
timer.stop()
|
|
}
|
|
}
|
|
|
|
export function CorsMiddleware(opts?: CorsOptions): MiddlewareHandler {
|
|
return cors({
|
|
maxAge: 86_400,
|
|
origin(input) {
|
|
if (isAllowedCorsOrigin(input, opts)) return input
|
|
},
|
|
})
|
|
}
|
|
|
|
const zipped = compress()
|
|
export const CompressionMiddleware: MiddlewareHandler = (c, next) => {
|
|
const path = c.req.path
|
|
const method = c.req.method
|
|
if (path === "/event" || path === "/global/event") return next()
|
|
if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next()
|
|
return zipped(c, next)
|
|
}
|