diff --git a/monitoring/.env.example b/monitoring/.env.example new file mode 100644 index 0000000..3ccfb27 --- /dev/null +++ b/monitoring/.env.example @@ -0,0 +1,12 @@ +# ============================================================================= +# MONITORING STACK ENVIRONMENT VARIABLES +# ============================================================================= +# Copy this file to .env and update with your actual values +# Command: copy .env.example .env + +# ============================================================================= +# REDIS CONNECTION (External Redis Server) +# ============================================================================= +REDIS_HOST=160.187.166.17 +REDIS_PORT=6379 +REDIS_PASSWORD=Redis@123 diff --git a/monitoring/.gitignore b/monitoring/.gitignore new file mode 100644 index 0000000..583a2bf --- /dev/null +++ b/monitoring/.gitignore @@ -0,0 +1,12 @@ +# Data volumes (mounted from Docker containers) +prometheus_data/ +grafana_data/ +alertmanager_data/ +loki_data/ + +# Environment files with sensitive data +.env + +# Logs +*.log + diff --git a/monitoring/DASHBOARD_METRICS_REFERENCE.md b/monitoring/DASHBOARD_METRICS_REFERENCE.md new file mode 100644 index 0000000..2d4ae7c --- /dev/null +++ b/monitoring/DASHBOARD_METRICS_REFERENCE.md @@ -0,0 +1,167 @@ +# RE Workflow Dashboard - Metrics Reference + +## 📊 Complete KPI List with Data Sources + +### **Section 1: API Overview** + +| Panel Name | Metric Query | Data Source | What It Measures | +|------------|--------------|-------------|------------------| +| **Request Rate** | `sum(rate(http_requests_total{job="re-workflow-backend"}[5m]))` | Backend metrics | HTTP requests per second (all endpoints) | +| **Error Rate** | `sum(rate(http_request_errors_total{job="re-workflow-backend"}[5m])) / sum(rate(http_requests_total{job="re-workflow-backend"}[5m]))` | Backend metrics | Percentage of failed HTTP requests | +| **P95 Latency** | `histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="re-workflow-backend"}[5m])) by (le))` | Backend metrics | 95th percentile response time (seconds) | +| **API Status** | `up{job="re-workflow-backend"}` | Prometheus | Backend service up/down status (1=up, 0=down) | +| **Request Rate by Method** | `sum(rate(http_requests_total{job="re-workflow-backend"}[5m])) by (method)` | Backend metrics | Requests per method (GET, POST, etc.) | +| **Response Time Percentiles** | `histogram_quantile(0.50/0.95/0.99, ...)` | Backend metrics | Response time distribution (P50, P95, P99) | + +--- + +### **Section 2: Logs** + +| Panel Name | Metric Query | Data Source | What It Measures | +|------------|--------------|-------------|------------------| +| **Errors (Time Range)** | `count_over_time({job="re-workflow-backend", level="error"}[...])` | Loki logs | Total error log entries in selected time range | +| **Warnings (Time Range)** | `count_over_time({job="re-workflow-backend", level="warn"}[...])` | Loki logs | Total warning log entries in selected time range | +| **TAT Breaches (Time Range)** | Log filter for TAT breaches | Loki logs | TAT breach events logged | +| **Auth Failures (Time Range)** | Log filter for auth failures | Loki logs | Authentication failure events | +| **Recent Errors & Warnings** | `{job="re-workflow-backend"} \|= "error" or "warn"` | Loki logs | Live log stream of errors and warnings | + +--- + +### **Section 3: Node.js Runtime** (Process-Level Metrics) + +| Panel Name | Metric Query | Data Source | What It Measures | +|------------|--------------|-------------|------------------| +| **Node.js Process Memory (Heap)** | `process_resident_memory_bytes{job="re-workflow-backend"}`
`nodejs_heap_size_used_bytes{job="re-workflow-backend"}`
`nodejs_heap_size_total_bytes{job="re-workflow-backend"}` | Node.js metrics (prom-client) | Node.js process memory usage:
- RSS (Resident Set Size)
- Heap Used
- Heap Total | +| **Node.js Event Loop Lag** | `nodejs_eventloop_lag_seconds{job="re-workflow-backend"}` | Node.js metrics | Event loop lag in seconds (high = performance issue) | +| **Node.js Active Handles & Requests** | `nodejs_active_handles_total{job="re-workflow-backend"}`
`nodejs_active_requests_total{job="re-workflow-backend"}` | Node.js metrics | Active file handles and pending async requests | +| **Node.js Process CPU Usage** | `rate(process_cpu_seconds_total{job="re-workflow-backend"}[5m])` | Node.js metrics | CPU usage by Node.js process only (0-1 = 0-100%) | + +**Key Point**: These metrics track the **Node.js application process** specifically, not the entire host system. + +--- + +### **Section 4: Redis & Queue Status** + +| Panel Name | Metric Query | Data Source | What It Measures | +|------------|--------------|-------------|------------------| +| **Redis Status** | `redis_up` | Redis Exporter | Redis server status (1=up, 0=down) | +| **Redis Connections** | `redis_connected_clients` | Redis Exporter | Number of active client connections to Redis | +| **Redis Memory** | `redis_memory_used_bytes` | Redis Exporter | Memory used by Redis (bytes) | +| **TAT Queue Waiting** | `queue_jobs_waiting{queue_name="tatQueue"}` | Backend queue metrics | Jobs waiting in TAT notification queue | +| **Pause/Resume Queue Waiting** | `queue_jobs_waiting{queue_name="pauseResumeQueue"}` | Backend queue metrics | Jobs waiting in pause/resume queue | +| **TAT Queue Failed** | `queue_jobs_failed{queue_name="tatQueue"}` | Backend queue metrics | Failed TAT notification jobs (should be 0) | +| **Pause/Resume Queue Failed** | `queue_jobs_failed{queue_name="pauseResumeQueue"}` | Backend queue metrics | Failed pause/resume jobs (should be 0) | +| **All Queues - Job Status** | `queue_jobs_waiting`
`queue_jobs_active`
`queue_jobs_delayed` | Backend queue metrics | Timeline of job status across all queues (stacked) | +| **Redis Commands Rate** | `rate(redis_commands_processed_total[1m])` | Redis Exporter | Redis commands executed per second | + +**Key Point**: Queue metrics are collected by the backend every 15 seconds via BullMQ queue API. + +--- + +### **Section 5: System Resources (Host)** (Host-Level Metrics) + +| Panel Name | Metric Query | Data Source | What It Measures | +|------------|--------------|-------------|------------------| +| **Host CPU Usage (All Cores)** | `100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)` | Node Exporter | Total CPU usage across all cores on host machine (%) | +| **Host Memory Usage (RAM)** | `(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100` | Node Exporter | RAM usage on host machine (%) | +| **Host Disk Usage (/root)** | `100 - ((node_filesystem_avail_bytes{mountpoint="/",fstype!="tmpfs"} / node_filesystem_size_bytes{mountpoint="/",fstype!="tmpfs"}) * 100)` | Node Exporter | Disk usage of root filesystem (%) | +| **Disk Space Left** | `node_filesystem_avail_bytes{mountpoint="/",fstype!="tmpfs"}` | Node Exporter | Available disk space in gigabytes | + +**Key Point**: These metrics track the **entire host system**, not just the Node.js process. + +--- + +## 🔍 Data Source Summary + +| Exporter/Service | Port | Metrics Provided | Collection Interval | +|------------------|------|------------------|---------------------| +| **RE Workflow Backend** | 5000 | HTTP metrics, custom business metrics, Node.js runtime | 10s (Prometheus scrape) | +| **Node Exporter** | 9100 | Host system metrics (CPU, memory, disk, network) | 15s (Prometheus scrape) | +| **Redis Exporter** | 9121 | Redis server metrics (connections, memory, commands) | 15s (Prometheus scrape) | +| **Queue Metrics** | 5000 | BullMQ queue job counts (via backend) | 15s (internal collection) | +| **Loki** | 3100 | Application logs | Real-time streaming | + +--- + +## 🎯 Renamed Panels for Clarity + +### Before → After + +**Node.js Runtime Section:** +- ❌ "Memory Usage" → ✅ "Node.js Process Memory (Heap)" +- ❌ "CPU Usage" → ✅ "Node.js Process CPU Usage" +- ❌ "Event Loop Lag" → ✅ "Node.js Event Loop Lag" +- ❌ "Active Handles & Requests" → ✅ "Node.js Active Handles & Requests" + +**System Resources Section:** +- ❌ "System CPU Usage" → ✅ "Host CPU Usage (All Cores)" +- ❌ "System Memory Usage" → ✅ "Host Memory Usage (RAM)" +- ❌ "System Disk Usage" → ✅ "Host Disk Usage (/root)" + +--- + +## 📈 Understanding the Difference + +### **Process vs Host Metrics** + +| Aspect | Node.js Process Metrics | Host System Metrics | +|--------|------------------------|---------------------| +| **Scope** | Single Node.js application | Entire server/container | +| **CPU** | CPU used by Node.js only | CPU used by all processes | +| **Memory** | Node.js heap memory | Total RAM on machine | +| **Purpose** | Application performance | Infrastructure health | +| **Example Use** | Detect memory leaks in app | Ensure server has capacity | + +**Example Scenario:** +- **Node.js Process CPU**: 15% → Your app is using 15% of one CPU core +- **Host CPU Usage**: 75% → The entire server is at 75% CPU (all processes combined) + +--- + +## 🚨 Alert Thresholds (Recommended) + +| Metric | Warning | Critical | Action | +|--------|---------|----------|--------| +| **Node.js Process Memory** | 80% of heap | 90% of heap | Investigate memory leaks | +| **Host Memory Usage** | 70% | 85% | Scale up or optimize | +| **Host CPU Usage** | 60% | 80% | Scale horizontally | +| **Redis Memory** | 500MB | 1GB | Review Redis usage | +| **Queue Jobs Waiting** | >10 | >50 | Check worker health | +| **Queue Jobs Failed** | >0 | >5 | Immediate investigation | +| **Event Loop Lag** | >100ms | >500ms | Performance optimization needed | + +--- + +## 🔧 Troubleshooting + +### No Data Showing? + +1. **Check Prometheus Targets**: http://localhost:9090/targets + - All targets should show "UP" status + +2. **Test Metric Availability**: + ```promql + up{job="re-workflow-backend"} + ``` + Should return `1` + +3. **Check Time Range**: Set to "Last 15 minutes" in Grafana + +4. **Verify Backend**: http://localhost:5000/metrics should show all metrics + +### Metrics Not Updating? + +1. **Backend**: Ensure backend is running with metrics collection enabled +2. **Prometheus**: Check scrape interval in prometheus.yml +3. **Queue Metrics**: Verify queue metrics collection started (check backend logs for "Queue Metrics ✅") + +--- + +## 📚 Additional Resources + +- **Prometheus Query Language**: https://prometheus.io/docs/prometheus/latest/querying/basics/ +- **Grafana Dashboard Guide**: https://grafana.com/docs/grafana/latest/dashboards/ +- **Node Exporter Metrics**: https://github.com/prometheus/node_exporter +- **Redis Exporter Metrics**: https://github.com/oliver006/redis_exporter +- **BullMQ Monitoring**: https://docs.bullmq.io/guide/metrics + diff --git a/monitoring/README.md b/monitoring/README.md index 51e24f4..b38ba0d 100644 --- a/monitoring/README.md +++ b/monitoring/README.md @@ -34,6 +34,18 @@ Complete monitoring solution with **Grafana**, **Prometheus**, **Loki**, and **P └────────────────────────────────────────────────────────────────────────┘ ``` +## 📦 What's Included + +The monitoring stack includes: +- **Redis** - In-memory data store for BullMQ job queues +- **Prometheus** - Metrics collection and storage +- **Grafana** - Visualization and dashboards +- **Loki** - Log aggregation +- **Promtail** - Log shipping agent +- **Node Exporter** - Host system metrics +- **Redis Exporter** - Redis server metrics +- **Alertmanager** - Alert routing and notifications + ## 🚀 Quick Start ### Prerequisites @@ -79,6 +91,37 @@ LOKI_HOST=http://localhost:3100 ## 📊 Available Dashboards +### **RE Workflow Overview** (Enhanced!) +**URL**: http://localhost:3001/d/re-workflow-overview + +**Sections:** + +1. **📊 API Overview** + - Request rate, error rate, response times + - HTTP status codes distribution + +2. **🔴 Redis & Queue Status** (NEW!) + - Redis connection status (Up/Down) + - Redis active connections + - Redis memory usage + - TAT Queue waiting/failed jobs + - Pause/Resume Queue waiting/failed jobs + - All queues job status timeline + - Redis commands rate + +3. **💻 System Resources** (NEW!) + - System CPU Usage (gauge) + - System Memory Usage (gauge) + - System Disk Usage (gauge) + - Disk Space Left (GB available) + +4. **🔄 Business Metrics** + - Workflow operations + - TAT breaches + - Node.js process metrics + +**Refresh Rate**: Auto-refresh every 30 seconds + ### 1. RE Workflow Overview Pre-configured dashboard with: - **API Metrics**: Request rate, error rate, latency percentiles diff --git a/monitoring/REDIS_MIGRATION.md b/monitoring/REDIS_MIGRATION.md new file mode 100644 index 0000000..0d02498 --- /dev/null +++ b/monitoring/REDIS_MIGRATION.md @@ -0,0 +1,176 @@ +# 🔄 Redis Migration Guide + +## Overview +Redis is now part of the monitoring stack and running locally in Docker. + +--- + +## ✅ What Was Done + +1. **Added Redis to monitoring stack** + - Image: `redis:7-alpine` + - Container name: `re_redis` + - Port: `6379` (mapped to host) + - Password: Uses `REDIS_PASSWORD` from environment (default: `Redis@123`) + - Data persistence: Volume `re_redis_data` + +2. **Updated Redis Exporter** + - Now connects to local Redis container + - Automatically starts after Redis is healthy + +--- + +## 🔧 Update Backend Configuration + +### Step 1: Update `.env` file + +Open `Re_Backend/.env` and change: + +```bash +# OLD (External Redis) +REDIS_HOST=160.187.166.17 + +# NEW (Local Docker Redis) +REDIS_HOST=localhost +``` + +Or if you want to use Docker network (recommended for production): +```bash +REDIS_HOST=re_redis # Use container name if backend is also in Docker +``` + +### Step 2: Restart Backend + +```powershell +# Stop backend (Ctrl+C in terminal) +# Then restart +npm run dev +``` + +--- + +## 📊 Verify Everything Works + +### 1. Check Redis is Running +```powershell +docker ps --filter "name=redis" +``` + +Should show: +``` +re_redis Up (healthy) +re_redis_exporter Up +``` + +### 2. Test Redis Connection +```powershell +# Test from host +redis-cli -h localhost -p 6379 -a Redis@123 ping +# Should return: PONG +``` + +### 3. Check Backend Logs +``` +[info]: [Redis] ✅ Connected successfully +[info]: [TAT Queue] ✅ Queue initialized +[info]: [Pause Resume Queue] ✅ Queue initialized +``` + +### 4. Refresh Grafana Dashboard +- Go to: http://localhost:3001/d/re-workflow-overview +- **Redis Status** should show "Up" (green) +- **Redis Connections** should show a number +- **Redis Memory** should show bytes used +- **Queue metrics** should work as before + +--- + +## 🎯 Benefits of Local Redis + +✅ **Simpler Setup** - Everything in one place +✅ **Faster Performance** - Local network, no external latency +✅ **Data Persistence** - Redis data saved in Docker volume +✅ **Easy Monitoring** - Redis Exporter automatically connected +✅ **Environment Isolation** - No conflicts with external Redis + +--- + +## 🔄 Docker Commands + +### Start all monitoring services (including Redis) +```powershell +cd Re_Backend/monitoring +docker-compose -f docker-compose.monitoring.yml up -d +``` + +### Stop all services +```powershell +docker-compose -f docker-compose.monitoring.yml down +``` + +### View Redis logs +```powershell +docker logs re_redis +``` + +### Redis CLI access +```powershell +docker exec -it re_redis redis-cli -a Redis@123 +``` + +### Check Redis data +```powershell +# Inside redis-cli +INFO +DBSIZE +KEYS * +``` + +--- + +## 🗄️ Data Persistence + +Redis data is persisted in Docker volume: +``` +Volume: re_redis_data +Location: Docker managed volume +``` + +To backup Redis data: +```powershell +docker exec re_redis redis-cli -a Redis@123 SAVE +docker cp re_redis:/data/dump.rdb ./redis-backup.rdb +``` + +--- + +## ⚠️ If You Want to Keep External Redis + +If you prefer to keep using the external Redis server, simply: + +1. Update `docker-compose.monitoring.yml`: +```yaml +redis-exporter: + environment: + - REDIS_ADDR=redis://160.187.166.17:6379 + command: + - '--redis.addr=160.187.166.17:6379' + - '--redis.password=Redis@123' +``` + +2. Don't change `.env` in backend + +3. Remove the `redis` service from docker-compose if you don't need it locally + +--- + +## 🎉 Summary + +Your setup now includes: +- ✅ Redis running locally in Docker +- ✅ Redis Exporter connected and working +- ✅ Backend ready to connect (just update `REDIS_HOST=localhost` in `.env`) +- ✅ All monitoring metrics available in Grafana + +**Next step**: Update `Re_Backend/.env` with `REDIS_HOST=localhost` and restart your backend! + diff --git a/monitoring/delete b/monitoring/delete new file mode 100644 index 0000000..0ad3568 --- /dev/null +++ b/monitoring/delete @@ -0,0 +1 @@ +Redis diff --git a/monitoring/docker-compose.monitoring.yml b/monitoring/docker-compose.monitoring.yml index 09f44c5..cdeb4ce 100644 --- a/monitoring/docker-compose.monitoring.yml +++ b/monitoring/docker-compose.monitoring.yml @@ -10,6 +10,26 @@ version: '3.8' services: + # =========================================================================== + # REDIS - In-Memory Data Store (for BullMQ queues) + # =========================================================================== + redis: + image: redis:7-alpine + container_name: re_redis + ports: + - "${REDIS_PORT:-6379}:6379" + command: redis-server --requirepass ${REDIS_PASSWORD:-Redis@123} + volumes: + - redis_data:/data + networks: + - monitoring_network + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 3s + retries: 5 + # =========================================================================== # PROMETHEUS - Metrics Collection # =========================================================================== @@ -127,6 +147,27 @@ services: - monitoring_network restart: unless-stopped + # =========================================================================== + # REDIS EXPORTER - Redis Metrics + # =========================================================================== + redis-exporter: + image: oliver006/redis_exporter:v1.55.0 + container_name: re_redis_exporter + ports: + - "9121:9121" + environment: + - REDIS_ADDR=redis://redis:6379 + - REDIS_PASSWORD=Redis@123 + command: + - '--redis.addr=redis:6379' + - '--redis.password=Redis@123' + networks: + - monitoring_network + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + # =========================================================================== # ALERTMANAGER - Alert Notifications (Optional) # =========================================================================== @@ -157,6 +198,8 @@ networks: # VOLUMES # =========================================================================== volumes: + redis_data: + name: re_redis_data prometheus_data: name: re_prometheus_data loki_data: diff --git a/monitoring/grafana/dashboards/re-workflow-overview.json b/monitoring/grafana/dashboards/re-workflow-overview.json index c25844d..ff784a5 100644 --- a/monitoring/grafana/dashboards/re-workflow-overview.json +++ b/monitoring/grafana/dashboards/re-workflow-overview.json @@ -47,7 +47,7 @@ "pluginVersion": "10.2.2", "targets": [ { - "expr": "sum(rate(http_requests_total{job=\"re-workflow-backend\"}[5m]))", + "expr": "sum(rate(http_requests_total{job=\"re-workflow-backend\"}[5m])) or vector(0)", "refId": "A" } ], @@ -83,7 +83,7 @@ }, "targets": [ { - "expr": "sum(rate(http_request_errors_total{job=\"re-workflow-backend\"}[5m])) / sum(rate(http_requests_total{job=\"re-workflow-backend\"}[5m]))", + "expr": "(sum(rate(http_request_errors_total{job=\"re-workflow-backend\"}[5m])) / sum(rate(http_requests_total{job=\"re-workflow-backend\"}[5m]))) or vector(0)", "refId": "A" } ], @@ -119,7 +119,7 @@ }, "targets": [ { - "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job=\"re-workflow-backend\"}[5m])) by (le))", + "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job=\"re-workflow-backend\"}[5m])) by (le)) or vector(0)", "refId": "A" } ], @@ -476,23 +476,28 @@ "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ - { - "expr": "process_resident_memory_bytes{job=\"re-workflow-backend\"}", - "legendFormat": "RSS Memory", - "refId": "A" - }, { "expr": "nodejs_heap_size_used_bytes{job=\"re-workflow-backend\"}", "legendFormat": "Heap Used", - "refId": "B" + "refId": "A" }, { "expr": "nodejs_heap_size_total_bytes{job=\"re-workflow-backend\"}", "legendFormat": "Heap Total", + "refId": "B" + }, + { + "expr": "nodejs_external_memory_bytes{job=\"re-workflow-backend\"}", + "legendFormat": "External Memory", "refId": "C" + }, + { + "expr": "process_resident_memory_bytes{job=\"re-workflow-backend\"}", + "legendFormat": "RSS Memory", + "refId": "D" } ], - "title": "Memory Usage", + "title": "Node.js Process Memory (Heap)", "type": "timeseries" }, { @@ -535,9 +540,19 @@ "expr": "nodejs_eventloop_lag_seconds{job=\"re-workflow-backend\"}", "legendFormat": "Event Loop Lag", "refId": "A" + }, + { + "expr": "nodejs_eventloop_lag_mean_seconds{job=\"re-workflow-backend\"}", + "legendFormat": "Mean Lag", + "refId": "B" + }, + { + "expr": "nodejs_eventloop_lag_p99_seconds{job=\"re-workflow-backend\"}", + "legendFormat": "P99 Lag", + "refId": "C" } ], - "title": "Event Loop Lag", + "title": "Node.js Event Loop Lag", "type": "timeseries" }, { @@ -587,7 +602,7 @@ "refId": "B" } ], - "title": "Active Handles & Requests", + "title": "Node.js Active Handles & Requests", "type": "timeseries" }, { @@ -628,12 +643,436 @@ "targets": [ { "expr": "rate(process_cpu_seconds_total{job=\"re-workflow-backend\"}[5m])", - "legendFormat": "CPU Usage", + "legendFormat": "Process CPU Usage", "refId": "A" } ], - "title": "CPU Usage", + "title": "Node.js Process CPU Usage", "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 38 }, + "id": 400, + "panels": [], + "title": "🔴 Redis & Queue Status", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { "options": { "0": { "color": "red", "index": 1, "text": "Down" }, "1": { "color": "green", "index": 0, "text": "Up" } }, "type": "value" } + ], + "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] } + } + }, + "gridPos": { "h": 4, "w": 3, "x": 0, "y": 39 }, + "id": 401, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value" + }, + "pluginVersion": "10.2.2", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "redis_up", + "refId": "A", + "instant": true + }], + "title": "Redis Status", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "short" + } + }, + "gridPos": { "h": 4, "w": 3, "x": 3, "y": 39 }, + "id": 402, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value" + }, + "pluginVersion": "10.2.2", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "redis_connected_clients", + "refId": "A", + "instant": true + }], + "title": "Redis Connections", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 500000000 }, { "color": "red", "value": 1000000000 }] }, + "unit": "decbytes" + } + }, + "gridPos": { "h": 4, "w": 3, "x": 6, "y": 39 }, + "id": 403, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value" + }, + "pluginVersion": "10.2.2", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "redis_memory_used_bytes", + "refId": "A", + "instant": true + }], + "title": "Redis Memory", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 10 }, { "color": "red", "value": 50 }] } + } + }, + "gridPos": { "h": 4, "w": 3, "x": 9, "y": 39 }, + "id": 404, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto", + "showPercentChange": false + }, + "pluginVersion": "10.2.2", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "queue_jobs_delayed{queue_name=\"tatQueue\"}", + "refId": "A", + "instant": true + }], + "title": "TAT Alerts - Scheduled", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "blue", "value": 1 }, { "color": "yellow", "value": 5 }] } + } + }, + "gridPos": { "h": 4, "w": 3, "x": 12, "y": 39 }, + "id": 405, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto", + "showPercentChange": false + }, + "pluginVersion": "10.2.2", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "queue_jobs_delayed{queue_name=\"pauseResumeQueue\"}", + "refId": "A", + "instant": true + }], + "title": "Resume Jobs - Scheduled", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { "options": { "match": "null", "result": { "text": "0" } }, "type": "special" } + ], + "noValue": "0", + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] } + } + }, + "gridPos": { "h": 4, "w": 3, "x": 15, "y": 39 }, + "id": 406, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto", + "showPercentChange": false + }, + "pluginVersion": "10.2.2", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "queue_jobs_failed{queue_name=\"tatQueue\"}", + "refId": "A", + "instant": true + }], + "title": "TAT Alerts - Failed", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [ + { "options": { "match": "null", "result": { "text": "0" } }, "type": "special" } + ], + "noValue": "0", + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }] } + } + }, + "gridPos": { "h": 4, "w": 3, "x": 18, "y": 39 }, + "id": 407, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto", + "showPercentChange": false + }, + "pluginVersion": "10.2.2", + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "queue_jobs_failed{queue_name=\"pauseResumeQueue\"}", + "refId": "A", + "instant": true + }], + "title": "Resume Jobs - Failed", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "normal" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } + } + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 43 }, + "id": 408, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "single", "sort": "none" } + }, + "targets": [ + { "expr": "queue_jobs_waiting", "legendFormat": "{{queue_name}} - Waiting", "refId": "A" }, + { "expr": "queue_jobs_active", "legendFormat": "{{queue_name}} - Active", "refId": "B" }, + { "expr": "queue_jobs_delayed", "legendFormat": "{{queue_name}} - Delayed", "refId": "C" } + ], + "title": "All Queues - Job Status", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "ops" + } + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 43 }, + "id": 409, + "options": { + "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "single", "sort": "none" } + }, + "targets": [{ "expr": "rate(redis_commands_processed_total[1m])", "legendFormat": "Commands/sec", "refId": "A" }], + "title": "Redis Commands Rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 51 }, + "id": 500, + "panels": [], + "title": "💻 System Resources (Host)", + "type": "row" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 60 }, { "color": "red", "value": 80 }] }, + "unit": "percent" + } + }, + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 52 }, + "id": 501, + "options": { + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "targets": [{ "expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)", "refId": "A" }], + "title": "Host CPU Usage (All Cores)", + "type": "gauge" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "red", "value": 85 }] }, + "unit": "percent" + } + }, + "gridPos": { "h": 6, "w": 6, "x": 6, "y": 52 }, + "id": 502, + "options": { + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "targets": [{ "expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100", "refId": "A" }], + "title": "Host Memory Usage (RAM)", + "type": "gauge" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 75 }, { "color": "red", "value": 90 }] }, + "unit": "percent" + } + }, + "gridPos": { "h": 6, "w": 6, "x": 12, "y": 52 }, + "id": 503, + "options": { + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "targets": [{ + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "100 - ((avg(node_filesystem_avail_bytes{fstype=~\"ext4|xfs|overlay2\"}) / avg(node_filesystem_size_bytes{fstype=~\"ext4|xfs|overlay2\"})) * 100)", + "refId": "A", + "instant": true + }], + "title": "Container Storage Usage", + "type": "gauge" + }, + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "decimals": 2, + "mappings": [], + "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, + "unit": "decgbytes" + } + }, + "gridPos": { "h": 6, "w": 6, "x": 18, "y": 52 }, + "id": 504, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "value" + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "avg(node_filesystem_avail_bytes{fstype=~\"ext4|xfs|overlay2\"})", + "refId": "A", + "instant": true + } + ], + "title": "Container Storage Available", + "type": "stat" } ], "refresh": "30s", diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml index bd842d3..b4dddc4 100644 --- a/monitoring/prometheus/prometheus.yml +++ b/monitoring/prometheus/prometheus.yml @@ -72,13 +72,14 @@ scrape_configs: # service: 'postgresql' # ============================================ - # Redis Metrics (if using redis_exporter) + # Redis Metrics # ============================================ - # - job_name: 'redis' - # static_configs: - # - targets: ['redis-exporter:9121'] - # labels: - # service: 'redis' + - job_name: 'redis' + static_configs: + - targets: ['redis-exporter:9121'] + labels: + service: 'redis' + environment: 'development' # ============================================ # Loki Metrics diff --git a/package-lock.json b/package-lock.json index 6762eb3..d4a32be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@anthropic-ai/sdk": "^0.68.0", "@google-cloud/storage": "^7.14.0", "@google/generative-ai": "^0.24.1", + "@types/nodemailer": "^7.0.4", "@types/uuid": "^8.3.4", "axios": "^1.7.9", "bcryptjs": "^2.4.3", @@ -27,6 +28,7 @@ "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", + "nodemailer": "^7.0.11", "openai": "^6.8.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -50,7 +52,7 @@ "@types/jsonwebtoken": "^9.0.7", "@types/morgan": "^1.9.9", "@types/multer": "^1.4.12", - "@types/node": "^22.10.5", + "@types/node": "^22.19.1", "@types/passport": "^1.0.16", "@types/passport-jwt": "^4.0.1", "@types/pg": "^8.15.6", @@ -96,6 +98,743 @@ } } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.943.0.tgz", + "integrity": "sha512-Zlyw/y5CbhhRK+ZdN/aMNPA8BYo427ddxkTG5yI/JasO05i7thPkhyqsUjyfsV6Z1tHfwFlLeicW57aLW8fAVw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/credential-provider-node": "3.943.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/signature-v4-multi-region": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.943.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.943.0.tgz", + "integrity": "sha512-kOTO2B8Ks2qX73CyKY8PAajtf5n39aMe2spoiOF5EkgSzGV7hZ/HONRDyADlyxwfsX39Q2F2SpPUaXzon32IGw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.943.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.943.0.tgz", + "integrity": "sha512-8CBy2hI9ABF7RBVQuY1bgf/ue+WPmM/hl0adrXFlhnhkaQP0tFY5zhiy1Y+n7V+5f3/ORoHBmCCQmcHDDYJqJQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.943.0.tgz", + "integrity": "sha512-WnS5w9fK9CTuoZRVSIHLOMcI63oODg9qd1vXMYb7QGLGlfwUm4aG3hdu7i9XvYrpkQfE3dzwWLtXF4ZBuL1Tew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.943.0.tgz", + "integrity": "sha512-SA8bUcYDEACdhnhLpZNnWusBpdmj4Vl67Vxp3Zke7SvoWSYbuxa+tiDiC+c92Z4Yq6xNOuLPW912ZPb9/NsSkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.943.0.tgz", + "integrity": "sha512-BcLDb8l4oVW+NkuqXMlO7TnM6lBOWW318ylf4FRED/ply5eaGxkQYqdGvHSqGSN5Rb3vr5Ek0xpzSjeYD7C8Kw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/credential-provider-env": "3.943.0", + "@aws-sdk/credential-provider-http": "3.943.0", + "@aws-sdk/credential-provider-login": "3.943.0", + "@aws-sdk/credential-provider-process": "3.943.0", + "@aws-sdk/credential-provider-sso": "3.943.0", + "@aws-sdk/credential-provider-web-identity": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.943.0.tgz", + "integrity": "sha512-9iCOVkiRW+evxiJE94RqosCwRrzptAVPhRhGWv4osfYDhjNAvUMyrnZl3T1bjqCoKNcETRKEZIU3dqYHnUkcwQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.943.0.tgz", + "integrity": "sha512-14eddaH/gjCWoLSAELVrFOQNyswUYwWphIt+PdsJ/FqVfP4ay2HsiZVEIYbQtmrKHaoLJhiZKwBQRjcqJDZG0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.943.0", + "@aws-sdk/credential-provider-http": "3.943.0", + "@aws-sdk/credential-provider-ini": "3.943.0", + "@aws-sdk/credential-provider-process": "3.943.0", + "@aws-sdk/credential-provider-sso": "3.943.0", + "@aws-sdk/credential-provider-web-identity": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.943.0.tgz", + "integrity": "sha512-GIY/vUkthL33AdjOJ8r9vOosKf/3X+X7LIiACzGxvZZrtoOiRq0LADppdiKIB48vTL63VvW+eRIOFAxE6UDekw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.943.0.tgz", + "integrity": "sha512-1c5G11syUrru3D9OO6Uk+ul5e2lX1adb+7zQNyluNaLPXP6Dina6Sy6DFGRLu7tM8+M7luYmbS3w63rpYpaL+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.943.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/token-providers": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.943.0.tgz", + "integrity": "sha512-VtyGKHxICSb4kKGuaqotxso8JVM8RjCS3UYdIMOxUt9TaFE/CZIfZKtjTr+IJ7M0P7t36wuSUb/jRLyNmGzUUA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", + "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.943.0.tgz", + "integrity": "sha512-kd2mALfthU+RS9NsPS+qvznFcPnVgVx9mgmStWCPn5Qc5BTnx4UAtm+HPA+XZs+zxOopp+zmAfE4qxDHRVONBA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.18.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.943.0.tgz", + "integrity": "sha512-956n4kVEwFNXndXfhSAN5wO+KRgqiWEEY+ECwLvxmmO8uQ0NWOa8l6l65nTtyuiWzMX81c9BvlyNR5EgUeeUvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.943.0.tgz", + "integrity": "sha512-anFtB0p2FPuyUnbOULwGmKYqYKSq1M73c9uZ08jR/NCq6Trjq9cuF5TFTeHwjJyPRb4wMf2Qk859oiVfFqnQiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.943.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.943.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.943.0.tgz", + "integrity": "sha512-KKvmxNQ/FZbM6ml6nKd8ltDulsUojsXnMJNgf1VHTcJEbADC/6mVWOq0+e9D0WP1qixUBEuMjlS2HqD5KoqwEg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.943.0.tgz", + "integrity": "sha512-cRKyIzwfkS+XztXIFPoWORuaxlIswP+a83BJzelX4S1gUZ7FcXB4+lj9Jxjn8SbQhR4TPU3Owbpu+S7pd6IRbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.943.0", + "@aws-sdk/nested-clients": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.943.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.943.0.tgz", + "integrity": "sha512-gn+ILprVRrgAgTIBk2TDsJLRClzIOdStQFeFTcN0qpL8Z4GBCqMFhw7O7X+MM55Stt5s4jAauQ/VvoqmCADnQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.943.0", + "@aws-sdk/types": "3.936.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/xml-builder/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -2128,6 +2867,586 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.18.7", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.7.tgz", + "integrity": "sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.6", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.14.tgz", + "integrity": "sha512-v0q4uTKgBM8dsqGjqsabZQyH85nFaTnFcgpWU1uydKFsdyyMzfvOkNum9G7VK+dOP01vUnoZxIeRiJ6uD0kjIg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.7", + "@smithy/middleware-serde": "^4.2.6", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.14.tgz", + "integrity": "sha512-Z2DG8Ej7FyWG1UA+7HceINtSLzswUgs2np3sZX0YBBxCt+CXG4QUxv88ZDS3+2/1ldW7LqtSY1UO/6VQ1pND8Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.10", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.10.tgz", + "integrity": "sha512-Jaoz4Jw1QYHc1EFww/E6gVtNjhoDU+gwRKqXP6C3LKYqqH2UQhP8tMP3+t/ePrhaze7fhLE8vS2q6vVxBANFTQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.18.7", + "@smithy/middleware-endpoint": "^4.3.14", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.13.tgz", + "integrity": "sha512-hlVLdAGrVfyNei+pKIgqDTxfu/ZI2NSyqj4IDxKd5bIsIqwR/dSlkxlPaYxFiIaDVrBy0he8orsFy+Cz119XvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.16", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.16.tgz", + "integrity": "sha512-F1t22IUiJLHrxW9W1CQ6B9PN+skZ9cqSuzB18Eh06HrJPbjsyZ7ZHecAKw80DQtyGTRcVfeukKaCRYebFwclbg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.10", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -2451,14 +3770,24 @@ } }, "node_modules/@types/node": { - "version": "22.18.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz", - "integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz", + "integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, "node_modules/@types/passport": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", @@ -3454,6 +4783,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -7576,6 +8911,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", diff --git a/package.json b/package.json index 7294ad8..27ff726 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@anthropic-ai/sdk": "^0.68.0", "@google-cloud/storage": "^7.14.0", "@google/generative-ai": "^0.24.1", + "@types/nodemailer": "^7.0.4", "@types/uuid": "^8.3.4", "axios": "^1.7.9", "bcryptjs": "^2.4.3", @@ -39,6 +40,7 @@ "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-cron": "^3.0.3", + "nodemailer": "^7.0.11", "openai": "^6.8.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -62,7 +64,7 @@ "@types/jsonwebtoken": "^9.0.7", "@types/morgan": "^1.9.9", "@types/multer": "^1.4.12", - "@types/node": "^22.10.5", + "@types/node": "^22.19.1", "@types/passport": "^1.0.16", "@types/passport-jwt": "^4.0.1", "@types/pg": "^8.15.6", diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 0ba689b..f39c04e 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -107,7 +107,7 @@ export class AuthController { async refreshToken(req: Request, res: Response): Promise { try { // Try to get refresh token from request body first, then from cookies - let refreshToken: string; + let refreshToken: string | undefined; if (req.body?.refreshToken) { const validated = validateRefreshToken(req.body); @@ -115,8 +115,16 @@ export class AuthController { } else if ((req as any).cookies?.refreshToken) { // Fallback to cookie if available (requires cookie-parser middleware) refreshToken = (req as any).cookies.refreshToken; - } else { - throw new Error('Refresh token is required'); + } + + if (!refreshToken) { + res.status(400).json({ + success: false, + error: 'Refresh token is required in request body or cookies', + message: 'Request body validation failed', + timestamp: new Date().toISOString() + }); + return; } const newAccessToken = await this.authService.refreshAccessToken(refreshToken); @@ -126,7 +134,7 @@ export class AuthController { const cookieOptions = { httpOnly: true, secure: isProduction, - sameSite: 'lax' as const, + sameSite: isProduction ? 'none' as const : 'lax' as const, // 'none' for cross-domain in production maxAge: 24 * 60 * 60 * 1000, // 24 hours }; @@ -340,7 +348,7 @@ export class AuthController { const cookieOptions = { httpOnly: true, secure: isProduction, - sameSite: 'lax' as const, + sameSite: isProduction ? 'none' as const : 'lax' as const, // 'none' for cross-domain in production maxAge: 24 * 60 * 60 * 1000, // 24 hours for access token }; diff --git a/src/emailtemplates/EMAIL_STRATEGY.md b/src/emailtemplates/EMAIL_STRATEGY.md new file mode 100644 index 0000000..d2351e0 --- /dev/null +++ b/src/emailtemplates/EMAIL_STRATEGY.md @@ -0,0 +1,285 @@ +# Email Notification Strategy - Quick Reference + +## 🎯 **9 Email Scenarios (Priority Order)** + +### **✅ CRITICAL (Always Send if Admin Enabled)** +1. **TAT Breached (100%)** → Approver + Management +2. **Request Rejected** → Initiator + +### **✅ HIGH PRIORITY (Check Preferences)** +3. **Request Created** → Initiator +4. **Approval Request** → Approver (action required) +5. **Request Approved** → Initiator +6. **TAT Reminder (75%)** → Approver +7. **Workflow Resumed** → Approver + Initiator + +### **✅ MEDIUM PRIORITY (Check Preferences)** +8. **TAT Reminder (50%)** → Approver + +### **✅ LOW PRIORITY (Check Preferences)** +9. **Request Closed** → All Participants + +--- + +## 🔐 **Preference Control Flow** + +### **Before Sending ANY Email:** + +```typescript +import { shouldSendEmail, EmailNotificationType } from '@/emailtemplates/emailPreferences.helper'; + +// Check preferences +const canSendEmail = await shouldSendEmail( + userId, + EmailNotificationType.APPROVAL_REQUEST +); + +if (!canSendEmail) { + logger.info(`Email skipped for user ${userId} due to preferences`); + return; // Don't send email +} + +// Proceed with email +await emailService.sendEmail(...); +``` + +### **Preference Logic:** + +``` +Admin Enabled? ──NO──> ❌ Stop (Don't send) + │ + YES + │ + ▼ +User Enabled? ──NO──> ❌ Stop (Don't send) + │ + YES + │ + ▼ + ✅ Send Email +``` + +### **Critical Email Override:** + +```typescript +// For CRITICAL emails (Rejection, TAT Breach) +const canSend = await shouldSendEmailWithOverride( + userId, + EmailNotificationType.REQUEST_REJECTED +); +// This checks admin only, ignores user preference +``` + +--- + +## 📊 **TAT Email Schedule (3 Reminders)** + +| TAT Progress | Template | Priority | Subject | When | +|--------------|----------|----------|---------|------| +| **50%** | TATReminder | Medium | TAT Reminder - Early Warning | Half TAT elapsed | +| **75%** | TATReminder | High | TAT Reminder - Action Needed | 3/4 TAT elapsed | +| **100%** | TATBreached | Critical | TAT BREACHED - Urgent | Deadline passed | + +### **TAT Reminder Template Customization:** + +```typescript +// 50% Reminder +await emailNotificationService.sendTATReminder( + workflow, + approver, + { + threshold: 50, + timeRemaining: '12 hours', + urgency: 'early-warning' + } +); + +// 75% Reminder +await emailNotificationService.sendTATReminder( + workflow, + approver, + { + threshold: 75, + timeRemaining: '6 hours', + urgency: 'urgent' + } +); + +// 100% Breach +await emailNotificationService.sendTATBreached( + workflow, + approver, + { + timeOverdue: '2 hours overdue', + notifyManagement: true + } +); +``` + +--- + +## 🚫 **Never Send Email For:** + +### **In-App Only (Real-Time Collaboration):** + +| Notification Type | Why In-App Only | Frequency | +|-------------------|-----------------|-----------| +| @Mentions | Real-time chat | High (50-200/day) | +| Comments | Active discussion | High (100-300/day) | +| Documents Added | Minor update | Medium (20-50/day) | +| Status Changes | Internal updates | Medium (30-80/day) | +| AI Generated | Background process | Low (10-20/day) | +| Summary Generated | Automatic | Low (10-20/day) | + +**Reason:** These would flood inboxes and cause email fatigue! + +--- + +## 💾 **Important: Activity Logs** + +### **ALWAYS Capture Activities:** + +```typescript +// Even if email/notification disabled, ALWAYS log activity +await activityService.log({ + requestId, + type: 'approval', + user: { userId, name }, + timestamp: new Date().toISOString(), + action: 'Request approved', + details: 'Approved by John Doe' +}); + +// This is independent of email/notification preferences +// Activity logs are for audit trail and compliance +``` + +**Activities Logged Regardless of Preferences:** +- ✅ Request created +- ✅ Assignments +- ✅ Approvals +- ✅ Rejections +- ✅ TAT events (50%, 75%, 100%) +- ✅ Pauses/Resumes +- ✅ Comments +- ✅ Documents +- ✅ Status changes + +--- + +## 🔧 **Implementation Checklist** + +### **Before Starting:** +- [ ] Create `emailPreferences.helper.ts` ✅ Done +- [ ] Add system config entries for email controls +- [ ] Update user preferences schema +- [ ] Test preference checking logic + +### **For Each Email Type:** +1. [ ] Check admin preference +2. [ ] Check user preference +3. [ ] Send email only if both enabled +4. [ ] Log activity regardless +5. [ ] Handle errors gracefully +6. [ ] Track metrics (sent/failed/skipped) + +### **Critical Emails (Special Handling):** +- [ ] Request Rejected - Override user preference +- [ ] TAT Breached - Override user preference +- [ ] Still respect admin disable + +--- + +## 📋 **User Preference UI (Frontend)** + +### **Admin Settings Page:** + +``` +Email Notifications (Global Control) +├── [✓] Enable Email Notifications +│ +├── Request Lifecycle +│ ├── [✓] Request Created +│ ├── [✓] Approval Requests +│ ├── [✓] Request Approved +│ └── [✓] Request Rejected (Critical - Recommended) +│ +├── TAT Alerts +│ ├── [✓] TAT 50% Reminder +│ ├── [✓] TAT 75% Reminder +│ └── [✓] TAT Breached (Critical - Recommended) +│ +└── Workflow Control + ├── [✓] Workflow Resumed + └── [✓] Request Closed +``` + +### **User Preferences Page:** + +``` +My Email Preferences +├── [✓] Receive Email Notifications +│ +├── Request Updates +│ ├── [✓] When my request is created +│ ├── [✓] When assigned to approve +│ ├── [✓] When my request is approved +│ ├── [⚠] When my request is rejected (Cannot disable - Critical) +│ +├── TAT Alerts +│ ├── [✓] TAT 50% reminder +│ ├── [✓] TAT 75% reminder +│ └── [⚠] TAT breached (Cannot disable - Critical) +│ +└── Other + ├── [✓] Workflow resumed + └── [ ] Request closed (Optional) +``` + +**Note:** Critical emails cannot be disabled by users! + +--- + +## 📊 **Email Volume with Preferences** + +### **Without Preferences (All Emails):** +- ~75-205 emails/day + +### **With Smart Defaults:** +- Users who want all: ~75-205 emails/day +- Users who opt for critical only: ~10-20 emails/day +- Average user: ~30-50 emails/day + +### **Email Savings:** +- @Mentions: 50-200/day saved ✅ +- Comments: 100-300/day saved ✅ +- Documents: 20-50/day saved ✅ +- Status changes: 30-80/day saved ✅ + +**Total saved: 200-630 potential emails/day!** + +--- + +## ✅ **Summary** + +### **Email Strategy:** +1. ✅ **9 email scenarios** (high-impact only) +2. ✅ **TAT reminders at 50%, 75%, 100%** +3. ✅ **Admin-level control** (can disable globally) +4. ✅ **User-level control** (individual preferences) +5. ✅ **Critical override** (rejection, breach always sent) +6. ✅ **Activity logs** always captured +7. ✅ **In-app only** for collaboration (@mentions, comments) + +### **Implementation Order:** +1. Phase 0: Preference system ← **START HERE** +2. Phase 1: Email service +3. Phase 2: Critical emails (rejection, breach) +4. Phase 3: High priority emails +5. Phase 4: Medium/low priority emails +6. Phase 5: User preference UI + +--- + +**Ready to implement Phase 0 (Preference System)?** 🚀 + diff --git a/src/emailtemplates/IMPLEMENTATION_PLAN.md b/src/emailtemplates/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..6128a32 --- /dev/null +++ b/src/emailtemplates/IMPLEMENTATION_PLAN.md @@ -0,0 +1,1062 @@ +# Email Notification Implementation Plan + +## 📊 **Current State Analysis** + +### **Existing Notification Types in System:** + +Based on codebase analysis, your system currently sends **in-app notifications** for: + +| Notification Type | Current Method | Should Send Email? | +|-------------------|----------------|-------------------| +| `request_submitted` | In-app | ✅ YES (High) | +| `assignment` | In-app | ✅ YES (High) | +| `approval` | In-app | ✅ YES (High) | +| `rejection` | In-app | ✅ YES (Critical) | +| `workflow_paused` | In-app | ⚠️ CONDITIONAL | +| `workflow_resumed` | In-app | ✅ YES (High) | +| `pause_retrigger_request` | In-app | ⚠️ CONDITIONAL | +| `pause_retriggered` | In-app | ❌ NO (In-app only) | +| `tat_reminder` (50%) | In-app | ✅ YES (Medium) | +| `tat_reminder` (75%) | In-app | ✅ YES (High) | +| `tat_breach` (100%) | In-app | ✅ YES (Critical) | +| `mention` (work notes) | In-app | ❌ NO (In-app only) | +| `comment` | In-app | ❌ NO (In-app only) | +| `status_change` | In-app | ❌ NO (In-app only) | +| `document_added` | In-app | ❌ NO (In-app only) | +| `ai_conclusion_generated` | In-app | ❌ NO (In-app only) | +| `summary_generated` | In-app | ❌ NO (In-app only) | +| `approval_pending_closure` | In-app | ⚠️ CONDITIONAL | +| `auto-resume-workflow` | In-app | ❌ NO (System) | +| `closed` | In-app | ✅ YES (Low) | + +--- + +## 🎯 **Email vs In-App Notification Strategy** + +### **✅ SEND EMAIL (High Impact, Action Required)** + +**Criteria:** +- Requires user action +- Time-sensitive +- Critical for workflow progress +- User might not be logged in + +**Scenarios:** +1. **Request Created** → Send to Initiator (confirmation) +2. **Approval Request** → Send to Approver (action required) +3. **Request Approved** → Send to Initiator (important update) +4. **Request Rejected** → Send to Initiator (critical) +5. **TAT Reminder 50%** → Send to Approver (early warning) +6. **TAT Reminder 75%** → Send to Approver (urgent) +7. **TAT Breached 100%** → Send to Approver + Management (critical) +8. **Workflow Resumed** → Send to Current Approver (action needed) +9. **Request Closed** → Send to All Participants (summary) + +### **❌ IN-APP ONLY (Low Impact, Real-time)** + +**Criteria:** +- Real-time collaboration +- Minor updates +- User likely logged in +- Frequent notifications + +**Scenarios:** +1. **@Mentions in Work Notes** → In-app only +2. **Comments Added** → In-app only +3. **Documents Uploaded** → In-app only +4. **Status Changes** → In-app only +5. **AI Conclusion Generated** → In-app only +6. **Summary Generated** → In-app only +7. **Pause Retriggered** → In-app only + +### **⚠️ CONDITIONAL (Based on Settings)** + +**Scenarios:** +1. **Workflow Paused** → Email if paused > 24 hours +2. **Participant Added** → Email if user preference enabled +3. **Approver Skipped** → Email to skipped approver only + +--- + +## 📋 **Template Mapping to Scenarios** + +### **Critical Priority Emails (Send Immediately)** + +| Scenario | Template | Trigger Point | Recipients | +|----------|----------|---------------|------------| +| TAT Breached | `getTATBreachedEmail()` | TAT timer breach | Approver, Management | +| Request Rejected | `getRejectionNotificationEmail()` | Approver rejects | Initiator, Spectators | + +### **High Priority Emails (Send within 1 minute)** + +| Scenario | Template | Trigger Point | Recipients | +|----------|----------|---------------|------------| +| Request Created | `getRequestCreatedEmail()` | Workflow created | Initiator | +| Approval Request | `getApprovalRequestEmail()` or `getMultiApproverRequestEmail()` | Level assigned | Approver | +| Request Approved | `getApprovalConfirmationEmail()` | Approver approves | Initiator | +| TAT Reminder | `getTATReminderEmail()` | 80% TAT elapsed | Approver | +| Workflow Resumed | `getWorkflowResumedEmail()` | Workflow resumed | Approver, Initiator | + +### **Medium Priority Emails (Send within 5 minutes)** + +| Scenario | Template | Trigger Point | Recipients | +|----------|----------|---------------|------------| +| Workflow Paused (>24h) | `getWorkflowPausedEmail()` | Long pause | Approver, Initiator | +| Participant Added | `getParticipantAddedEmail()` | User preference | New participant | +| Approver Skipped | `getApproverSkippedEmail()` | Skip action | Skipped approver | + +### **Low Priority Emails (Batch send)** + +| Scenario | Template | Trigger Point | Recipients | +|----------|----------|---------------|------------| +| Request Closed | `getRequestClosedEmail()` | Workflow closed | All participants | + +--- + +## 🔐 **CRITICAL: Email Preference Control** + +### **Two-Level Preference System:** + +``` +┌─────────────────────────────────────┐ +│ Email Notification Request │ +└──────────────┬──────────────────────┘ + │ + ▼ + ┌──────────────┐ + │ Admin Level │ + │ Enabled? │ ──NO──> ❌ Don't send (admin disabled) + └──────┬───────┘ + │ YES + ▼ + ┌──────────────┐ + │ User Level │ + │ Enabled? │ ──NO──> ❌ Don't send (user disabled) + └──────┬───────┘ + │ YES + ▼ + ┌──────────────┐ + │ ✅ Send Email │ + └───────────────┘ + +IMPORTANT: Activity logs are ALWAYS captured regardless of preferences! +``` + +### **Preference Logic:** + +```typescript +// Check before sending ANY email +const shouldSend = await shouldSendEmail(userId, EmailNotificationType.APPROVAL_REQUEST); + +if (shouldSend) { + await emailService.sendEmail(...); +} else { + logger.info('Email skipped due to preferences'); +} + +// Activity ALWAYS logged regardless +await activityService.log({ + type: 'assignment', + // ... activity data +}); +``` + +### **Critical Emails (Override User Preference):** + +Some emails are TOO important to be disabled by users: +- ✅ **Request Rejected** - User must know +- ✅ **TAT Breached** - Critical escalation + +For these, only admin can disable (user preference ignored): + +```typescript +if (CRITICAL_EMAILS.includes(emailType)) { + // Check admin only, ignore user preference + const adminEnabled = await isAdminEmailEnabled(emailType); + if (adminEnabled) return true; +} +``` + +--- + +## 🔧 **Implementation Architecture** + +### **Phase 0: Preference System (REQUIRED FIRST)** + +Create preference checking before ANY email: + +**A. Email Preferences Helper** (`emailPreferences.helper.ts`) ✅ Created + +**B. System Config Table:** +```sql +-- Admin-level email controls +INSERT INTO system_configs (config_key, config_value, description) VALUES +('email.enabled', 'true', 'Global email notifications enabled/disabled'), +('email.request_created.enabled', 'true', 'Request created emails'), +('email.approval_request.enabled', 'true', 'Approval request emails'), +('email.request_approved.enabled', 'true', 'Approval confirmation emails'), +('email.request_rejected.enabled', 'true', 'Rejection emails (critical)'), +('email.tat_reminder_50.enabled', 'true', 'TAT 50% reminder emails'), +('email.tat_reminder_75.enabled', 'true', 'TAT 75% reminder emails'), +('email.tat_breached.enabled', 'true', 'TAT breach emails (critical)'), +('email.workflow_resumed.enabled', 'true', 'Workflow resumed emails'), +('email.request_closed.enabled', 'true', 'Request closed emails'); +``` + +**C. User Preferences Table:** +```json +// UserPreference.preferences column (JSONB) +{ + "email": { + "enabled": true, // Global email preference + "request_created": true, + "approval_request": true, + "request_approved": true, + "request_rejected": true, // Can be overridden for critical + "tat_reminder_50": true, + "tat_reminder_75": true, + "tat_breached": true, // Can be overridden for critical + "workflow_resumed": true, + "request_closed": false // User can opt-out of closure emails + }, + "notification": { + "enabled": true, + "mention": true, + "comment": true, + "assignment": true, + "status_change": true + } +} +``` + +### **Phase 1: Email Service Creation** + +Create `email.service.ts`: + +```typescript +import nodemailer from 'nodemailer'; +import { + getRequestCreatedEmail, + getApprovalRequestEmail, + // ... all template functions +} from '@/emailtemplates'; + +export class EmailService { + private transporter: nodemailer.Transporter; + + constructor() { + this.transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: parseInt(process.env.SMTP_PORT || '587'), + secure: process.env.SMTP_SECURE === 'true', + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD + } + }); + } + + async sendEmail(to: string, subject: string, html: string): Promise { + try { + await this.transporter.sendMail({ + from: process.env.EMAIL_FROM, + to, + subject, + html + }); + logger.info(`✅ Email sent to ${to}: ${subject}`); + } catch (error) { + logger.error(`❌ Failed to send email to ${to}:`, error); + throw error; + } + } +} + +export const emailService = new EmailService(); +``` + +### **Phase 2: Email Notification Service** + +Create `emailNotification.service.ts`: + +```typescript +import { emailService } from './email.service'; +import { + getRequestCreatedEmail, + getApprovalRequestEmail, + getMultiApproverRequestEmail, + getApprovalConfirmationEmail, + getRejectionNotificationEmail, + getTATReminderEmail, + getTATBreachedEmail, + getWorkflowPausedEmail, + getWorkflowResumedEmail, + getParticipantAddedEmail, + getApproverSkippedEmail, + getRequestClosedEmail, + getViewDetailsLink +} from '@/emailtemplates'; + +export class EmailNotificationService { + + /** + * Send request created email to initiator + */ + async sendRequestCreated(request: any, initiator: any, firstApprover: any) { + const data = { + recipientName: initiator.name, + requestId: request.requestNumber, + requestTitle: request.title, + initiatorName: initiator.name, + firstApproverName: firstApprover.name, + requestType: request.type, + priority: request.priority, + requestDate: formatDate(request.createdAt), + requestTime: formatTime(request.createdAt), + totalApprovers: request.totalApprovers, + expectedTAT: request.tatHours, + viewDetailsLink: getViewDetailsLink(request.requestNumber), + companyName: 'Royal Enfield' + }; + + const html = getRequestCreatedEmail(data); + await emailService.sendEmail( + initiator.email, + `[${request.requestNumber}] Request Created Successfully`, + html + ); + } + + /** + * Send approval request email to approver + */ + async sendApprovalRequest(request: any, approver: any, initiator: any, isMultiLevel: boolean) { + if (isMultiLevel) { + // Use multi-approver template with approval chain + const data = { + recipientName: approver.name, + requestId: request.requestNumber, + approverName: approver.name, + initiatorName: initiator.name, + requestType: request.type, + requestDescription: request.description, + priority: request.priority, + requestDate: formatDate(request.createdAt), + requestTime: formatTime(request.createdAt), + approverLevel: approver.levelNumber, + totalApprovers: request.totalApprovers, + approversList: buildApprovalChain(request.approvalLevels, approver.levelNumber), + viewDetailsLink: getViewDetailsLink(request.requestNumber), + companyName: 'Royal Enfield' + }; + + const html = getMultiApproverRequestEmail(data); + await emailService.sendEmail( + approver.email, + `[${request.requestNumber}] Multi-Level Approval Request - Your Turn`, + html + ); + } else { + // Use single approver template + const data = { + recipientName: approver.name, + requestId: request.requestNumber, + approverName: approver.name, + initiatorName: initiator.name, + requestType: request.type, + requestDescription: request.description, + priority: request.priority, + requestDate: formatDate(request.createdAt), + requestTime: formatTime(request.createdAt), + viewDetailsLink: getViewDetailsLink(request.requestNumber), + companyName: 'Royal Enfield' + }; + + const html = getApprovalRequestEmail(data); + await emailService.sendEmail( + approver.email, + `[${request.requestNumber}] Approval Request - Action Required`, + html + ); + } + } + + // ... Add methods for other email types +} + +export const emailNotificationService = new EmailNotificationService(); +``` + +### **Phase 3: Integration Points** + +Update existing services to send emails alongside in-app notifications: + +#### **A. workflow.service.ts - createWorkflow()** + +```typescript +// After creating workflow (around line 2362) + +import { shouldSendEmail, shouldSendInAppNotification, EmailNotificationType } from '@/emailtemplates/emailPreferences.helper'; + +// Check preferences before sending in-app notification +const sendInAppToInitiator = await shouldSendInAppNotification( + initiatorId, + 'request_submitted' +); + +if (sendInAppToInitiator) { + await notificationService.sendToUsers([initiatorId], { + title: 'Request Submitted Successfully', + body: `Your request "${workflowData.title}" has been submitted...`, + type: 'request_submitted', + priority: 'MEDIUM' + }); +} + +// Check preferences before sending email +const sendEmailToInitiator = await shouldSendEmail( + initiatorId, + EmailNotificationType.REQUEST_CREATED +); + +if (sendEmailToInitiator) { + await emailNotificationService.sendRequestCreated( + workflow, + initiator, + firstApprover + ); +} + +// APPROVER NOTIFICATION +const sendInAppToApprover = await shouldSendInAppNotification( + approverId, + 'assignment' +); + +if (sendInAppToApprover) { + await notificationService.sendToUsers([approverId], { + title: 'New Request Assigned', + type: 'assignment', + priority: 'HIGH', + actionRequired: true + }); +} + +// Check preferences before sending email to approver +const sendEmailToApprover = await shouldSendEmail( + approverId, + EmailNotificationType.APPROVAL_REQUEST +); + +if (sendEmailToApprover) { + const isMultiLevel = approvalLevels.length > 1; + await emailNotificationService.sendApprovalRequest( + workflow, + approver, + initiator, + isMultiLevel + ); +} + +// ACTIVITY LOG - Always captured regardless of preferences +await activityService.log({ + requestId: workflow.requestId, + type: 'created', + user: { userId: initiatorId, name: initiatorName }, + timestamp: new Date().toISOString(), + action: 'Request created', + details: `Request ${requestNumber} created and assigned to ${approverName}` +}); +``` + +#### **B. approval.service.ts - approveRequest()** + +```typescript +// After approval (around line 134) + +// EXISTING: In-app notification +await notificationService.sendToUsers([initiatorId], { + title: 'Request Approved', + type: 'approval', + priority: 'HIGH' +}); + +// NEW: Send email +await emailNotificationService.sendApprovalConfirmation( + request, + approver, + initiator, + isFinalApproval, + nextApprover +); +``` + +#### **C. approval.service.ts - rejectRequest()** + +```typescript +// After rejection (around line 513) + +// EXISTING: In-app notification +await notificationService.sendToUsers([initiatorId], { + title: 'Request Rejected', + type: 'rejection', + priority: 'HIGH' +}); + +// NEW: Send email (CRITICAL) +await emailNotificationService.sendRejectionNotification( + request, + approver, + initiator, + rejectionReason +); +``` + +#### **D. tatProcessor.ts - TAT Notifications** + +```typescript +// TAT Reminder (80%) +if (type === 'threshold1') { + // In-app notification + await notificationService.sendToUsers([approverId], {...}); + + // Email notification + await emailNotificationService.sendTATReminder(request, approver); +} + +// TAT Breached +if (type === 'breach') { + // In-app notification + await notificationService.sendToUsers([approverId], {...}); + + // Email notification (CRITICAL) + await emailNotificationService.sendTATBreached( + request, + approver, + managementUsers // Also notify management + ); +} +``` + +#### **E. pause.service.ts - Pause/Resume** + +```typescript +// Workflow Paused +await notificationService.sendToUsers([...], {...}); + +// Email ONLY if pause > 24 hours +const pauseDuration = dayjs(resumeDate).diff(dayjs(), 'hours'); +if (pauseDuration > 24) { + await emailNotificationService.sendWorkflowPaused(...); +} + +// Workflow Resumed +await notificationService.sendToUsers([...], {...}); +await emailNotificationService.sendWorkflowResumed(...); +``` + +--- + +## 🎯 **Email Sending Decision Tree** + +``` +┌─────────────────────────────────┐ +│ Notification Triggered │ +└────────────┬────────────────────┘ + │ + ▼ + ┌──────────────┐ + │ Is Critical? │ ──YES──> Send Email Immediately + │ (Rejection, │ (Critical Priority) + │ TAT Breach) │ + └──────┬───────┘ + │ NO + ▼ + ┌──────────────┐ + │ Requires │ ──YES──> Send Email + │ Action? │ (High Priority) + │ (Approval, │ + │ Assignment) │ + └──────┬───────┘ + │ NO + ▼ + ┌──────────────┐ + │ Important │ ──YES──> Send Email + │ Update? │ (Medium Priority) + │ (Approved, │ + │ Resumed) │ + └──────┬───────┘ + │ NO + ▼ + ┌──────────────┐ + │ Real-time │ ──YES──> In-App ONLY + │ Interaction? │ (No email) + │ (@mention, │ + │ comment, │ + │ status) │ + └──────────────┘ +``` + +--- + +## 📧 **Email Scenarios - Detailed Implementation** + +### **Scenario 1: Request Created** + +**When:** User submits a new workflow request +**Template:** `getRequestCreatedEmail()` +**Recipients:** Initiator (confirmation) +**Priority:** High +**Also Send In-App:** Yes + +**Integration Point:** +```typescript +// File: services/workflow.service.ts +// Method: createWorkflow() +// Line: ~2362-2373 + +// Add after in-app notification +await emailNotificationService.sendRequestCreated( + workflow, + initiatorUser, + firstApproverUser +); +``` + +--- + +### **Scenario 2: Approval Request (Assignment)** + +**When:** Request assigned to approver +**Template:** `getApprovalRequestEmail()` OR `getMultiApproverRequestEmail()` +**Recipients:** Approver +**Priority:** High +**Also Send In-App:** Yes +**Action Required:** YES + +**Decision Logic:** +- If `totalApprovers > 1` → Use `MultiApproverRequest` template +- If `totalApprovers === 1` → Use `ApprovalRequest` template + +**Integration Point:** +```typescript +// File: services/workflow.service.ts +// Method: createWorkflow() +// Line: ~2374-2396 + +// Add after in-app notification to approver +const isMultiLevel = approvalLevels.length > 1; +await emailNotificationService.sendApprovalRequest( + workflow, + approverUser, + initiatorUser, + isMultiLevel +); +``` + +--- + +### **Scenario 3: Request Approved** + +**When:** Approver approves the request +**Template:** `getApprovalConfirmationEmail()` +**Recipients:** Initiator +**Priority:** High +**Also Send In-App:** Yes + +**Integration Point:** +```typescript +// File: services/approval.service.ts +// Method: approveRequest() +// Line: ~134-145 + +// Add after in-app notification +await emailNotificationService.sendApprovalConfirmation( + workflow, + approverUser, + initiatorUser, + isFinalApproval, + nextApproverUser +); +``` + +--- + +### **Scenario 4: Request Rejected** + +**When:** Approver rejects the request +**Template:** `getRejectionNotificationEmail()` +**Recipients:** Initiator, Spectators +**Priority:** CRITICAL +**Also Send In-App:** Yes + +**Integration Point:** +```typescript +// File: services/approval.service.ts +// Method: rejectRequest() +// Line: ~513-525 + +// Add after in-app notification +await emailNotificationService.sendRejectionNotification( + workflow, + approverUser, + initiatorUser, + rejectionReason +); +``` + +--- + +### **Scenario 5: TAT Reminders (3 Levels)** + +**When:** 50%, 75%, and 100% (breach) of TAT elapsed +**Template:** `getTATReminderEmail()` (50%, 75%), `getTATBreachedEmail()` (100%) +**Recipients:** Approver +**Priority:** Medium (50%), High (75%), Critical (100%) +**Also Send In-App:** Yes + +**Integration Point:** +```typescript +// File: queues/tatProcessor.ts +// Update TAT thresholds to: 50%, 75%, 100% (breach) + +// TAT 50% - Early Warning +if (elapsedPercentage >= 50 && !reminded50) { + // In-app notification + await notificationService.sendToUsers([approverId], { + title: 'TAT Reminder - 50% Elapsed', + type: 'tat_reminder_50', + priority: 'MEDIUM' + }); + + // Email (check preferences) + const shouldEmail = await shouldSendEmail( + approverId, + EmailNotificationType.TAT_REMINDER_50 + ); + + if (shouldEmail) { + await emailNotificationService.sendTATReminder( + workflow, + approver, + { threshold: 50, timeRemaining: '...' } + ); + } +} + +// TAT 75% - Urgent Warning +if (elapsedPercentage >= 75 && !reminded75) { + // In-app notification + await notificationService.sendToUsers([approverId], { + title: 'TAT Reminder - 75% Elapsed', + type: 'tat_reminder_75', + priority: 'HIGH' + }); + + // Email (check preferences) + const shouldEmail = await shouldSendEmail( + approverId, + EmailNotificationType.TAT_REMINDER_75 + ); + + if (shouldEmail) { + await emailNotificationService.sendTATReminder( + workflow, + approver, + { threshold: 75, timeRemaining: '...' } + ); + } +} + +// TAT 100% - BREACH (Critical) +if (elapsedPercentage >= 100) { + // In-app notification + await notificationService.sendToUsers([approverId], { + title: 'TAT BREACHED', + type: 'tat_breach', + priority: 'URGENT' + }); + + // Email (CRITICAL - check with override) + const shouldEmail = await shouldSendEmailWithOverride( + approverId, + EmailNotificationType.TAT_BREACHED + ); + + if (shouldEmail) { + await emailNotificationService.sendTATBreached( + workflow, + approver, + { timeOverdue: '...' } + ); + + // Also notify management + const mgmtUsers = await getManagementUsers(); + for (const mgmt of mgmtUsers) { + await emailNotificationService.sendTATBreached( + workflow, + mgmt, + { timeOverdue: '...' } + ); + } + } +} +``` + +**Note:** Activity logs are captured at ALL TAT thresholds regardless of email/notification preferences! + +--- + +### **Scenario 6: TAT Breached** + +**When:** TAT deadline passed +**Template:** `getTATBreachedEmail()` +**Recipients:** Approver, Management (escalation) +**Priority:** CRITICAL +**Also Send In-App:** Yes + +**Integration Point:** +```typescript +// File: queues/tatProcessor.ts +// When: breach occurs +// Line: ~250-265 + +if (type === 'breach') { + // Existing in-app notification + await notificationService.sendToUsers([...]); + + // Add email to approver + await emailNotificationService.sendTATBreached( + workflow, + approverUser, + tatData + ); + + // Also notify management + const managementUsers = await getManagementUsers(); + for (const mgmt of managementUsers) { + await emailNotificationService.sendTATBreached( + workflow, + mgmt, + tatData + ); + } +} +``` + +--- + +### **Scenario 7: Workflow Resumed** + +**When:** Paused workflow resumes (auto or manual) +**Template:** `getWorkflowResumedEmail()` +**Recipients:** Current Approver, Initiator +**Priority:** High +**Also Send In-App:** Yes + +**Integration Point:** +```typescript +// File: services/pause.service.ts +// Method: resumeWorkflow() +// Line: ~455-510 + +// Add after in-app notifications +await emailNotificationService.sendWorkflowResumed( + workflow, + approverUser, + initiatorUser, + resumedByUser, + pauseDuration +); +``` + +--- + +### **Scenario 8: Request Closed** + +**When:** Initiator closes the request after all approvals +**Template:** `getRequestClosedEmail()` +**Recipients:** All participants (approvers, spectators) +**Priority:** Low (can be batched) +**Also Send In-App:** Yes + +**Integration Point:** +```typescript +// File: controllers/conclusion.controller.ts +// Method: finalizeConclusion() +// Line: ~365-380 + +// Add after workflow closure +const allParticipants = await getRequestParticipants(requestId); +for (const participant of allParticipants) { + await emailNotificationService.sendRequestClosed( + workflow, + participant, + conclusionData + ); +} +``` + +--- + +## 🚫 **DO NOT Send Email For:** + +### **Real-Time Collaboration (In-App Only):** + +1. **@Mentions in Work Notes** + - Type: `mention` + - Why: Real-time, user is likely active + - Keep: In-app notification only + +2. **Comments/Work Notes Added** + - Type: `comment` + - Why: Frequent, real-time discussion + - Keep: In-app notification only + +3. **Documents Uploaded** + - Type: `document_added` + - Why: Minor update, user can see in activity + - Keep: In-app notification only + +4. **Status Changes** + - Type: `status_change` + - Why: Covered by other email notifications + - Keep: In-app notification only + +5. **AI/Summary Generation** + - Type: `ai_conclusion_generated`, `summary_generated` + - Why: Background process, not urgent + - Keep: In-app notification only + +6. **System Events** + - Type: `auto-resume-workflow` + - Why: System-triggered, covered by resume email + - Keep: In-app notification only + +--- + +## 🔄 **Implementation Phases** + +### **Phase 1: Core Email Service (Week 1)** +- [ ] Create `email.service.ts` +- [ ] Configure SMTP with nodemailer +- [ ] Test email sending with test account +- [ ] Add error handling and retry logic + +### **Phase 2: Email Notification Service (Week 1-2)** +- [ ] Create `emailNotification.service.ts` +- [ ] Implement all 12 template functions +- [ ] Add data formatting helpers +- [ ] Test with sample data + +### **Phase 3: Critical Notifications (Week 2)** +- [ ] Integrate TAT Breached emails +- [ ] Integrate Rejection emails +- [ ] Test critical scenarios + +### **Phase 4: High Priority Notifications (Week 2-3)** +- [ ] Integrate Request Created emails +- [ ] Integrate Approval Request emails +- [ ] Integrate Approval Confirmation emails +- [ ] Integrate TAT Reminder emails +- [ ] Integrate Workflow Resumed emails + +### **Phase 5: Medium/Low Priority (Week 3)** +- [ ] Integrate Workflow Paused emails (conditional) +- [ ] Integrate Participant Added emails (conditional) +- [ ] Integrate Approver Skipped emails +- [ ] Integrate Request Closed emails + +### **Phase 6: Email Queue System (Week 4)** +- [ ] Install Bull/BullMQ +- [ ] Create email queue +- [ ] Implement priority-based sending +- [ ] Add retry mechanism +- [ ] Monitor email delivery + +### **Phase 7: User Preferences (Week 4-5)** +- [ ] Add email preferences to user settings +- [ ] Allow users to opt-in/out of certain emails +- [ ] Keep critical emails always enabled +- [ ] Add digest email option (daily summary) + +--- + +## ⚙️ **Environment Configuration** + +Add to `.env`: + +```env +# Email Service +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=notifications@royalenfield.com +SMTP_PASSWORD=your-app-specific-password +EMAIL_FROM=RE Flow + +# Email Settings +EMAIL_ENABLED=true +EMAIL_QUEUE_ENABLED=true +EMAIL_BATCH_SIZE=50 +EMAIL_RETRY_ATTEMPTS=3 + +# Application +BASE_URL=https://workflow.royalenfield.com +COMPANY_NAME=Royal Enfield +COMPANY_WEBSITE=https://www.royalenfield.com +SUPPORT_EMAIL=support@royalenfield.com +``` + +--- + +## 📊 **Expected Email Volume** + +Based on typical workflow usage: + +| Email Type | Frequency | Daily Volume (Est.) | +|------------|-----------|-------------------| +| Request Created | Per new request | 20-50 | +| Approval Request | Per assignment | 20-50 | +| Approval Confirmation | Per approval | 15-40 | +| Rejection | Occasional | 2-5 | +| TAT Reminder | 80% threshold | 5-15 | +| TAT Breached | Critical | 1-5 | +| Workflow Resumed | Occasional | 2-10 | +| Request Closed | Per closure | 10-30 | +| **TOTAL** | | **75-205 emails/day** | + +**No emails** for @mentions, comments, documents (could be 100s per day). + +--- + +## ✅ **Success Metrics** + +### **Email Delivery:** +- Delivery rate > 98% +- Bounce rate < 2% +- Open rate > 40% (approvers) +- Click rate > 25% (View Details button) + +### **User Experience:** +- Reduced missed approvals +- Faster TAT completion +- Better stakeholder awareness +- No email fatigue (not too many emails) + +--- + +## 🚀 **Next Steps** + +1. **Review this plan** - Confirm scenarios and priorities +2. **Configure SMTP** - Set up email credentials +3. **Start with Phase 1** - Build email service +4. **Test thoroughly** - Use test account first +5. **Roll out gradually** - Start with critical emails only +6. **Monitor metrics** - Track delivery and engagement +7. **Gather feedback** - Adjust based on user preferences + +--- + +**Ready to start implementation?** 🎯 + diff --git a/src/emailtemplates/README.md b/src/emailtemplates/README.md new file mode 100644 index 0000000..8fb5c59 --- /dev/null +++ b/src/emailtemplates/README.md @@ -0,0 +1,373 @@ +# 📧 Royal Enfield Workflow - Email Templates + +Professional, responsive, and dynamic email templates for the Royal Enfield Workflow System. + +--- + +## 🎯 Key Features + +✅ **Dynamic & Adaptive** - Templates adjust based on request data, priority, and workflow state +✅ **Single Action Button** - Only "View Request Details" button that redirects to request URL +✅ **Fully Responsive** - Mobile-friendly design that works across all devices +✅ **Professional Design** - Royal Enfield branded with color-coded scenarios +✅ **Email Client Compatible** - Table-based layout for maximum compatibility +✅ **Placeholder-Based** - Easy to integrate with backend templating engines + +--- + +## 📁 Template Files (12 Templates) + +### Core Workflow Templates +1. **RequestCreated.html** - Request submission confirmation +2. **ApprovalRequest.html** - Single approver notification +3. **MultiApproverRequest.html** - Multi-level approver notification with chain +4. **ApprovalConfirmation.html** - Approval confirmation notification +5. **RejectionNotification.html** - Rejection notification + +### TAT Management Templates +6. **TATReminder.html** - TAT deadline reminder (80% threshold) +7. **TATBreached.html** - TAT breach escalation + +### Workflow Control Templates +8. **WorkflowPaused.html** - Workflow pause notification +9. **WorkflowResumed.html** - Workflow resume notification + +### Participant Management Templates +10. **ParticipantAdded.html** - New approver/spectator welcome +11. **ApproverSkipped.html** - Approver skip notification +12. **RequestClosed.html** - Request closure summary + +--- + +## 🎨 Visual Design + +Each template uses color-coded gradients to indicate the scenario: + +| Template | Header Color | Purpose | +|----------|-------------|---------| +| RequestCreated | Purple (#667eea) | Information | +| ApprovalRequest | Purple (#667eea) | Action Required | +| MultiApproverRequest | Purple (#667eea) | Action Required | +| ApprovalConfirmation | Green (#28a745) | Success | +| RejectionNotification | Red (#dc3545) | Error/Rejection | +| TATReminder | Orange (#ff9800) | Warning | +| TATBreached | Red (#dc3545) | Critical | +| WorkflowPaused | Gray (#6c757d) | Neutral | +| WorkflowResumed | Green (#28a745) | Success | +| ParticipantAdded | Purple (#667eea) | Information | +| ApproverSkipped | Cyan (#17a2b8) | Information | +| RequestClosed | Purple (#6f42c1) | Complete | + +--- + +## 🔗 View Details Button + +All templates feature a single action button: +- **Text:** "View Request Details" / "Review Request Now" / "Take Action Now" +- **Link Format:** `{baseURL}/request/{requestNumber}` +- **Example:** `https://workflow.royalenfield.com/request/REQ-2025-12-0013` + +No approval/rejection buttons in emails - all actions happen within the application. + +--- + +## 📋 How to Use Templates + +### 1. Load Template File +```javascript +const fs = require('fs'); +const template = fs.readFileSync('./emailtemplates/ApprovalRequest.html', 'utf8'); +``` + +### 2. Replace Placeholders +```javascript +let emailContent = template + .replace(/\[ApproverName\]/g, approverName) + .replace(/\[InitiatorName\]/g, initiatorName) + .replace(/\[RequestId\]/g, requestId) + .replace(/\[ViewDetailsLink\]/g, `${baseURL}/request/${requestNumber}`) + .replace(/\[CompanyName\]/g, 'Royal Enfield'); +``` + +### 3. Handle Dynamic Sections +```javascript +// Priority Section Example +if (priority === 'HIGH' || priority === 'CRITICAL') { + const priorityHTML = ` +
+

