File size: 3,530 Bytes
4c98a98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/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)));