Publishing a port on the host

Bind a real host port to a server running inside the box, so curl, your browser and any local tool can reach it — no relay, no service worker.

A box's ports are entries in a map, not sockets, so nothing outside the process can reach them. In the browser that's what the service worker or the SW-free preview is for. On a host with real sockets — the CLI, or any Node program — you can just bind a port:

lifo --expose 3000            # in-VM 3000 → http://127.0.0.1:3000
lifo --expose 3000:5000       # in-VM 3000 → http://127.0.0.1:5000
lifo --detach --expose 5173   # background sessions too

Then curl http://127.0.0.1:5000/ reaches the server inside the box, and so does your browser, Postman, or anything else on the machine. Repeat --expose for several ports.

From code

import * as http from "node:http";
import { exposePort } from "@lifo-sh/core";

const exposed = await exposePort(sandbox.kernel.portRegistry, {
  vmPort: 3000,
  hostPort: 5000,   // omit for an OS-assigned free port
  http,             // node:http, injected
});

console.log(exposed.url);   // http://127.0.0.1:5000
await exposed.close();

http is passed in rather than imported, exactly as NativeFsProvider takes its fs: @lifo-sh/core is bundled for the browser, and a hard node:http dependency would either break that build or need externalising.

optiondefaultnotes
vmPortthe in-VM port to publish
hostPort00/omitted lets the OS pick; read it back from exposed.hostPort
host127.0.0.1loopback only unless you ask otherwise
timeoutMs120000passed to the dispatcher
onRequest(method, url, status) hook, for logging

What gets forwarded

  • HTTP, through the same dispatcher every other transport uses — so slow servers, timeouts and unbound ports behave identically.
  • WebSockets, forwarded at the byte level. The client's socket is handed to the in-VM server, which performs its own RFC 6455 handshake, so HMR works and nothing in between has to parse a frame.

Binding does not require the server to exist yet. A forwarder set up before your dev server starts answers 404 with x-lifo: no-server until the port is live — friendlier than a connection refused while something is still booting. The CLI binds forwards before the shell starts for exactly that reason.

Gotcha

Loopback by default, deliberately: a box runs project code, and publishing it on every interface is something you should have to ask for. Pass host: '0.0.0.0' only when you mean it.

This is not the same as tunnel

They solve different problems and compose:

reachesneeds
--expose / exposePort()a local host portnothing — no relay, no network, works offline
tunnel 5173 (inside the shell)a public URLthe tunnel.lifo.sh relay, network, maybe auth

So share an in-VM port publicly with the in-shell tunnel command; publish it to your own machine with --expose. If you want a host port shared publicly, expose it first and then point lifo tunnel <hostPort> at it — host-side lifo tunnel proxies a host port and cannot reach into the box on its own.