Files
ukmesh/.env.example
T
gadgethd 1ebc496965 Map UI redesign, live-path visibility, feed latency, and security hardening (#19)
* Fix map node freshness consistency

* Harden output, ingest, caches, and WebSocket limits

* Enforce public visibility across derived data

* Harden proxy and operator deployment boundary

* Make owner grants authoritative and reconcile ACLs safely

* Bound path, spam, and statistics analysis

* Make link and coverage jobs crash-safe

* Implement strategic security remediation

* Fix production cutover configuration

* Fix disabled viewshed worker health signal

* Serve stale stats during background refresh

* Retain stale stats through refresh windows

* Bound analytics work to protect ingestion

* Prioritize summary warmup over chart scans

* Throttle path history rebuilds

* Bound path history result memory

* Stream path history aggregation

* Give bounded path rebuild one CPU

* Serve stale charts during bounded refresh

* Prioritize startup stats before chart scans

* Bound path history segment cardinality

* Pin path rebuild context to privacy generation

* Self-host original frontend fonts

* Allow bounded path rebuild to complete

* Improve live map UI and low-latency group feed

- Dock node details on the right with selection highlight and collapsible layers
- Add node legend, 24h activity sparkline, copy-link, and layout/overlap fixes
- Keep all repeaters visible during Live Path focus
- Send GroupText feed packets immediately over WebSocket (no batch delay)
- Cache expensive stats/observer activity more aggressively to protect ingest
- Remove stale local planning/audit markdown from the tree

* fix(ci): supply OPERATOR_SITE_TOKEN for compose validation

Workers/Compose CI failed because docker-compose requires
OPERATOR_SITE_TOKEN. Add CI placeholders for that and MQTT_PASSWORD.
2026-07-27 02:39:12 +01:00

237 lines
12 KiB
Bash

# ─────────────────────────────────────────
# MeshCore Analytics — Environment Config
# ─────────────────────────────────────────
# Copy this file to .env and fill in your values.
# Never commit .env to source control.
# Optional host interface for database, MQTT WebSocket, and Redis port
# publishing. The loopback default keeps infrastructure private; set this to a
# trusted LAN interface only when remote access is required and firewalled.
INFRA_BIND_ADDRESS=127.0.0.1
# Required for the backend/operator dashboard. Send as
# `Authorization: Bearer <token>` or `X-Operator-Token`.
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
OPERATOR_SITE_TOKEN=change_me_64_hex_chars
# ── Database ──────────────────────────────────────────────────────────────────
POSTGRES_DB=meshcore
POSTGRES_USER=meshcore
POSTGRES_PASSWORD=change_me_strong_password
# ── MQTT broker ───────────────────────────────────────────────────────────────
# Internal docker network URL for Mosquitto WebSocket listener
MQTT_BROKER_URL=ws://mosquitto:9001
# Credentials for the backend MQTT client
MQTT_USERNAME=backend
MQTT_PASSWORD=change_me_strong_password
# Bound in-process MQTT work during a burst. MQTT is QoS 0, so an overload is
# logged and shed rather than letting the backend consume unbounded memory.
MQTT_MAX_PAYLOAD_BYTES=65536
MQTT_INGEST_CONCURRENCY=8
MQTT_INGEST_QUEUE_MAX=1000
# Bound expensive statistics scans so they cannot exhaust the OLTP pool.
STATS_DB_QUERY_CONCURRENCY=2
# ── Redis ─────────────────────────────────────────────────────────────────────
# Docker Compose protects the internal Redis service with this password. The
# application passes it separately, so REDIS_URL can remain a plain endpoint.
REDIS_PASSWORD=change_me_strong_redis_password
# Optional endpoint override (for an external Redis service). Leave this as the
# internal service URL when using the bundled Compose stack.
REDIS_URL=redis://redis:6379
# ── JWT ───────────────────────────────────────────────────────────────────────
# Generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
JWT_SECRET=change_me_64_hex_chars
# ── CORS / allowed origins ────────────────────────────────────────────────────
# Comma-separated list of browser origins allowed to access the API and WebSocket.
# For local development, the defaults in docker-compose.yml cover localhost.
# Set this to your production domains for deployment.
ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
# ── Frontend hostname (build-time) ────────────────────────────────────────────
# If you're running two separate frontends (an analytics app and a public website)
# on different hostnames, set this to the analytics app's hostname.
# When a visitor hits this hostname they see the analytics dashboard;
# all other hostnames serve the public website layout.
# Leave blank to always show the analytics dashboard (simpler single-domain setup).
VITE_APP_HOSTNAME=app.example.com
# ── MeshCore channel secrets ──────────────────────────────────────────────────
# Comma-separated list of AES-128 channel secrets for decrypting GroupText packets.
# Format: name:hex or bare hex (name defaults to first 6 chars of hex).
# The default MeshCore public channel key is always included automatically.
# Example: MESHCORE_CHANNEL_SECRETS=mychannel:9cd4e1f2a3b4c5d6e7f8a9b0c1d2e3f4,aabbccddeeff00112233445566778899
MESHCORE_CHANNEL_SECRETS=
# ── Health check app ──────────────────────────────────────────────────────────
# Optional MeshCore health-check web app for healthcheck.ukmesh.com.
# The app subscribes to observer packet MQTT and waits for users to send a
# generated code to the configured MeshCore channel.
HEALTHCHECK_APP_TITLE=UKMesh Health Check
HEALTHCHECK_SOURCE_REF=main
HEALTHCHECK_TEST_CHANNEL_NAME=ukmeshtest
# Optional source entry name in MESHCORE_CHANNEL_SECRETS when the stored secret
# is named differently from the user-facing channel name.
HEALTHCHECK_TEST_CHANNEL_SECRET_SOURCE_NAME=test
# Optional override. If left blank, the container will reuse the matching
# name:secret entry from MESHCORE_CHANNEL_SECRETS.
HEALTHCHECK_TEST_CHANNEL_SECRET=
# Optional: restrict default scoring to a comma-separated list of observer public keys.
HEALTHCHECK_KNOWN_OBSERVERS=
# Stock defaults are 900 active / 14400 retained. Set both to 3600 if listed
# observers should have sent packets, status, or internal MQTT within one hour.
HEALTHCHECK_OBSERVER_ACTIVE_WINDOW_SECONDS=900
HEALTHCHECK_OBSERVER_RETENTION_SECONDS=14400
# Enable these after adding Cloudflare Turnstile keys.
HEALTHCHECK_TURNSTILE_ENABLED=false
HEALTHCHECK_TURNSTILE_SITE_KEY=
HEALTHCHECK_TURNSTILE_SECRET_KEY=
# ── Owner portal auth ─────────────────────────────────────────────────────────
# Map MQTT usernames to owned node public keys (lowercase 64-hex), comma-separated.
# Format:
# OWNER_MQTT_USERNAME_MAP=username1=nodeid1|nodeid2,username2=nodeid3
# Example:
# OWNER_MQTT_USERNAME_MAP=node1=10cfab...713c95,node-ukmesh=20ed75...fe2ac
OWNER_MQTT_USERNAME_MAP=
# Start in shadow/shadow, review `owner-auth:inventory`, then switch both to
# enforce/apply only after the inventory has no ambiguous users or missing grants.
OWNER_AUTHORIZATION_MODE=shadow
OWNER_ACL_MODE=shadow
OWNER_ACL_UNMANAGED_USERS=backend,test,test2
# Explicit exception list for intentionally revoked/disabled accounts that
# should retain an empty broker stanza during apply cutover.
OWNER_ACL_ALLOW_EMPTY_USERS=
# Bounded analytics controls. These defaults are intentionally conservative;
# raise them only after reviewing query plans, RSS, and queue latency.
PATH_RESOLVE_WORKERS=2
PATH_RESOLVE_BACKGROUND_QUEUE_MAX=128
PATH_RESOLVE_INTERACTIVE_QUEUE_MAX=32
PATH_RESOLVE_END_TO_END_TIMEOUT_MS=15000
PATH_RESOLVE_UNIQUE_INFLIGHT_MAX=34
PATH_RESOLVE_OBSERVER_VARIANTS_PER_PACKET_MAX=4
PATH_SINGLE_HISTORY_HOURS=168
PATH_SINGLE_MAX_OBSERVERS=128
PATH_MULTI_HISTORY_WINDOW_HOURS=168
PATH_MULTI_MAX_SCAN_ROWS=2048
PATH_MULTI_MAX_OBSERVERS=128
PATH_HISTORY_RUN_DEADLINE_MS=5400000
PATH_HISTORY_CONCURRENCY=2
LAZY_PATH_HISTORY_WINDOW_HOURS=168
LAZY_PATH_MAX_OBSERVATIONS=2048
LAZY_PATH_MAX_OBSERVERS=128
LAZY_PATH_MAX_UNIQUE_HASHES=64
LAZY_PATH_MAX_CANDIDATE_NODES=2048
PATH_LEARNING_MAX_TRAINING_PACKETS=120000
PATH_LEARNING_MAX_NODES=50000
PATH_LEARNING_MAX_LINKS=200000
PATH_LEARNING_MAX_AGGREGATE_KEYS=1000000
PATH_LEARNING_RUN_DEADLINE_MS=2700000
SPAM_MESSAGE_MAX_CANDIDATE_ROWS=10000
SPAM_MESSAGE_MAX_MESSAGES=2000
SPAM_MESSAGE_MAX_OBSERVERS=32
SPAM_MESSAGE_MAX_NORMALIZED_CHARS=512
SPAM_MESSAGE_MAX_INCIDENTS=100
SPAM_MESSAGE_MAX_CANDIDATE_CLUSTERS=64
SPAM_MESSAGE_MAX_EVIDENCE_MEMBERS=256
SPAM_MESSAGE_ANALYSIS_BUDGET_MS=5000
SPAM_MESSAGE_DB_TIMEOUT_MS=15000
STATS_UNIQUE_INFLIGHT_MAX=32
LINK_QUEUE_V3_PRODUCER_ENABLED=1
LINK_QUEUE_V3_MAX_JOBS=5000
LINK_QUEUE_V3_MAX_BYTES=67108864
LINK_QUEUE_V3_MAX_PAYLOAD_BYTES=32768
LINK_QUEUE_V3_MAX_ATTEMPTS=5
LINK_QUEUE_V3_LEASE_MS=120000
LINK_REBUILD_ADMISSION_TIMEOUT_MS=1800000
LINK_REBUILD_COMPLETION_TIMEOUT_MS=21600000
LINK_REBUILD_MAX_NODES=5000
LINK_REBUILD_MAX_PHYSICAL_JOBS=200000
LINK_REBUILD_LEASE_MS=120000
PLANNED_COVERAGE_TTL_MS=86400000
PLANNED_COVERAGE_TTL_SECONDS=86400
PLANNED_COVERAGE_QUEUE_MAX=100
PLANNED_COVERAGE_MAX_OUTSTANDING_JOBS=100
PLANNED_COVERAGE_MAX_OUTSTANDING_HANDLES=10000
PLANNED_COVERAGE_MAX_HANDLES_PER_JOB=256
MQTT_AUDIT_MAX_OBSERVATIONS_PER_SCAN=4096
MQTT_AUDIT_MAX_NODES_PER_USER=256
MQTT_AUDIT_RETENTION_DAYS=30
# ── Viewshed / elevation API ──────────────────────────────────────────────────
# Viewshed work is opt-in because the worker is profile-gated and resource-heavy.
# Set true only when it is started with:
# docker compose --profile viewshed up -d viewshed-worker
VIEWSHED_ENABLED=false
OPENTOPODATA_API=https://api.opentopodata.org
# Suppress duplicate advert-triggered coverage jobs after a successful enqueue.
# A coordinate change or explicit coverage request bypasses this cooldown.
VIEWSHED_JOB_COOLDOWN_SECONDS=300
# Synthetic monitoring and optional alert delivery
SYNTHETIC_INTERVAL_MS=60000
SYNTHETIC_TIMEOUT_MS=10000
SYNTHETIC_FAILURE_THRESHOLD=3
ALERT_WEBHOOK_URL=
MAINTENANCE_ACTIVE=0
MAINTENANCE_MESSAGE=
# Per-client WebSocket safeguards for browser input and slow consumers.
WS_MAX_PAYLOAD_BYTES=65536
WS_MAX_QUEUE_BYTES=8388608
WS_MAX_BUFFERED_BYTES=33554432
WS_INITIAL_STATE_MAX_BYTES=16777216
# ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
# Get from: Cloudflare Zero Trust → Access → Tunnels → your tunnel → token
CLOUDFLARE_TUNNEL_TOKEN=paste_your_tunnel_token_here
# ── Anubis bot protection ─────────────────────────────────────────────────────
# Signing key shared across all Anubis sidecar containers.
# Generate with: openssl rand -hex 32
# Keep this secret — it signs the PoW challenge cookies.
ANUBIS_ED25519_PRIVATE_KEY_HEX=
# ── App port ──────────────────────────────────────────────────────────────────
PORT=3000
# ── Message-spam detection (Spam Watch) ───────────────────────────────────────
# Detects repeated near-duplicate chat messages and groups them into incidents
# for the public /spam dashboard. All defaults are sensible; see
# docs/spam-detection.md for the full list and tuning guidance.
SPAM_MESSAGE_ANALYZER_ENABLED=true
SPAM_MESSAGE_INTERVAL_MS=300000
SPAM_MESSAGE_WINDOW_HOURS=24
# Min distinct near-duplicate messages before a cluster can be an incident.
# Legit chatter is a few repeats; real spam is dozens+. Raise to reduce false
# positives from small bursts of normal traffic.
SPAM_MESSAGE_MIN_TRANSMISSIONS=8
# Confidence floor for the public list / "ongoing" status (0..1).
SPAM_MESSAGE_PUBLIC_MIN_SCORE=0.5
# At/above this many near-duplicate messages a qualified cluster is treated as a
# sustained automated flood and scored up (unless it is plain test/connectivity
# traffic). Catches templated floods even when they embed the word "test".
SPAM_MESSAGE_FLOOD_MIN_MESSAGES=25
# Channel labels excluded from detection (comma-separated). The test channel
# carries repeated test traffic by design. NOTE: a flood that also hits other
# channels (e.g. Public) is still caught via those copies.
SPAM_MESSAGE_EXCLUDE_CHANNELS=test
# Origin estimation. Primary method resolves relay PATHS: the first repeater in
# the path is the source of truth, resolved next to the closest (lowest-hop)
# receiver. Falls back to the observer-signal estimate (closest-receiver cohort,
# min-hop + slack) when paths can't be resolved. Per-hop km = radius per hop.
SPAM_MESSAGE_ORIGIN_USE_PATHS=true
SPAM_MESSAGE_ORIGIN_PATH_MAX_PACKETS=40
SPAM_MESSAGE_ORIGIN_PATH_MIN_VOTES=3
SPAM_MESSAGE_ORIGIN_PATH_CLUSTER_KM=45
SPAM_MESSAGE_ORIGIN_PATH_AMBIGUOUS_WEIGHT=0.4
SPAM_MESSAGE_ORIGIN_NEAR_HOP_MAX=2
SPAM_MESSAGE_ORIGIN_NEAR_HOP_SLACK=1
SPAM_MESSAGE_ORIGIN_PER_HOP_KM=12