OSVauco/opax-web/backend/server.js

110 lines
4.0 KiB
JavaScript

import { randomUUID } from 'node:crypto';
import { callAgent, AgentNotConfiguredError } from './agentClient.js';
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import session from 'express-session';
import { OAuth2Client } from 'google-auth-library';
import { createServer } from 'node:http';
import { Server } from 'socket.io';
const app = express();
const http = createServer(app);
const front = process.env.FRONTEND_URL || 'http://localhost:5173';
const emails = new Set((process.env.ALLOWED_EMAILS || '').split(',').filter(Boolean));
const oauth = new OAuth2Client(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, `${front}/auth/callback`);
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
}
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret || (sessionSecret === 'CHANGE_ME' && process.env.NODE_ENV !== 'production')) {
throw new Error('A valid SESSION_SECRET must be set.');
}
const sm = session({
secret: sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax'
},
});
const io = new Server(http, { cors: { origin: front, credentials: true } });
const allowed = new Set(['get_health', 'get_build_status', 'get_state', 'get_telemetry', 'get_git_status', 'get_git_branch', 'get_git_log', 'get_git_diff', 'list_repo_files', 'read_repo_file']);
app.use(cors({ origin: front, credentials: true }));
app.use(sm);
io.use((s, n) => sm(s.request, {}, n));
app.get('/health', (_, r) => r.json({ status: 'ok' }));
app.get('/auth/google', (q, r) => r.redirect(oauth.generateAuthUrl({ scope: ['openid', 'email', 'profile'], prompt: 'consent' })));
app.get('/auth/callback', async (q, r) => {
try {
let { tokens } = await oauth.getToken(q.query.code);
let p = (await oauth.verifyIdToken({ idToken: tokens.id_token, audience: process.env.GOOGLE_CLIENT_ID })).getPayload();
if (!p?.email || !emails.has(p.email)) return r.status(403).send('Not authorised');
q.session.user = { email: p.email, name: p.name || p.email };
r.redirect('/');
} catch (e) {
r.status(500).send('Login failed');
}
});
app.get('/auth/me', (q, r) => (q.session.user ? r.json(q.session.user) : r.status(401).end()));
app.post('/auth/logout', (q, r) => q.session.destroy(() => r.json({ ok: true })));
io.on('connection', (s) => {
let u = s.request.session?.user;
if (!u) return s.disconnect();
s.on('mcp-call', async ({ id, tool, args }) => {
if (typeof id !== 'string' || id.length > 128) {
return s.emit('mcp-result', { id, error: 'Invalid request id' });
}
if (typeof tool !== 'string' || !allowed.has(tool)) {
return s.emit('mcp-result', { id, error: 'Tool denied by server allowlist' });
}
try {
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
return s.emit('mcp-result', { id, error: 'Invalid arguments' });
}
const argsString = JSON.stringify(args);
if (argsString.length > 16384) {
return s.emit('mcp-result', { id, error: 'Invalid arguments' });
}
} catch {
return s.emit('mcp-result', { id, error: 'Invalid arguments' });
}
const request_id = randomUUID();
try {
const result = await callAgent(tool, args);
s.emit('mcp-result', { id, result });
} catch (e) {
if (e instanceof AgentNotConfiguredError) {
console.log(JSON.stringify({
request_id,
event: 'agent_not_configured',
category: 'agent_not_configured',
}));
s.emit('mcp-result', { id, error: 'Agent integration is not configured.' });
} else {
console.log(JSON.stringify({
request_id,
event: 'agent_request_failed',
category: 'agent_request_failed',
}));
s.emit('mcp-result', { id, error: 'An unexpected error occurred.' });
}
}
});
});
http.listen(Number(process.env.PORT || 8080), '0.0.0.0');