Add local proxy server for AI draft button; point button at /api/claude
This commit is contained in:
128
server.js
Normal file
128
server.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Local server for the SDE meeting toolkit.
|
||||
* Serves the static tool files.
|
||||
* Proxies AI draft requests to the Anthropic API, so the API key
|
||||
* stays on the server and never appears in the browser.
|
||||
*/
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = __dirname;
|
||||
const PORT = process.env.PORT || 5173;
|
||||
const ANTHROPIC_VERSION = '2023-06-01';
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.skill': 'application/zip'
|
||||
};
|
||||
|
||||
function loadEnvFile() {
|
||||
const envPath = path.join(ROOT, '.env');
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(envPath, 'utf8');
|
||||
} catch (err) {
|
||||
return; // no .env file; that is allowed, the AI draft route will report it
|
||||
}
|
||||
content.split('\n').forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) return;
|
||||
const idx = trimmed.indexOf('=');
|
||||
if (idx === -1) return;
|
||||
const key = trimmed.slice(0, idx).trim();
|
||||
const value = trimmed.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
|
||||
if (key && !(key in process.env)) process.env[key] = value;
|
||||
});
|
||||
}
|
||||
loadEnvFile();
|
||||
|
||||
function sendJson(res, status, obj) {
|
||||
const body = JSON.stringify(obj);
|
||||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function proxyToClaude(req, res) {
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
sendJson(res, 500, {
|
||||
error: 'ANTHROPIC_API_KEY is not set. Add it to the .env file in this folder, then restart the server.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
let body = '';
|
||||
req.on('data', (chunk) => { body += chunk; });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
||||
'anthropic-version': ANTHROPIC_VERSION
|
||||
},
|
||||
body
|
||||
});
|
||||
const text = await upstream.text();
|
||||
res.writeHead(upstream.status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(text);
|
||||
} catch (err) {
|
||||
sendJson(res, 502, { error: 'Could not reach the Anthropic API: ' + err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function serveStatic(req, res) {
|
||||
const urlPath = decodeURIComponent(req.url.split('?')[0]);
|
||||
if (urlPath === '/') {
|
||||
const index = `<!doctype html><html><body style="font:14px system-ui;padding:2rem">
|
||||
<h1>SDE Meeting Toolkit</h1>
|
||||
<ul>
|
||||
<li><a href="/tools/field-problem-workshop.html">Field Problem Workshop</a></li>
|
||||
<li><a href="/tools/scope-lock-meeting-suite.html">Scope Lock Meeting Suite</a></li>
|
||||
</ul>
|
||||
</body></html>`;
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(index);
|
||||
return;
|
||||
}
|
||||
const filePath = path.normalize(path.join(ROOT, urlPath));
|
||||
if (!filePath.startsWith(ROOT)) {
|
||||
res.writeHead(403);
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not found: ' + urlPath);
|
||||
return;
|
||||
}
|
||||
const ext = path.extname(filePath);
|
||||
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
||||
res.end(data);
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method === 'POST' && req.url === '/api/claude') {
|
||||
proxyToClaude(req, res);
|
||||
return;
|
||||
}
|
||||
if (req.method === 'GET') {
|
||||
serveStatic(req, res);
|
||||
return;
|
||||
}
|
||||
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
||||
res.end('Method not allowed');
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`SDE meeting toolkit running at http://localhost:${PORT}`);
|
||||
console.log(process.env.ANTHROPIC_API_KEY ? 'ANTHROPIC_API_KEY loaded: AI draft button is live.' : 'No ANTHROPIC_API_KEY found: AI draft button will return an error until you add one to .env.');
|
||||
});
|
||||
Reference in New Issue
Block a user