feat: Production Hardening: Wallet/Nonce Persistence, Twilio Security, and Dockerfile#113
Merged
Merged
Conversation
…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>
|
@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! 🚀 |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production Hardening: Wallet/Nonce Persistence, Twilio Security, and Dockerfile
Summary
walletStore)Changes per issue
closes #102 — Custodial wallet DB persistence
Problem:
src/stellar/wallet.tsstored encrypted secrets in a module-levelMap. Restarts wiped all wallets; horizontal scaling was impossible.Fix:
CustodialWalletPrisma model (userIdunique,publicKeyunique,encryptedSecret/iv/authTagcolumns)prisma/migrations/20260529000001_add_custodial_wallets/createCustodialWallet,getWalletByUserId,getKeypairForUser, andlistWalletsto read/writedb.custodialWalletKey rotation / backup: rotate
WALLET_ENCRYPTION_KEYby re-encrypting allcustodial_walletsrows 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.tsstored challenge nonces in an in-memoryMap. Rolling deploys and multiple app instances broke/api/auth/verify.Fix:
AuthNoncePrisma model (stellarPubKeyunique,expiresAtindexed for cleanup)prisma/migrations/20260529000002_add_auth_nonces/StellarVerificationclass is now stateless (no nonce map)challenge()upserts nonces viadb.authNonce; expired rows are pruned lazilyverify()reads/deletes nonces from DB — expiry check and replay prevention are preserveddb.authNonceinstead of the in-memory store; added cross-instance testcloses #112 — Twilio webhook signature validation
Problem:
src/routes/whatsapp.tsskippedvalidateRequestwhenNODE_ENV !== 'production', allowing spoofed requests on staging/dev.Fix:
TWILIO_AUTH_TOKENis set, regardless ofNODE_ENV403immediately ifTWILIO_AUTH_TOKENis absent — no silent skipTWILIO_AUTH_TOKENto the required-vars list insrc/config/env.tssrc/index.tsinitServices()so the server refuses to start without the tokencloses #104 — Production Dockerfile and deployment runbook
Added:
Dockerfile— multi-stage build:node:20-alpinebuilder (npm ci→prisma generate→tsc→ prod-only deps), then slim runtime image running as non-rootappuser; CMD runsprisma migrate deploy && node dist/index.js.dockerignore— excludesnode_modules,dist,.env*, logs, tests, docsdocs/PRODUCTION_DEPLOYMENT.md— new sections covering:NODE_ENV=production,CORS_ORIGINS,WALLET_ENCRYPTION_KEY,ADMIN_API_TOKEN,TWILIO_AUTH_TOKEN, etc.)prisma migrate deployas pre-start step; Kubernetes initContainer patternGET /health/liveliveness,GET /health/readyreadiness 200/503) with Kubernetes and ALB examplesWALLET_ENCRYPTION_KEY,JWT_SEED, and auth noncesTest plan
npx jest tests/unit/stellar/wallet.test.ts— 9 tests passnpx jest src/controllers/__tests__/auth.test.ts— all auth tests passnpx jest tests/unit/whatsapp/webhook.test.ts— 5 tests passdocker build -t neurowealth-backend .completes without errorGET /health/livereturns 200 after startupGET /health/readyreturns 503 before DB connects, 200 after all services readyTWILIO_AUTH_TOKENset fails fast with a clear error message/api/whatsapp/webhookwith a bad signature returns 403 in allNODE_ENVvalues🤖 Generated with Claude Code