From 0b3d0378d40a33688b19cda0c6d72c271c0c26b7 Mon Sep 17 00:00:00 2001 From: plfavreau <68549239+plfavreau@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:54:29 +0100 Subject: [PATCH] fix(cli): surface tunnel bootstrap connect URL for --try-cf-tunnel (#561) The CLI tunnel startup generated a bootstrap token but discarded it, then built a URL with ?token= which the tunnel auth system ignores. Remote users always saw 'Tunnel access required' with no way to authenticate. Capture the bootstrap token, build the /connect?t=... URL, and pass it through onTunnelReady so the CLI prints the correct one-time connect link (and QR code). --- packages/web/bin/cli.js | 8 +++++--- packages/web/server/index.js | 14 +++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/web/bin/cli.js b/packages/web/bin/cli.js index 3414aea9..c277a81e 100755 --- a/packages/web/bin/cli.js +++ b/packages/web/bin/cli.js @@ -636,10 +636,12 @@ const commands = { exitOnShutdown: true, uiPassword: typeof effectiveUiPassword === 'string' ? effectiveUiPassword : null, tryCfTunnel: options.tryCfTunnel, - onTunnelReady: async (url) => { - const displayUrl = buildTunnelUrl(url, effectiveUiPassword, options.tunnelPasswordUrl); + onTunnelReady: async (url, connectUrl) => { + const displayUrl = connectUrl || buildTunnelUrl(url, effectiveUiPassword, options.tunnelPasswordUrl); console.log(`\n🌐 Tunnel URL: \x1b[36m${displayUrl}\x1b[0m\n`); - if (options.tunnelPasswordUrl && effectiveUiPassword) { + if (connectUrl) { + console.log('šŸ”‘ One-time connect link (expires after first use)\n'); + } else if (options.tunnelPasswordUrl && effectiveUiPassword) { console.log('šŸ”‘ Password is embedded in URL for auto-login\n'); } if (options.tunnelQr) { diff --git a/packages/web/server/index.js b/packages/web/server/index.js index a09b967d..02036a09 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -13371,12 +13371,16 @@ async function main(options = {}) { const bootstrapTtlMs = settings?.tunnelBootstrapTtlMs === null ? null : normalizeTunnelBootstrapTtlMs(settings?.tunnelBootstrapTtlMs); - tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs }); - } - if (onTunnelReady) { - if (tunnelUrl) { - onTunnelReady(tunnelUrl); + const bootstrapToken = tunnelAuthController.issueBootstrapToken({ ttlMs: bootstrapTtlMs }); + const connectUrl = `${tunnelUrl.replace(/\/$/, '')}/connect?t=${encodeURIComponent(bootstrapToken.token)}`; + if (onTunnelReady) { + onTunnelReady(tunnelUrl, connectUrl); + } else { + console.log(`\n🌐 Tunnel URL: ${connectUrl}`); + console.log('šŸ”‘ One-time connect link (expires after first use)\n'); } + } else if (onTunnelReady) { + onTunnelReady(tunnelUrl, null); } } catch (error) { console.error(`Failed to start Cloudflare tunnel: ${error.message}`);