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
182 changes: 172 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,31 @@ GAME_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/game
PAY_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/pay

NIMBUS_PORT=4001
# Encrypts Nimbus's RS256 signing key at rest. REQUIRED, min 24 chars,
# placeholders refused — Nimbus does not boot without it and will not fall back
# to storing the key in the clear. Generate: openssl rand -hex 32
#
# That key is the estate's universal forging credential: every service checks
# `iss` plus `aud: cloudsforge` and nothing else, so a token minted with it is
# admin in Pay, the game, Crucible, ForgeMint and ForgeKeyvault alike. It used to
# be plaintext JSONB in a database every service shares one Postgres role on.
# Keep this string OUT of that database and out of any backup of it — the
# separation is the whole mechanism. Same rule, same reason, as
# KEYVAULT_MASTER_SECRET below.
NIMBUS_KEY_SECRET=
# Baked into every JWT and checked by every service. COMMENTED for the same
# reason as the lists below: the compose default is already this exact value,
# so setting it here only pins localhost onto a public deployment — see the
# PUBLIC DEPLOYMENT block at the bottom, where you set the real one.
# NIMBUS_ISSUER=http://localhost:4001
# The origin Nimbus builds a password-reset link from — the one URL it produces
# that is read somewhere other than the browser that asked for it, so it comes
# from here and never from the request's Host header (which let a stranger point
# a genuine reset email at a host they controlled). COMMENTED for the same
# reason as NIMBUS_ISSUER: compose already defaults it to
# https://account.${CLOUDSFORGE_APEX}, and pinning localhost here would put a
# localhost link in every reset email a public deployment sends.
# NIMBUS_PUBLIC_URL=http://localhost:4001
# Origins the account portal may hand a sign-in code back to. Explicit list of
# real app surfaces only — never a wildcard. Left COMMENTED on purpose: the
# compose default already covers localhost plus the CLOUDSFORGE_APEX subdomains,
Expand All @@ -31,6 +51,48 @@ ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=
ADMIN_HANDLE=admin

# ─── Outbound mail (Nimbus) — password reset links ───────────────────────────
#
# The only mail this estate sends. Set it and a user who clicks "forgot
# password" is emailed a single-use link that expires in 30 minutes. Leave
# SMTP_HOST unset and nothing is sent — the request is still recorded and an
# operator issues the link from the admin console (Users → Reset password),
# which is how it worked before this existed and is a supported way to run.
# Nimbus says which of the two it is, at info, on every boot.
#
# There is no provider code and no SDK: this is plain SMTP, so Brevo, Resend,
# SendGrid, Mailtrap, a Gmail app password and your own postfix are all the same
# four settings. Changing provider is an edit here and a restart.
#
# FREE CREDENTIALS IN TWO MINUTES (Brevo, 300 emails/day, no custom domain and
# no card):
# 1. Sign up at https://www.brevo.com and confirm the address.
# 2. SMTP & API → SMTP → "Generate a new SMTP key".
# 3. SMTP_HOST=smtp-relay.brevo.com, SMTP_PORT=587,
# SMTP_USER=<the login shown on that page, e.g. 8a1b2c001@smtp-brevo.com>,
# SMTP_PASS=<the generated key, NOT your account password>.
# 4. SMTP_FROM must be an address Brevo has verified — the one you signed up
# with works immediately under Senders & IP. A custom domain is optional.
# Any other provider works with the same four variables; only the host and the
# credentials change.
#
# Unset = no mail. Everything else is only read when this is set.
SMTP_HOST=
# 587 is STARTTLS and is what every provider above documents first. 465 is
# implicit TLS and additionally needs SMTP_SECURE=true; the two are NOT
# interchangeable, and 465 with secure unset hangs until the connect timeout.
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
# Treat as a secret: it can send mail as you, and nothing rate-limits that but
# the provider. Never your account password — providers issue a separate key.
SMTP_PASS=
# The From header, as a full RFC 5322 address. Must be an address the provider
# has authorised for you, or it accepts the connection and rejects the message.
SMTP_FROM=CloudsForge <no-reply@example.com>
# Optional. Where a reply goes, if you would rather it was not the From address.
SMTP_REPLY_TO=

GAME_PORT=4002
NIMBUS_JWKS_URL=http://localhost:4001/.well-known/jwks.json

