From d4fef43342baf824060cf06276e799d7422067d2 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sat, 31 Oct 2026 13:57:49 -0400 Subject: [PATCH] fix(convert): review-serve listens on loopback only --- skills/convert-site/scripts/review-serve.mjs | 2 +- test/convert/review-serve.test.mjs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/skills/convert-site/scripts/review-serve.mjs b/skills/convert-site/scripts/review-serve.mjs index 1e793d9..5e24ec6 100644 --- a/skills/convert-site/scripts/review-serve.mjs +++ b/skills/convert-site/scripts/review-serve.mjs @@ -278,7 +278,7 @@ async function serveForeground({ dir, port }) { if (err.code === 'EADDRINUSE') fail(inUse(port, lsofListener(port) ?? { pid: 'unknown', command: 'unknown command' })); else fail(new UsageError(`cannot listen on port ${port}: ${err.message}`)); }); - server.listen(port, done); + server.listen(port, '127.0.0.1', done); }); const stop = () => server.close(() => process.exit(0)); process.once('SIGTERM', stop); diff --git a/test/convert/review-serve.test.mjs b/test/convert/review-serve.test.mjs index 8b525b7..75d1323 100644 --- a/test/convert/review-serve.test.mjs +++ b/test/convert/review-serve.test.mjs @@ -5,7 +5,7 @@ import { createServer } from 'node:http'; import { createServer as createNetServer } from 'node:net'; import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { gzipSync } from 'node:zlib'; -import { tmpdir } from 'node:os'; +import { networkInterfaces, tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { startFixture } from '../fixture/serve.mjs'; import { compileSource, fillDestination } from '../../skills/convert-site/scripts/lib/review-routes.mjs'; @@ -194,6 +194,19 @@ test('serves dist/, applies vercel.json redirects and rewrites, proxies other or } }); +test('listens on loopback only, not on the machine\'s network addresses', async (t) => { + const external = Object.values(networkInterfaces()).flat().find((a) => a.family === 'IPv4' && !a.internal); + if (!external) return t.skip('no non-loopback IPv4 address on this machine'); + const port = await freePort(); + const { child } = await serve(['--dir', site, '--port', String(port)]); + try { + assert.equal((await fetch(`http://127.0.0.1:${port}/`)).status, 200); + await assert.rejects(fetch(`http://${external.address}:${port}/`)); + } finally { + child.kill('SIGTERM'); + } +}); + test('a port held by another process exits 2 naming the pid instead of binding another interface', async () => { const holder = createNetServer(); await new Promise((done) => holder.listen(0, '127.0.0.1', done));