Skip to content

feat: Production Hardening: Wallet/Nonce Persistence, Twilio Security, and Dockerfile#113

Merged
robertocarlous merged 10 commits into
Neurowealth:mainfrom
Samuel1505:main
May 30, 2026
Merged

feat: Production Hardening: Wallet/Nonce Persistence, Twilio Security, and Dockerfile#113
robertocarlous merged 10 commits into
Neurowealth:mainfrom
Samuel1505:main

Conversation

@Samuel1505
Copy link
Copy Markdown
Contributor

Production Hardening: Wallet/Nonce Persistence, Twilio Security, and Dockerfile

Summary

Changes per issue

closes #102 — Custodial wallet DB persistence

Problem: src/stellar/wallet.ts stored encrypted secrets in a module-level Map. Restarts wiped all wallets; horizontal scaling was impossible.

Fix:

  • Added CustodialWallet Prisma model (userId unique, publicKey unique, encryptedSecret/iv/authTag columns)
  • New migration: prisma/migrations/20260529000001_add_custodial_wallets/
  • Rewrote createCustodialWallet, getWalletByUserId, getKeypairForUser, and listWallets to read/write db.custodialWallet
  • 9 unit tests covering create, duplicate prevention, read, keypair decrypt round-trip, and simulated restart persistence

Key rotation / backup: rotate WALLET_ENCRYPTION_KEY by re-encrypting all custodial_wallets rows with the new key before swapping the env var. The database is the authoritative backup — losing the key makes wallets unrecoverable.


closes #103 — Auth nonces in Postgres

Problem: stellar-verification.ts stored challenge nonces in an in-memory Map. Rolling deploys and multiple app instances broke /api/auth/verify.

Fix:

  • Added AuthNonce Prisma model (stellarPubKey unique, expiresAt indexed for cleanup)
  • New migration: prisma/migrations/20260529000002_add_auth_nonces/
  • StellarVerification class is now stateless (no nonce map)
  • challenge() upserts nonces via db.authNonce; expired rows are pruned lazily
  • verify() reads/deletes nonces from DB — expiry check and replay prevention are preserved
  • Auth unit tests updated to mock db.authNonce instead of the in-memory store; added cross-instance test

closes #112 — Twilio webhook signature validation

Problem: src/routes/whatsapp.ts skipped validateRequest when NODE_ENV !== 'production', allowing spoofed requests on staging/dev.

Fix:

  • Signature validation now runs whenever TWILIO_AUTH_TOKEN is set, regardless of NODE_ENV
  • Returns 403 immediately if TWILIO_AUTH_TOKEN is absent — no silent skip
  • Added TWILIO_AUTH_TOKEN to the required-vars list in src/config/env.ts
  • Added fail-fast check in src/index.ts initServices() so the server refuses to start without the token
  • 5 unit tests: no-token 403, invalid-signature staging, invalid in development, valid happy path, env-agnostic enforcement

closes #104 — Production Dockerfile and deployment runbook

Added:

  • Dockerfile — multi-stage build: node:20-alpine builder (npm ciprisma generatetsc → prod-only deps), then slim runtime image running as non-root app user; CMD runs prisma migrate deploy && node dist/index.js
  • .dockerignore — excludes node_modules, dist, .env*, logs, tests, docs
  • docs/PRODUCTION_DEPLOYMENT.md — new sections covering:
    • Build/push commands
    • Minimum required env vars (NODE_ENV=production, CORS_ORIGINS, WALLET_ENCRYPTION_KEY, ADMIN_API_TOKEN, TWILIO_AUTH_TOKEN, etc.)
    • prisma migrate deploy as pre-start step; Kubernetes initContainer pattern
    • Health/readiness probe table (GET /health/live liveness, GET /health/ready readiness 200/503) with Kubernetes and ALB examples
    • Key rotation and backup expectations for WALLET_ENCRYPTION_KEY, JWT_SEED, and auth nonces

Test plan

  • npx jest tests/unit/stellar/wallet.test.ts — 9 tests pass
  • npx jest src/controllers/__tests__/auth.test.ts — all auth tests pass
  • npx jest tests/unit/whatsapp/webhook.test.ts — 5 tests pass
  • docker build -t neurowealth-backend . completes without error
  • GET /health/live returns 200 after startup
  • GET /health/ready returns 503 before DB connects, 200 after all services ready
  • Starting without TWILIO_AUTH_TOKEN set fails fast with a clear error message
  • POST /api/whatsapp/webhook with a bad signature returns 403 in all NODE_ENV values

🤖 Generated with Claude Code

Samuel1505 and others added 6 commits May 29, 2026 14:54
…n-memory walletStore

