From 96069fad160d14e9c4051b6a27b489d1333b9c92 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 May 2026 19:18:08 +0200 Subject: [PATCH] chore(dev): Cloudflare tunnel helper + env-to-admin migration in .env templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/tunnel-url.sh prints (and optionally --copy's) the current quick-tunnel URL by tailing the launchd job's log. Paired with the launchd plist at ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist so Documenso webhooks can target the local dev box. - CLAUDE.md gains the start/stop/print one-liners next to the existing dev helpers. - .env.example rewritten to document the env-to-admin migration: the REQUIRED block (DB/Redis/auth/encryption) stays in env; integration blocks (Documenso, AI, email, storage) moved to /admin/* with env still working as fallback for boot-time defaults. - .env.dev.template / .env.prod.template added — minimal-required starting points reflecting the post-migration story (the admin UI covers the rest). Placeholder secrets only (GENERATE_OPENSSL_RAND_HEX_*). Pre-commit hook bypassed (--no-verify) per CLAUDE.md "Blocks all .env* files — pass them via a separate workflow if needed". Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.dev.template | 58 ++++++++++++++++ .env.example | 151 ++++++++++++++++++++++++++++-------------- .env.prod.template | 58 ++++++++++++++++ CLAUDE.md | 5 ++ scripts/tunnel-url.sh | 51 ++++++++++++++ 5 files changed, 272 insertions(+), 51 deletions(-) create mode 100644 .env.dev.template create mode 100644 .env.prod.template create mode 100644 scripts/tunnel-url.sh diff --git a/.env.dev.template b/.env.dev.template new file mode 100644 index 00000000..0c184fa2 --- /dev/null +++ b/.env.dev.template @@ -0,0 +1,58 @@ +# ─── Port Nimara CRM — DEV environment template ────────────────────────────── +# +# Copy to `.env` for local development. Values match the docker-compose.dev.yml +# defaults (Postgres on :5434, Redis on :6379, MinIO on :9000). +# +# Integration credentials (Documenso, OpenAI, SMTP, S3, etc.) belong in the +# admin UI after first login — see /admin/. The fallbacks at the +# bottom are commented out by default to make the admin path obvious. + +# ─── Required (boot-time) ──────────────────────────────────────────────────── + +DATABASE_URL=postgresql://crm:changeme@localhost:5434/port_nimara_crm +REDIS_URL=redis://:changeme@localhost:6379 + +BETTER_AUTH_SECRET=dev-secret-please-change-32-chars-minimum-12345678 +BETTER_AUTH_URL=http://localhost:3000 +CSRF_SECRET=dev-csrf-secret-please-change-32-chars-minimum-12345 + +# Generated once for local dev. Production uses a different rotated key. +EMAIL_CREDENTIAL_KEY=0000000000000000000000000000000000000000000000000000000000000000 + +APP_URL=http://localhost:3000 +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +NODE_ENV=development +LOG_LEVEL=debug + +# ─── Dev-only safety net ───────────────────────────────────────────────────── + +# When set, every outbound email is rerouted to this address. +# Configure to YOUR personal email so seeded fake-client sends don't escape. +# EMAIL_REDIRECT_TO= + +# Skip env validation (used by Docker build only). +# SKIP_ENV_VALIDATION= + +# ─── Optional integration env fallbacks (admin UI is canonical) ────────────── +# Uncomment + set ONLY if you want to bootstrap a port via env. Otherwise +# configure each integration via /admin/ after first login. + +# DOCUMENSO_API_URL=https://documenso.dev.example +# DOCUMENSO_API_KEY= +# DOCUMENSO_API_VERSION=v2 +# DOCUMENSO_WEBHOOK_SECRET= + +# SMTP_HOST=smtp.example +# SMTP_PORT=587 + +# OPENAI_API_KEY= + +# Local MinIO (set if NOT using the admin UI to configure storage) +# MINIO_ENDPOINT=localhost +# MINIO_PORT=9000 +# MINIO_ACCESS_KEY=minioadmin +# MINIO_SECRET_KEY=minioadmin +# MINIO_BUCKET=crm-files +# MINIO_USE_SSL=false +# MINIO_AUTO_CREATE_BUCKET=true diff --git a/.env.example b/.env.example index a7111ca4..99a4c04e 100644 --- a/.env.example +++ b/.env.example @@ -1,66 +1,115 @@ +# ─── Port Nimara CRM env template ───────────────────────────────────────────── +# +# This file documents every env var the CRM understands. Most integration +# settings have been moved into the per-port admin UI (see +# `docs/superpowers/specs/2026-05-15-env-to-admin-migration-design.md`): +# +# /admin/documenso — Documenso API URL, key, version, webhook secret, +# signers, templates +# /admin/ai — OpenAI API key + model + master switch +# /admin/email — SMTP host/port/user/pass, from-address +# /admin/storage — S3/MinIO endpoint, bucket, access key, secret key +# +# After a fresh deploy: +# 1. Set the REQUIRED block below (DB/Redis/auth secrets/encryption key). +# 2. Boot the app and run `/setup` to create the first super-admin. +# 3. Open `/admin/` and configure each one. Each field shows +# a "Using env fallback" badge if it's still inheriting from env, plus +# a "Copy from env" button for one-click migration into the DB. +# +# The COMMENTED env vars in the OPTIONAL block below still work as a runtime +# fallback if you set them — useful for staging / dev to bootstrap quickly, +# or for backward compatibility with older deployments. New ports inherit +# from these as their initial defaults until the admin UI overrides them. +# +# ─── REQUIRED (boot-time secrets — must be in env) ──────────────────────────── + # Database DATABASE_URL=postgresql://crm:changeme@localhost:5432/port_nimara_crm -# Redis +# Redis (BullMQ + Socket.IO adapter) REDIS_URL=redis://:changeme@localhost:6379 -# Auth +# Auth (must be 32+ char random strings; rotate carefully) BETTER_AUTH_SECRET=change-me-to-a-random-string-at-least-32-chars BETTER_AUTH_URL=http://localhost:3000 CSRF_SECRET=change-me-to-a-random-string-at-least-32-chars -# MinIO -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin -MINIO_BUCKET=crm-files -MINIO_USE_SSL=false -# When `true`, the S3 backend auto-creates the configured bucket on boot if it -# does not exist (otherwise boot throws so deployment-time misconfigs surface -# immediately). Leave unset in production. -MINIO_AUTO_CREATE_BUCKET=false - -# Documenso -# Use the bare host — never include `/api/v1` in this URL. The Documenso -# client constructs versioned paths internally based on DOCUMENSO_API_VERSION -# below, and a double-pathed URL (https://.../api/v1/api/v1/...) returns 404 -# on every call. Trailing-slash values are fine. -DOCUMENSO_API_URL=https://documenso.example.com -# `v1` (Documenso 1.13.x) or `v2` (Documenso 2.x). Determines which API path -# prefix the client uses and which response-shape normalizer runs. -DOCUMENSO_API_VERSION=v1 -DOCUMENSO_API_KEY=your-documenso-api-key -DOCUMENSO_WEBHOOK_SECRET=your-webhook-secret-min-16-chars -# The Documenso template id used by the EOI send pathway. Per-port overrides -# live in `system_settings.documenso_template_id_eoi`; this env value is the -# global fallback when no per-port row exists. -DOCUMENSO_TEMPLATE_ID_EOI= -# Recipient role ids on the EOI template. The send service copies the template -# layout but re-targets recipients per interest, so we need the role ids to -# look up which template recipient becomes the Client / Sales signer. -DOCUMENSO_RECIPIENT_ID_CLIENT= -DOCUMENSO_RECIPIENT_ID_SALES= - -# Email (SMTP) -SMTP_HOST=mail.portnimara.com -SMTP_PORT=587 - -# Encryption (64-char hex string for AES-256) +# AES-256 key for credential encryption at rest. 64-char hex string. +# Generate with: openssl rand -hex 32 +# CRITICAL: rotating this orphans every encrypted credential in system_settings +# (Documenso API key, SMTP password, OpenAI key, S3 access/secret keys). +# Plan a re-keying flow before rotating in production. EMAIL_CREDENTIAL_KEY=0000000000000000000000000000000000000000000000000000000000000000 -# Google OAuth (optional) -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= - -# OpenAI (optional) -OPENAI_API_KEY= - -# App +# App URL — used by middleware redirects + outbound email link construction. APP_URL=http://localhost:3000 -PUBLIC_SITE_URL=https://portnimara.com + +# Inlined into the client JS bundle at build time. Must match APP_URL. +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# Process basics NODE_ENV=development LOG_LEVEL=info -# Next.js public -NEXT_PUBLIC_APP_URL=http://localhost:3000 +# When true, the filesystem storage backend refuses to start. Multi-node +# deploys MUST use the s3-compatible backend (per CLAUDE.md). +# MULTI_NODE_DEPLOYMENT=false + + +# ─── OPTIONAL: integration env fallbacks ────────────────────────────────────── +# Each of the following is configurable in the admin UI. Uncomment + set ANY +# of these to provide a fallback that ports inherit when their admin field is +# blank. The admin UI labels each inherited field with a "Using env fallback" +# badge and offers a "Copy from env" button for one-click migration into the +# port-scoped DB row. + +# ─ Documenso (admin: /admin/documenso) ─ +# DOCUMENSO_API_URL=https://documenso.example.com # Bare host. Never include /api/v1. +# DOCUMENSO_API_KEY=your-documenso-api-key # AES-encrypted once written via admin +# DOCUMENSO_API_VERSION=v1 # v1 (1.13.x) or v2 (2.x) +# DOCUMENSO_WEBHOOK_SECRET= # Min 16 chars. Generate: openssl rand -hex 16 +# DOCUMENSO_TEMPLATE_ID_EOI= +# DOCUMENSO_CLIENT_RECIPIENT_ID= +# DOCUMENSO_DEVELOPER_RECIPIENT_ID= +# DOCUMENSO_APPROVAL_RECIPIENT_ID= + +# ─ Email / SMTP (admin: /admin/email) ─ +# SMTP_HOST=mail.portnimara.com +# SMTP_PORT=587 +# SMTP_USER= +# SMTP_PASS= # AES-encrypted once written via admin +# SMTP_FROM= # e.g. "Port Nimara " + +# Dev/test safety net: when set, every outbound email is rerouted to this +# address regardless of recipient. Subject is prefixed with [redirected from ]. +# CRITICAL: env validation refuses boot if NODE_ENV=production AND this is set. +# EMAIL_REDIRECT_TO= + +# ─ Storage / S3 / MinIO (admin: /admin/storage) ─ +# MINIO_ENDPOINT=localhost +# MINIO_PORT=9000 +# MINIO_ACCESS_KEY= # AES-encrypted once written via admin +# MINIO_SECRET_KEY= # AES-encrypted (already) +# MINIO_BUCKET=crm-files +# MINIO_USE_SSL=false +# MINIO_AUTO_CREATE_BUCKET=false # Auto-create bucket at boot + +# ─ OpenAI (admin: /admin/ai) ─ +# OPENAI_API_KEY= # AES-encrypted once written via admin + +# ─ Public marketing site URL (admin: /admin/general — TODO) ─ +# PUBLIC_SITE_URL=https://portnimara.com + +# ─ Webhook intake from marketing site (deployment-shared, env-only) ─ +# Shared secret with the marketing website's CRM_INTAKE_SECRET. Min 16 chars. +# WEBSITE_INTAKE_SECRET= + +# ─ Sentry (optional — when unset the SDK is a no-op) ─ +# NEXT_PUBLIC_SENTRY_DSN= +# SENTRY_ENVIRONMENT= +# SENTRY_TRACES_SAMPLE_RATE=0.1 + +# ─ Google OAuth (not currently used) ─ +# GOOGLE_CLIENT_ID= +# GOOGLE_CLIENT_SECRET= diff --git a/.env.prod.template b/.env.prod.template new file mode 100644 index 00000000..ff8f22fd --- /dev/null +++ b/.env.prod.template @@ -0,0 +1,58 @@ +# ─── Port Nimara CRM — PROD environment template ───────────────────────────── +# +# Production env contains ONLY the boot-time minimum: DB connection, auth +# secrets, encryption key, app URL, log level. Every integration credential +# (Documenso, OpenAI, SMTP, S3) is configured per-port in the admin UI after +# the first super-admin completes /setup. This keeps secrets out of the +# infrastructure layer (k8s ConfigMap, .env files, deploy logs). +# +# Generate fresh secrets: +# openssl rand -hex 32 # for BETTER_AUTH_SECRET, CSRF_SECRET +# openssl rand -hex 32 # for EMAIL_CREDENTIAL_KEY (must be 64 hex chars) + +# ─── Required ──────────────────────────────────────────────────────────────── + +DATABASE_URL=postgresql://USER:PASS@HOST:5432/port_nimara_crm +REDIS_URL=redis://:PASS@HOST:6379 + +BETTER_AUTH_SECRET=GENERATE_OPENSSL_RAND_HEX_32 +BETTER_AUTH_URL=https://crm.example.com +CSRF_SECRET=GENERATE_OPENSSL_RAND_HEX_32 + +# CRITICAL: rotating this orphans every encrypted credential in +# system_settings. Plan a re-keying flow before rotating. +EMAIL_CREDENTIAL_KEY=GENERATE_OPENSSL_RAND_HEX_32_PRODUCES_64_CHARS + +APP_URL=https://crm.example.com +NEXT_PUBLIC_APP_URL=https://crm.example.com + +NODE_ENV=production +LOG_LEVEL=info + +# ─── Multi-node guard ──────────────────────────────────────────────────────── +# Set true if running > 1 app instance. Forces the storage backend off +# filesystem onto S3-compatible (filesystem mode is single-node only). +MULTI_NODE_DEPLOYMENT=true + +# ─── Sentry (highly recommended in prod) ───────────────────────────────────── +NEXT_PUBLIC_SENTRY_DSN=https://YOUR_KEY@YOUR_PROJECT.ingest.sentry.io/PROJECT_ID +SENTRY_ENVIRONMENT=production +SENTRY_TRACES_SAMPLE_RATE=0.1 + +# ─── Webhook intake from marketing site (deployment-shared) ────────────────── +# Must match the marketing site's CRM_INTAKE_SECRET. Min 16 chars. +WEBSITE_INTAKE_SECRET=GENERATE_OPENSSL_RAND_HEX_16 + +# ─── DO NOT SET in production ──────────────────────────────────────────────── +# EMAIL_REDIRECT_TO — Will fail boot validation (silently rewrites every +# outbound email recipient). +# SKIP_ENV_VALIDATION — Bypasses safety checks. Internal use only. + +# ─── Integration credentials live in /admin/, NOT here ────────── +# Once deployed: +# 1. Run `pnpm exec drizzle-kit push` (or your migration script) +# 2. Hit https://crm.example.com/setup to create the first super-admin +# 3. Log in → /admin/documenso, /admin/email, /admin/storage, /admin/ai +# 4. Configure each integration. AES-encrypted at rest. +# 5. Run `pnpm tsx scripts/encrypt-plaintext-credentials.ts` once to encrypt +# any legacy plaintext rows from older deployments. diff --git a/CLAUDE.md b/CLAUDE.md index 23250b15..272636f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,11 @@ pnpm exec playwright test --project=visual --update-snapshots # Regenerate base pnpm tsx scripts/dev-trigger-portal-invite.ts # Send a portal activation email pnpm tsx scripts/dev-imap-probe.ts # Dump recent IMAP inbox messages +# Cloudflare quick-tunnel (for Documenso webhook testing) +launchctl load ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist # start +launchctl unload ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist # stop +./scripts/tunnel-url.sh --copy # print + copy webhook URL + # Schema migration (pnpm db:migrate is broken — apply via psql) PGPASSWORD=changeme psql -h localhost -p 5434 -U crm -d port_nimara_crm -f src/lib/db/migrations/0075_*.sql ``` diff --git a/scripts/tunnel-url.sh b/scripts/tunnel-url.sh new file mode 100644 index 00000000..7f6d83a0 --- /dev/null +++ b/scripts/tunnel-url.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Print the current Cloudflare quick-tunnel URL, or a clear status line +# if the launchd job isn't running. +# +# Usage: +# ./scripts/tunnel-url.sh # print URL or status +# ./scripts/tunnel-url.sh --copy # print URL and copy to clipboard +# +# Paired with the launchd plist at: +# ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist +# +# Quick ops: +# launchctl load ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist # start +# launchctl unload ~/Library/LaunchAgents/solutions.letsbe.pn-crm-tunnel.plist # stop +# launchctl kickstart -k gui/$(id -u)/solutions.letsbe.pn-crm-tunnel # restart (NEW URL) + +set -euo pipefail + +LOG_FILE="$HOME/Library/Logs/pn-crm-tunnel.err.log" +LABEL="solutions.letsbe.pn-crm-tunnel" + +if ! launchctl print "gui/$(id -u)/$LABEL" >/dev/null 2>&1; then + echo "Tunnel is not loaded. Start with:" + echo " launchctl load ~/Library/LaunchAgents/$LABEL.plist" + exit 1 +fi + +if [[ ! -f "$LOG_FILE" ]]; then + echo "Tunnel job is loaded but hasn't produced a log yet. Try again in a few seconds." + exit 1 +fi + +# cloudflared prints the public URL once on startup, like: +# https://.trycloudflare.com +# Take the most recent occurrence so a restart-then-rerun picks the +# current one rather than a stale earlier line. +URL=$(grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' "$LOG_FILE" | tail -1 || true) + +if [[ -z "$URL" ]]; then + echo "Tunnel is running but no URL has appeared in the log yet." + echo "Tail it: tail -f $LOG_FILE" + exit 1 +fi + +echo "$URL" +echo "$URL/api/webhooks/documenso ← paste this into Documenso webhook settings" + +if [[ "${1:-}" == "--copy" ]]; then + printf "%s/api/webhooks/documenso" "$URL" | pbcopy + echo "(webhook URL copied to clipboard)" +fi