2025-05-31 18:41:00 +00:00
import { Log } from "../util/log"
2025-11-08 01:59:02 +00:00
import { describeRoute , generateSpecs , validator , resolver , openAPIRouteHandler } from "hono-openapi"
2025-05-31 18:41:00 +00:00
import { Hono } from "hono"
2026-03-26 18:41:22 +00:00
import { compress } from "hono/compress"
2026-04-09 05:18:46 +00:00
import { createNodeWebSocket } from "@hono/node-ws"
2025-09-15 07:28:08 +00:00
import { cors } from "hono/cors"
2026-01-12 20:23:12 +00:00
import { basicAuth } from "hono/basic-auth"
2026-04-06 17:24:55 +00:00
import type { UpgradeWebSocket } from "hono/ws"
2025-10-26 19:50:41 +00:00
import z from "zod"
2025-08-14 20:24:46 +00:00
import { Auth } from "../auth"
2026-01-12 20:23:12 +00:00
import { Flag } from "../flag/flag"
2026-03-12 13:27:52 +00:00
import { ProviderID } from "../provider/schema"
2026-03-27 20:33:56 +00:00
import { WorkspaceRouterMiddleware } from "./router"
2025-12-05 02:32:08 +00:00
import { errors } from "./error"
2026-01-16 21:21:13 +00:00
import { GlobalRoutes } from "./routes/global"
2025-12-26 20:24:44 +00:00
import { MDNS } from "./mdns"
2026-03-09 21:13:42 +00:00
import { lazy } from "@/util/lazy"
2026-03-27 15:51:21 +00:00
import { errorHandler } from "./middleware"
import { InstanceRoutes } from "./instance"
2026-03-25 14:47:40 +00:00
import { initProjectors } from "./projectors"
2026-04-09 05:18:46 +00:00
import { createAdaptorServer , type ServerType } from "@hono/node-server"
2025-06-09 18:01:11 +00:00
2025-11-20 14:42:24 +00:00
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis . AI_SDK_LOG_WARNINGS = false
2026-03-25 14:47:40 +00:00
initProjectors ( )
2025-05-18 18:13:04 +00:00
export namespace Server {
2026-04-06 17:24:55 +00:00
export type Listener = {
hostname : string
port : number
url : URL
stop : ( close? : boolean ) = > Promise < void >
}
2025-05-18 18:13:04 +00:00
2026-04-06 17:24:55 +00:00
const log = Log . create ( { service : "server" } )
2026-03-26 18:41:22 +00:00
const zipped = compress ( )
const skipCompress = ( path : string , method : string ) = > {
if ( path === "/event" || path === "/global/event" || path === "/global/sync-event" ) return true
if ( method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/ . test ( path ) ) return true
return false
}
2026-04-09 05:18:46 +00:00
export const Default = lazy ( ( ) = > create ( { } ) )
2025-12-30 03:05:08 +00:00
2026-04-06 17:24:55 +00:00
export function ControlPlaneRoutes ( upgrade : UpgradeWebSocket , app = new Hono ( ) , opts ? : { cors? : string [ ] } ) : Hono {
2026-03-09 21:13:42 +00:00
return app
2026-03-27 15:51:21 +00:00
. onError ( errorHandler ( log ) )
2026-03-09 21:13:42 +00:00
. use ( ( 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"
2026-04-09 05:18:46 +00:00
if ( c . req . query ( "auth_token" ) ) c . req . raw . headers . set ( "authorization" , ` Basic ${ c . req . query ( "auth_token" ) } ` )
2026-03-09 21:13:42 +00:00
return basicAuth ( { username , password } ) ( c , next )
} )
. use ( async ( c , next ) = > {
2026-03-27 15:51:21 +00:00
const skip = c . req . path === "/log"
if ( ! skip ) {
2026-03-09 21:13:42 +00:00
log . info ( "request" , {
2025-09-17 05:16:49 +00:00
method : c.req.method ,
path : c.req.path ,
} )
2026-03-09 21:13:42 +00:00
}
const timer = log . time ( "request" , {
method : c.req.method ,
path : c.req.path ,
2025-11-09 01:17:46 +00:00
} )
2026-03-09 21:13:42 +00:00
await next ( )
2026-04-06 17:24:55 +00:00
if ( ! skip ) timer . stop ( )
2026-03-09 21:13:42 +00:00
} )
. use (
cors ( {
2026-03-26 18:41:22 +00:00
maxAge : 86_400 ,
2026-03-09 21:13:42 +00:00
origin ( input ) {
if ( ! input ) return
2025-12-30 18:04:18 +00:00
2026-03-09 21:13:42 +00:00
if ( input . startsWith ( "http://localhost:" ) ) return input
if ( input . startsWith ( "http://127.0.0.1:" ) ) return input
if (
input === "tauri://localhost" ||
input === "http://tauri.localhost" ||
input === "https://tauri.localhost"
)
return input
2025-12-30 18:04:18 +00:00
2026-04-06 17:24:55 +00:00
if ( /^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/ . test ( input ) ) return input
if ( opts ? . cors ? . includes ( input ) ) return input
2026-03-09 21:13:42 +00:00
} ,
} ) ,
)
2026-03-26 18:41:22 +00:00
. use ( ( c , next ) = > {
if ( skipCompress ( c . req . path , c . req . method ) ) return next ( )
return zipped ( c , next )
} )
2026-03-09 21:13:42 +00:00
. route ( "/global" , GlobalRoutes ( ) )
. put (
"/auth/:providerID" ,
describeRoute ( {
summary : "Set auth credentials" ,
description : "Set authentication credentials" ,
operationId : "auth.set" ,
responses : {
200 : {
description : "Successfully set authentication credentials" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
2026-01-27 22:53:35 +00:00
} ,
} ,
} ,
2026-03-09 21:13:42 +00:00
. . . errors ( 400 ) ,
2026-01-27 22:53:35 +00:00
} ,
2026-03-09 21:13:42 +00:00
} ) ,
validator (
"param" ,
z . object ( {
2026-03-12 13:27:52 +00:00
providerID : ProviderID.zod ,
2026-03-09 21:13:42 +00:00
} ) ,
) ,
2026-03-21 04:51:35 +00:00
validator ( "json" , Auth . Info . zod ) ,
2026-03-09 21:13:42 +00:00
async ( c ) = > {
const providerID = c . req . valid ( "param" ) . providerID
const info = c . req . valid ( "json" )
await Auth . set ( providerID , info )
return c . json ( true )
} ,
)
. delete (
"/auth/:providerID" ,
describeRoute ( {
summary : "Remove auth credentials" ,
description : "Remove authentication credentials" ,
operationId : "auth.remove" ,
responses : {
200 : {
description : "Successfully removed authentication credentials" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
2026-01-27 22:53:35 +00:00
} ,
} ,
} ,
2026-03-09 21:13:42 +00:00
. . . errors ( 400 ) ,
2026-01-27 22:53:35 +00:00
} ,
2026-03-09 21:13:42 +00:00
} ) ,
validator (
"param" ,
z . object ( {
2026-03-12 13:27:52 +00:00
providerID : ProviderID.zod ,
2026-03-09 21:13:42 +00:00
} ) ,
) ,
async ( c ) = > {
const providerID = c . req . valid ( "param" ) . providerID
await Auth . remove ( providerID )
return c . json ( true )
} ,
)
. get (
"/doc" ,
openAPIRouteHandler ( app , {
documentation : {
info : {
title : "opencode" ,
version : "0.0.3" ,
description : "opencode api" ,
2026-01-08 03:29:42 +00:00
} ,
2026-03-09 21:13:42 +00:00
openapi : "3.1.1" ,
} ,
} ) ,
)
. use (
validator (
"query" ,
z . object ( {
directory : z.string ( ) . optional ( ) ,
workspace : z.string ( ) . optional ( ) ,
2026-01-08 03:29:42 +00:00
} ) ,
2026-03-09 21:13:42 +00:00
) ,
)
. post (
"/log" ,
describeRoute ( {
summary : "Write log" ,
description : "Write a log entry to the server logs with specified level and metadata." ,
operationId : "app.log" ,
responses : {
200 : {
description : "Log entry written successfully" ,
content : {
"application/json" : {
schema : resolver ( z . boolean ( ) ) ,
2026-01-08 03:29:42 +00:00
} ,
} ,
} ,
2026-03-09 21:13:42 +00:00
. . . errors ( 400 ) ,
} ,
} ) ,
validator (
"json" ,
z . object ( {
service : z.string ( ) . meta ( { description : "Service name for the log entry" } ) ,
level : z.enum ( [ "debug" , "info" , "error" , "warn" ] ) . meta ( { description : "Log level" } ) ,
message : z.string ( ) . meta ( { description : "Log message" } ) ,
extra : z
. record ( z . string ( ) , z . any ( ) )
. optional ( )
. meta ( { description : "Additional metadata for the log entry" } ) ,
2026-01-08 03:29:42 +00:00
} ) ,
2026-03-09 21:13:42 +00:00
) ,
async ( c ) = > {
const { service , level , message , extra } = c . req . valid ( "json" )
const logger = Log . create ( { service } )
2026-01-16 21:21:13 +00:00
2026-03-09 21:13:42 +00:00
switch ( level ) {
case "debug" :
logger . debug ( message , extra )
break
case "info" :
logger . info ( message , extra )
break
case "error" :
logger . error ( message , extra )
break
case "warn" :
logger . warn ( message , extra )
break
}
2025-12-05 02:32:08 +00:00
2026-03-09 21:13:42 +00:00
return c . json ( true )
} ,
)
2026-04-06 17:24:55 +00:00
. use ( WorkspaceRouterMiddleware ( upgrade ) )
}
function create ( opts : { cors? : string [ ] } ) {
const app = new Hono ( )
const ws = createNodeWebSocket ( { app } )
return {
app : ControlPlaneRoutes ( ws . upgradeWebSocket , app , opts ) ,
ws ,
}
2026-03-27 15:51:21 +00:00
}
2026-03-09 21:13:42 +00:00
2026-03-27 15:51:21 +00:00
export function createApp ( opts : { cors? : string [ ] } ) {
2026-04-06 17:24:55 +00:00
return create ( opts ) . app
2026-03-09 21:13:42 +00:00
}
2025-05-19 02:30:41 +00:00
export async function openapi() {
2026-03-27 15:51:21 +00:00
// Build a fresh app with all routes registered directly so
// hono-openapi can see describeRoute metadata (`.route()` wraps
// handlers when the sub-app has a custom errorHandler, which
// strips the metadata symbol).
2026-04-06 17:24:55 +00:00
const { app , ws } = create ( { } )
InstanceRoutes ( ws . upgradeWebSocket , app )
2026-03-27 15:51:21 +00:00
const result = await generateSpecs ( app , {
2025-05-19 02:30:41 +00:00
documentation : {
info : {
title : "opencode" ,
version : "1.0.0" ,
description : "opencode api" ,
} ,
2025-08-14 16:10:32 +00:00
openapi : "3.1.1" ,
2025-05-19 02:30:41 +00:00
} ,
2025-05-31 18:41:00 +00:00
} )
return result
2025-05-18 18:28:08 +00:00
}
2025-05-18 18:13:04 +00:00
2026-03-11 21:41:49 +00:00
export let url : URL
2026-04-06 17:24:55 +00:00
export async function listen ( opts : {
2026-02-02 20:52:32 +00:00
port : number
hostname : string
mdns? : boolean
mdnsDomain? : string
cors? : string [ ]
2026-04-06 17:24:55 +00:00
} ) : Promise < Listener > {
const built = create ( opts )
const start = ( port : number ) = >
new Promise < ServerType > ( ( resolve , reject ) = > {
const server = createAdaptorServer ( { fetch : built.app.fetch } )
built . ws . injectWebSocket ( server )
const fail = ( err : Error ) = > {
cleanup ( )
reject ( err )
}
const ready = ( ) = > {
cleanup ( )
resolve ( server )
}
const cleanup = ( ) = > {
server . off ( "error" , fail )
server . off ( "listening" , ready )
}
server . once ( "error" , fail )
server . once ( "listening" , ready )
server . listen ( port , opts . hostname )
} )
const server = opts . port === 0 ? await start ( 4096 ) . catch ( ( ) = > start ( 0 ) ) : await start ( opts . port )
const addr = server . address ( )
if ( ! addr || typeof addr === "string" ) {
throw new Error ( ` Failed to resolve server address for port ${ opts . port } ` )
2025-12-22 12:05:10 +00:00
}
2025-12-26 20:24:44 +00:00
2026-04-06 17:24:55 +00:00
const next = new URL ( "http://localhost" )
next . hostname = opts . hostname
next . port = String ( addr . port )
url = next
const mdns =
2025-12-26 20:24:44 +00:00
opts . mdns &&
2026-04-06 17:24:55 +00:00
addr . port &&
2025-12-26 20:24:44 +00:00
opts . hostname !== "127.0.0.1" &&
opts . hostname !== "localhost" &&
opts . hostname !== "::1"
2026-04-06 17:24:55 +00:00
if ( mdns ) {
MDNS . publish ( addr . port , opts . mdnsDomain )
2025-12-26 20:24:44 +00:00
} else if ( opts . mdns ) {
log . warn ( "mDNS enabled but hostname is loopback; skipping mDNS publish" )
}
2026-04-06 17:24:55 +00:00
let closing : Promise < void > | undefined
return {
hostname : opts.hostname ,
port : addr.port ,
url : next ,
stop ( close? : boolean ) {
closing ? ? = new Promise ( ( resolve , reject ) = > {
if ( mdns ) MDNS . unpublish ( )
server . close ( ( err ) = > {
if ( err ) {
reject ( err )
return
}
resolve ( )
} )
if ( close ) {
if ( "closeAllConnections" in server && typeof server . closeAllConnections === "function" ) {
server . closeAllConnections ( )
}
if ( "closeIdleConnections" in server && typeof server . closeIdleConnections === "function" ) {
server . closeIdleConnections ( )
}
}
} )
return closing
} ,
2025-12-26 20:24:44 +00:00
}
2025-05-18 01:31:42 +00:00
}
}