Replaces the in-memory Map in wallet.ts with Prisma DB operations so wallet
data survives restarts and works correctly under horizontal scaling.

- Add CustodialWallet model to schema.prisma (userId unique, publicKey unique,
  encryptedSecret/iv/authTag fields)
- Add migration 20260529000001_add_custodial_wallets
- Rewrite createCustodialWallet, getWalletByUserId, getKeypairForUser, and
  listWallets to use db.custodialWallet (encrypted secret stored in DB, never
  in memory beyond the duration of a single request)
- Add 9 unit tests covering create/read, keypair round-trip, and simulated
  restart persistence
- Update testDb helpers to include custodialWallet and authNonce mock/teardown

Key rotation / backup: rotate WALLET_ENCRYPTION_KEY by re-encrypting all rows
with the new key before deploying. The DB itself is the authoritative backup.

Closes Neurowealth#102

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… safety

Replaces the in-memory Map in stellar-verification.ts with Postgres-backed
AuthNonce rows, so nonces survive rolling deploys and work correctly when
multiple app instances handle requests.

- Add AuthNonce model to schema.prisma (stellarPubKey unique, TTL-able via
  expiresAt, indexed for efficient expiry sweeps)
- Add migration 20260529000002_add_auth_nonces
- Remove nonceStore Map and _nonceStoreForTests export from
  stellar-verification.ts; StellarVerification class is now stateless
- Rewrite challenge() to upsert nonces via db.authNonce and purge expired
  rows lazily on each challenge request
- Rewrite verify() to findUnique/delete nonces from DB, preserving expiry
  check and replay prevention semantics
- Update all auth tests to mock db.authNonce instead of the in-memory store;
  add cross-instance test that simulates a second instance finding a DB nonce

Closes Neurowealth#103

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…all environments

Previously signature checking was skipped when NODE_ENV != 'production',
allowing spoofed requests on staging/dev to slip through. Now validation
runs whenever TWILIO_AUTH_TOKEN is set, and the app fails fast at startup
if the token is absent — making misconfiguration impossible to miss.

- Remove production-only guard in whatsapp.ts; signature is now validated
  on every POST /api/whatsapp/webhook regardless of NODE_ENV
- Return 403 with a clear message when TWILIO_AUTH_TOKEN is not set
  (reject immediately, without calling validateRequest with an empty token)
- Add TWILIO_AUTH_TOKEN to the required-vars list in env.ts so startup
  validation catches it together with all other missing config
- Add fail-fast check in index.ts initServices() so the server refuses to
  start if TWILIO_AUTH_TOKEN is absent
- Add 5 unit tests covering: no-token 403, invalid-signature staging, invalid
  in development, valid happy path, and env-agnostic enforcement

Closes Neurowealth#112

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-stage Dockerfile builds a slim runtime image (node:20-alpine) and
documents the full production deployment path including migrate, probes,
and secret expectations.

Dockerfile:
- Stage 1 (builder): npm ci → prisma generate → tsc → npm ci --omit=dev
- Stage 2 (runtime): copy dist/, prod node_modules, prisma schema; runs as
  least-privilege non-root user (app:app)
- CMD runs prisma migrate deploy then node dist/index.js; Kubernetes users
  should split this into an initContainer

.dockerignore: excludes node_modules, dist, .env*, logs, tests, docs

docs/PRODUCTION_DEPLOYMENT.md additions:
- Build/push commands and minimum required env vars with NODE_ENV=production
- prisma migrate deploy documented as pre-start step and initContainer pattern
- Health/readiness probe table: GET /health/live (liveness) and
  GET /health/ready (readiness — 200/503) with Kubernetes and ALB examples
- Key rotation / backup expectations for WALLET_ENCRYPTION_KEY, JWT_SEED,
  and auth nonces

Closes Neurowealth#104

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 29, 2026

@Samuel1505 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Samuel1505 and others added 4 commits May 29, 2026 16:21
Adding TWILIO_AUTH_TOKEN to requiredVars in env.ts (issue Neurowealth#112) caused all
tests that transitively import src/config/env to throw at module load time
because the jest setup files didn't provide the new required variable.

- Add TWILIO_AUTH_TOKEN stub to tests/setupEnv.ts
- Add TWILIO_AUTH_TOKEN stub to tests/unit/config/jest.setup.env.ts
- Add TWILIO_AUTH_TOKEN to setValidEnv() in env.test.ts so all existing env
  validation tests continue to pass
- Add test asserting TWILIO_AUTH_TOKEN is required by validateAllRequiredEnvVars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@robertocarlous robertocarlous merged commit 70fc770 into Neurowealth:main May 30, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants