diff --git a/.env.example b/.env.example index 09d0bce..2834f87 100644 --- a/.env.example +++ b/.env.example @@ -65,24 +65,52 @@ TURNSTILE_SECRET_KEY= REQUIRE_CAPTCHA=false # ============================================================================= -# NGROK TUNNEL (OPTIONAL — for OAuth callback testing) +# PUBLIC TUNNEL (OPTIONAL — for OAuth callback testing) # ============================================================================= -# Run with `npm run tunnel:ngrok`. Required when an OAuth provider (e.g. -# QuickBooks) needs a public HTTPS redirect URI but the rest of the app -# runs against localhost. Prereqs: +# Required when an OAuth provider (e.g. QuickBooks) needs a public HTTPS +# redirect URI but the rest of the app runs against localhost. Pick ONE of +# the two tunnel options below. +# +# PUBLIC_TUNNEL_DOMAIN is the public hostname (no scheme, no trailing slash). +# Read by both tunnel scripts AND next.config.js (which uses it to enable +# the dev-server API proxy + override NEXT_PUBLIC_ROBOSYSTEMS_API_URL so the +# browser only talks to one origin). +# +# In robosystems/.env, add the same value to EXTRA_CORS_ORIGINS: +# EXTRA_CORS_ORIGINS=https:// +# Register https:///connections/qb-callback in the OAuth app. + +# PUBLIC_TUNNEL_DOMAIN=qb.your-domain.com # cloudflared +# PUBLIC_TUNNEL_DOMAIN=your-reserved.ngrok-free.dev # ngrok + +# ----------------------------------------------------------------------------- +# Option A — ngrok (`npm run tunnel:ngrok`) +# ----------------------------------------------------------------------------- +# Prereqs: # 1. brew install ngrok && ngrok config add-authtoken # 2. Reserve a free static domain: https://dashboard.ngrok.com/domains -# 3. Set NGROK_DOMAIN below to that domain (allowedDevOrigins auto-derives) -# 4. In robosystems/.env, set EXTRA_CORS_ORIGINS=https:// -# 5. Register https:///connections/qb-callback in the OAuth app -NGROK_DOMAIN= +# 3. Set PUBLIC_TUNNEL_DOMAIN above to that ngrok-reserved domain. + +# ----------------------------------------------------------------------------- +# Option B — cloudflared (`npm run tunnel:cloudflared`) +# ----------------------------------------------------------------------------- +# Use when you own a Cloudflare-managed domain. No interstitial warning page +# and you keep your own branded hostname. +# Prereqs: +# 1. brew install cloudflared && cloudflared tunnel login +# 2. Create a named tunnel + DNS route (one-time): +# cloudflared tunnel create roboledger-local +# cloudflared tunnel route dns roboledger-local qb.your-domain.com +# 3. Set PUBLIC_TUNNEL_DOMAIN above to the hostname from step 2, +# and set CLOUDFLARED_TUNNEL_NAME below to the tunnel name from step 2. +# CLOUDFLARED_TUNNEL_NAME=roboledger-local # ============================================================================= # DEV-SERVER ALLOWED HOSTS (Next.js 16+ allowedDevOrigins) — OPTIONAL OVERRIDE # ============================================================================= # Comma-separated hostnames allowed cross-origin access to HMR / dev resources. -# Leave empty to auto-derive from NGROK_DOMAIN below (the common case). Set -# explicitly only when you need additional non-ngrok hosts (LAN IP, Tailscale, -# etc.) — when set, this list REPLACES the auto-derivation, so include the -# ngrok host yourself if you still want it. +# Leave empty to auto-derive from PUBLIC_TUNNEL_DOMAIN above (the common case). +# Set explicitly only when you need additional non-tunnel hosts (LAN IP, +# Tailscale, etc.) — when set, this list REPLACES the auto-derivation, so +# include the tunnel host yourself if you still want it. # NEXT_ALLOWED_DEV_ORIGINS= diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a99315d..b58da4e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,6 +13,12 @@ "command": "npm run tunnel:ngrok -- ${input:portRL}", "problemMatcher": [] }, + { + "label": "cloudflared Tunnel", + "type": "shell", + "command": "npm run tunnel:cloudflared -- ${input:portRL}", + "problemMatcher": [] + }, { "label": "Install Dependencies", "type": "shell", diff --git a/bin/tunnel-cloudflared.sh b/bin/tunnel-cloudflared.sh new file mode 100755 index 0000000..74e2124 --- /dev/null +++ b/bin/tunnel-cloudflared.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# ============================================================================= +# CLOUDFLARED TUNNEL +# ============================================================================= +# +# Exposes the local dev server (port 3001) at a public HTTPS URL on a +# Cloudflare-managed domain. Alternative to ngrok — no interstitial warning +# and uses your own branded domain. +# +# PREREQUISITES: +# 1. Install cloudflared: brew install cloudflared +# 2. Authenticate: cloudflared tunnel login +# (opens a browser; pick the zone you'll route the tunnel under) +# 3. Create a named tunnel + DNS route (one-time): +# cloudflared tunnel create roboledger-local +# cloudflared tunnel route dns roboledger-local qb.your-domain.com +# 4. Set CLOUDFLARED_TUNNEL_NAME + PUBLIC_TUNNEL_DOMAIN in .env: +# CLOUDFLARED_TUNNEL_NAME=roboledger-local +# PUBLIC_TUNNEL_DOMAIN=qb.your-domain.com +# 5. In robosystems/.env, add to EXTRA_CORS_ORIGINS: +# EXTRA_CORS_ORIGINS=https://qb.your-domain.com +# +# USAGE: +# npm run tunnel:cloudflared # forwards to port 3001 (default) +# npm run tunnel:cloudflared -- 3002 # forwards to a different port +# +# ============================================================================= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +PORT="${1:-3001}" + +if [ -f "$REPO_ROOT/.env" ]; then + set -a + # shellcheck disable=SC1091 + . "$REPO_ROOT/.env" + set +a +fi + +if [ -z "${CLOUDFLARED_TUNNEL_NAME:-}" ]; then + echo "Error: CLOUDFLARED_TUNNEL_NAME not set." + echo "" + echo "Set it in $REPO_ROOT/.env to the name of a tunnel you've created:" + echo " cloudflared tunnel create roboledger-local" + echo " cloudflared tunnel route dns roboledger-local qb.your-domain.com" + echo " echo 'CLOUDFLARED_TUNNEL_NAME=roboledger-local' >> .env" + echo " echo 'PUBLIC_TUNNEL_DOMAIN=qb.your-domain.com' >> .env" + exit 1 +fi + +# next.config.js reads PUBLIC_TUNNEL_DOMAIN to wire the dev-server API proxy +# and rewrite NEXT_PUBLIC_ROBOSYSTEMS_API_URL. Without it the tunnel will +# carry traffic but the frontend will still call http://localhost:8000 from +# the browser and trip Chrome's Private Network Access guard. +if [ -z "${PUBLIC_TUNNEL_DOMAIN:-}" ]; then + echo "Error: PUBLIC_TUNNEL_DOMAIN not set." + echo "" + echo "Set it in $REPO_ROOT/.env to the hostname routed to this tunnel" + echo "(the same hostname you passed to 'cloudflared tunnel route dns')." + exit 1 +fi + +if ! command -v cloudflared >/dev/null 2>&1; then + echo "Error: cloudflared not installed. See https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ (macOS: brew install cloudflared)" + exit 1 +fi + +# Warn if nothing is listening on the target port. Non-fatal — the dev +# server may start after the tunnel — but most "tunnel up, nothing +# serving" confusion lands here. +if command -v nc >/dev/null 2>&1 && ! nc -z localhost "$PORT" 2>/dev/null; then + echo "Warning: nothing listening on port $PORT. Start the dev server first (e.g., 'npm run dev')." +fi + +exec cloudflared tunnel --url "http://localhost:${PORT}" run "${CLOUDFLARED_TUNNEL_NAME}" diff --git a/bin/tunnel-ngrok.sh b/bin/tunnel-ngrok.sh index e12a996..faaca62 100755 --- a/bin/tunnel-ngrok.sh +++ b/bin/tunnel-ngrok.sh @@ -12,7 +12,7 @@ # (get a token at https://dashboard.ngrok.com/get-started/your-authtoken) # 3. Reserve a static domain (free tier includes one): # https://dashboard.ngrok.com/domains -# 4. Set NGROK_DOMAIN in .env to that domain (allowedDevOrigins auto-derives) +# 4. Set PUBLIC_TUNNEL_DOMAIN in .env to that domain. # 5. In robosystems/.env, set EXTRA_CORS_ORIGINS=https:// # # USAGE: @@ -35,18 +35,18 @@ if [ -f "$REPO_ROOT/.env" ]; then set +a fi -if [ -z "${NGROK_DOMAIN:-}" ]; then - echo "Error: NGROK_DOMAIN not set." +if [ -z "${PUBLIC_TUNNEL_DOMAIN:-}" ]; then + echo "Error: PUBLIC_TUNNEL_DOMAIN not set." echo "" - echo "Set it in $REPO_ROOT/.env. Reserve a free static domain at:" - echo " https://dashboard.ngrok.com/domains" + echo "Set it in $REPO_ROOT/.env to your reserved ngrok static domain." + echo "Reserve one at: https://dashboard.ngrok.com/domains" exit 1 fi # Strip any accidental scheme prefix (a developer pasting the dashboard URL # verbatim would otherwise end up with https://https://...). -NGROK_DOMAIN="${NGROK_DOMAIN#https://}" -NGROK_DOMAIN="${NGROK_DOMAIN#http://}" +PUBLIC_TUNNEL_DOMAIN="${PUBLIC_TUNNEL_DOMAIN#https://}" +PUBLIC_TUNNEL_DOMAIN="${PUBLIC_TUNNEL_DOMAIN#http://}" if ! command -v ngrok >/dev/null 2>&1; then echo "Error: ngrok not installed. See https://ngrok.com/download (macOS: brew install ngrok)" @@ -57,7 +57,7 @@ fi # server may start after the tunnel — but most "tunnel up, nothing # serving" confusion lands here. if command -v nc >/dev/null 2>&1 && ! nc -z localhost "$PORT" 2>/dev/null; then - echo "Warning: nothing listening on port $PORT. Start the dev server first (e.g., 'npm run dev:webpack')." + echo "Warning: nothing listening on port $PORT. Start the dev server first (e.g., 'npm run dev')." fi -exec ngrok http --url="https://${NGROK_DOMAIN}" "$PORT" +exec ngrok http --url="https://${PUBLIC_TUNNEL_DOMAIN}" "$PORT" diff --git a/next.config.js b/next.config.js index 5834a66..f20b809 100644 --- a/next.config.js +++ b/next.config.js @@ -1,40 +1,46 @@ import withFlowbiteReact from 'flowbite-react/plugin/nextjs' -// When tunneling via ngrok in local dev, the browser hits the ngrok URL while -// the API runs on localhost:8000. Chrome's Private Network Access blocks -// public→loopback fetches, so we proxy API paths through the Next dev server -// (same-origin from the browser) to localhost. Active only when NGROK_DOMAIN -// is set in the local env — inert in prod and in non-tunneled dev. +// When tunneling in local dev (ngrok or cloudflared), the browser hits the +// public tunnel URL while the API runs on localhost:8000. Chrome's Private +// Network Access blocks public→loopback fetches, so we proxy API paths +// through the Next dev server (same-origin from the browser) to localhost. +// Active only when PUBLIC_TUNNEL_DOMAIN is set — inert in prod and in +// non-tunneled dev. // // NOTE: This is intended for host-run `next dev`. Running roboledger-app // inside Docker uses `next start` with NEXT_PUBLIC_* baked at build time, so // the env override below wouldn't take effect there. Follow the Connecting- // QuickBooks-Locally wiki for the host-run setup. -const ngrokDomain = process.env.NGROK_DOMAIN?.replace( +const tunnelDomain = process.env.PUBLIC_TUNNEL_DOMAIN?.replace( /^https?:\/\//, '' ).replace(/\/$/, '') +// .env files are loaded before next.config.js, so a NEXT_PUBLIC_ROBOSYSTEMS_API_URL +// already set there would normally end up baked into the client bundle as-is. +// Mutate process.env here (before Next compiles the bundle) so the client SDK +// targets the tunnel origin when a tunnel is active. +if (tunnelDomain) { + process.env.NEXT_PUBLIC_ROBOSYSTEMS_API_URL = `https://${tunnelDomain}` +} + // allowedDevOrigins precedence: explicit NEXT_ALLOWED_DEV_ORIGINS takes over -// entirely (override — user owns the full list, including the ngrok host if -// they want it). Otherwise auto-derive from NGROK_DOMAIN when set. +// entirely (override — user owns the full list, including the tunnel host if +// they want it). Otherwise auto-derive from the tunnel domain when set. /** @type {import('next').NextConfig} */ const allowedDevOrigins = process.env.NEXT_ALLOWED_DEV_ORIGINS ? process.env.NEXT_ALLOWED_DEV_ORIGINS.split(',') .map((o) => o.trim()) .filter(Boolean) - : ngrokDomain - ? [ngrokDomain] + : tunnelDomain + ? [tunnelDomain] : [] const nextConfig = { reactStrictMode: true, allowedDevOrigins, - env: ngrokDomain - ? { NEXT_PUBLIC_ROBOSYSTEMS_API_URL: `https://${ngrokDomain}` } - : {}, async rewrites() { - if (!ngrokDomain) return [] + if (!tunnelDomain) return [] return [ { source: '/v1/:path*', destination: 'http://localhost:8000/v1/:path*' }, { diff --git a/package.json b/package.json index ded6f8d..ac4cc47 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dev": "PORT=3001 next dev", "dev:webpack": "PORT=3001 next dev --webpack", "tunnel:ngrok": "./bin/tunnel-ngrok.sh", + "tunnel:cloudflared": "./bin/tunnel-cloudflared.sh", "build": "next build", "format": "prettier . --write", "format:check": "prettier . --check",