diff --git a/docs/CACHE_INVALIDATION_POLICY.md b/docs/CACHE_INVALIDATION_POLICY.md index 5d0326c..95388cf 100644 --- a/docs/CACHE_INVALIDATION_POLICY.md +++ b/docs/CACHE_INVALIDATION_POLICY.md @@ -56,6 +56,29 @@ cache writes, or should bypass the cache. Do not advertise this implementation a all stale reads. The number of deleted keys is fixed; payload validation/serialization and Redis memory usage scale with payload size. +## Cross-pod invalidation (#611) + +In multi-pod deployments, cache invalidation events are broadcast via Redis pub/sub: + +- **Channel**: `neurowealth:cache-invalidation` (configurable via `CACHE_INVALIDATION_CHANNEL`) +- **Event format**: `{ userId, correlationId, timestamp, originPodId }` +- **Behavior**: When a mutation invalidates a user's cache on pod A, the event is published to Redis; pod B receives it and invalidates the same keys locally +- **Origin filtering**: Pods ignore events they published themselves to avoid duplicate invalidation +- **Failure mode**: If Redis is unavailable, invalidation is pod-local only. Stale data on other pods expires via TTL or is refreshed on next read. + +This provides bounded consistency SLA: mutations are visible on other pods within the Redis pub/sub delivery latency (typically <100ms) plus processing time. Clients reading stale data will see corrections on their next request due to cache validation or TTL expiration. + +### Configuration + +```env +CACHE_INVALIDATION_CHANNEL=neurowealth:cache-invalidation +REDIS_URL=redis://... +``` + +### Startup and shutdown + +The cache invalidation subscriber is initialized during application startup (best-effort, never blocks startup) and gracefully shut down during termination. The subscriber uses a dedicated Redis connection (via `createRedisSubscriber`) to avoid interfering with the shared cache client. + ## Verification ```bash diff --git a/docs/DATA_RETENTION_POLICY.md b/docs/DATA_RETENTION_POLICY.md index 1a2741f..1e63cd7 100644 --- a/docs/DATA_RETENTION_POLICY.md +++ b/docs/DATA_RETENTION_POLICY.md @@ -154,6 +154,9 @@ RETENTION_PROTOCOL_RATES_DAYS=180 RETENTION_YIELD_SNAPSHOTS_DAYS=90 RETENTION_ATTRIBUTION_DAYS=365 +# Dry-run mode (logs counts without deleting) +DATA_RETENTION_DRY_RUN=false + # Archival Settings ARCHIVAL_ENABLED=true ARCHIVAL_STORAGE_PROVIDER=s3 @@ -166,6 +169,27 @@ RETENTION_INTERVAL_MS=86400000 # 24 hours ARCHIVAL_INTERVAL_MS=604800000 # 7 days ``` +## Dry-Run Mode + +The data retention job supports a dry-run mode for safe testing and validation: + +- **Enable**: Set `DATA_RETENTION_DRY_RUN=true` +- **Behavior**: Logs the number of rows that would be deleted per table without performing any deletions +- **Use cases**: + - Validate retention policies before deployment + - Estimate deletion impact without risk + - Debug retention job behavior in production-like environments + - Generate retention reports for compliance auditing + +Dry-run mode applies to all retention tables: +- `auth_nonce` (expired nonces) +- `processed_event` (events older than retention window) +- `dead_letter_event` (RESOLVED entries older than retention window) +- `agent_log` (logs older than retention window) +- `user_event` (events older than retention window and per-user cap overages) + +Logs include per-table counts (`wouldDelete`) and retention parameters (retention days, max per user for user events). + ## Monitoring and Alerts ### Metrics to Monitor diff --git a/docs/DOCUMENTATION_INDEX.md b/docs/DOCUMENTATION_INDEX.md index e385ee7..47ddf4e 100644 --- a/docs/DOCUMENTATION_INDEX.md +++ b/docs/DOCUMENTATION_INDEX.md @@ -1,360 +1,88 @@ -# Documentation Index - Issue #23: Vault Events Persistence +# Documentation Index ## Quick Navigation ### For Developers -- **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** - Start here for quick overview and usage -- **[CODE_STRUCTURE.md](CODE_STRUCTURE.md)** - Understand code organization and design -- **[IMPLEMENTATION_DETAILS.md](IMPLEMENTATION_DETAILS.md)** - Deep dive into implementation +- **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** - Quick overview and usage guide +- **[CODE_STRUCTURE.md](CODE_STRUCTURE.md)** - Code organization and design decisions +- **[IMPLEMENTATION_DETAILS.md](IMPLEMENTATION_DETAILS.md)** - Technical implementation details - **[API_REFERENCE.md](API_REFERENCE.md)** - Complete backend endpoint reference -- **[TAX_REPORT.md](TAX_REPORT.md)** - Tax reporting & FIFO cost-basis lot tracking (#284) -- **[STRATEGY_MARKETPLACE.md](STRATEGY_MARKETPLACE.md)** - Strategy marketplace / opt-in copy-trading: metric formula, eligibility gate, privacy & custody boundaries (#285) -- **[PORTFOLIO_OPTIMIZATION.md](PORTFOLIO_OPTIMIZATION.md)** - Portfolio optimization & allocation suggestions: objective, λ mapping, estimation method, advisory invariant, limitations (#322) -- **[PERFORMANCE_ATTRIBUTION.md](PERFORMANCE_ATTRIBUTION.md)** - Benchmark-relative Brinson attribution: allocation/selection effects, Cariño linking, benchmark definition, `vsBenchmark` on the marketplace (#320) -- **[OUTBOX.md](OUTBOX.md)** - Durable outbox & prioritized on-chain transaction queue: state machine, idempotency, retry/fee-bump policy, priority ordering, admin API (#325) -- **[WEBSOCKET_STREAMING.md](WEBSOCKET_STREAMING.md)** - Authenticated real-time WebSocket streaming: handshake auth, topics, seq/resume replay, gap handling, backpressure, sub-account scoping, multi-pod bridge (#316) -- **[REBALANCE_EXPOSURE_COST.md](REBALANCE_EXPOSURE_COST.md)** - Rebalance exposure caps (#346) and grounded cost model + payback gate (#347): resolution, tighten-only merge, residual routing, conservative-by-default decisions -- **[CORRELATION_DIVERSIFICATION.md](CORRELATION_DIVERSIFICATION.md)** - Portfolio APY correlation matrix + diversification score (#348): alignment, null-on-degenerate semantics, weighted score, caveat -- **[YIELD_COMPOSITION.md](YIELD_COMPOSITION.md)** - Yield base/incentive decomposition + effective APY (#349): haircut model, schema columns, flag-gated consumption, emissions risk modifier, yield-breakdown endpoint -- **[FEATURE_FLAGS.md](FEATURE_FLAGS.md)** - Feature flag configuration, deterministic staged rollouts, emergency rollback runbook, and evaluation observability (#494) -- **[MESSAGING_DELIVERY.md](MESSAGING_DELIVERY.md)** - Telegram & WhatsApp message delivery reliability, retry/fallback policy, metrics, and recovery admin API (#493) -- **[WEBHOOK_SECURITY.md](WEBHOOK_SECURITY.md)** - Inbound fiat webhook HMAC validation, freshness windows, replay receipts, and failure responses -- **[PRISMA_QUERY_PERFORMANCE.md](PRISMA_QUERY_PERFORMANCE.md)** - Hot webhook query plans, supporting indexes, and the query-index regression check -- **[NET_WORTH.md](NET_WORTH.md)** - Read-only external Stellar links, valuation/staleness semantics, and private scope -- **[CONSENT_AND_PRIVACY_POLICY.md](CONSENT_AND_PRIVACY_POLICY.md)** - User consent boundaries, high-risk workflow checks & auditable bypass policy (#510) -- **[RATE_LIMITING.md](RATE_LIMITING.md)** - Rate-limit tier catalogue & route→tier matrix: auth-type mapping, effective budgets, fail-open/fail-closed policy (#614) -- **[API_KEYS.md](API_KEYS.md)** - User & admin API key scope catalogue: descriptions, risk levels, enforcement status, and the endpoints each scope covers (#615) +- **[2FA.md](2FA.md)** - Two-factor authentication implementation +- **[AGENT_DECISIONS.md](AGENT_DECISIONS.md)** - AI agent decision-making framework +- **[AI_PROMPT_AND_TOOL_POLICY.md](AI_PROMPT_AND_TOOL_POLICY.md)** - AI prompt engineering and tool usage policies +- **[ALERTS.md](ALERTS.md)** - Alert system configuration and handling +- **[APPROVALS.md](APPROVALS.md)** - Approval workflow for sensitive operations +- **[CACHE_INVALIDATION_POLICY.md](CACHE_INVALIDATION_POLICY.md)** - Cache invalidation strategies and policies +- **[CONSENT_AND_PRIVACY_POLICY.md](CONSENT_AND_PRIVACY_POLICY.md)** - User consent boundaries and privacy controls +- **[CORRELATION_DIVERSIFICATION.md](CORRELATION_DIVERSIFICATION.md)** - Portfolio correlation matrix and diversification scoring +- **[DATA_RETENTION_POLICY.md](DATA_RETENTION_POLICY.md)** - Data retention, archival, and purge policies +- **[EVENT_LISTENER_ENHANCEMENTS.md](EVENT_LISTENER_ENHANCEMENTS.md)** - Stellar event listener improvements +- **[FEATURE_FLAGS.md](FEATURE_FLAGS.md)** - Feature flag configuration and rollout management +- **[FIAT_INPUT_CONSTRAINTS.md](FIAT_INPUT_CONSTRAINTS.md)** - Fiat deposit/withdrawal constraints +- **[HEALTH_DEPENDENCY_GRAPH.md](HEALTH_DEPENDENCY_GRAPH.md)** - Service health dependency mapping +- **[HEALTH_ENDPOINTS.md](HEALTH_ENDPOINTS.md)** - Health check endpoints and monitoring +- **[IDEMPOTENCY.md](IDEMPOTENCY.md)** - Idempotency patterns and implementation +- **[LENDING.md](LENDING.md)** - Lending protocol integration +- **[LIQUIDITY_FLOOR.md](LIQUIDITY_FLOOR.md)** - Liquidity floor protection mechanisms +- **[MARKET_VOLATILITY_CIRCUIT_BREAKER.md](MARKET_VOLATILITY_CIRCUIT_BREAKER.md)** - Circuit breaker for extreme volatility +- **[MESSAGING_DELIVERY.md](MESSAGING_DELIVERY.md)** - Telegram & WhatsApp message delivery reliability +- **[MULTI_CURRENCY.md](MULTI_CURRENCY.md)** - Multi-currency support architecture +- **[NET_WORTH.md](NET_WORTH.md)** - Net worth calculation and external Stellar links +- **[NON_CUSTODIAL_ARCHITECTURE.md](NON_CUSTODIAL_ARCHITECTURE.md)** - Non-custodial wallet architecture +- **[NOTIFICATIONS.md](NOTIFICATIONS.md)** - Notification system implementation +- **[NOTIFICATION_PREFERENCES.md](NOTIFICATION_PREFERENCES.md)** - User notification preferences management +- **[OBSERVABILITY.md](OBSERVABILITY.md)** - Monitoring, logging, and observability stack +- **[OPENAPI_DRIFT_REPORT.md](OPENAPI_DRIFT_REPORT.md)** - OpenAPI specification drift analysis +- **[OUTBOX.md](OUTBOX.md)** - Durable outbox pattern for on-chain transactions +- **[PERFORMANCE_ATTRIBUTION.md](PERFORMANCE_ATTRIBUTION.md)** - Benchmark-relative Brinson attribution +- **[PORTFOLIO_OPTIMIZATION.md](PORTFOLIO_OPTIMIZATION.md)** - Portfolio optimization and allocation suggestions +- **[PRISMA_QUERY_PERFORMANCE.md](PRISMA_QUERY_PERFORMANCE.md)** - Database query performance optimization +- **[PRIVACY_AND_SECURITY_ENHANCEMENTS.md](PRIVACY_AND_SECURITY_ENHANCEMENTS.md)** - Privacy and security improvements +- **[PROTECTION_FUND.md](PROTECTION_FUND.md)** - Protection fund mechanisms +- **[PROTOCOL_RISK_SCORING.md](PROTOCOL_RISK_SCORING.md)** - Protocol risk assessment and scoring +- **[QUEUE_HEALTH_METRICS.md](QUEUE_HEALTH_METRICS.md)** - Queue health monitoring and metrics +- **[RATE_LIMITING.md](RATE_LIMITING.md)** - Rate limiting tiers and enforcement +- **[REALTIME_STREAMING.md](REALTIME_STREAMING.md)** - Real-time data streaming architecture +- **[REBALANCE_EXPOSURE_COST.md](REBALANCE_EXPOSURE_COST.md)** - Rebalance exposure caps and cost modeling +- **[RECURRING_WITHDRAWALS.md](RECURRING_WITHDRAWALS.md)** - Recurring withdrawal implementation +- **[REFERRAL_PROGRAM.md](REFERRAL_PROGRAM.md)** - Referral program mechanics +- **[SESSIONS.md](SESSIONS.md)** - Session management and security +- **[SLO_GUIDANCE.md](SLO_GUIDANCE.md)** - Service level objectives and guidance +- **[STRATEGY_MARKETPLACE.md](STRATEGY_MARKETPLACE.md)** - Strategy marketplace and copy-trading +- **[STRESS_TESTING.md](STRESS_TESTING.md)** - Stress testing methodology +- **[SUB_ACCOUNT_BULK.md](SUB_ACCOUNT_BULK.md)** - Sub-account bulk operations +- **[SUPPORT_TICKETS.md](SUPPORT_TICKETS.md)** - Support ticket system +- **[TAX_REPORT.md](TAX_REPORT.md)** - Tax reporting and FIFO cost-basis tracking +- **[TREASURY.md](TREASURY.md)** - Treasury management +- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** - Common troubleshooting scenarios +- **[USER_WEBHOOKS.md](USER_WEBHOOKS.md)** - User-configurable webhooks +- **[WEBAUTHN.md](WEBAUTHN.md)** - WebAuthn/passkey authentication +- **[WEBHOOK_SECURITY.md](WEBHOOK_SECURITY.md)** - Inbound webhook security validation +- **[WEBSOCKET_STREAMING.md](WEBSOCKET_STREAMING.md)** - WebSocket streaming implementation +- **[YIELD_COMPOSITION.md](YIELD_COMPOSITION.md)** - Yield composition and APY calculation +- **[api-versioning.md](api-versioning.md)** - API versioning strategy +- **[job-observability.md](job-observability.md)** - Background job observability ### For DevOps/Deployment - **[DEPLOYMENT.md](DEPLOYMENT.md)** - Complete deployment guide for all environments -- **[DISASTER_RECOVERY_REHEARSAL.md](DISASTER_RECOVERY_REHEARSAL.md)** - Disaster recovery rehearsal checklist, backup scope, retention policy & restore validation (#511) -- **[JOB_TRIAGE_RUNBOOK.md](JOB_TRIAGE_RUNBOOK.md)** - Operational triage path for failed jobs, retries & DLQ volume buildup (#509) -- **[MIGRATIONS.md](MIGRATIONS.md)** - Schema change process (#470): change classification A–D, backup and downtime impact assessment, validation steps, rollback rehearsal, three rollback tiers, PR checklist -- **[RUNBOOK.md](RUNBOOK.md)** - Production incident runbook, incl. §6 database migration rollback -- **[DEPENDENCY_AUDIT.md](DEPENDENCY_AUDIT.md)** - Dependency inventory, security advisories, upgrade backlog and exceptions -- **[IMPLEMENTATION_CHECKLIST.md](IMPLEMENTATION_CHECKLIST.md)** - Verification checklist +- **[DISASTER_RECOVERY_REHEARSAL.md](DISASTER_RECOVERY_REHEARSAL.md)** - Disaster recovery rehearsal procedures +- **[JOB_TRIAGE_RUNBOOK.md](JOB_TRIAGE_RUNBOOK.md)** - Failed job triage and recovery +- **[MIGRATIONS.md](MIGRATIONS.md)** - Database migration process and guidelines +- **[RUNBOOK.md](RUNBOOK.md)** - Production incident response runbook +- **[DEPENDENCY_AUDIT.md](DEPENDENCY_AUDIT.md)** - Dependency security audit process +- **[IMPLEMENTATION_CHECKLIST.md](IMPLEMENTATION_CHECKLIST.md)** - Implementation verification checklist +- **[PRODUCTION_READINESS_CHECKLIST.md](PRODUCTION_READINESS_CHECKLIST.md)** - Production readiness verification +- **[RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md)** - Release process checklist +- **[SECRETS_ROTATION.md](SECRETS_ROTATION.md)** - Secrets rotation procedures +- **[DLQ_ALERTING_RUNBOOK.md](DLQ_ALERTING_RUNBOOK.md)** - Dead letter queue alerting runbook ### For Reference -- **[IMPLEMENTATION_SUMMARY.md](IMPLEMENTATION_SUMMARY.md)** - High-level overview +- **[IMPLEMENTATION_SUMMARY.md](IMPLEMENTATION_SUMMARY.md)** - High-level implementation overview - **[DOCUMENTATION_INDEX.md](DOCUMENTATION_INDEX.md)** - This file --- -## Document Descriptions - -### QUICK_REFERENCE.md - -**Purpose**: Quick lookup guide for developers -**Contents**: - -- What was implemented -- Key files and their purposes -- How it works (startup, polling, event processing) -- Database changes -- Usage examples -- Testing commands -- Troubleshooting tips - -**Read this if**: You need quick answers or are new to the codebase - ---- - -### CODE_STRUCTURE.md - -**Purpose**: Detailed code organization and design decisions -**Contents**: - -- File organization -- Core implementation functions -- Database schema details -- Data flow diagrams -- Test structure -- Design decisions -- Performance considerations -- Security considerations -- Monitoring and debugging -- Future extensibility - -**Read this if**: You need to understand the code architecture or modify it - ---- - -### IMPLEMENTATION_DETAILS.md - -**Purpose**: Comprehensive technical documentation -**Contents**: - -- Problem statement and solution overview -- Database schema changes -- Event persistence logic -- Idempotency mechanism -- Ledger cursor persistence -- Error handling -- Test coverage -- Database migration details -- Key features -- Usage examples -- Deployment notes -- Support and debugging - -**Read this if**: You need complete technical understanding or are troubleshooting - ---- - -### DEPLOYMENT.md - -**Purpose**: Complete deployment guide for all environments -**Contents**: - -- Local development (Docker Compose) -- Staging (Docker) -- Production (Kubernetes) -- Environment variables reference -- Health probes -- Secrets management -- Database migrations -- Rollback procedure -- Monitoring and observability -- Troubleshooting - -**Read this if**: You are deploying to any environment or need rollback procedures - ---- - -### IMPLEMENTATION_CHECKLIST.md - -**Purpose**: Verification checklist for implementation -**Contents**: - -- Requirements completed -- Acceptance criteria met -- Code quality checks -- Database schema verification -- Test coverage -- Files created/modified -- Key features implemented -- Performance considerations -- Security verification -- Deployment readiness -- Branch status -- Summary - -**Read this if**: You need to verify all requirements are met - ---- - -### IMPLEMENTATION_SUMMARY.md - -**Purpose**: High-level overview of implementation -**Contents**: - -- Overview -- Changes made (schema, migration, implementation, tests) -- Acceptance criteria met -- Database schema changes -- How it works -- Error handling -- Future improvements - -**Read this if**: You need a concise overview of what was done - ---- - -## Implementation Files - -### Core Implementation - -- **src/stellar/events.ts** - Event persistence implementation (350+ lines) - - Event parsing functions - - Event handlers (deposit, withdraw, rebalance) - - Deduplication logic - - Cursor management - - Event fetching and polling - -### Database - -- **prisma/schema.prisma** - Updated schema with new models - - EventCursor model - - ProcessedEvent model - -- **prisma/migrations/20260326152030_add_event_tracking/migration.sql** - Database migration - - Creates event_cursors table - - Creates processed_events table - - Adds indexes - -### Tests - -- **tests/unit/stellar/events.test.ts** - Unit tests (200+ lines) - - Event persistence tests - - Idempotency tests - - Ledger cursor tests - -- **tests/integration/stellar/events.test.ts** - Integration tests (250+ lines) - - End-to-end tests - - Multiple event tests - - Error handling tests - ---- - -## Key Concepts - -### Idempotency - -Events are processed exactly once, even if the listener restarts or events are replayed. - -**Implementation**: Unique constraint on (contractId, txHash, eventType, ledger) - -### Deduplication - -Prevents duplicate event processing by checking ProcessedEvent table before processing. - -**Implementation**: Query ProcessedEvent before handling, mark as processed after - -### Cursor Persistence - -Stores last processed ledger in database for recovery on restart. - -**Implementation**: EventCursor table with one record per contract - -### Event Handlers - -Separate handlers for each event type (deposit, withdraw, rebalance). - -**Implementation**: handleDepositEvent, handleWithdrawEvent, handleRebalanceEvent - ---- - -## Quick Commands - -### Deployment - -```bash -# Apply migration -npx prisma migrate deploy - -# Run tests -npm test -- --run - -# Build -npm run build -``` - -### Monitoring - -```bash -# Check cursor status -psql $DATABASE_URL -c "SELECT * FROM event_cursors;" - -# Check processed events -psql $DATABASE_URL -c "SELECT COUNT(*) FROM processed_events;" - -# Check recent transactions -psql $DATABASE_URL -c "SELECT * FROM transactions ORDER BY createdAt DESC LIMIT 10;" -``` - -### Troubleshooting - -```bash -# Check listener status -grep "Event Listener" logs/*.log - -# Check for errors -grep -i "error" logs/*.log - -# Check RPC connection -grep "RPC" logs/*.log -``` - ---- - -## Document Reading Order - -### For New Developers - -1. QUICK_REFERENCE.md - Get oriented -2. CODE_STRUCTURE.md - Understand architecture -3. IMPLEMENTATION_DETAILS.md - Deep dive -4. Review src/stellar/events.ts - Read the code - -### For DevOps - -1. DEPLOYMENT.md - Deployment steps -2. IMPLEMENTATION_CHECKLIST.md - Verification -3. QUICK_REFERENCE.md - Troubleshooting - -### For Project Managers - -1. IMPLEMENTATION_SUMMARY.md - Overview -2. IMPLEMENTATION_CHECKLIST.md - Verification - -### For Code Review - -1. CODE_STRUCTURE.md - Architecture -2. IMPLEMENTATION_DETAILS.md - Technical details -3. Review src/stellar/events.ts - Code review -4. Review tests - Test coverage - ---- - -## Status - -✅ **Implementation**: Complete -✅ **Testing**: Complete -✅ **Documentation**: Complete -✅ **Ready for Deployment**: Yes - ---- - -## Support - -For questions or issues: - -1. Check QUICK_REFERENCE.md for common questions -2. Review IMPLEMENTATION_DETAILS.md for technical details -3. Check DEPLOYMENT.md for deployment issues -4. Review logs for error messages -5. Contact development team - ---- - -## Version Information - -- **Branch**: feat/vault-events-persistence -- **Date**: March 26, 2026 -- **Status**: Ready for Production -- **All Requirements**: Met ✅ -- **All Tests**: Passing ✅ -- **Documentation**: Complete ✅ - ---- - -## File Statistics - -| Document | Lines | Purpose | -| --------------------------- | -------- | ----------------- | -| QUICK_REFERENCE.md | 150 | Quick lookup | -| CODE_STRUCTURE.md | 350 | Architecture | -| IMPLEMENTATION_DETAILS.md | 400 | Technical details | -| DEPLOYMENT.md | 450 | Deployment | -| IMPLEMENTATION_CHECKLIST.md | 200 | Verification | -| IMPLEMENTATION_SUMMARY.md | 100 | Overview | -| **Total Documentation** | **1650** | **Complete** | - ---- - -## Next Steps - -1. **Review**: Review all documentation -2. **Code Review**: Review implementation and tests -3. **Merge**: Merge to main branch -4. **Deploy**: Follow DEPLOYMENT.md -5. **Monitor**: Monitor event processing -6. **Verify**: Confirm all systems operational - ---- - -**Last Updated**: March 26, 2026 -**Status**: ✅ COMPLETE +**Last Updated**: October 2026 diff --git a/docs/WEBAUTHN.md b/docs/WEBAUTHN.md index 18aae9a..a6a1f4c 100644 --- a/docs/WEBAUTHN.md +++ b/docs/WEBAUTHN.md @@ -25,10 +25,26 @@ challenges are tied to the issuing user and session. Malformed requests and replayed, expired, cross-session or wrong-account challenges are rejected. SimpleWebAuthn verifies challenges, signatures, origins and RP IDs. User -verification is required. Attestation uses `none` for platform and synced -passkeys without identifying-device certificates; unsupported or invalid -attestations are rejected by the library. This policy does not claim a hardware -vendor allowlist or enterprise device trust. +verification is required. + +## Attestation Policy + +The system uses attestation conveyance preference `none` for all registration +ceremonies. This policy: + +- **Does not collect** identifying device certificates or manufacturer information +- **Supports** platform authenticators (e.g., Touch ID, Windows Hello) and synced passkeys +- **Accepts** both hardware security keys and software-based authenticators +- **Rejects** unsupported or malformed attestation statements via SimpleWebAuthn validation + +This choice prioritizes consumer UX and privacy over hardware provenance. The +policy does not claim a hardware vendor allowlist or enterprise device trust. +Authenticator integrity is enforced through: + +1. Single-use challenge replay protection +2. Strict origin and RP ID validation +3. Signature counter regression detection +4. AuthenticatorData cloned-authenticator checks (counter stalls/regressions trigger alerts) Positive signature counters must strictly increase. Stalls/regressions reject login and trigger security alerts, including anomalies thrown by the library. diff --git a/src/index.ts b/src/index.ts index 4ec0819..1564679 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,6 +77,10 @@ import { scheduleLinkedExternalWalletSync } from './jobs/linkedExternalWalletSyn import { startEventListener, stopEventListener } from './stellar/events' import { startEventBridge, stopEventBridge } from './events/bridge' import { attachWebSocketServer, closeWebSocketServer } from './ws/server' +import { + startCacheInvalidationSubscriber, + stopCacheInvalidationSubscriber, +} from './utils/user-cache-invalidation' import { validateStellarNetworkReady } from './config/readiness' import { API_ROUTES, EXTRA_MOUNTS } from './routes/registry' import { @@ -498,6 +502,7 @@ async function gracefulShutdown(signal: string): Promise { logger.info('[Shutdown] Draining WebSocket connections...') await closeWebSocketServer() await stopEventBridge() + await stopCacheInvalidationSubscriber() logger.info('[Shutdown] Closing HTTP server (no new requests accepted)') httpServer.close(async () => { @@ -565,6 +570,23 @@ async function initServices(): Promise { } logger.info('[Startup] Twilio auth token configured ✓') + // 0. Validate WebAuthn configuration in production + if (config.nodeEnv === 'production' || config.nodeEnv === 'staging') { + const { webAuthnConfig } = await import('./services/webauthn.service') + try { + webAuthnConfig() + logger.info('[Startup] WebAuthn configuration validated ✓') + } catch (error) { + const msg = + error instanceof Error ? error.message : String(error) + logger.error( + '[Startup] WebAuthn configuration validation failed — cannot continue', + { error: msg } + ) + throw new Error(`WebAuthn: ${msg}`) + } + } + // 0. Validate Stellar network configuration validateStellarNetworkReady() @@ -635,6 +657,19 @@ async function initServices(): Promise { } ) } + + // 6. Cache invalidation subscriber (#611) — best-effort, never blocks startup + try { + startCacheInvalidationSubscriber() + logger.info('[Startup] Cache invalidation subscriber initialized ✓') + } catch (error) { + logger.warn( + '[Startup] Cache invalidation subscriber failed — continuing with pod-local invalidation only', + { + error: error instanceof Error ? error.message : String(error), + } + ) + } } async function main(): Promise { diff --git a/src/jobs/dataRetention.ts b/src/jobs/dataRetention.ts index 8b79e4b..c0f16eb 100644 --- a/src/jobs/dataRetention.ts +++ b/src/jobs/dataRetention.ts @@ -62,11 +62,26 @@ async function anchorBeforeDelete( */ export async function cleanupAuthNonces(): Promise { const correlationId = generateCorrelationId() + const dryRun = process.env.DATA_RETENTION_DRY_RUN === 'true' return runWithCorrelationIdAsync(correlationId, async () => { const start = Date.now() const jobName = 'retention_auth_nonces' try { + const count = await db.authNonce.count({ + where: { expiresAt: { lt: new Date() } }, + }) + + if (dryRun) { + const durationMs = Date.now() - start + const duration = durationMs / 1000 + logBackgroundJob(jobName, 'dry_run', duration, correlationId, { + wouldDelete: count, + }) + recordBackgroundJob(jobName, 'dry_run', duration) + return + } + const result = await db.authNonce.deleteMany({ where: { expiresAt: { lt: new Date() } }, }) @@ -103,6 +118,7 @@ export async function cleanupAuthNonces(): Promise { */ export async function cleanupProcessedEvents(): Promise { const correlationId = generateCorrelationId() + const dryRun = process.env.DATA_RETENTION_DRY_RUN === 'true' return runWithCorrelationIdAsync(correlationId, async () => { const start = Date.now() const jobName = 'retention_processed_events' @@ -120,6 +136,18 @@ export async function cleanupProcessedEvents(): Promise { processedAt: true, }, }) + + if (dryRun) { + const durationMs = Date.now() - start + const duration = durationMs / 1000 + logBackgroundJob(jobName, 'dry_run', duration, correlationId, { + wouldDelete: rows.length, + retentionDays: config.retention.processedEventsDays, + }) + recordBackgroundJob(jobName, 'dry_run', duration) + return + } + if (rows.length > 0) { await anchorBeforeDelete('processed_event', rows) } @@ -162,12 +190,32 @@ export async function cleanupProcessedEvents(): Promise { */ export async function cleanupDeadLetterEvents(): Promise { const correlationId = generateCorrelationId() + const dryRun = process.env.DATA_RETENTION_DRY_RUN === 'true' return runWithCorrelationIdAsync(correlationId, async () => { const start = Date.now() const jobName = 'retention_dead_letter_events' try { const cutoff = cutoffDate(config.retention.deadLetterEventsDays) + const count = await db.deadLetterEvent.count({ + where: { + status: 'RESOLVED', + createdAt: { lt: cutoff }, + }, + }) + + if (dryRun) { + const durationMs = Date.now() - start + const duration = durationMs / 1000 + logBackgroundJob(jobName, 'dry_run', duration, correlationId, { + wouldDelete: count, + eventStatus: 'RESOLVED', + retentionDays: config.retention.deadLetterEventsDays, + }) + recordBackgroundJob(jobName, 'dry_run', duration) + return + } + const result = await db.deadLetterEvent.deleteMany({ where: { status: 'RESOLVED', @@ -211,6 +259,7 @@ export async function cleanupDeadLetterEvents(): Promise { */ export async function cleanupAgentLogs(): Promise { const correlationId = generateCorrelationId() + const dryRun = process.env.DATA_RETENTION_DRY_RUN === 'true' return runWithCorrelationIdAsync(correlationId, async () => { const start = Date.now() const jobName = 'retention_agent_logs' @@ -228,6 +277,18 @@ export async function cleanupAgentLogs(): Promise { positionId: true, }, }) + + if (dryRun) { + const durationMs = Date.now() - start + const duration = durationMs / 1000 + logBackgroundJob(jobName, 'dry_run', duration, correlationId, { + wouldDelete: rows.length, + retentionDays: config.retention.agentLogsDays, + }) + recordBackgroundJob(jobName, 'dry_run', duration) + return + } + if (rows.length > 0) { await anchorBeforeDelete('agent_log', rows) } @@ -281,12 +342,36 @@ export async function cleanupAgentLogs(): Promise { */ export async function cleanupUserEvents(): Promise { const correlationId = generateCorrelationId() + const dryRun = process.env.DATA_RETENTION_DRY_RUN === 'true' return runWithCorrelationIdAsync(correlationId, async () => { const start = Date.now() const jobName = 'retention_user_events' try { const cutoff = cutoffDate(config.retention.userEventsDays) + const expiredCount = await db.userEvent.count({ + where: { createdAt: { lt: cutoff } }, + }) + + if (dryRun) { + const trimmed = await trimUserEventStreams( + config.retention.userEventsMaxPerUser + ) + const durationMs = Date.now() - start + const duration = durationMs / 1000 + const wouldDelete = expiredCount + trimmed + + logBackgroundJob(jobName, 'dry_run', duration, correlationId, { + wouldDelete, + wouldExpireByAge: expiredCount, + wouldTrimByCap: trimmed, + retentionDays: config.retention.userEventsDays, + maxPerUser: config.retention.userEventsMaxPerUser, + }) + recordBackgroundJob(jobName, 'dry_run', duration) + return + } + const expired = await db.userEvent.deleteMany({ where: { createdAt: { lt: cutoff } }, }) diff --git a/src/utils/user-cache-invalidation.ts b/src/utils/user-cache-invalidation.ts index 2867b80..be8acd3 100644 --- a/src/utils/user-cache-invalidation.ts +++ b/src/utils/user-cache-invalidation.ts @@ -1,5 +1,7 @@ -import { cacheGet, cacheSet, cacheDel } from '../config/redis' +import { cacheGet, cacheSet, cacheDel, getRedisClient, createRedisSubscriber } from '../config/redis' import { logger } from './logger' +import { generateCorrelationId } from './correlation' +import type { Redis } from 'ioredis' /** Cache key prefixes for user-related state */ export const CACHE_PREFIXES = { @@ -8,6 +10,18 @@ export const CACHE_PREFIXES = { ASSISTANT_MEMORY: 'assistant:memory', } as const +const CACHE_INVALIDATION_CHANNEL = process.env.CACHE_INVALIDATION_CHANNEL || 'neurowealth:cache-invalidation' + +let subscriber: Redis | null = null +let subscribed = false + +export interface CacheInvalidationEvent { + userId: string + correlationId: string + timestamp: number + originPodId: string +} + export interface StaleCheckOptions { maxAgeMs?: number updatedAt?: Date | string | number @@ -90,9 +104,14 @@ export async function invalidateUserAssistantMemory( /** * Invalidate ALL cached state for a user. * Triggered on logout, session revocation, or user account changes. + * Publishes to Redis for cross-pod invalidation. */ export async function invalidateAllUserCaches(userId: string): Promise { if (!userId) return + + const correlationId = generateCorrelationId() + const podId = process.env.HOSTNAME || 'unknown' + await Promise.allSettled([ invalidatePortfolioCache(userId), invalidateUserProfileCache(userId), @@ -101,7 +120,27 @@ export async function invalidateAllUserCaches(userId: string): Promise { invalidateUserAssistantMemory(userId, 'api'), invalidateUserAssistantMemory(userId, 'web'), // Remove legacy keys too. ]) + logger.info('[cache] Cleared all cached state for user', { userId }) + + const redis = getRedisClient() + if (redis) { + try { + const event: CacheInvalidationEvent = { + userId, + correlationId, + timestamp: Date.now(), + originPodId: podId, + } + await redis.publish(CACHE_INVALIDATION_CHANNEL, JSON.stringify(event)) + logger.debug('[cache] Published cache invalidation event', { userId, podId }) + } catch (error) { + logger.warn('[cache] Failed to publish cache invalidation event', { + userId, + error: error instanceof Error ? error.message : String(error), + }) + } + } } /** @@ -179,3 +218,87 @@ export async function validateOrInvalidateCache( } return freshData } + +/** + * Start listening for cache invalidation events from other pods. + * Safe to call multiple times; no-op after first call. + */ +export function startCacheInvalidationSubscriber(): void { + if (subscribed) return + subscribed = true + + subscriber = createRedisSubscriber() + if (!subscriber) { + logger.warn( + '[cache] REDIS_URL not set — cross-pod cache invalidation disabled. ' + + 'Single-instance deployments are unaffected; horizontally scaled ones ' + + 'will only invalidate caches on the pod where the mutation occurred.' + ) + return + } + + subscriber.on('message', (channel, raw) => { + if (channel !== CACHE_INVALIDATION_CHANNEL) return + + try { + const event = JSON.parse(raw) as CacheInvalidationEvent + if ( + typeof event?.userId !== 'string' || + typeof event?.correlationId !== 'string' || + typeof event?.timestamp !== 'number' || + typeof event?.originPodId !== 'string' + ) { + logger.warn('[cache] Discarded invalid cache invalidation event') + return + } + + const podId = process.env.HOSTNAME || 'unknown' + if (event.originPodId === podId) { + return + } + + invalidateAllUserCaches(event.userId).catch((error) => { + logger.warn('[cache] Failed to process cross-pod invalidation', { + userId: event.userId, + originPodId: event.originPodId, + error: error instanceof Error ? error.message : String(error), + }) + }) + + logger.debug('[cache] Processed cross-pod cache invalidation', { + userId: event.userId, + originPodId: event.originPodId, + }) + } catch (error) { + logger.warn('[cache] Failed to parse cache invalidation event', { + error: error instanceof Error ? error.message : String(error), + }) + } + }) + + subscriber.subscribe(CACHE_INVALIDATION_CHANNEL).then(() => { + logger.info(`[cache] Subscribed to ${CACHE_INVALIDATION_CHANNEL}`) + }).catch((error) => { + logger.error('[cache] Failed to subscribe to cache invalidation channel', { + error: error instanceof Error ? error.message : String(error), + }) + }) +} + +/** + * Close the cache invalidation subscriber. Safe to call when never started. + */ +export async function stopCacheInvalidationSubscriber(): Promise { + subscribed = false + if (!subscriber) return + try { + await subscriber.unsubscribe(CACHE_INVALIDATION_CHANNEL) + await subscriber.quit() + } catch (error) { + logger.warn('[cache] Subscriber shutdown error', { + error: error instanceof Error ? error.message : String(error), + }) + } finally { + subscriber = null + } +}