'use strict'; // Zero-dependency local server. Binds loopback only; it holds real OAuth tokens. const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); const { URL } = require('node:url'); const P = require('./lib/paths'); const store = require('./lib/store'); const oauth = require('./lib/oauth'); const usage = require('./lib/usage'); const swap = require('./lib/swap'); const credentials = require('./lib/credentials'); const targets = require('./lib/targets'); const terminal = require('./lib/terminal'); const auto = require('./lib/auto'); // Codex lives in its own package, with its own store: nothing below this line ever sees it. const codexRoutes = require('./lib/codex/routes'); const codexSwap = require('./lib/codex/swap'); const codexPaseo = require('./lib/codex/paseo'); const codexDaemon = require('./lib/codex/daemon'); // The host the BROWSER uses, and the only one the Host-header allowlist accepts. Kept separate // from the bind address on purpose: a container has to listen on all of its own interfaces to be // reachable at all, but that must not widen what this server accepts as a legitimate origin. const HOST = '127.0.0.1'; // Where the socket actually binds. Loopback everywhere except inside a container, where the // Dockerfile sets 0.0.0.0. The loopback guarantee then moves outward, to how the port is // published: -p 127.0.0.1:7373:7373 - see the README. const BIND = process.env.SWAPPER_BIND || HOST; const BASE_PORT = Number(process.env.PORT) || 7373; const PUBLIC_DIR = path.join(__dirname, 'public'); const MAX_BODY = 1024 * 1024; const deps = { store, oauth, usage, swap }; const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.png': 'image/png', '.woff2': 'font/woff2', }; function send(res, status, body, headers = {}) { const payload = typeof body === 'string' ? body : JSON.stringify(body); res.writeHead(status, { 'Content-Type': typeof body === 'string' ? 'text/html; charset=utf-8' : 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(payload), 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', ...headers, }); res.end(payload); } const fail = (res, status, message) => send(res, status, { ok: false, error: oauth.scrub(message) }); function readBody(req) { return new Promise((resolve, reject) => { let size = 0; const chunks = []; req.on('data', (c) => { size += c.length; if (size > MAX_BODY) { reject(new Error('Cuerpo de la petición demasiado grande')); req.destroy(); return; } chunks.push(c); }); req.on('end', () => { const text = Buffer.concat(chunks).toString('utf8').trim(); if (!text) return resolve({}); try { resolve(JSON.parse(text)); } catch { reject(new Error('JSON inválido en el cuerpo')); } }); req.on('error', reject); }); } /** * Two guards against a random website in the user's browser driving this API: * a same-origin check, and a custom header that no cross-site form, or