101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import './servers/bootstrap';
|
|
import type { ServerWebSocket } from 'bun';
|
|
import { serve } from 'bun';
|
|
import { eq } from 'drizzle-orm';
|
|
import { honoServer } from './servers/hono';
|
|
import { verify } from './servers/jwt';
|
|
import { officerdb, TokenBlacklist } from 'officerdb';
|
|
import { claudeWebsocket } from './servers/api/claude/websocket';
|
|
import { opencodeWebsocket } from './servers/api/opencode/websocket';
|
|
import { terminalWebsocket, initTerminalSidecars } from './servers/api/terminal/websocket';
|
|
import officerWeb from './apps/officer-web/index.html';
|
|
|
|
const { PORT = '5000' } = process.env;
|
|
|
|
type WSData = {
|
|
userId: number;
|
|
email: string;
|
|
role: string;
|
|
provider: 'claude' | 'opencode' | 'terminal';
|
|
sandboxed: boolean;
|
|
sessionId?: string;
|
|
};
|
|
|
|
const handlers: Record<string, typeof claudeWebsocket> = {
|
|
claude: claudeWebsocket,
|
|
opencode: opencodeWebsocket,
|
|
terminal: terminalWebsocket,
|
|
};
|
|
|
|
async function upgradeWs(req: Request, server: any, provider: 'claude' | 'opencode' | 'terminal') {
|
|
const token = new URL(req.url).searchParams.get('token');
|
|
if (!token) return new Response('Unauthorized', { status: 401 });
|
|
|
|
try {
|
|
const user = await verify(token);
|
|
if (!user) return new Response('Unauthorized', { status: 401 });
|
|
|
|
if (user.jti) {
|
|
const blacklisted = await officerdb.query.TokenBlacklist.findFirst({
|
|
where: eq(TokenBlacklist.jti, user.jti),
|
|
});
|
|
if (blacklisted) return new Response('Unauthorized', { status: 401 });
|
|
}
|
|
|
|
const url = new URL(req.url);
|
|
const sessionId = url.searchParams.get('sessionId') ?? undefined;
|
|
const sandboxed = url.searchParams.get('sandboxed') !== 'false';
|
|
const ok = server.upgrade(req, {
|
|
data: { userId: user.id, email: user.email, role: user.role, provider, sandboxed, sessionId },
|
|
});
|
|
if (!ok) return new Response('Upgrade failed', { status: 500 });
|
|
} catch {
|
|
return new Response('Unauthorized', { status: 401 });
|
|
}
|
|
}
|
|
|
|
const server = serve({
|
|
port: Number(PORT),
|
|
idleTimeout: 60,
|
|
maxRequestBodySize: 1024 * 1024 * 1024 * 50, // 50 GB
|
|
routes: {
|
|
'/static/*': async (req) => {
|
|
const path = new URL(req.url).pathname.slice('/static'.length);
|
|
const file = Bun.file(`public${path}`);
|
|
if (await file.exists()) return new Response(file);
|
|
return new Response(null, { status: 404 });
|
|
},
|
|
'/api/harness/claudecode/ws': (req, server) => upgradeWs(req, server, 'claude'),
|
|
'/api/harness/opencode/ws': (req, server) => upgradeWs(req, server, 'opencode'),
|
|
'/api/terminal/ws': (req, server) => upgradeWs(req, server, 'terminal'),
|
|
'/': officerWeb,
|
|
'/*': officerWeb,
|
|
'/api': honoServer.fetch,
|
|
'/api/*': honoServer.fetch,
|
|
},
|
|
|
|
websocket: {
|
|
open(ws) {
|
|
const { provider } = (ws as unknown as ServerWebSocket<WSData>).data;
|
|
handlers[provider]!.open(ws as any);
|
|
},
|
|
message(ws, raw) {
|
|
const { provider } = (ws as unknown as ServerWebSocket<WSData>).data;
|
|
handlers[provider]!.message(ws as any, raw);
|
|
},
|
|
close(ws) {
|
|
const { provider } = (ws as unknown as ServerWebSocket<WSData>).data;
|
|
handlers[provider]!.close(ws as any);
|
|
},
|
|
drain() {},
|
|
},
|
|
|
|
development: process.env.NODE_ENV !== 'production' && {
|
|
hmr: true,
|
|
},
|
|
});
|
|
|
|
console.log(`🚀 Server running at ${server.url}`);
|
|
|
|
void initTerminalSidecars();
|