Expand Down Expand Up @@ -75,18 +137,45 @@ PAY_CONVERSION_SPREAD_BPS=200
# interact badly:
# PAY_WITHDRAWALS_ENABLED default TRUE — withdrawals are accepted out of the box
# PAY_SWEEP_ENABLED default false — the sweeper that funds the treasury
# cannot run until ForgeKeyvault grows a signable
# 'treasury' purpose (it refuses to sign for the
# 'deposit' addresses customers pay into). See
# repos/forge-pay/services/pay/src/treasury.ts
# opens no new sweeps. This is a DECISION, not a
# limitation: keyvault has had a signable `sweep`
# shape since it grew one, and its destination is a
# treasury an operator pinned inside the vault
# (forge-keyvault/src/signing.ts EvmShape,
# routes/vault.ts SWEEPABLE_FAMILIES).
#
# So on a fresh deployment a withdrawal is ACCEPTED, sits `pending` while pay
# logs "the treasury cannot cover it" every 20s, and is then automatically
# REFUNDED once PAY_WITHDRAWAL_STUCK_MINUTES passes (default 60) — safe to do,
# because nothing was ever signed. Not limbo, but an hour of a user's time spent
# on an answer we could have given immediately. Until the sweeper lands, either
# fund the treasury by hand or set PAY_WITHDRAWALS_ENABLED=false so the refusal
# is instant and truthful.
# on an answer we could have given immediately.
#
# Turning sweeping on takes three things, and only the last two are env vars:
#
# 1. Pin a treasury, per chain and network, through ForgeKeyvault's Nimbus
# admin API — POST /admin/treasuries/:chain/:network/mint to create the
# address, then PUT /admin/treasuries/:chain/:network to pin it, in that
# order. Nothing sweeps to an unpinned candidate. This is an admin
# credential and a deliberate act, not a value in this file.
# 2. PAY_SWEEP_ENABLED=true.
# 3. Optionally PAY_TREASURY_TARGET_<COIN> (e.g. PAY_TREASURY_TARGET_EMBER=250)
# to hold a float. It defaults to zero, which makes the sweeper purely
# demand-driven — it moves what queued withdrawals actually need, nothing
# more.
#
# Decide (3) on custody grounds rather than convenience. A withdrawal must be
# payable to any address a user names, so the treasury's destination is
# unconstrained by design: every coin in the treasury is inside the blast radius
# of the KEYVAULT_SERVICE_TOKEN, and every coin left in a deposit address is
# outside it. A float buys immediacy on the first withdrawal after a quiet
# period — otherwise a confirmation depth of latency, ~45s on XRP, about a
# quarter of an hour on EMBER — and pays for it in standing exposure.
#
# Sweeping is available for the EVM, Ember and XRP families only. Bitcoin and
# Solana deposit addresses are still refused before decryption, because neither
# has an output policy built yet — so on those two chains the treasury really
# must be funded by hand. The full argument is DEPOSIT-SWEEP.md and the
# authoritative commentary is repos/forge-pay/services/pay/src/env.ts.
# Hearth local testnet coin (EMBER) — the seed node's ETHEREUM JSON-RPC, not its
# REST port (compose: http://hearth-testnet-seed:8545). Pay speaks eth_* here.
# Pointed at 8645 it gets a 200 carrying the legacy getinfo shape, which every
Expand Down Expand Up @@ -114,6 +203,27 @@ KEYVAULT_URL=http://localhost:4005
# Shared secret for forge-mint -> keyvault server-to-server calls.
# REQUIRED, min 24 chars. Generate: openssl rand -hex 24
KEYVAULT_SERVICE_TOKEN=
# RPC endpoints for the chains ForgeMint deploys to. Each chain in
# SUPPORTED_CHAINS ships a working public default for both of its networks, so
# every one of these is optional — set one only to replace the public node for
# that exact chain and network, which is usually because the public node is
# rate-limiting you.
#
# The variable is per chain AND per network: RPC_<CHAINID>_<NETWORK>, chain ids
# ethereum | bsc | polygon | arbitrum | base | solana, network mainnet |
# testnet. There is deliberately no single EVM_RPC_URL — one URL cannot serve
# five different EVM chains, and a shared value would point a Polygon deploy at
# an Ethereum endpoint. (Two such variables did exist here until CF-43. They
# were never read: the fallback that consulted them only fired when a chain had
# no baked default, which none does.)
#
# These are usually keyed provider URLs with the credential in the path
# (Infura /v3/<key>, Alchemy /v2/<key>). forge-mint never logs one in full —
# `safeUrl` in services/forge-mint/src/obs.ts keeps the origin and the first
# path segment and drops the rest — but treat the value as a secret in .env.
# RPC_ETHEREUM_MAINNET=https://mainnet.infura.io/v3/<key>
# RPC_ETHEREUM_TESTNET=https://sepolia.infura.io/v3/<key>
# RPC_SOLANA_MAINNET=https://<name>.solana-mainnet.quiknode.pro/<key>/

