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>
This commit is contained in:
Khalid Abdi
2026-06-22 23:56:29 +03:00
parent 014b4ddf8f
commit a68b7f2573
5 changed files with 89 additions and 34 deletions
+13 -6
View File
@@ -18,7 +18,7 @@ import {
import { emitToWallet } from "../realtime.js";
import { recordActivity } from "../services/activity.js";
import * as patientService from "../services/patients.js";
import { getDiscoveredRelayUrl } from "../services/relay-url.js";
import { awaitQuickTunnelUrl } from "../services/relay-url.js";
import * as walletShare from "../services/wallet-share.js";
export const patientsWalletRouter = Router();
@@ -28,11 +28,14 @@ patientsWalletRouter.use(requireAuth, requireOrg);
// The device-reachable URL the patient's app should connect to (baked into the
// QR). Prefer an explicit PUBLIC_RELAY_URL; otherwise derive it from the request
// host so that opening the web app over the LAN yields a reachable LAN URL.
function resolveRelayUrl(req: Request): string {
async function resolveRelayUrl(req: Request): Promise<string> {
if (env.PUBLIC_RELAY_URL) return env.PUBLIC_RELAY_URL;
// A cloudflared quick tunnel (Docker `--profile tunnel`), if one was found.
const tunnel = getDiscoveredRelayUrl();
if (tunnel) return tunnel;
// A cloudflared quick tunnel (`npm run docker:tunnel`). Wait briefly for it to
// become reachable so the QR never carries a not-yet-live URL.
if (env.CLOUDFLARED_METRICS_URL) {
const tunnel = await awaitQuickTunnelUrl(env.CLOUDFLARED_METRICS_URL);
if (tunnel) return tunnel;
}
const host = req.get("host");
if (host) {
// Behind a TLS-terminating proxy (Fly/Render/etc.) req.protocol is "http";
@@ -71,7 +74,11 @@ patientsWalletRouter.post(
input.mode,
input.durationHours,
);
res.status(201).json({ ...view, ephemeralPubKey, relayUrl: resolveRelayUrl(req) });
res.status(201).json({
...view,
ephemeralPubKey,
relayUrl: await resolveRelayUrl(req),
});
} catch (err) {
next(err);
}