Files
temetro/backend/scripts/dev-tunnel.mjs
T
Khalid Abdi a68b7f2573 fix(backend): make tunnel relay reachable before baking it into QR
Two changes so off-network wallet scanning actually works:

- Force the cloudflared edge connection over http2 (TCP/443) instead of
  QUIC (UDP/7844) in both the Docker tunnel and the dev-tunnel script.
  QUIC is blocked on many networks/Docker setups, which left the tunnel
  stuck "Failed to dial a quic connection" and the QR pointing at a dead
  URL — the root cause of "must be on the same network" failures.
- Gate the discovered quick-tunnel URL on real end-to-end reachability:
  poll the tunnel's own /health until it answers (Cloudflare 1033 clears
  in ~30s) before publishing it, and have /pair await that discovery so
  the QR never carries a not-yet-live URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:56:29 +03:00

87 lines
2.8 KiB
JavaScript

// Run the backend with a public Cloudflare tunnel so a patient's phone can reach
// the wallet relay from ANY network (cellular, other Wi-Fi) — not just the LAN.
//
// npm run dev:tunnel
//
// It opens `cloudflared tunnel --url http://localhost:4000`, grabs the public
// https://<sub>.trycloudflare.com URL, and starts the backend with
// PUBLIC_RELAY_URL set to it — so the QR in "Import from a patient app" carries
// that public URL (over wss://). Requires cloudflared: `brew install cloudflared`.
import { spawn, spawnSync } from "node:child_process";
const PORT = process.env.PORT || "4000";
function hasCloudflared() {
const probe = spawnSync("cloudflared", ["--version"], { stdio: "ignore" });
return !probe.error;
}
if (!hasCloudflared()) {
console.error(
"\n✖ cloudflared is not installed.\n" +
" Install it, then re-run `npm run dev:tunnel`:\n\n" +
" brew install cloudflared # macOS\n" +
" # or see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/\n",
);
process.exit(1);
}
let backend = null;
let resolvedUrl = null;
const TUNNEL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
console.log(`\n⛅ Starting Cloudflare tunnel to http://localhost:${PORT}\n`);
const tunnel = spawn(
"cloudflared",
// --protocol http2 keeps the edge connection on TCP/443 (QUIC/UDP is blocked
// on many networks and would leave the tunnel unable to connect).
["tunnel", "--no-autoupdate", "--protocol", "http2", "--url", `http://localhost:${PORT}`],
{ stdio: ["ignore", "pipe", "pipe"] },
);
function onTunnelOutput(buf) {
const text = buf.toString();
process.stderr.write(text); // keep cloudflared's own logs visible
if (resolvedUrl) return;
const match = text.match(TUNNEL_RE);
if (match) {
resolvedUrl = match[0];
startBackend(resolvedUrl);
}
}
tunnel.stdout.on("data", onTunnelOutput);
tunnel.stderr.on("data", onTunnelOutput);
function startBackend(publicUrl) {
console.log(
`\n✅ Public relay URL: ${publicUrl}\n` +
" Generate a QR in the web app (Patients → Import from a patient app → QR);\n" +
" it will carry this URL, so a phone on any network can scan to connect.\n",
);
backend = spawn("npm", ["run", "dev"], {
stdio: "inherit",
env: { ...process.env, PUBLIC_RELAY_URL: publicUrl },
});
backend.on("exit", (code) => shutdown(code ?? 0));
}
function shutdown(code) {
for (const proc of [backend, tunnel]) {
if (proc && !proc.killed) proc.kill("SIGTERM");
}
process.exit(code);
}
tunnel.on("exit", (code) => {
if (!resolvedUrl) {
console.error("\n✖ cloudflared exited before a tunnel URL was established.");
}
shutdown(code ?? 0);
});
process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));