# ForgeKeyvault — per-address key custody
KEYVAULT_PORT=4005
Expand All @@ -127,9 +237,6 @@ KEYVAULT_MASTER_SECRET=
KEYVAULT_DATA_DIR=./.keyvault-data
# Optional: mount /var/run/docker.sock to provision a container+volume per address
KEYVAULT_DOCKER_SOCKET=/var/run/docker.sock
# EVM/Solana testnet RPCs (used for balance checks + testnet deploys)
EVM_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
SOLANA_RPC_URL=https://api.devnet.solana.com

# Crucible — algorithmic trading (http://localhost:4006)
# Serves both its SPA and its API on one port, like ForgeMint.
Expand Down Expand Up @@ -162,6 +269,14 @@ LANTERN_TOKEN=
# Days of raw log lines to keep. Grouped issues are kept for 90 regardless,
# since they are small and are the thing you actually read.
LANTERN_RETENTION_DAYS=7
# Which socket peers may name the client with cf-connecting-ip or
# x-forwarded-for. /ingest/client is quota'd per client address, and behind the
# cloudflared tunnel every public request has the same socket peer — so without
# this the quota is one bucket of 120/min shared by the whole internet, and
# anyone can hold it empty. The default covers the connector (loopback, or the
# compose bridge) and trusts nothing that arrives from a public address.
# Keywords `loopback`, `private` and `all`, literal addresses, IPv4 CIDR.
# LANTERN_TRUSTED_PROXIES=loopback,private
# Raise to `debug` on any service to turn detail up without a code change.
# Applies to every service and to Lantern itself.
LOG_LEVEL=info
Expand Down Expand Up @@ -221,6 +336,44 @@ BEACON_PUBLIC_STATUS=false
# it simply stops being an outage that an external miner has not shown up.
BEACON_CHAIN_EXPECT_BLOCKS=true
#
# The EIP-155 chain id every Hearth node here must report over JSON-RPC, in hex
# from eth_chainId and in decimal from net_version.
#
# 7412 is hearth-testnet, 7411 is hearth mainnet, and this is the number that
# stops a transaction signed for one from being valid on the other. A node
# refuses to start on a network it does not recognise, so the drift worth
# monitoring is the one that passes its own validation: HEARTH_CHAIN_ID set
# explicitly to something valid but wrong. Nothing then misroutes and no
# balance reads incorrectly — the loss is silent. Every testnet transaction
# becomes replayable on mainnet, and ForgeKeyvault, which resolves the expected
# id independently, begins refusing withdrawals with 403 binding_mismatch from
# a service that cannot explain why.
#
# Change this only when you change what the nodes are. It is pinned rather than
# read back from the node on purpose: a monitor that adopts the value it is
# meant to be checking asserts nothing at all.
#
# It defaults to HEARTH_CHAIN_ID below, so there is ONE number to change.
BEACON_HEARTH_CHAIN_ID=7412
#
# The same id, for the browser wallet and the block explorer (`hearth-web`).
#
# It is separate from the node's own HEARTH_NETWORK because the bundle is a set
# of static files with no build step: nginx templates this into a
# `<meta name="hearth-chain-id">` at boot (repos/hearth/web/nginx.conf), and
# repos/hearth/web/assets/chain.js reads it once for the whole page. The wallet
# then binds every EIP-155 signature to it.
#
# Deliberately NOT taken from whatever eth_chainId the node reports: the wallet
# accepts a `?rpc=<url>` override, so a node that got to choose the chain id
# would get to choose what its visitor's signature is valid on. The reported id
# is compared against this one and the mismatch is shown; it is never adopted.
#
# BEACON_HEARTH_CHAIN_ID falls back to this, so setting it here moves the
# monitor and the wallet together — the drift this replaced was a wallet
# signing for 7411 while every node in the file ran 7412.
HEARTH_CHAIN_ID=7412
#
# One webhook, fired on incident open and close and on nothing else. Plain
# JSON with a `text` field, so Slack, Discord and a shell script all work.
# An alert per failed probe is an alert nobody reads.
Expand All @@ -243,6 +396,15 @@ BEACON_SELF_URL=
# internet can reach.
# NIMBUS_ISSUER=https://nimbus.cloudsforge.online
#
# NIMBUS_PUBLIC_URL follows the apex too (compose defaults it to
# https://account.${CLOUDSFORGE_APEX}), so set it only if the account portal is
# reached on some other hostname. It is the origin of every password-reset link
# this estate sends, so a wrong value sends users somewhere that does not answer
# — and it is deliberately not taken from the request, because that let anyone
# who knew an address have this deployment mail them a link to a host of their
# choosing.
# NIMBUS_PUBLIC_URL=https://account.cloudsforge.online
#
# Only set the lists below to REPLACE the derived defaults (they win outright —
# the apex-derived and localhost origins are then dropped). Both are explicit
# lists, never a wildcard: for the hand-back list any one compromised subdomain
Expand Down
Loading
Loading