Files
Shadowbroker/backend/.env.example
T
Shadowbroker 76750caa92 Round 7a: per-operator outbound attribution + GDELT GCS-direct fix (#292)
== Per-install operator handle for every third-party API call ==

Before this PR, every Shadowbroker install identified itself to
Wikipedia, Wikidata, Nominatim, GDELT, OpenMHz, Broadcastify,
weather.gov, NUFORC, Sentinel/Planetary Computer, TinyGS / CelesTrak,
Shodan, Finnhub, and others with a single project-wide User-Agent
("Shadowbroker/1.0" or "ShadowBroker-OSINT/1.0"). From the upstream's
perspective every install in the world looked like one giant scraper.
If one install misbehaved, the upstream's only recourse was to block
"Shadowbroker" as a whole.

PR #284 inadvertently doubled down on this in the frontend by
introducing a shared `WIKIMEDIA_API_USER_AGENT` constant. This PR
retrofits both backends to per-operator attribution.

  New setting: OPERATOR_HANDLE (env var / settings UI / auto-gen)
  New helper:  network_utils.outbound_user_agent("purpose")

The handle is auto-generated as "operator-XXXXXX" on first call (the
"shadow-" prefix from earlier drafts was deliberately dropped — too
suspicious-looking for abuse-detection systems). Operators can
override via OPERATOR_HANDLE; the value is sanitized to lowercase
alphanumeric+dash+underscore and capped at 48 chars. Persisted to
backend/data/operator_handle.json so it survives container restarts.

Retrofitted call sites (every previously-MONSTER User-Agent):
  - services/region_dossier.py (Wikipedia + Wikidata + Nominatim)
  - services/geocode.py         (Nominatim)
  - services/sentinel_search.py (Microsoft Planetary Computer)
  - services/feed_ingester.py   (operator-curated RSS feeds)
  - services/fetchers/earth_observation.py (weather.gov, NUFORC)
  - services/fetchers/infrastructure.py
  - services/fetchers/aircraft_database.py
  - services/fetchers/route_database.py
  - services/fetchers/trains.py
  - services/fetchers/meshtastic_map.py
  - services/shodan_connector.py
  - services/unusual_whales_connector.py (Finnhub)
  - services/tinygs_fetcher.py            (CelesTrak + TinyGS)
  - services/sar/sar_products_client.py
  - services/geopolitics.py               (GDELT)
  - services/radio_intercept.py           (Broadcastify + OpenMHz)
  - routers/cctv.py + main.py             (CCTV proxy)
  - routers/ai_intel.py
  - scripts/convert_power_plants.py       (release-time data refresh)

Spoofed browser UAs removed (issues #289 / #290 / #291 — tg12 audit):
  - cloudscraper-based Chrome impersonation against api.openmhz.com
    -> replaced with honest requests + per-install UA
  - Mozilla/5.0 spoofed UA on Broadcastify scrape
    -> replaced with honest UA
  - Mozilla/5.0 + fake first-party Referer on OpenMHz audio relay
    -> replaced with honest UA
  - cloudscraper dependency dropped from pyproject.toml + uv.lock

Frontend retrofit:
  - new GET /api/settings/operator-handle endpoint (local-operator
    gated) returns the install's handle
  - frontend/src/lib/wikimediaClient.ts fetches the handle once on
    first use, caches it for page lifetime, embeds it in the
    Api-User-Agent for every Wikipedia / Wikidata browser-direct call

== GDELT GCS-direct fix ==

GDELT's data.gdeltproject.org is a CNAME to a Google Cloud Storage
bucket. GCS responds with the wildcard *.storage.googleapis.com cert
which legitimately does NOT cover the GDELT custom domain, so Python's
TLS verification correctly refuses the connection. Some networks
happen to route through a path where this works; many (notably Docker
Desktop's outbound NAT on local installs) do not. Verified on the
maintainer's local install: GDELT was unreachable; 1610 geopolitical
events / 48 export files were dropping silently.

Fix: services/geopolitics._gcs_direct_gdelt_url() rewrites any
data.gdeltproject.org URL to its GCS-direct equivalent
(storage.googleapis.com/data.gdeltproject.org/...) where the standard
GCS cert is genuinely valid. api.gdeltproject.org and every other host
are left untouched.

Confirmed live: backend log goes from
  GDELT lastupdate failed: 500
to
  Downloading 48 GDELT export files...
  Downloaded 48/48 GDELT exports
  GDELT parsed: 1610 conflict locations from 48 files

== Tests ==

  backend/tests/test_per_operator_outbound_attribution.py (12 tests)
  backend/tests/test_gdelt_gcs_direct_rewrite.py          (6 tests)
  backend/tests/test_region_dossier_wikimedia_ua.py       (updated to
    pin the helper + per-operator handle, not the old constant)
  frontend/src/__tests__/utils/wikimediaClient.test.ts    (rewritten
    to mock /api/settings/operator-handle and assert per-operator UA)

Local: backend 114/114 security+audit+round7a suite green;
       frontend 718/718 vitest suite green.

Credit: tg12 (external security audit, issues #289/#290/#291
relating to spoofed UAs); BigBodyCobain (operator-prefix call,
GDELT cloud-vs-local diagnosis).
2026-05-21 15:11:28 -06:00

356 lines
18 KiB
Bash

# ShadowBroker Backend — Environment Variables
# Copy this file to .env and fill in your keys:
# cp .env.example .env
# ── Required Keys ──────────────────────────────────────────────
# Without these, the corresponding data layers will be empty.
OPENSKY_CLIENT_ID= # https://opensky-network.org/ — free account, OAuth2 client ID
OPENSKY_CLIENT_SECRET= # OAuth2 client secret from your OpenSky dashboard
AIS_API_KEY= # https://aisstream.io/ — free tier WebSocket key
# ── Optional ───────────────────────────────────────────────────
# Override allowed CORS origins (comma-separated). Defaults to localhost + LAN auto-detect.
# CORS_ORIGINS=http://192.168.1.50:3000,https://my-domain.com
# Admin key — protects sensitive endpoints (API key management, system update).
# If unset, loopback/localhost requests still work for local single-host dev.
# Remote/non-loopback admin access requires ADMIN_KEY, or ALLOW_INSECURE_ADMIN=true in debug-only setups.
# Set this in production and enter the same key in Settings → Admin Key.
# ADMIN_KEY=your-secret-admin-key-here
# Allow insecure admin access without ADMIN_KEY (local dev only, beyond loopback).
# Requires MESH_DEBUG_MODE=true; do not enable this for ordinary use.
# ALLOW_INSECURE_ADMIN=false
# Per-install operator handle. Round 7a: every outbound third-party API
# call (Wikipedia, Wikidata, Nominatim, GDELT, OpenMHz, Broadcastify,
# weather.gov, NUFORC, etc.) includes this handle in the User-Agent so
# upstreams can rate-limit / contact the specific install instead of
# treating every Shadowbroker user as one entity.
#
# Default empty -> a stable pseudonymous handle (e.g. "operator-7f3a92") is
# auto-generated on first run and persisted to backend/data/operator_handle.json.
# Operators who want a meaningful handle (real name, org, GitHub login) can
# set it here. Special characters are sanitized to dashes.
# OPERATOR_HANDLE=
# Default outbound User-Agent for all third-party HTTP fetchers. Operators
# who run a public relay and want a completely custom UA can set this; it
# bypasses the per-operator helper entirely. Most installs should leave it
# unset and use OPERATOR_HANDLE instead.
# SHADOWBROKER_USER_AGENT=
# Nominatim-specific User-Agent override (OSM usage policy). Leave unset to
# use the per-install handle (default) — set only if you have a registered
# Nominatim relay identity.
# NOMINATIM_USER_AGENT=
# ── Third-party fetcher opt-ins ────────────────────────────────
# These data sources phone home to politically/commercially sensitive
# upstreams. Disabled by default; set to "true" only if the operator
# explicitly wants the node's IP to contact these services.
#
# CrowdThreat — backend.crowdthreat.world (paid threat-intel aggregator).
# CROWDTHREAT_ENABLED=false
#
# EUvsDisinfo FIMI — euvsdisinfo.eu (EU disinformation tracker).
# FIMI_ENABLED=false
#
# Polymarket + Kalshi — US political/election prediction markets.
# PREDICTION_MARKETS_ENABLED=false
#
# Finnhub fallback / yfinance — financial market data.
# Set FINNHUB_API_KEY to enable Finnhub, or set FINANCIAL_ENABLED=true to allow
# the unauthenticated yfinance fallback to call Yahoo Finance.
# FINANCIAL_ENABLED=false
#
# NUFORC UAP sightings — huggingface.co dataset download.
# NUFORC_ENABLED=false
#
# News RSS aggregator — defaults ON. Set to "false" to disable all
# configured news feeds (kill switch for the news layer).
# NEWS_ENABLED=true
# LTA Singapore traffic cameras — leave blank to skip this data source.
# LTA_ACCOUNT_KEY=
# NASA FIRMS country-scoped fire data — enriches global CSV with conflict-zone hotspots.
# Free MAP_KEY from https://firms.modaps.eosdis.nasa.gov/map/#d:24hrs;@0.0,0.0,3.0z
# FIRMS_MAP_KEY=
# Ukraine air raid alerts from alerts.in.ua — free token from https://alerts.in.ua/
# ALERTS_IN_UA_TOKEN=
# Optional NUFORC UAP sighting map enrichment via Mapbox Tilequery.
# Leave blank to skip this optional enrichment.
# NUFORC_MAPBOX_TOKEN=
# Google Earth Engine service account for VIIRS change detection (optional).
# Download JSON key from https://console.cloud.google.com/iam-admin/serviceaccounts
# pip install earthengine-api
# GEE_SERVICE_ACCOUNT_KEY=
# ── Meshtastic MQTT Bridge ─────────────────────────────────────
# Disabled by default to respect the public Meshtastic broker.
# When enabled, subscribes to US region only. Add more regions via MESH_MQTT_EXTRA_ROOTS.
# MESH_MQTT_ENABLED=false
# MESH_MQTT_EXTRA_ROOTS=EU_868,ANZ # comma-separated additional region roots
# MESH_MQTT_INCLUDE_DEFAULT_ROOTS=true
# MESH_MQTT_BROKER=mqtt.meshtastic.org
# MESH_MQTT_PORT=1883
# Leave user/pass blank for the public Meshtastic broker default.
# MESH_MQTT_USER=
# MESH_MQTT_PASS=
# Optional Meshtastic node ID (e.g. "!abcd1234"). When set, included in the
# User-Agent sent to meshtastic.liamcottle.net so the upstream service operator
# can identify per-install traffic instead of aggregated "ShadowBroker" hits.
# Leave blank to send a generic UA. If you set MESHTASTIC_OPERATOR_CALLSIGN,
# it is included in outbound headers to meshtastic.org by default so they
# can rate-limit per-operator. Set MESHTASTIC_SEND_CALLSIGN_HEADER=false to
# suppress the callsign while still using it locally (e.g. for APRS).
# MESHTASTIC_OPERATOR_CALLSIGN=
# MESHTASTIC_SEND_CALLSIGN_HEADER=true
# MESH_MQTT_PSK= # hex-encoded, empty = default LongFast key
# ── Mesh / Reticulum (RNS) ─────────────────────────────────────
# Full-node / participant-node posture for public Infonet sync.
# MESH_NODE_MODE=participant # participant | relay | perimeter
# Legacy compatibility sunset toggles. Default posture is to block these.
# Legacy 16-hex node-id binding no longer has a boolean escape hatch; use a
# dated migration override only when you intentionally need older peers during
# migration before the hard removal target in v0.10.0 / 2026-06-01.
# MESH_BLOCK_LEGACY_NODE_ID_COMPAT=true
# MESH_ALLOW_LEGACY_NODE_ID_COMPAT_UNTIL=2026-05-15
# MESH_BLOCK_LEGACY_AGENT_ID_LOOKUP=true
# Temporary DM invite migration escape hatch. Default posture blocks importing
# legacy/compat v1/v2 DM invites; use a dated override only while retiring
# older exports and ask senders to re-export a current signed invite.
# MESH_ALLOW_COMPAT_DM_INVITE_IMPORT_UNTIL=2026-05-15
# Temporary legacy GET DM poll/count escape hatch. Default posture requires the
# signed mailbox-claim POST APIs; only use this dated override while retiring
# older clients that still call GET poll/count directly.
# MESH_ALLOW_LEGACY_DM_GET_UNTIL=2026-05-15
# Temporary raw dm1 compose/decrypt escape hatch. Default posture expects MLS
# DM bootstrap on supported peers; only use this dated override while retiring
# older clients that still need the raw dm1 helper path.
# MESH_ALLOW_LEGACY_DM1_UNTIL=2026-05-15
# Temporary legacy dm_message signature escape hatch. Default posture requires
# the full modern signed payload; only enable this with a dated migration
# override while older senders are being retired.
# MESH_ALLOW_LEGACY_DM_SIGNATURE_COMPAT_UNTIL=2026-05-15
# Rotate voter-blinding salts so new reputation events stop reusing one
# forever-stable blinded ID. Keep grace >= rotation cadence so older votes
# remain matchable while they age out of the ledger.
# MESH_VOTER_BLIND_SALT_ROTATE_DAYS=30
# MESH_VOTER_BLIND_SALT_GRACE_DAYS=30
# Deprecated legacy env vars kept only for backward config compatibility.
# Ordinary shipped gate flows keep MLS decrypt local; service-side decrypt is
# reserved for explicit recovery reads.
# MESH_GATE_BACKEND_DECRYPT_COMPAT=false
# MESH_GATE_BACKEND_DECRYPT_COMPAT_ACKNOWLEDGE=false
# Deprecated legacy env vars kept only for backward config compatibility.
# Ordinary shipped gate flows keep plaintext compose/post local and only submit
# encrypted envelopes to the backend for sign/post.
# MESH_GATE_BACKEND_PLAINTEXT_COMPAT=false
# MESH_GATE_BACKEND_PLAINTEXT_COMPAT_ACKNOWLEDGE=false
# Legacy runtime switches for recovery envelopes. Per-gate envelope_policy is
# the source of truth; leave these at the default unless testing old behavior.
# MESH_GATE_RECOVERY_ENVELOPE_ENABLE=true
# MESH_GATE_RECOVERY_ENVELOPE_ENABLE_ACKNOWLEDGE=true
# Optional operator-only recovery tradeoff. Leave off for the default posture:
# ordinary gate reads keep plaintext local/in-memory unless you explicitly use
# the recovery-envelope path.
# MESH_GATE_PLAINTEXT_PERSIST=false
# MESH_GATE_PLAINTEXT_PERSIST_ACKNOWLEDGE=false
# Legacy Phase-1 gate envelope fallback is now explicit and time-bounded per
# gate. This only controls the default expiry window when you deliberately
# re-enable that migration path for older stored envelopes.
# MESH_GATE_LEGACY_ENVELOPE_FALLBACK_MAX_DAYS=30
# Feature-flagged multiplexed gate session stream. Stream-first room ownership
# is implemented; keep off until you want that rollout enabled in your env.
# MESH_GATE_SESSION_STREAM_ENABLED=false
# MESH_GATE_SESSION_STREAM_HEARTBEAT_S=20
# MESH_GATE_SESSION_STREAM_BATCH_MS=1500
# MESH_GATE_SESSION_STREAM_MAX_GATES=16
# MESH_BOOTSTRAP_DISABLED=false
# MESH_BOOTSTRAP_MANIFEST_PATH=data/bootstrap_peers.json
# MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY=
# Infonet/Wormhole fails closed to onion/RNS by default. Only enable clearnet
# sync for local relay development or an explicitly public testnet.
# MESH_INFONET_ALLOW_CLEARNET_SYNC=false
# MESH_BOOTSTRAP_SEED_PEERS=http://gqpbunqbgtkcqilvclm3xrkt3zowjyl3s62kkktvojgvxzizamvbrqid.onion:8000
# Add comma-separated http://*.onion peers as more private seed/relay nodes come online.
# MESH_DEFAULT_SYNC_PEERS= # legacy alias; prefer MESH_BOOTSTRAP_SEED_PEERS
# MESH_RELAY_PEERS= # comma-separated operator-trusted sync/push peers (empty by default)
# MESH_PEER_PUSH_SECRET= # REQUIRED when relay/RNS peers are configured (min 16 chars, generate with: python -c "import secrets; print(secrets.token_urlsafe(32))")
# MESH_SYNC_INTERVAL_S=300
# MESH_SYNC_FAILURE_BACKOFF_S=60
#
# Enable Reticulum bridge for Infonet event gossip.
# MESH_RNS_ENABLED=false
# MESH_RNS_APP_NAME=shadowbroker
# MESH_RNS_ASPECT=infonet
# MESH_RNS_IDENTITY_PATH=
# MESH_RNS_PEERS= # comma-separated destination hashes
# MESH_RNS_DANDELION_HOPS=2
# MESH_RNS_DANDELION_DELAY_MS=400
# MESH_RNS_CHURN_INTERVAL_S=300
# MESH_RNS_MAX_PEERS=32
# MESH_RNS_MAX_PAYLOAD=8192
# MESH_RNS_PEER_BUCKET_PREFIX=4
# MESH_RNS_MAX_PEERS_PER_BUCKET=4
# MESH_RNS_PEER_FAIL_THRESHOLD=3
# MESH_RNS_PEER_COOLDOWN_S=300
# MESH_RNS_SHARD_ENABLED=false
# MESH_RNS_SHARD_DATA_SHARDS=3
# MESH_RNS_SHARD_PARITY_SHARDS=1
# MESH_RNS_SHARD_TTL_S=30
# MESH_RNS_FEC_CODEC=xor
# MESH_RNS_BATCH_MS=200
# MESH_RNS_COVER_INTERVAL_S=0
# MESH_RNS_COVER_SIZE=64
# MESH_RNS_IBF_WINDOW=256
# MESH_RNS_IBF_TABLE_SIZE=64
# MESH_RNS_IBF_MINHASH_SIZE=16
# MESH_RNS_IBF_MINHASH_THRESHOLD=0.25
# MESH_RNS_IBF_WINDOW_JITTER=32
# MESH_RNS_IBF_INTERVAL_S=120
# MESH_RNS_IBF_SYNC_PEERS=3
# MESH_RNS_IBF_QUORUM_TIMEOUT_S=6
# MESH_RNS_IBF_MAX_REQUEST_IDS=64
# MESH_RNS_IBF_MAX_EVENTS=64
# MESH_RNS_SESSION_ROTATE_S=0
# MESH_RNS_IBF_FAIL_THRESHOLD=3
# MESH_RNS_IBF_COOLDOWN_S=120
# MESH_VERIFY_INTERVAL_S=600
# MESH_VERIFY_SIGNATURES=false
# ── Secure Storage (non-Windows) ───────────────────────────────
# Required on Linux/Docker to protect Wormhole key material at rest.
# Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"
# Also supports Docker secrets via MESH_SECURE_STORAGE_SECRET_FILE.
# MESH_SECURE_STORAGE_SECRET=
#
# To rotate the storage secret, stop the backend and run:
# 1. Dry-run first (validates without writing):
# MESH_OLD_STORAGE_SECRET=<current> MESH_NEW_STORAGE_SECRET=<new> \
# python -m scripts.rotate_secure_storage_secret --dry-run
# 2. Rotate (creates .bak backups, then rewraps envelopes):
# MESH_OLD_STORAGE_SECRET=<current> MESH_NEW_STORAGE_SECRET=<new> \
# python -m scripts.rotate_secure_storage_secret
# 3. Update MESH_SECURE_STORAGE_SECRET to the new value and restart.
#
# If rotation is interrupted, .bak files preserve the old envelopes.
# To repair corrupted secure-json payloads (not key envelopes), use:
# python -m scripts.repair_wormhole_secure_storage
# ── Mesh DM Relay ──────────────────────────────────────────────
# MESH_DM_TOKEN_PEPPER=change-me
# Keep DM relay metadata retention explicit and bounded.
# MESH_DM_KEY_TTL_DAYS=30
# MESH_DM_PREKEY_LOOKUP_ALIAS_TTL_DAYS=14
# MESH_DM_WITNESS_TTL_DAYS=14
# MESH_DM_BINDING_TTL_DAYS=3
# Optional operational bridge for externally sourced root witnesses / transparency.
# Relative paths resolve from the backend directory.
# MESH_DM_ROOT_EXTERNAL_WITNESS_IMPORT_PATH=data/root_witness_import.json
# Local single-host dev example after bootstrapping an external witness locally:
# MESH_DM_ROOT_EXTERNAL_WITNESS_IMPORT_PATH=../ops/root_witness_receipt_import.json
# Optional URI bridge for externally retrieved root witness packages.
# MESH_DM_ROOT_EXTERNAL_WITNESS_IMPORT_URI=file:///absolute/path/root_witness_import.json
# Maximum acceptable age for external witness packages before strong DM trust fails closed.
# MESH_DM_ROOT_EXTERNAL_WITNESS_MAX_AGE_S=3600
# Warning threshold for external witness packages before fail-closed max age.
# MESH_DM_ROOT_EXTERNAL_WITNESS_WARN_AGE_S=2700
# MESH_DM_ROOT_TRANSPARENCY_LEDGER_EXPORT_PATH=data/root_transparency_ledger.json
# Local single-host dev example after publishing the transparency ledger locally:
# MESH_DM_ROOT_TRANSPARENCY_LEDGER_EXPORT_PATH=../ops/root_transparency_ledger.json
# Optional URI used to read back and verify a published transparency ledger.
# MESH_DM_ROOT_TRANSPARENCY_LEDGER_READBACK_URI=file:///absolute/path/root_transparency_ledger.json
# Local single-host dev readback example:
# MESH_DM_ROOT_TRANSPARENCY_LEDGER_READBACK_URI=../ops/root_transparency_ledger.json
# Maximum acceptable age for external transparency ledgers before strong DM trust fails closed.
# MESH_DM_ROOT_TRANSPARENCY_LEDGER_MAX_AGE_S=3600
# Warning threshold for external transparency ledgers before fail-closed max age.
# MESH_DM_ROOT_TRANSPARENCY_LEDGER_WARN_AGE_S=2700
# ── Self Update ────────────────────────────────────────────────
# MESH_UPDATE_SHA256=
# ── Wormhole (Local Agent) ─────────────────────────────────────
# WORMHOLE_HOST=127.0.0.1
# WORMHOLE_PORT=8787
# WORMHOLE_RELOAD=false
# WORMHOLE_TRANSPORT=direct
# WORMHOLE_SOCKS_PROXY=127.0.0.1:9050
# WORMHOLE_SOCKS_DNS=true
# Optional override for the loaded Rust privacy-core shared library. Leave
# unset for the default repo search order. When you override this, verify the
# authenticated wormhole status surfaces show the expected version, absolute
# library path, and SHA-256 for the loaded artifact before making stronger
# privacy claims about the deployment.
# PRIVACY_CORE_LIB=
# Minimum privacy-core version accepted when hidden/private carriers are
# enabled. Private-lane startup fails closed if the loaded artifact is
# missing, reports no parseable version, or falls below this minimum.
# PRIVACY_CORE_MIN_VERSION=0.1.0
# Comma-separated SHA-256 allowlist for the exact privacy-core artifact(s)
# your deployment is allowed to load. Required for Arti/RNS private-lane
# startup. Generate with:
# PowerShell: Get-FileHash .\privacy-core\target\release\privacy_core.dll -Algorithm SHA256
# macOS/Linux: sha256sum ./privacy-core/target/release/libprivacy_core.so
# PRIVACY_CORE_ALLOWED_SHA256=
# Optional structured release attestation artifact for the Sprint 8 release gate.
# Relative paths resolve from the backend directory. When set explicitly, a
# missing or unreadable file fails the DM relay security-suite criterion closed.
# CI/release tooling can generate this automatically via:
# uv run python scripts/release_helper.py write-attestation ...
# MESH_RELEASE_ATTESTATION_PATH=data/release_attestation.json
# Operator-only Sprint 8 release attestation. Set this only when the DM relay
# security suite has been run and passed for the current release candidate.
# File-based release attestation takes precedence when present.
# MESH_RELEASE_DM_RELAY_SECURITY_SUITE_GREEN=false
# ── OpenClaw Agent ─────────────────────────────────────────────
# HMAC shared secret for remote OpenClaw agent authentication.
# Auto-generated via the Connect OpenClaw modal — do not set manually.
# OPENCLAW_HMAC_SECRET=
# Access tier: "restricted" (read-only) or "full" (read+write+inject)
# OPENCLAW_ACCESS_TIER=restricted
# ── SAR (Synthetic Aperture Radar) Layer ───────────────────────
# Mode A — Free catalog metadata from Alaska Satellite Facility (ASF Search).
# No account, no downloads. Default-on. Set to false to disable entirely.
# MESH_SAR_CATALOG_ENABLED=true
#
# Mode B — Free pre-processed ground-change anomalies (deformation, flood,
# damage assessments) from NASA OPERA, Copernicus EGMS, GFM, EMS, UNOSAT.
# Two-step opt-in: BOTH of the following must be set together.
# 1. MESH_SAR_PRODUCTS_FETCH=allow
# 2. MESH_SAR_PRODUCTS_FETCH_ACKNOWLEDGE=true
# Either flag alone keeps Mode B disabled. You can also enable this from
# the Settings → SAR panel inside the app.
# MESH_SAR_PRODUCTS_FETCH=block
# MESH_SAR_PRODUCTS_FETCH_ACKNOWLEDGE=false
#
# NASA Earthdata Login (free, ~1 minute signup) — required for OPERA products.
# Sign up: https://urs.earthdata.nasa.gov/users/new
# Generate token: https://urs.earthdata.nasa.gov/profile → "Generate Token"
# MESH_SAR_EARTHDATA_USER=
# MESH_SAR_EARTHDATA_TOKEN=
#
# Copernicus Data Space (free, ~1 minute signup) — required for EGMS / EMS.
# Sign up: https://dataspace.copernicus.eu/
# MESH_SAR_COPERNICUS_USER=
# MESH_SAR_COPERNICUS_TOKEN=
#
# Allow OpenClaw agents to read and act on the SAR layer (default true).
# MESH_SAR_OPENCLAW_ENABLED=true
#
# Require private-tier transport (Tor / RNS) before signing and broadcasting
# SAR anomalies to the mesh. Default true — disable only for testnet/local use.
# MESH_SAR_REQUIRE_PRIVATE_TIER=true