mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* fix(email): route Intelligence Brief off the alerts@ mailbox The daily "WorldMonitor Intelligence Brief" email was shipping from `alerts@worldmonitor.app` with a display name that — if the Railway env override dropped the `Name <…>` wrapper — Gmail/Outlook fell back to rendering the local-part ("alerts" / "alert") as the sender name. Recipients saw a scary-looking "alert" in their inbox for what is actually a curated editorial read. Split the sender so editorial mail can't share the `alerts@` mailbox with incident pushes: - New env var `RESEND_FROM_BRIEF` (default `WorldMonitor Brief <brief@worldmonitor.app>`) consumed by seed-digest-notifications.mjs. - Falls back to `RESEND_FROM_EMAIL`, then to the built-in default, so existing deploys keep working and the rollout is a single Railway env flip on the digest service. - notification-relay.cjs (realtime push alerts) intentionally keeps `RESEND_FROM_EMAIL` / `alerts@` — accurate for that path. - .env.example documents the display-name rule so the bare-address trap can't re-introduce the bug. Rollout: set `RESEND_FROM_BRIEF=WorldMonitor Brief <brief@worldmonitor.app>` on the `seed-digest-notifications` Railway service. Domain-level Resend verification already covers the new local-part; no DNS change needed. * fix(email): runtime normalize sender to prevent bare-address regression PR review feedback from codex: > P2 — RESEND_FROM_BRIEF is consumed verbatim, so an operator can > still set brief@worldmonitor.app without a display name and > recreate the same Gmail/Outlook rendering bug for the daily brief. > Today that protection is only documentation in .env.example, not > runtime enforcement. Add a small shared helper `scripts/lib/resend-from.cjs` that coerces a bare email address into a "Name <addr>" wrapper with a loud warning log, and wire it into the digest path. - Bare-address input (e.g. `brief@worldmonitor.app`) is rewritten to `WorldMonitor Brief <brief@worldmonitor.app>` so Gmail/Outlook stop falling back to the local-part as the display name. - Coercion emits a single `console.warn` line per boot so operators see the signal in Railway logs and can fix the underlying env. - Fail-safe (not fail-closed) — a misconfigured env does NOT take the cron down. Also resolves the P3 doc-vs-runtime divergence by reverting .env.example's RESEND_FROM_EMAIL default from "WorldMonitor Alerts <...>" back to "WorldMonitor <...>" to match the existing notification-relay.cjs runtime default. The realtime-alert path will get the same normalizer treatment in a follow-up PR that cohesively touches notification-relay.cjs + Dockerfile.relay. tests: 7 new cases in tests/resend-sender-normalize.test.mjs covering empty/null/whitespace input, wrapped passthrough, trim, bare-address coercion, warning emission, no-warning on wrapped, console.warn default sink. Runs under `npm run test:data`.
431 lines
15 KiB
Plaintext
431 lines
15 KiB
Plaintext
# ============================================
|
|
# World Monitor — Environment Variables
|
|
# ============================================
|
|
# Copy this file to .env.local and fill in the values you need.
|
|
# All keys are optional — the dashboard works without them,
|
|
# but the corresponding features will be disabled.
|
|
#
|
|
# cp .env.example .env.local
|
|
#
|
|
# For self-hosted Docker deployments, see SELF_HOSTING.md.
|
|
# Use docker-compose.override.yml (gitignored) for local secrets.
|
|
# ============================================
|
|
|
|
|
|
# ------ AI Summarization (Vercel) ------
|
|
|
|
# Groq API (primary — 14,400 req/day on free tier)
|
|
# Get yours at: https://console.groq.com/
|
|
GROQ_API_KEY=
|
|
|
|
# OpenRouter API (fallback — 50 req/day on free tier)
|
|
# Get yours at: https://openrouter.ai/
|
|
OPENROUTER_API_KEY=
|
|
|
|
# Optional: forecast enrichment model routing
|
|
# Defaults stay unchanged unless you set these.
|
|
# Precedence:
|
|
# combined-stage override -> critical-signal override -> global forecast override -> built-in defaults.
|
|
# Examples:
|
|
# FORECAST_LLM_COMBINED_PROVIDER_ORDER=openrouter
|
|
# FORECAST_LLM_COMBINED_MODEL_OPENROUTER=google/gemini-2.5-pro
|
|
# FORECAST_LLM_CRITICAL_PROVIDER_ORDER=openrouter
|
|
# FORECAST_LLM_CRITICAL_MODEL_OPENROUTER=anthropic/claude-3.5-haiku
|
|
FORECAST_LLM_PROVIDER_ORDER=
|
|
FORECAST_LLM_MODEL_OPENROUTER=
|
|
FORECAST_LLM_COMBINED_PROVIDER_ORDER=
|
|
FORECAST_LLM_COMBINED_MODEL_OPENROUTER=
|
|
FORECAST_LLM_CRITICAL_PROVIDER_ORDER=
|
|
FORECAST_LLM_CRITICAL_MODEL_OPENROUTER=
|
|
FORECAST_LLM_MARKET_IMPLICATIONS_PROVIDER_ORDER=
|
|
FORECAST_LLM_MARKET_IMPLICATIONS_MODEL_OPENROUTER=
|
|
|
|
|
|
# ------ Cross-User Cache (Vercel — Upstash Redis) ------
|
|
|
|
# Used to deduplicate AI calls and cache risk scores across visitors.
|
|
# Create a free Redis database at: https://upstash.com/
|
|
UPSTASH_REDIS_REST_URL=
|
|
UPSTASH_REDIS_REST_TOKEN=
|
|
|
|
|
|
# ------ Market Data (Vercel) ------
|
|
|
|
# Finnhub (primary stock quotes — free tier available)
|
|
# Register at: https://finnhub.io/
|
|
FINNHUB_API_KEY=
|
|
|
|
|
|
# ------ Energy Data (Vercel) ------
|
|
|
|
# U.S. Energy Information Administration (oil prices, production, inventory)
|
|
# Register at: https://www.eia.gov/opendata/
|
|
EIA_API_KEY=
|
|
|
|
|
|
# ------ Economic Data (Vercel) ------
|
|
|
|
# FRED (Federal Reserve Economic Data)
|
|
# Register at: https://fred.stlouisfed.org/docs/api/api_key.html
|
|
FRED_API_KEY=
|
|
|
|
|
|
# ------ Air Quality Intelligence (Railway seed) ------
|
|
|
|
# OpenAQ API v3 (required for scripts/seed-health-air-quality.mjs)
|
|
# Register at: https://docs.openaq.org/using-the-api/api-key
|
|
OPENAQ_API_KEY=
|
|
|
|
# WAQI API (optional supplement for additional city/station coverage)
|
|
# Register at: https://aqicn.org/data-platform/token/
|
|
WAQI_API_KEY=
|
|
|
|
|
|
# ------ Aviation Intelligence (Vercel) ------
|
|
|
|
# AviationStack (live flight data, airport flights, carrier ops)
|
|
# Register at: https://aviationstack.com/
|
|
AVIATIONSTACK_API=
|
|
|
|
# ICAO API (NOTAM airport closures — optional, MENA region)
|
|
# Register at: https://applications.icao.int/
|
|
ICAO_API_KEY=
|
|
|
|
# Travelpayouts (flight price search — optional, demo only)
|
|
# Register at: https://www.travelpayouts.com/
|
|
TRAVELPAYOUTS_API_TOKEN=
|
|
|
|
|
|
# ------ Aircraft Tracking (Vercel) ------
|
|
|
|
# Wingbits aircraft enrichment (owner, operator, type)
|
|
# Contact: https://wingbits.com/
|
|
WINGBITS_API_KEY=
|
|
|
|
|
|
# ------ Conflict & Protest Data (Vercel) ------
|
|
|
|
# ACLED (Armed Conflict Location & Event Data — free for researchers)
|
|
# Register at: https://acleddata.com/
|
|
#
|
|
# RECOMMENDED: Set email + password for automatic OAuth token refresh.
|
|
# ACLED access tokens expire every 24 hours; with these credentials,
|
|
# the server will automatically exchange them for a fresh token.
|
|
#
|
|
# SECURITY NOTE: These credentials are stored in plaintext in .env.local.
|
|
# This is an acceptable trade-off for a self-hosted dashboard because:
|
|
# (a) .env.local is gitignored and never committed,
|
|
# (b) ACLED accounts are free and grant read-only API access,
|
|
# (c) the alternative (manual token rotation every 24 h) is impractical.
|
|
# If this is a concern for your deployment, use ACLED_ACCESS_TOKEN instead
|
|
# and manually refresh the token daily.
|
|
ACLED_EMAIL=
|
|
ACLED_PASSWORD=
|
|
#
|
|
# LEGACY: Static access token (optional fallback — expires after 24 h).
|
|
# Only needed if you prefer not to store email/password above.
|
|
# Generate at: https://acleddata.com/ → My Account → API Access
|
|
ACLED_ACCESS_TOKEN=
|
|
|
|
# UCDP (Uppsala Conflict Data Program — access token required since 2025)
|
|
# Register at: https://ucdp.uu.se/apidocs/
|
|
UCDP_ACCESS_TOKEN=
|
|
|
|
|
|
# ------ Internet Outages (Vercel) ------
|
|
|
|
# Cloudflare Radar API (requires free Cloudflare account with Radar access)
|
|
CLOUDFLARE_API_TOKEN=
|
|
|
|
# Cloudflare R2 account id for seed scripts that read or write R2 objects
|
|
CLOUDFLARE_R2_ACCOUNT_ID=
|
|
|
|
# Cloudflare R2 trace storage for forecast seed review artifacts
|
|
# Create R2 access keys in Cloudflare and target the bucket you want to use for forecast traces.
|
|
CLOUDFLARE_R2_BUCKET=
|
|
CLOUDFLARE_R2_TRACE_BUCKET=
|
|
CLOUDFLARE_R2_ACCESS_KEY_ID=
|
|
CLOUDFLARE_R2_SECRET_ACCESS_KEY=
|
|
CLOUDFLARE_R2_REGION=auto
|
|
CLOUDFLARE_R2_TRACE_PREFIX=seed-data/forecast-traces
|
|
|
|
|
|
# ------ Satellite Fire Detection (Vercel) ------
|
|
|
|
# NASA FIRMS (Fire Information for Resource Management System)
|
|
# Register at: https://firms.modaps.eosdis.nasa.gov/
|
|
NASA_FIRMS_API_KEY=
|
|
|
|
# ------ Climate Disasters Seed (Railway cron) ------
|
|
|
|
# Required ReliefWeb app name for seed-climate-disasters.mjs.
|
|
# ReliefWeb now rejects anonymous requests; use a pre-approved app name.
|
|
RELIEFWEB_APPNAME=
|
|
|
|
|
|
# ------ Railway Relay (scripts/ais-relay.cjs) ------
|
|
# The relay server handles AIS vessel tracking + OpenSky aircraft data + RSS proxy.
|
|
# It can also run the Telegram OSINT poller (stateful MTProto) when configured.
|
|
# Deploy on Railway with: node scripts/ais-relay.cjs
|
|
|
|
# AISStream API key for live vessel positions
|
|
# Get yours at: https://aisstream.io/
|
|
AISSTREAM_API_KEY=
|
|
|
|
# OpenSky Network OAuth2 credentials (higher rate limits for cloud IPs)
|
|
# Register at: https://opensky-network.org/
|
|
OPENSKY_CLIENT_ID=
|
|
OPENSKY_CLIENT_SECRET=
|
|
|
|
|
|
# ------ Telegram OSINT (Railway relay) ------
|
|
# Telegram MTProto keys (free): https://my.telegram.org/apps
|
|
TELEGRAM_API_ID=
|
|
TELEGRAM_API_HASH=
|
|
|
|
# GramJS StringSession generated locally (see: scripts/telegram/session-auth.mjs)
|
|
TELEGRAM_SESSION=
|
|
|
|
# Which curated list bucket to ingest: full | tech | finance
|
|
TELEGRAM_CHANNEL_SET=full
|
|
|
|
# ------ Self-Hosted LLM (Docker — any OpenAI-compatible endpoint) ------
|
|
|
|
# Point to your own LLM server (Ollama, vLLM, llama.cpp, etc.)
|
|
# Used for intelligence assessments in the correlation engine.
|
|
LLM_API_URL=
|
|
LLM_API_KEY=
|
|
LLM_MODEL=
|
|
|
|
# Alternative: Ollama-specific URL (used if LLM_API_URL is not set)
|
|
OLLAMA_API_URL=
|
|
OLLAMA_MODEL=
|
|
|
|
|
|
# ------ Railway Relay Connection (Vercel → Railway) ------
|
|
|
|
# Server-side URL (https://) — used by Vercel edge functions to reach the relay
|
|
WS_RELAY_URL=
|
|
|
|
# Optional client-side URL (wss://) — local/dev fallback only
|
|
VITE_WS_RELAY_URL=
|
|
|
|
# Shared secret between Vercel and Railway relay.
|
|
# Must be set to the SAME value on both platforms in production.
|
|
RELAY_SHARED_SECRET=
|
|
|
|
# Header name used to send the relay secret (must match on both platforms)
|
|
RELAY_AUTH_HEADER=x-relay-key
|
|
|
|
# Emergency production override to allow unauthenticated relay traffic.
|
|
# Leave unset/false in production.
|
|
ALLOW_UNAUTHENTICATED_RELAY=false
|
|
|
|
# Rolling window size (seconds) used by relay /metrics endpoint.
|
|
RELAY_METRICS_WINDOW_SECONDS=60
|
|
|
|
|
|
# ------ Supply Chain Intelligence (Vercel / Railway relay) ------
|
|
|
|
# CorridorRisk API (maritime corridor risk scoring — optional)
|
|
# Register at: https://corridorrisk.io/
|
|
CORRIDOR_RISK_API_KEY=
|
|
|
|
|
|
# ------ Public Data Sources (no keys required) ------
|
|
|
|
# UNHCR (UN Refugee Agency) — public API, no auth (CC BY 4.0)
|
|
# Open-Meteo — public API, no auth (processes Copernicus ERA5)
|
|
# WorldPop — public API, no auth needed
|
|
|
|
|
|
# ------ Site Configuration ------
|
|
|
|
# Site variant: "full" (worldmonitor.app) or "tech" (tech.worldmonitor.app)
|
|
VITE_VARIANT=full
|
|
|
|
# API base URL for web redirect. When set, browser fetch calls to /api/*
|
|
# are redirected to this URL. Leave empty for same-domain API (local installs).
|
|
# Production: https://api.worldmonitor.app
|
|
VITE_WS_API_URL=
|
|
|
|
# Client-side Sentry DSN (optional). Leave empty to disable error reporting.
|
|
VITE_SENTRY_DSN=
|
|
|
|
# Map interaction mode:
|
|
# - "flat" keeps pitch/rotation disabled (2D interaction)
|
|
# - "3d" enables pitch/rotation interactions (default)
|
|
VITE_MAP_INTERACTION_MODE=3d
|
|
|
|
# Self-hosted map tiles (optional — PMTiles on Cloudflare R2 or any HTTP server)
|
|
# Leave empty to use free OpenFreeMap tiles. Set to your own PMTiles URL for self-hosted tiles.
|
|
# See: https://protomaps.com/docs/pmtiles for how to generate PMTiles files.
|
|
VITE_PMTILES_URL=
|
|
# Public CORS-enabled URL for the same PMTiles file (used by Tauri desktop app).
|
|
# If your VITE_PMTILES_URL is behind a reverse proxy without CORS, set this to the
|
|
# direct R2/S3 public URL. The desktop app uses this URL; the web app uses VITE_PMTILES_URL.
|
|
VITE_PMTILES_URL_PUBLIC=
|
|
|
|
|
|
# ------ Desktop Cloud Fallback (Vercel) ------
|
|
|
|
# Comma-separated list of valid API keys for desktop cloud fallback.
|
|
# Generate with: openssl rand -hex 24 | sed 's/^/wm_/'
|
|
WORLDMONITOR_VALID_KEYS=
|
|
|
|
|
|
# ------ Registration DB (Convex) ------
|
|
|
|
# Convex deployment URL for email registration storage.
|
|
# Set up at: https://dashboard.convex.dev/
|
|
CONVEX_URL=
|
|
|
|
# Convex HTTP actions / webhooks base URL (.convex.site host).
|
|
# Use for httpRouter endpoints such as internal entitlement fallback and webhooks.
|
|
# Example: https://happy-animal-123.convex.site
|
|
CONVEX_SITE_URL=
|
|
|
|
# Shared secret for Vercel gateway -> Convex internal entitlement fallback.
|
|
# Must be set to the same value in both environments.
|
|
# Generate: openssl rand -hex 32
|
|
CONVEX_SERVER_SHARED_SECRET=
|
|
|
|
# Vite-exposed Convex URL for frontend entitlement service (VITE_ prefix required for client-side access)
|
|
VITE_CONVEX_URL=
|
|
|
|
|
|
# ------ Dodo Payments (Convex + Vercel) ------
|
|
|
|
# Dodo Payments API key (test mode or live mode)
|
|
# Canonical name: DODO_API_KEY (used by convex/lib/dodo.ts and billing actions)
|
|
# Get yours at: https://app.dodopayments.com/ -> Settings -> API Keys
|
|
DODO_API_KEY=
|
|
|
|
# Dodo Payments webhook secret for signature verification
|
|
# NOTE: The @dodopayments/convex library reads DODO_PAYMENTS_WEBHOOK_SECRET internally.
|
|
# Set BOTH this value AND DODO_PAYMENTS_WEBHOOK_SECRET to the same secret.
|
|
# Get it at: https://app.dodopayments.com/ -> Developers -> Webhooks
|
|
# Webhook URL to register in Dodo dashboard:
|
|
# https://<your-convex-deployment>.convex.site/dodopayments-webhook
|
|
DODO_WEBHOOK_SECRET=
|
|
DODO_PAYMENTS_WEBHOOK_SECRET=
|
|
|
|
# HMAC key for signing userId in checkout metadata (identity verification).
|
|
# SEPARATE from DODO_PAYMENTS_WEBHOOK_SECRET — never reuse the same value.
|
|
# Rotating one must not affect the other.
|
|
# Generate: openssl rand -hex 32
|
|
DODO_IDENTITY_SIGNING_SECRET=
|
|
|
|
# Dodo Payments business ID
|
|
# Found at: https://app.dodopayments.com/ -> Settings
|
|
DODO_BUSINESS_ID=
|
|
|
|
# Dodo Payments environment for client-side checkout overlay
|
|
# Values: "test_mode" (default) or "live_mode"
|
|
VITE_DODO_ENVIRONMENT=test_mode
|
|
|
|
|
|
# ------ Auth (Clerk) ------
|
|
|
|
# Clerk publishable key (browser-side, safe to expose)
|
|
# Get from: Clerk Dashboard -> API Keys
|
|
VITE_CLERK_PUBLISHABLE_KEY=
|
|
|
|
# Clerk secret key (server-side only, never expose to browser)
|
|
# Get from: Clerk Dashboard -> API Keys
|
|
CLERK_SECRET_KEY=
|
|
|
|
# Clerk JWT issuer domain (for Convex auth config)
|
|
# Format: https://your-clerk-app.clerk.accounts.dev
|
|
CLERK_JWT_ISSUER_DOMAIN=
|
|
|
|
|
|
# ------ Cloud Preferences Sync ------
|
|
|
|
# Set to 'true' to enable server-side preferences sync for signed-in users.
|
|
# Keep false until Phase 2 is QA-verified in staging.
|
|
VITE_CLOUD_PREFS_ENABLED=false
|
|
|
|
# Telegram bot username for deep link generation (without @)
|
|
VITE_TELEGRAM_BOT_USERNAME=WorldMonitorBot
|
|
|
|
|
|
# ------ Notification Delivery (Railway notification-relay service) ------
|
|
|
|
# @WorldMonitorBot token from BotFather. SEPARATE from TELEGRAM_API_ID/HASH/SESSION (MTProto).
|
|
TELEGRAM_BOT_TOKEN=
|
|
|
|
# Random 256-char secret for X-Telegram-Bot-Api-Secret-Token webhook verification.
|
|
# Generate: openssl rand -hex 128
|
|
TELEGRAM_WEBHOOK_SECRET=
|
|
|
|
# 32-byte base64 AES-256-GCM key for encrypting Slack webhook URLs at rest.
|
|
# Generate: openssl rand -base64 32
|
|
# Railway env ONLY — never add to Convex dashboard.
|
|
NOTIFICATION_ENCRYPTION_KEY=
|
|
|
|
# Resend API key for email notification delivery.
|
|
# Get from: resend.com/api-keys
|
|
RESEND_API_KEY=
|
|
|
|
# "From" address for email notifications (must be a verified Resend sender domain).
|
|
# ALWAYS include a display name in "Name <addr@domain>" form — without it, Gmail
|
|
# and Outlook fall back to rendering the local-part ("alerts") as the sender
|
|
# name, which reads like a scary alarm even for editorial content. The digest
|
|
# path (see RESEND_FROM_BRIEF below) enforces this at runtime via
|
|
# scripts/lib/resend-from.cjs; set this var with the wrapper anyway so the
|
|
# realtime-alert path (notification-relay.cjs) gets the same protection once
|
|
# the relay-side normalizer lands.
|
|
# RESEND_FROM_EMAIL is used by notification-relay.cjs for realtime push alerts.
|
|
RESEND_FROM_EMAIL=WorldMonitor <alerts@worldmonitor.app>
|
|
|
|
# "From" address for the daily Intelligence Brief / digest email. Kept
|
|
# separate from RESEND_FROM_EMAIL so editorial mail doesn't ship from the
|
|
# `alerts@` mailbox (which reads like an incident alarm). Used by
|
|
# scripts/seed-digest-notifications.mjs, which normalizes the value via
|
|
# scripts/lib/resend-from.cjs — a bare address is coerced to
|
|
# "WorldMonitor Brief <addr>" at runtime with a loud warning, so a
|
|
# misconfigured env cannot silently re-introduce the bare-local-part bug.
|
|
# Falls back to RESEND_FROM_EMAIL if unset, so existing deploys keep working.
|
|
RESEND_FROM_BRIEF=WorldMonitor Brief <brief@worldmonitor.app>
|
|
|
|
# Vite-exposed Convex URL for frontend entitlement service (VITE_ prefix required for client-side access)
|
|
VITE_CONVEX_URL=
|
|
|
|
|
|
# ------ Dodo Payments (Convex + Vercel) ------
|
|
|
|
# Dodo Payments API key (test mode or live mode)
|
|
# Canonical name: DODO_API_KEY (used by convex/lib/dodo.ts and billing actions)
|
|
# Get yours at: https://app.dodopayments.com/ -> Settings -> API Keys
|
|
DODO_API_KEY=
|
|
|
|
# Dodo Payments webhook secret for signature verification
|
|
# NOTE: The @dodopayments/convex library reads DODO_PAYMENTS_WEBHOOK_SECRET internally.
|
|
# Set BOTH this value AND DODO_PAYMENTS_WEBHOOK_SECRET to the same secret.
|
|
# Get it at: https://app.dodopayments.com/ -> Developers -> Webhooks
|
|
DODO_WEBHOOK_SECRET=
|
|
DODO_PAYMENTS_WEBHOOK_SECRET=
|
|
|
|
# Dodo Payments business ID
|
|
# Found at: https://app.dodopayments.com/ -> Settings
|
|
DODO_BUSINESS_ID=
|
|
|
|
# Dodo Payments environment for client-side checkout overlay
|
|
# Values: "test_mode" (default) or "live_mode"
|
|
VITE_DODO_ENVIRONMENT=test_mode
|
|
|
|
|
|
# ------ Clerk Auth (Gateway JWT verification) ------
|
|
|
|
# Clerk JWT issuer domain — enables bearer token auth in the API gateway.
|
|
# When set, the gateway verifies Authorization: Bearer <token> headers using
|
|
# Clerk's JWKS endpoint and extracts the userId for entitlement checks.
|
|
# Example: https://your-app.clerk.accounts.dev
|
|
CLERK_JWT_ISSUER_DOMAIN=
|
|
|
|
# Server-side Clerk publishable key used as an allowed JWT audience for
|
|
# standard Clerk session tokens. Use the same value as the browser key, but
|
|
# set it as a normal server env var too; the gateway does not read VITE_ vars.
|
|
CLERK_PUBLISHABLE_KEY=
|