Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 40 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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://<your-domain>
# Register https://<your-domain>/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 <token>
# 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://<your-domain>
# 5. Register https://<your-domain>/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=
6 changes: 6 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
78 changes: 78 additions & 0 deletions bin/tunnel-cloudflared.sh
Original file line number Diff line number Diff line change
@@ -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}"
18 changes: 9 additions & 9 deletions bin/tunnel-ngrok.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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://<your-domain>
#
# USAGE:
Expand All @@ -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)"
Expand All @@ -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"
34 changes: 20 additions & 14 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -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*' },
{
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down