Compare commits
1 Commits
dev
...
refactor/s
| Author | SHA1 | Date |
|---|---|---|
|
|
f3fd3488ee |
|
|
@ -67,141 +67,8 @@ export namespace Server {
|
||||||
|
|
||||||
export const createApp = (opts: { cors?: string[] }): Hono => {
|
export const createApp = (opts: { cors?: string[] }): Hono => {
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
return app
|
|
||||||
.onError((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 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,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.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"
|
|
||||||
return basicAuth({ username, password })(c, next)
|
|
||||||
})
|
|
||||||
.use(async (c, next) => {
|
|
||||||
const skipLogging = c.req.path === "/log"
|
|
||||||
if (!skipLogging) {
|
|
||||||
log.info("request", {
|
|
||||||
method: c.req.method,
|
|
||||||
path: c.req.path,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const timer = log.time("request", {
|
|
||||||
method: c.req.method,
|
|
||||||
path: c.req.path,
|
|
||||||
})
|
|
||||||
await next()
|
|
||||||
if (!skipLogging) {
|
|
||||||
timer.stop()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.use(
|
|
||||||
cors({
|
|
||||||
origin(input) {
|
|
||||||
if (!input) return
|
|
||||||
|
|
||||||
if (input.startsWith("http://localhost:")) return input
|
const InstanceRoutes: Hono = new Hono()
|
||||||
if (input.startsWith("http://127.0.0.1:")) return input
|
|
||||||
if (
|
|
||||||
input === "tauri://localhost" ||
|
|
||||||
input === "http://tauri.localhost" ||
|
|
||||||
input === "https://tauri.localhost"
|
|
||||||
)
|
|
||||||
return input
|
|
||||||
|
|
||||||
// *.opencode.ai (https only, adjust if needed)
|
|
||||||
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
if (opts?.cors?.includes(input)) {
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.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()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...errors(400),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
validator(
|
|
||||||
"param",
|
|
||||||
z.object({
|
|
||||||
providerID: ProviderID.zod,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
validator("json", Auth.Info.zod),
|
|
||||||
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()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...errors(400),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
validator(
|
|
||||||
"param",
|
|
||||||
z.object({
|
|
||||||
providerID: ProviderID.zod,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const providerID = c.req.valid("param").providerID
|
|
||||||
await Auth.remove(providerID)
|
|
||||||
return c.json(true)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.use(async (c, next) => {
|
.use(async (c, next) => {
|
||||||
if (c.req.path === "/log") return next()
|
if (c.req.path === "/log") return next()
|
||||||
const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
|
const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
|
||||||
|
|
@ -509,6 +376,143 @@ export namespace Server {
|
||||||
return c.json(await Format.status())
|
return c.json(await Format.status())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
.onError((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 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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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"
|
||||||
|
return basicAuth({ username, password })(c, next)
|
||||||
|
})
|
||||||
|
.use(async (c, next) => {
|
||||||
|
const skipLogging = c.req.path === "/log"
|
||||||
|
if (!skipLogging) {
|
||||||
|
log.info("request", {
|
||||||
|
method: c.req.method,
|
||||||
|
path: c.req.path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const timer = log.time("request", {
|
||||||
|
method: c.req.method,
|
||||||
|
path: c.req.path,
|
||||||
|
})
|
||||||
|
await next()
|
||||||
|
if (!skipLogging) {
|
||||||
|
timer.stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.use(
|
||||||
|
cors({
|
||||||
|
origin(input) {
|
||||||
|
if (!input) return
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// *.opencode.ai (https only, adjust if needed)
|
||||||
|
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
if (opts?.cors?.includes(input)) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.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()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
providerID: ProviderID.zod,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
validator("json", Auth.Info.zod),
|
||||||
|
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()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator(
|
||||||
|
"param",
|
||||||
|
z.object({
|
||||||
|
providerID: ProviderID.zod,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const providerID = c.req.valid("param").providerID
|
||||||
|
await Auth.remove(providerID)
|
||||||
|
return c.json(true)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.route("/", InstanceRoutes)
|
||||||
.all("/*", async (c) => {
|
.all("/*", async (c) => {
|
||||||
const embeddedWebUI = await embeddedUIPromise
|
const embeddedWebUI = await embeddedUIPromise
|
||||||
const path = c.req.path
|
const path = c.req.path
|
||||||
|
|
@ -516,13 +520,14 @@ export namespace Server {
|
||||||
if (embeddedWebUI) {
|
if (embeddedWebUI) {
|
||||||
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
|
||||||
if (!match) return c.json({ error: "Not Found" }, 404)
|
if (!match) return c.json({ error: "Not Found" }, 404)
|
||||||
const file = Bun.file(match)
|
const file = await Filesystem.readArrayBuffer(match).catch(() => null)
|
||||||
if (await file.exists()) {
|
if (file) {
|
||||||
c.header("Content-Type", file.type)
|
const mime = Filesystem.mimeType(match)
|
||||||
if (file.type.startsWith("text/html")) {
|
c.header("Content-Type", mime)
|
||||||
|
if (mime.startsWith("text/html")) {
|
||||||
c.header("Content-Security-Policy", DEFAULT_CSP)
|
c.header("Content-Security-Policy", DEFAULT_CSP)
|
||||||
}
|
}
|
||||||
return c.body(await file.arrayBuffer())
|
return c.body(file)
|
||||||
} else {
|
} else {
|
||||||
return c.json({ error: "Not Found" }, 404)
|
return c.json({ error: "Not Found" }, 404)
|
||||||
}
|
}
|
||||||
|
|
@ -543,7 +548,7 @@ export namespace Server {
|
||||||
response.headers.set("Content-Security-Policy", csp(hash))
|
response.headers.set("Content-Security-Policy", csp(hash))
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
}) as unknown as Hono
|
}) as Hono
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openapi() {
|
export async function openapi() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue