| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import index from "./index.html"; |
|
|
| const LOAD_BALANCER_URL = (Bun.env.LOAD_BALANCER_URL ?? "").trim().replace(/\/$/, ""); |
| const SESSION_PROXY_URL = (Bun.env.SESSION_PROXY_URL ?? "").trim().replace(/\/$/, ""); |
| const UPSTREAM = LOAD_BALANCER_URL || SESSION_PROXY_URL; |
| const PORT = Number(Bun.env.PORT ?? 3000); |
|
|
| |
| |
| |
| |
| |
| |
| function sanitizeSetCookie(sc: string): string { |
| |
| return sc |
| .split(";") |
| .map((p) => p.trim()) |
| .filter((p) => !/^domain=/i.test(p)) |
| .join("; "); |
| } |
|
|
| |
| |
| async function proxy(path: string, req: Request, init: RequestInit = {}): Promise<Response> { |
| const headers = new Headers(init.headers); |
| headers.set("Content-Type", "application/json"); |
| const cookie = req.headers.get("cookie"); |
| if (cookie) headers.set("Cookie", cookie); |
| const resp = await fetch(`${UPSTREAM}${path}`, { ...init, headers }); |
| const body = await resp.text(); |
| const out = new Response(body, { |
| status: resp.status, |
| headers: { "Content-Type": "application/json" }, |
| }); |
| const setCookies = |
| resp.headers.getSetCookie?.() ?? |
| (resp.headers.get("set-cookie") ? [resp.headers.get("set-cookie") as string] : []); |
| for (const sc of setCookies) out.headers.append("Set-Cookie", sanitizeSetCookie(sc)); |
| return out; |
| } |
|
|
| function staticFile(dir: string, name: string): Response { |
| |
| const file = Bun.file(`${import.meta.dir}/public/${dir}/${name}`); |
| return new Response(file); |
| } |
|
|
| const server = Bun.serve({ |
| port: PORT, |
| routes: { |
| "/": index, |
|
|
| "/api/config": { |
| GET: () => |
| Response.json({ |
| lb: Boolean(UPSTREAM), |
| allowDirect: !UPSTREAM, |
| }), |
| }, |
|
|
| "/api/session": { |
| POST: async (req) => { |
| if (!UPSTREAM) return Response.json({ error: "Not configured." }, { status: 404 }); |
| try { |
| return await proxy("/session", req, { method: "POST", body: "{}" }); |
| } catch (err) { |
| console.warn("session handshake failed:", err); |
| return Response.json({ error: "Speech service unreachable." }, { status: 502 }); |
| } |
| }, |
| }, |
|
|
| "/api/queue/:id": { |
| GET: async (req) => { |
| if (!UPSTREAM) return Response.json({ error: "Not configured." }, { status: 404 }); |
| try { |
| return await proxy(`/queue/${encodeURIComponent(req.params.id)}`, req); |
| } catch { |
| return Response.json({ error: "Speech service unreachable." }, { status: 502 }); |
| } |
| }, |
| DELETE: async (req) => { |
| if (!UPSTREAM) return Response.json({ error: "Not configured." }, { status: 404 }); |
| try { |
| return await proxy(`/queue/${encodeURIComponent(req.params.id)}`, req, { method: "DELETE" }); |
| } catch { |
| return Response.json({ error: "Speech service unreachable." }, { status: 502 }); |
| } |
| }, |
| }, |
|
|
| |
| |
| "/worklets/:name": (req) => staticFile("worklets", req.params.name), |
| "/vendor/:name": (req) => staticFile("vendor", req.params.name), |
| "/avatars/:name": (req) => staticFile("avatars", req.params.name), |
| }, |
|
|
| development: |
| Bun.env.NODE_ENV === "production" |
| ? false |
| : { |
| hmr: true, |
| console: true, |
| }, |
| }); |
|
|
| console.log(`gemma-avatar listening on ${server.url}`); |
| console.log( |
| UPSTREAM |
| ? `session backend: ${LOAD_BALANCER_URL ? "load balancer" : "session proxy"} (${UPSTREAM})` |
| : "session backend: none — direct mode (set LOAD_BALANCER_URL or SESSION_PROXY_URL)", |
| ); |