n8n / scripts /sandbox-api.js
subham
Sync local n8n deployment: refactor to single Dockerfile, add runners, sandbox, cleanup legacy compose/nginx
4c98a98
Raw History Blame Contribute Delete
3.53 kB
#!/usr/bin/env node
// sandbox-api.js — minimal HF-compatible sandbox API matching n8n-sandbox contract
// n8n talks to http://127.0.0.1:8080 (N8N_SANDBOX_SERVICE_URL) with header x-api-key
const http = require('http');
const { URL } = require('url');
const PORT = parseInt(process.env.SANDBOX_API_PORT || '8080', 10);
const RUNNER_GRPC = process.env.SANDBOX_RUNNER_GRPC || '127.0.0.1:9091';
const API_KEYS = (process.env.SANDBOX_API_KEYS || process.env.N8N_SANDBOX_SERVICE_API_KEY || '').split(',').map(s=>s.trim()).filter(Boolean);
const RUNNER_URL = `http://127.0.0.1:${(RUNNER_GRPC.split(':')[1]||'9091')}`; // http fallback for our JS runner
function authorized(req) {
if (API_KEYS.length===0) return true;
const k = req.headers['x-api-key'] || req.headers['authorization']?.replace(/^Bearer\s+/,'');
return API_KEYS.includes(k);
}
function json(res, code, obj){ res.writeHead(code, {'Content-Type':'application/json'}); res.end(JSON.stringify(obj)); }
const server = http.createServer(async (req,res)=>{
const url = new URL(req.url, `http://localhost:${PORT}`);
// health
if (url.pathname === '/healthz' || url.pathname === '/health') {
res.writeHead(200, {'Content-Type':'text/plain'}); res.end('ok\n'); return;
}
if (!authorized(req)) { json(res,401,{error:'unauthorized'}); return; }
// n8n sandbox service expects POST /v1/sandbox or POST /execute — we support both
// Docs: N8N_SANDBOX_SERVICE_URL=http://sandbox-api:8080 — n8n does POST /execute or /v1/...
// Implement simple execute endpoint:
if ((url.pathname === '/execute' || url.pathname === '/v1/execute' || url.pathname === '/v1/sandbox' || url.pathname === '/run') && req.method==='POST') {
let body=''; req.on('data',c=>body+=c); req.on('end', async()=>{
try{
const data = body ? JSON.parse(body) : {};
// Support both {code, language} and n8n format {code, language, context?}
const code = data.code || data.content || '';
const language = (data.language || data.lang || 'python').toLowerCase();
const timeoutMs = data.timeoutMs || 30000;
if (!code) { json(res,400,{error:'missing code'}); return; }
// Forward to sandbox-runner (our JS runner) via HTTP POST /run
const runnerBody = JSON.stringify({code, language, timeoutMs});
const r = await new Promise((resolve,reject)=>{
const rq = http.request({hostname:'127.0.0.1', port: RUNNER_URL.split(':').pop(), path:'/run', method:'POST', headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(runnerBody)}}, (rr)=>{
let b=''; rr.on('data',c=>b+=c); rr.on('end',()=>resolve({status:rr.statusCode, body:b}));
});
rq.on('error',reject);
rq.write(runnerBody); rq.end();
}).catch(e=>({status:500, body: JSON.stringify({error: e.message})}));
// Try parse runner response
let out;
try{ out = JSON.parse(r.body);}catch{ out={stdout:r.body, stderr:'', exitCode: r.status===200?0:1};}
json(res,200, out);
}catch(e){ json(res,500,{error:e.message}); }
});
return;
}
// list/info
if (url.pathname === '/' && req.method==='GET'){ json(res,200,{service:'sandbox-api', mode:'rootless-bwrap', runner: RUNNER_GRPC}); return; }
json(res,404,{error:'not found', path:url.pathname});
});
server.listen(PORT, '127.0.0.1', ()=>console.log(`[sandbox-api] listening 127.0.0.1:${PORT} runner=${RUNNER_GRPC}`));
process.on('SIGTERM',()=>server.close(()=>process.exit(0)));