This commit is contained in:
Brendan Allan
2026-04-01 12:35:55 +08:00
parent 0b93d2d8fb
commit 751d42d6a6
4 changed files with 66 additions and 70 deletions
+2 -2
View File
@@ -41,8 +41,8 @@ const DEFAULT_CSP =
const csp = (hash = "") =>
`default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
export const InstanceRoutes = (app?: Hono, upgrade?: UpgradeWebSocket) =>
(app ?? new Hono())
export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()) =>
app
.onError(errorHandler(log))
.route("/project", ProjectRoutes())
.route("/pty", PtyRoutes(upgrade))
+2 -2
View File
@@ -25,8 +25,8 @@ function local(method: string, path: string) {
return false
}
export function WorkspaceRouterMiddleware(upgrade?: UpgradeWebSocket): MiddlewareHandler {
const routes = lazy(() => InstanceRoutes(undefined, upgrade))
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
const routes = lazy(() => InstanceRoutes(upgrade))
return async (c) => {
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
+58 -62
View File
@@ -7,67 +7,7 @@ import { PtyID } from "@/pty/schema"
import { NotFoundError } from "../../storage/db"
import { errors } from "../error"
export function PtyRoutes(upgradeWebSocket?: UpgradeWebSocket) {
const connect: MiddlewareHandler = upgradeWebSocket
? upgradeWebSocket(async (c) => {
const id = PtyID.zod.parse(c.req.param("ptyID"))
const cursor = (() => {
const value = c.req.query("cursor")
if (!value) return
const parsed = Number(value)
if (!Number.isSafeInteger(parsed) || parsed < -1) return
return parsed
})()
let handler: Awaited<ReturnType<typeof Pty.connect>>
if (!(await Pty.get(id))) throw new Error("Session not found")
type Socket = {
readyState: number
send: (data: string | Uint8Array | ArrayBuffer) => void
close: (code?: number, reason?: string) => void
}
const isSocket = (value: unknown): value is Socket => {
if (!value || typeof value !== "object") return false
if (!("readyState" in value)) return false
if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false
if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false
return typeof (value as { readyState?: unknown }).readyState === "number"
}
const pending: string[] = []
let ready = false
return {
async onOpen(_event, ws) {
const socket = ws.raw
if (!isSocket(socket)) {
ws.close()
return
}
handler = await Pty.connect(id, socket, cursor)
ready = true
for (const msg of pending) handler?.onMessage(msg)
pending.length = 0
},
onMessage(event) {
if (typeof event.data !== "string") return
if (!ready) {
pending.push(event.data)
return
}
handler?.onMessage(event.data)
},
onClose() {
handler?.onClose()
},
onError() {
handler?.onClose()
},
}
})
: async (c) => c.json({ error: "WebSocket upgrade unavailable" }, 501)
export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) {
return new Hono()
.get(
"/",
@@ -209,6 +149,62 @@ export function PtyRoutes(upgradeWebSocket?: UpgradeWebSocket) {
},
}),
validator("param", z.object({ ptyID: PtyID.zod })),
connect,
upgradeWebSocket(async (c) => {
const id = PtyID.zod.parse(c.req.param("ptyID"))
const cursor = (() => {
const value = c.req.query("cursor")
if (!value) return
const parsed = Number(value)
if (!Number.isSafeInteger(parsed) || parsed < -1) return
return parsed
})()
let handler: Awaited<ReturnType<typeof Pty.connect>>
if (!(await Pty.get(id))) throw new Error("Session not found")
type Socket = {
readyState: number
send: (data: string | Uint8Array | ArrayBuffer) => void
close: (code?: number, reason?: string) => void
}
const isSocket = (value: unknown): value is Socket => {
if (!value || typeof value !== "object") return false
if (!("readyState" in value)) return false
if (!("send" in value) || typeof (value as { send?: unknown }).send !== "function") return false
if (!("close" in value) || typeof (value as { close?: unknown }).close !== "function") return false
return typeof (value as { readyState?: unknown }).readyState === "number"
}
const pending: string[] = []
let ready = false
return {
async onOpen(_event, ws) {
const socket = ws.raw
if (!isSocket(socket)) {
ws.close()
return
}
handler = await Pty.connect(id, socket, cursor)
ready = true
for (const msg of pending) handler?.onMessage(msg)
pending.length = 0
},
onMessage(event) {
if (typeof event.data !== "string") return
if (!ready) {
pending.push(event.data)
return
}
handler?.onMessage(event.data)
},
onClose() {
handler?.onClose()
},
onError() {
handler?.onClose()
},
}
}),
)
}
+4 -4
View File
@@ -44,7 +44,7 @@ export namespace Server {
export const Default = lazy(() => create({}).app)
export function ControlPlaneRoutes(app = new Hono(), opts?: { cors?: string[] }, upgrade?: UpgradeWebSocket): Hono {
export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
return app
.onError(errorHandler(log))
.use((c, next) => {
@@ -239,7 +239,7 @@ export namespace Server {
const app = new Hono()
const ws = createNodeWebSocket({ app })
return {
app: ControlPlaneRoutes(app, opts, ws.upgradeWebSocket),
app: ControlPlaneRoutes(ws.upgradeWebSocket, app, opts),
ws,
}
}
@@ -253,8 +253,8 @@ export namespace Server {
// hono-openapi can see describeRoute metadata (`.route()` wraps
// handlers when the sub-app has a custom errorHandler, which
// strips the metadata symbol).
const app = ControlPlaneRoutes(new Hono())
InstanceRoutes(app)
const { app, ws } = create({})
InstanceRoutes(ws.upgradeWebSocket, app)
const result = await generateSpecs(app, {
documentation: {
info: {