High Priority

+

+ This request has been marked as ${priority} priority and requires prompt attention. +

+
+ `; + emailContent = emailContent.replace('[PrioritySection]', priorityHTML); +} else { + emailContent = emailContent.replace('[PrioritySection]', ''); +} +``` + +### 4. Send Email +```javascript +await sendEmail({ + to: recipientEmail, + subject: `[${requestId}] New Approval Request`, + html: emailContent +}); +``` + +--- + +## 📖 Complete Documentation + +See **TEMPLATE_MAPPING.md** for: +- Complete list of all placeholders for each template +- Dynamic section handling instructions +- Priority-based sending strategy +- Usage scenarios and triggers +- Security considerations +- Implementation examples + +--- + +## 🚀 Integration Steps + +### Step 1: Email Service Setup +```typescript +// services/email.service.ts +import nodemailer from 'nodemailer'; +import fs from 'fs'; +import path from 'path'; + +class EmailService { + private transporter; + + constructor() { + this.transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + secure: process.env.SMTP_SECURE === 'true', + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD + } + }); + } + + async sendEmail(to: string, subject: string, html: string) { + return await this.transporter.sendMail({ + from: process.env.EMAIL_FROM, + to, + subject, + html + }); + } + + loadTemplate(templateName: string): string { + const templatePath = path.join(__dirname, '../emailtemplates', `${templateName}.html`); + return fs.readFileSync(templatePath, 'utf8'); + } + + replaceplaceholders(template: string, data: Record): string { + let result = template; + for (const [key, value] of Object.entries(data)) { + const regex = new RegExp(`\\[${key}\\]`, 'g'); + result = result.replace(regex, value); + } + return result; + } +} + +export const emailService = new EmailService(); +``` + +### Step 2: Create Email Handlers +```typescript +// services/emailNotification.service.ts +export async function sendApprovalRequest(approverEmail: string, requestData: any) { + const template = emailService.loadTemplate('ApprovalRequest'); + + const placeholders = { + ApproverName: approverEmail.split('@')[0], + InitiatorName: requestData.initiatorName, + RequestId: requestData.requestNumber, + RequestDate: formatDate(requestData.createdAt), + RequestTime: formatTime(requestData.createdAt), + RequestType: requestData.requestType, + RequestDescription: requestData.description, + PrioritySection: getPrioritySection(requestData.priority), + ViewDetailsLink: `${process.env.BASE_URL}/request/${requestData.requestNumber}`, + CompanyName: 'Royal Enfield' + }; + + const html = emailService.replaceplaceholders(template, placeholders); + + await emailService.sendEmail( + approverEmail, + `[${requestData.requestNumber}] New Approval Request`, + html + ); +} +``` + +### Step 3: Add to Workflow Events +```typescript +// In workflow.service.ts +await sendApprovalRequest(approverEmail, requestData); +``` + +--- + +## 🔧 Environment Variables Required + +Add to your `.env` file: + +```env +# SMTP Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=your-email@domain.com +SMTP_PASSWORD=your-app-password + +# Email Settings +EMAIL_FROM=RE Workflow System +BASE_URL=https://workflow.royalenfield.com +COMPANY_NAME=Royal Enfield +``` + +--- + +## ✅ Testing Templates + +### Preview in Browser +Open any `.html` file directly in a browser to see the design. Placeholders will be visible as `[PlaceholderName]`. + +### Test Email Sending +```typescript +// test/email.test.ts +import { emailService } from '../services/email.service'; + +describe('Email Templates', () => { + it('should send approval request email', async () => { + const template = emailService.loadTemplate('ApprovalRequest'); + const testData = { + ApproverName: 'John Doe', + InitiatorName: 'Jane Smith', + RequestId: 'REQ-2025-12-0013', + // ... other test data + }; + + const html = emailService.replaceplaceholders(template, testData); + + // Send to test email + await emailService.sendEmail('test@example.com', 'Test Email', html); + }); +}); +``` + +--- + +## 📊 Template Selection Logic + +```typescript +function getTemplateForScenario(scenario: string, hasMultipleApprovers: boolean): string { + switch(scenario) { + case 'request_created': + return 'RequestCreated'; + case 'approval_required': + return hasMultipleApprovers ? 'MultiApproverRequest' : 'ApprovalRequest'; + case 'request_approved': + return 'ApprovalConfirmation'; + case 'request_rejected': + return 'RejectionNotification'; + case 'tat_reminder': + return 'TATReminder'; + case 'tat_breached': + return 'TATBreached'; + case 'workflow_paused': + return 'WorkflowPaused'; + case 'workflow_resumed': + return 'WorkflowResumed'; + case 'participant_added': + return 'ParticipantAdded'; + case 'approver_skipped': + return 'ApproverSkipped'; + case 'request_closed': + return 'RequestClosed'; + default: + throw new Error(`Unknown scenario: ${scenario}`); + } +} +``` + +--- + +## 🎯 Priority Queue Implementation + +Use Bull/BullMQ for reliable email delivery: + +```typescript +import Queue from 'bull'; + +const emailQueue = new Queue('email-notifications', { + redis: { + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT + } +}); + +// Add email to queue with priority +export async function queueEmail(emailData: any, priority: 'critical' | 'high' | 'medium' | 'low') { + const priorityMap = { critical: 1, high: 2, medium: 3, low: 4 }; + + await emailQueue.add('send-email', emailData, { + priority: priorityMap[priority], + attempts: 3, + backoff: { + type: 'exponential', + delay: 5000 + } + }); +} + +// Process email queue +emailQueue.process('send-email', async (job) => { + const { to, subject, html } = job.data; + await emailService.sendEmail(to, subject, html); +}); +``` + +--- + +## 📞 Support & Customization + +For template customization or issues: +1. Check **TEMPLATE_MAPPING.md** for complete documentation +2. Test templates in email clients (Gmail, Outlook, Apple Mail) +3. Validate HTML using online validators +4. Use email testing services (Litmus, Email on Acid) + +--- + +## 📝 Change Log + +**Version 2.0** (Dec 4, 2025) +- ✅ Removed all action buttons (Approve/Reject) +- ✅ Added single "View Request Details" button +- ✅ Made templates fully dynamic with conditional sections +- ✅ Added 12 templates for all scenarios +- ✅ Improved mobile responsiveness +- ✅ Professional design without emojis +- ✅ Added comprehensive documentation + +**Version 1.0** (Initial Release) +- Basic templates with action buttons +- 4 templates (Approval, Rejection, Multi-Approver, Confirmation) + +--- + +**Maintained by:** Royal Enfield Development Team +**Last Updated:** December 4, 2025 +**Email Template Version:** 2.0 + diff --git a/src/emailtemplates/RICHTEXT_SUPPORT.md b/src/emailtemplates/RICHTEXT_SUPPORT.md new file mode 100644 index 0000000..0a97079 --- /dev/null +++ b/src/emailtemplates/RICHTEXT_SUPPORT.md @@ -0,0 +1,293 @@ +# Rich Text Editor Support in Email Templates + +## ✅ **Rich Text Format Ready** + +Your email templates now support HTML content from rich text editors like: +- Quill +- TinyMCE +- CKEditor +- Draft.js +- Slate +- Tiptap + +--- + +## 🎨 **Supported HTML Elements** + +### **Text Formatting** +✅ ``, `` - **Bold text** +✅ ``, `` - *Italic text* +✅ `` - Underlined text +✅ `

