diff --git a/docs/MAINNET.md b/docs/MAINNET.md index bf8c8fca4..79d82527b 100644 --- a/docs/MAINNET.md +++ b/docs/MAINNET.md @@ -8,10 +8,15 @@ therefore mostly configuration, one genuine code decision (which anchor), and a set of checks. It is written as a runbook: do the steps in order, and do not skip the verification at the end of each phase. -**Read this first:** the public-network guardrails in `apps/api/src/env.ts` -throw at boot rather than warn. A service that refuses to start is loud; one -that quietly settles into a sandbox anchor is not. If a guard fires, fix the -configuration — never relax the guard to get a green deploy. +**Read this first:** the public-network guardrails throw at boot rather than +warn. A service that refuses to start is loud; one that quietly settles into a +sandbox anchor is not. The guards are split across two files: the OFFRAMP and +anchor-URL checks fire in `apps/api/src/env.ts` at module load (lines +114–142), as does the USDC issuer check (line 189); the +`DEFAULT_SELLER_WALLET` (line 386), +`SERVER_SIGNING_SECRET` (line 489), and `JWT_SECRET` (line 512) checks fire in +`apps/api/src/services/container.ts` inside `createContainer()`. If a guard +fires, fix the configuration — never relax the guard to get a green deploy. --- @@ -30,7 +35,11 @@ configuration — never relax the guard to get a green deploy. | Missing `JWT_SECRET` | Every seller is logged out on each deploy. | | A blank or non-numeric numeric var | A blank value used to yield `0` — `TRUST_PROXY_HOPS=` silently collapsed every client into one rate-limit bucket. A typo yields `NaN`, and `setInterval(NaN)` is a tight loop against Horizon, not a slow poll. | -These are covered by `apps/api/test/env-mainnet-guards.test.ts`. +The OFFRAMP, USDC issuer, anchor-URL, and KYC key guards are covered by +`apps/api/test/env-mainnet-guards.test.ts`. The `DEFAULT_SELLER_WALLET`, +`SERVER_SIGNING_SECRET`, and `JWT_SECRET` guards live in +`apps/api/src/services/container.ts` and are not yet covered by a dedicated +test file. --- diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index d85af825c..7a72a2226 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -33,8 +33,9 @@ default. | `DATABASE_URL` · `DATABASE_AUTH_TOKEN` | always (prod) | No persistence; falls back to a local SQLite file inside the container, which is destroyed on every deploy | | `KYC_ENCRYPTION_KEY` | `OFFRAMP=testanchor` | **Process will not boot.** `env.ts` resolves it with `req()` at module load and throws `Missing required env var: KYC_ENCRYPTION_KEY` | | `WEBHOOK_SECRET_ENCRYPTION_KEY` | `NODE_ENV=production` | **Process will not boot** (`createContainer()` calls `assertKeyConfigured()`). Before that check existed, it fell back to a hardcoded public dev key and 500'd on the first webhook registration | -| `JWT_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | Auto-generated per boot, so every restart and deploy logs every seller out | -| `SERVER_SIGNING_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | Auto-generated per boot, so the `SIGNING_KEY` published in `stellar.toml` changes on every restart and any wallet that cached it breaks | +| `JWT_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | **Process will not boot on public network** (`resolveJwtSecret()` in `apps/api/src/services/container.ts:512` throws). On testnet: auto-generated per boot, so every restart and deploy logs every seller out | +| `SERVER_SIGNING_SECRET` | `STELLAR_NETWORK=public`; strongly advised on testnet | **Process will not boot on public network** (`resolveServerSigningKeypair()` in `apps/api/src/services/container.ts:489` throws). On testnet: auto-generated per boot, so the `SIGNING_KEY` published in `stellar.toml` changes on every restart and any wallet that cached it breaks | +| `DEFAULT_SELLER_WALLET` | `STELLAR_NETWORK=public` | **Process will not boot on public network** (`resolveSellerKeypairOrWallet()` in `apps/api/src/services/container.ts:386` throws). On testnet: auto-generates a throwaway keypair and prints it (funds land in a key nobody kept across a restart) | | `HOME_DOMAIN` | any real deployment | Falls back to `localhost:8787`. SEP-10 challenges are issued for localhost and `stellar.toml` advertises `WEB_AUTH_ENDPOINT="https://localhost:8787/auth"` — **wallet login cannot work at all** | | `CORS_ORIGINS` | always | The browser refuses the dashboard's cross-origin calls | | `DEFAULT_SELLER_SECRET` | `OFFRAMP=testanchor` with `DEFAULT_SELLER_WALLET` set | SEP-10 cannot sign the anchor's auth challenge, so every cash-out fails | @@ -50,7 +51,10 @@ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" `render.yaml` declares all of these; the `sync: false` entries must be filled in from the Render dashboard on first deploy. Adding a new `req()` call to `apps/api/src/env.ts` without adding the matching `render.yaml` entry is what -caused the 2026-07-31 outage — see `docs/FIXLOG.md` `BUG-4.11`. +caused the 2026-07-31 outage — see `docs/FIXLOG.md` `BUG-4.11`. The same +mistake is possible in `apps/api/src/services/container.ts`, where +`DEFAULT_SELLER_WALLET` (line 386), `SERVER_SIGNING_SECRET` (line 489), and +`JWT_SECRET` (line 512) are also enforced at boot on public network. ### Scaling past one instance