UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

252 lines • 10.5 kB
/** * `neurolink proxy expose` — put this node's proxy on a public URL. * * Wraps `cloudflared` because that is the shortest path from a laptop to a URL * a peer can reach, with no port forwarding and no inbound firewall change. * * **The safety check is the point.** Whether a port is gated depends on the * proxy process — which listener it is, and how that process was started — * none of which this command can read. So instead of trusting configuration, it * asks the running proxy directly: a request with no share token must be * refused. A port that answers one is open, and exposing it would publish the * operator's subscription to anyone who finds the URL, so the tunnel is refused * rather than opened. * * With no `--port` it targets the gate-only share listener, which is the port * that exists to face outward. * * @module cli/commands/proxyExpose */ import { spawn } from "node:child_process"; /** Cloudflare prints the assigned hostname to stderr as it comes up. */ const QUICK_TUNNEL_URL = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i; /** * Ask the running proxy whether it refuses untokened traffic. * * A refusal carrying `x-neurolink-grant-reason: missing_token` is proof the * gate is live. Anything else — an answer, an upstream error, a credentials * complaint — means the request got past the gate, which is the dangerous case. * * `scheme` matters: probing a TLS address over plain http fails to connect, * which reads as unreachable, and an unreachable address is reported as *not* * dangerous. A public `https://` URL would therefore never raise the warning it * exists to raise. */ export async function probeProxyGate(host, port, scheme = "http") { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10_000); try { const response = await fetch(`${scheme}://${host}:${port}/v1/messages`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "neurolink-gate-probe", max_tokens: 1, messages: [{ role: "user", content: "probe" }], }), signal: controller.signal, }); await response.text().catch(() => ""); const reason = response.headers.get("x-neurolink-grant-reason"); if (reason === "missing_token") { return { gated: true, reachable: true, detail: "the proxy refuses requests without a share token", }; } return { gated: false, reachable: true, detail: "the proxy served a request that carried no share token — anyone who reaches the tunnel could spend your subscription", }; } catch (error) { return { gated: false, reachable: false, detail: `could not reach the proxy: ${error instanceof Error ? error.message : String(error)}`, }; } finally { clearTimeout(timeout); } } /** * Which port to expose. * * The gate-only share listener when the running proxy has one, because that is * the port whose whole purpose is to face outward. An explicit `--port` always * wins — an operator pointing at something specific is not to be second-guessed. */ async function resolveExposePort(argv) { if (argv.port !== undefined) { return { port: argv.port, isShareListener: false }; } try { const { StateFileManager } = await import("../utils/serverUtils.js"); const state = new StateFileManager("proxy-state.json").load(); if (state?.sharePort) { return { port: state.sharePort, isShareListener: true }; } if (state?.port) { return { port: state.port, isShareListener: false }; } } catch { // No readable state — fall through to the historical default. } return { port: 3000, isShareListener: false }; } async function runExpose(argv) { const host = argv.host ?? "127.0.0.1"; const resolved = await resolveExposePort(argv); const port = resolved.port; if (resolved.isShareListener) { console.info(`Exposing the share listener on port ${port} — your own client keeps using the main port.`); } const probe = await probeProxyGate(host, port); if (!probe.reachable) { throw new Error(resolved.isShareListener ? `${probe.detail}\nThe share listener runs only while an active grant exists. Issue one first:\n neurolink proxy share create --peer <name> --preset spare` : `${probe.detail}\nStart it first: neurolink proxy start --port ${port}`); } if (!probe.gated && !argv.force) { throw new Error([ `Refusing to expose an ungated proxy: ${probe.detail}.`, "", "Issue a grant, which brings up the gate-only share listener, and", "expose that instead:", " neurolink proxy share create --peer <name> --preset spare", " neurolink proxy expose", "", "Or gate this port itself, which also refuses your own local client:", ` NEUROLINK_PROXY_REQUIRE_GRANT=1 neurolink proxy start --port ${port}`, "", "Pass --force only if something in front of the tunnel is already", "authenticating every request.", ].join("\n")); } if (!probe.gated && argv.force) { console.warn("⚠ Exposing an ungated proxy because --force was given. Every request that reaches the tunnel will be served."); } const args = argv.named ? ["tunnel", "run", argv.named] : ["tunnel", "--url", `http://${host}:${port}`]; if (argv.named) { // Say plainly what the check above did and did not establish. A quick // tunnel is pointed at `http://host:port` on the line below, so probing // that address settles what the tunnel will front. A named tunnel is not: // its ingress lives in cloudflared's own configuration, and it may route // to the main ungated port — or to something else entirely — no matter // what this command just probed. The gate result is still worth having, // but it describes a local port, not this tunnel. console.warn(""); console.warn(`⚠ The gate check covered http://${host}:${port}. A named tunnel's ingress is`); console.warn(` configured in cloudflared, not here, so confirm ${argv.named} actually points at`); console.warn(" that port — routing it at an ungated port exposes the pool regardless of the"); console.warn(" result above."); console.warn(""); } console.info(`Starting cloudflared: cloudflared ${args.join(" ")}`); const child = spawn("cloudflared", args, { stdio: ["ignore", "pipe", "pipe"], }); let announced = false; const announce = (chunk) => { if (announced) { return; } const match = QUICK_TUNNEL_URL.exec(chunk); if (!match) { return; } announced = true; const url = match[0]; console.info(""); console.info(` Public URL: ${url}`); console.info(""); console.info(" Mint a token and hand the peer a link:"); console.info(` neurolink proxy share create --peer <name> --preset spare --public-url ${url}`); console.info(""); console.info(" A quick tunnel's URL changes every restart. For a peer you expect to"); console.info(" keep, use a named tunnel so their configuration does not go stale:"); console.info(" neurolink proxy expose --named <tunnel-name>"); }; child.stdout?.on("data", (chunk) => { const text = chunk.toString(); announce(text); process.stdout.write(text); }); child.stderr?.on("data", (chunk) => { const text = chunk.toString(); announce(text); process.stderr.write(text); }); child.on("error", (error) => { if (error.code === "ENOENT") { console.error("cloudflared is not installed. See https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"); process.exitCode = 1; return; } console.error(error.message); process.exitCode = 1; }); // `close` rather than `exit`: a spawn that never started — cloudflared not on // PATH — emits `error` and `close` but no `exit`, and waiting on `exit` alone // left the command hanging forever on the one failure it explains best. await new Promise((resolve) => { let settled = false; const settle = () => { if (settled) { return; } settled = true; resolve(); }; child.on("close", (code) => { if (code !== 0) { process.exitCode = code ?? 1; } settle(); }); child.on("error", settle); }); } export const proxyExposeCommand = { command: "expose", describe: "Publish this node's proxy through a cloudflared tunnel", builder: (yargs) => yargs .option("port", { type: "number", // Deliberately no default. `resolveExposePort` reads the running // proxy's state file to find the gate-only share listener, and it can // only do that when `--port` is genuinely absent — a yargs default // makes every invocation look explicit and pins the tunnel to 3000. description: "Local proxy port to expose (default: the share listener, else the running proxy's port)", }) .option("host", { type: "string", default: "127.0.0.1", description: "Local proxy host", }) .option("named", { type: "string", description: "Run a pre-created named tunnel instead of a quick tunnel (stable URL)", }) .option("force", { type: "boolean", default: false, description: "Expose even though the proxy serves untokened requests (dangerous)", }), handler: async (argv) => { try { await runExpose(argv); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; } }, }; //# sourceMappingURL=proxyExpose.js.map