` - Paragraphs with spacing +✅ `
` - Line breaks + +### **Lists** +✅ `

    `, `
  • ` - Bulleted lists +✅ `
      `, `
    1. ` - Numbered lists + +### **Headings** +✅ `

      ` - 20px heading +✅ `

      ` - 18px heading +✅ `

      ` - 16px heading +✅ `

      ` - 14px heading + +### **Links** +✅ `` - Clickable links (Royal Enfield Red color) + +### **Special Elements** +✅ `
      ` - Quote blocks with gold accent +✅ `` - Inline code snippets +✅ `
      ` - Code blocks  
      +✅ `
      ` - Horizontal dividers +✅ `` - Images (auto-scaled for mobile) + +--- + +## 📝 **Usage Example** + +### **From Rich Text Editor:** + +Your rich text editor might output: +```html +

      This is a purchase request for new equipment.

      +
        +
      • Item 1: Motorcycle helmet
      • +
      • Item 2: Safety gear
      • +
      +

      Total cost: $5,000

      +``` + +### **In Your Code:** + +```typescript +import { getApprovalRequestEmail } from '@/emailtemplates'; + +const data = { + // ... other fields + requestDescription: editorHtmlContent, // Pass HTML directly + // ... other fields +}; + +const html = getApprovalRequestEmail(data); +``` + +### **Result in Email:** + +The HTML will be properly styled with: +- Royal Enfield brand colors +- Proper spacing and line heights +- Mobile-responsive fonts +- Email-safe formatting + +--- + +## 🔒 **Security - HTML Sanitization** + +**IMPORTANT:** Always sanitize HTML content before using in emails! + +### **Recommended: Use DOMPurify or similar** + +```typescript +import DOMPurify from 'isomorphic-dompurify'; + +// Sanitize rich text content +const sanitizedDescription = DOMPurify.sanitize(editorHtmlContent, { + ALLOWED_TAGS: [ + 'p', 'br', 'strong', 'b', 'em', 'i', 'u', + 'ul', 'ol', 'li', 'a', 'h1', 'h2', 'h3', 'h4', + 'blockquote', 'code', 'pre', 'hr', 'img' + ], + ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'target'], + ALLOW_DATA_ATTR: false +}); + +const data = { + requestDescription: sanitizedDescription, + // ... other fields +}; +``` + +### **Install DOMPurify:** + +```bash +npm install isomorphic-dompurify +npm install --save-dev @types/dompurify +``` + +--- + +## 🎯 **Rich Text Styling** + +### **Brand Colors Applied:** + +| Element | Color | Usage | +|---------|-------|-------| +| Links | #DB281B (RE Red) | Clickable links | +| Strong text | #1a1a1a (Black) | Bold emphasis | +| Blockquotes | Gold border (#DEB219) | Quoted text | +| Code | #DB281B (RE Red) | Code snippets | +| Headings | #1a1a1a (Black) | Section titles | + +### **Spacing:** + +- Paragraphs: 12px bottom margin +- Lists: 25px left padding, 8px item spacing +- Headings: 12px bottom margin +- Blockquotes: 12px padding, gold left border +- Images: Auto-scaled, 12px margin + +--- + +## 📱 **Mobile Responsive** + +Rich text content automatically adjusts for mobile: + +| Element | Desktop | Mobile | +|---------|---------|--------| +| Paragraphs | 14px | 13px | +| Headings H1 | 20px | 18px | +| Headings H2 | 18px | 16px | +| Lists | 14px | 13px | +| Images | 100% width | 100% width | + +--- + +## 💡 **Example Rich Text Content** + +### **Complex Example:** + +```html +

      Equipment Purchase Request

      + +

      We need to purchase the following items for the testing department:

      + +
        +
      • 2× Royal Enfield Himalayan - Latest model
      • +
      • Safety gear - Helmets and protective equipment
      • +
      • Maintenance tools - Standard toolkit
      • +
      + +

      Budget Breakdown

      + +

      Total estimated cost: $15,000

      + +
      + This purchase is critical for our Q1 testing schedule and has been approved by the department head. +
      + +

      Please review and approve at your earliest convenience.

      + +

      For questions, contact: john@example.com

      +``` + +### **Will Render As:** + +✅ Styled headings with proper hierarchy +✅ Formatted lists with Royal Enfield accents +✅ Bold text in black +✅ Gold-bordered quote box +✅ Red links +✅ Proper spacing throughout +✅ Mobile-optimized fonts + +--- + +## 🛡️ **Best Practices** + +### **1. Sanitize User Input** +```typescript +const cleanHTML = DOMPurify.sanitize(userInput); +``` + +### **2. Use wrapRichText Helper** +```typescript +${wrapRichText(data.requestDescription)} +``` + +### **3. Test with Rich Content** +Test emails with: +- Multiple paragraphs +- Lists (bulleted and numbered) +- Links +- Bold and italic text +- Headings at different levels + +### **4. Avoid These in Emails** +❌ JavaScript or `