diff --git a/backend-go/.env.example b/backend-go/.env.example new file mode 100644 index 0000000..b1740a6 --- /dev/null +++ b/backend-go/.env.example @@ -0,0 +1,16 @@ +# Server +PORT=3001 +API_BASE_PATH=/v1 +NODE_ENV=development + +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/koin_ping + +# Ethereum JSON-RPC +ETH_RPC_URL=https://mainnet.infura.io/v3/YOUR-PROJECT-ID + +# Polling interval (ms, minimum 1000) +POLL_INTERVAL_MS=60000 + +# Firebase +FIREBASE_PROJECT_ID=koin-ping diff --git a/backend/ENV_TEMPLATE.md b/backend-go/ENV_TEMPLATE.md similarity index 98% rename from backend/ENV_TEMPLATE.md rename to backend-go/ENV_TEMPLATE.md index ac5ac13..6992c7f 100644 --- a/backend/ENV_TEMPLATE.md +++ b/backend-go/ENV_TEMPLATE.md @@ -5,6 +5,7 @@ Copy this to `.env` in the backend directory and fill in your values. ```bash # Server Configuration PORT=3001 +API_BASE_PATH=/v1 NODE_ENV=development # Database Configuration diff --git a/backend-go/README.md b/backend-go/README.md new file mode 100644 index 0000000..8f6f137 --- /dev/null +++ b/backend-go/README.md @@ -0,0 +1,4 @@ +Run Backend: + +cd /Users/kjannette/workspace/koin_ping/backend-go +go run ./cmd/api \ No newline at end of file diff --git a/backend-go/cmd/api/main.go b/backend-go/cmd/api/main.go index 1ba221b..cef0028 100644 --- a/backend-go/cmd/api/main.go +++ b/backend-go/cmd/api/main.go @@ -44,34 +44,36 @@ func main() { notifConfigHandler := handlers.NewNotificationConfigHandler(notifConfigModel) mux := http.NewServeMux() + b := cfg.APIBasePath // e.g. "/v1" // Public routes - mux.HandleFunc("GET /api/health", handlers.HealthCheck) - mux.HandleFunc("GET /api/status", handlers.SystemStatus) + mux.HandleFunc("GET "+b+"/health", handlers.HealthCheck) + mux.HandleFunc("GET "+b+"/status", handlers.SystemStatus) // Authenticated routes — addresses - mux.Handle("POST /api/addresses", middleware.Authenticate(http.HandlerFunc(addressHandler.Create))) - mux.Handle("GET /api/addresses", middleware.Authenticate(http.HandlerFunc(addressHandler.List))) - mux.Handle("DELETE /api/addresses/{addressId}", middleware.Authenticate(http.HandlerFunc(addressHandler.Remove))) + mux.Handle("POST "+b+"/addresses", middleware.Authenticate(http.HandlerFunc(addressHandler.Create))) + mux.Handle("GET "+b+"/addresses", middleware.Authenticate(http.HandlerFunc(addressHandler.List))) + mux.Handle("DELETE "+b+"/addresses/{addressId}", middleware.Authenticate(http.HandlerFunc(addressHandler.Remove))) // Authenticated routes — alert rules - mux.Handle("POST /api/addresses/{addressId}/alerts", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Create))) - mux.Handle("GET /api/addresses/{addressId}/alerts", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.ListByAddress))) - mux.Handle("PATCH /api/alerts/{alertId}", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.UpdateStatus))) - mux.Handle("DELETE /api/alerts/{alertId}", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Remove))) + mux.Handle("POST "+b+"/addresses/{addressId}/alerts", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Create))) + mux.Handle("GET "+b+"/addresses/{addressId}/alerts", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.ListByAddress))) + mux.Handle("PATCH "+b+"/alerts/{alertId}", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.UpdateStatus))) + mux.Handle("DELETE "+b+"/alerts/{alertId}", middleware.Authenticate(http.HandlerFunc(alertRuleHandler.Remove))) // Authenticated routes — alert events - mux.Handle("GET /api/alert-events", middleware.Authenticate(http.HandlerFunc(alertEventHandler.List))) + mux.Handle("GET "+b+"/alert-events", middleware.Authenticate(http.HandlerFunc(alertEventHandler.List))) // Authenticated routes — notification config - mux.Handle("GET /api/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.GetConfig))) - mux.Handle("PUT /api/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.UpdateConfig))) - mux.Handle("DELETE /api/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.DeleteConfig))) + mux.Handle("GET "+b+"/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.GetConfig))) + mux.Handle("PUT "+b+"/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.UpdateConfig))) + mux.Handle("DELETE "+b+"/notification-config", middleware.Authenticate(http.HandlerFunc(notifConfigHandler.DeleteConfig))) handler := corsMiddleware(mux) addr := fmt.Sprintf(":%d", cfg.Port) log.Printf("Server running on port %d", cfg.Port) + log.Printf("API base path: %s", cfg.APIBasePath) log.Printf("Environment: %s", cfg.NodeEnv) if err := http.ListenAndServe(addr, handler); err != nil { diff --git a/backend-go/internal/config/config.go b/backend-go/internal/config/config.go index 4f37d8e..29f969b 100644 --- a/backend-go/internal/config/config.go +++ b/backend-go/internal/config/config.go @@ -9,6 +9,7 @@ import ( type Config struct { Port int + APIBasePath string DatabaseURL string DBHost string DBPort int @@ -24,6 +25,7 @@ type Config struct { func Load() (*Config, error) { cfg := &Config{ Port: getEnvInt("PORT", 3001), + APIBasePath: getEnv("API_BASE_PATH", "/v1"), DatabaseURL: os.Getenv("DATABASE_URL"), DBHost: getEnv("DB_HOST", "localhost"), DBPort: getEnvInt("DB_PORT", 5432), diff --git a/backend-go/internal/handlers/address.go b/backend-go/internal/handlers/address.go index da3d6b7..07c8b91 100644 --- a/backend-go/internal/handlers/address.go +++ b/backend-go/internal/handlers/address.go @@ -30,6 +30,7 @@ func (h *AddressHandler) Create(w http.ResponseWriter, r *http.Request) { Label *string `json:"label"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + log.Printf("Failed to decode address request body: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body") return } diff --git a/backend-go/internal/handlers/alert_rule.go b/backend-go/internal/handlers/alert_rule.go index 01a8799..1c77880 100644 --- a/backend-go/internal/handlers/alert_rule.go +++ b/backend-go/internal/handlers/alert_rule.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "strconv" "strings" "github.com/kjannette/koin-ping/backend-go/internal/domain" @@ -30,15 +31,23 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { } var body struct { - Type string `json:"type"` - Threshold *float64 `json:"threshold"` + Type string `json:"type"` + Threshold json.RawMessage `json:"threshold"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + log.Printf("Failed to decode alert request body: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body") return } - log.Printf("User %s creating alert for address ID: %d", userID, addressID) + threshold, err := parseThreshold(body.Threshold) + if err != nil { + log.Printf("Failed to parse threshold: %v", err) + writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "threshold must be a valid number") + return + } + + log.Printf("User %s creating alert: type=%s, addressID=%d", userID, body.Type, addressID) if body.Type == "" { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Alert type is required") @@ -57,7 +66,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { alertType := domain.AlertType(body.Type) if domain.IsThresholdRequired(alertType) { - if body.Threshold == nil || *body.Threshold <= 0 { + if threshold == nil || *threshold <= 0 { writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", fmt.Sprintf("Alert type '%s' requires a positive threshold value", body.Type)) return @@ -76,7 +85,7 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { return } - newAlert, err := h.alertRules.Create(r.Context(), addressID, alertType, body.Threshold) + newAlert, err := h.alertRules.Create(r.Context(), addressID, alertType, threshold) if err != nil { log.Printf("Error creating alert rule: %v", err) writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Failed to create alert rule") @@ -87,6 +96,37 @@ func (h *AlertRuleHandler) Create(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusCreated, newAlert) } +func parseThreshold(raw json.RawMessage) (*float64, error) { + if len(raw) == 0 { + return nil, nil + } + + // Check null before number -- json.Unmarshal treats null as valid for float64 (sets to 0). + if string(raw) == "null" { + return nil, nil + } + + var asNumber float64 + if err := json.Unmarshal(raw, &asNumber); err == nil { + return &asNumber, nil + } + + var asString string + if err := json.Unmarshal(raw, &asString); err == nil { + asString = strings.TrimSpace(asString) + if asString == "" { + return nil, nil + } + parsed, parseErr := strconv.ParseFloat(asString, 64) + if parseErr != nil { + return nil, parseErr + } + return &parsed, nil + } + + return nil, fmt.Errorf("unsupported threshold format") +} + func (h *AlertRuleHandler) ListByAddress(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) addressID, ok := parseIntParam(r.PathValue("addressId")) @@ -136,6 +176,7 @@ func (h *AlertRuleHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) Enabled *bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + log.Printf("Failed to decode update request body: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body") return } diff --git a/backend-go/internal/handlers/alert_rule_test.go b/backend-go/internal/handlers/alert_rule_test.go new file mode 100644 index 0000000..b9a2d50 --- /dev/null +++ b/backend-go/internal/handlers/alert_rule_test.go @@ -0,0 +1,214 @@ +package handlers + +import ( + "encoding/json" + "math" + "testing" +) + +func TestParseThreshold(t *testing.T) { + t.Run("nil/empty raw message returns nil", func(t *testing.T) { + val, err := parseThreshold(nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val != nil { + t.Fatalf("expected nil, got %v", *val) + } + }) + + t.Run("empty slice returns nil", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val != nil { + t.Fatalf("expected nil, got %v", *val) + } + }) + + t.Run("JSON null returns nil", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage("null")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val != nil { + t.Fatalf("expected nil, got %v", *val) + } + }) + + t.Run("number 10 returns 10.0", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage("10")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val == nil { + t.Fatal("expected non-nil value") + } + if *val != 10.0 { + t.Fatalf("expected 10.0, got %v", *val) + } + }) + + t.Run("number 0.5 returns 0.5", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage("0.5")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val == nil || *val != 0.5 { + t.Fatalf("expected 0.5, got %v", val) + } + }) + + t.Run("string '10' returns 10.0", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage(`"10"`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val == nil || *val != 10.0 { + t.Fatalf("expected 10.0, got %v", val) + } + }) + + t.Run("string '0.001' returns 0.001", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage(`"0.001"`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val == nil || math.Abs(*val-0.001) > 1e-9 { + t.Fatalf("expected 0.001, got %v", val) + } + }) + + t.Run("string with spaces ' 10 ' returns 10.0", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage(`" 10 "`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val == nil || *val != 10.0 { + t.Fatalf("expected 10.0, got %v", val) + } + }) + + t.Run("empty string returns nil", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage(`""`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val != nil { + t.Fatalf("expected nil, got %v", *val) + } + }) + + t.Run("whitespace-only string returns nil", func(t *testing.T) { + val, err := parseThreshold(json.RawMessage(`" "`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if val != nil { + t.Fatalf("expected nil, got %v", *val) + } + }) + + t.Run("invalid string returns error", func(t *testing.T) { + _, err := parseThreshold(json.RawMessage(`"abc"`)) + if err == nil { + t.Fatal("expected error for non-numeric string") + } + }) + + t.Run("boolean returns error", func(t *testing.T) { + _, err := parseThreshold(json.RawMessage("true")) + if err == nil { + t.Fatal("expected error for boolean") + } + }) + + t.Run("array returns error", func(t *testing.T) { + _, err := parseThreshold(json.RawMessage("[1,2]")) + if err == nil { + t.Fatal("expected error for array") + } + }) +} + +func TestDecodeAlertBody(t *testing.T) { + // Verifies that the struct used in Create handler can decode all + // payload shapes the frontend might send. + + type alertBody struct { + Type string `json:"type"` + Threshold json.RawMessage `json:"threshold"` + } + + tests := []struct { + name string + payload string + wantErr bool + }{ + { + name: "incoming_tx without threshold", + payload: `{"type":"incoming_tx"}`, + }, + { + name: "outgoing_tx without threshold", + payload: `{"type":"outgoing_tx"}`, + }, + { + name: "large_transfer with number threshold", + payload: `{"type":"large_transfer","threshold":10}`, + }, + { + name: "large_transfer with string threshold", + payload: `{"type":"large_transfer","threshold":"10"}`, + }, + { + name: "balance_below with number threshold", + payload: `{"type":"balance_below","threshold":0.5}`, + }, + { + name: "threshold null", + payload: `{"type":"incoming_tx","threshold":null}`, + }, + { + name: "empty object", + payload: `{}`, + }, + { + name: "empty body", + payload: ``, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var body alertBody + err := json.Unmarshal([]byte(tt.payload), &body) + if tt.wantErr { + if err == nil { + t.Fatal("expected decode error") + } + return + } + if err != nil { + t.Fatalf("unexpected decode error: %v", err) + } + }) + } +} + +func TestOldStructFailsWithStringThreshold(t *testing.T) { + // Documents the original bug: *float64 cannot decode a string threshold. + type oldAlertBody struct { + Type string `json:"type"` + Threshold *float64 `json:"threshold"` + } + + payload := `{"type":"large_transfer","threshold":"10"}` + var body oldAlertBody + err := json.Unmarshal([]byte(payload), &body) + if err == nil { + t.Fatal("expected error: old struct with *float64 should reject string threshold") + } +} diff --git a/backend-go/internal/handlers/notification_config.go b/backend-go/internal/handlers/notification_config.go index 52bd6b5..d39c534 100644 --- a/backend-go/internal/handlers/notification_config.go +++ b/backend-go/internal/handlers/notification_config.go @@ -55,6 +55,7 @@ func (h *NotificationConfigHandler) UpdateConfig(w http.ResponseWriter, r *http. NotificationEnabled *bool `json:"notification_enabled"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + log.Printf("Failed to decode notification config request body: %v", err) writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "Invalid request body") return } diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 2829408..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -.env -*.log -.DS_Store -dist/ -build/ - diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 240a1be..0000000 --- a/backend/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Koin Ping Backend - -On-chain monitoring and alerting system backend. - -## Environment Variables - -Create a `.env` file in the backend directory with the following variables: - -```env -PORT=3001 -NODE_ENV=development - -# PostgreSQL Database Configuration -DB_HOST=localhost -DB_PORT=5432 -DB_USER=your_db_user -DB_PASSWORD=your_db_password -DB_NAME=koin_ping - -# Blockchain RPC Endpoints (examples) -ETH_RPC_URL=https://mainnet.infura.io/v3/YOUR_INFURA_KEY -POLYGON_RPC_URL=https://polygon-rpc.com - -# Polling Configuration -POLL_INTERVAL_MS=10000 -``` - -## Getting Started - -1. Install dependencies: - ```bash - npm install - ``` - -2. Set up your `.env` file with the required environment variables - -3. Run the development server: - ```bash - npm run dev - ``` - -4. Run in production: - ```bash - npm start - ``` - -## Project Structure - -- `api/` - API endpoints and server configuration -- `poller/` - Blockchain polling logic -- `alerts/` - Alert evaluation and management -- `notifications/` - Notification delivery system -- `domain/` - Domain models and business logic -- `infra/` - Infrastructure (database, external services) -- `shared/` - Shared utilities and helpers - diff --git a/backend/api/routes/addresses.js b/backend/api/routes/addresses.js deleted file mode 100644 index e8252f2..0000000 --- a/backend/api/routes/addresses.js +++ /dev/null @@ -1,20 +0,0 @@ -import express from 'express'; -import { authenticate } from '../../middleware/authenticate.js'; -import * as AddressController from '../../controllers/AddressController.js'; - -const router = express.Router(); - -// Apply authentication to all routes -router.use(authenticate); - -// POST /addresses - Create Blockchain Address -router.post('/', AddressController.create); - -// GET /addresses - List Addresses -router.get('/', AddressController.list); - -// DELETE /addresses/:addressId - Delete Address -router.delete('/:addressId', AddressController.remove); - -export default router; - diff --git a/backend/api/routes/alertEvents.js b/backend/api/routes/alertEvents.js deleted file mode 100644 index 4abac60..0000000 --- a/backend/api/routes/alertEvents.js +++ /dev/null @@ -1,12 +0,0 @@ -import express from 'express'; -import { authenticate } from '../../middleware/authenticate.js'; -import * as AlertEventController from '../../controllers/AlertEventController.js'; - -const router = express.Router(); - -router.use(authenticate); - -router.get('/', AlertEventController.list); - -export default router; - diff --git a/backend/api/routes/alerts.js b/backend/api/routes/alerts.js deleted file mode 100644 index 4d0f5f5..0000000 --- a/backend/api/routes/alerts.js +++ /dev/null @@ -1,18 +0,0 @@ -import express from 'express'; -import { authenticate } from '../../middleware/authenticate.js'; -import * as AlertRuleController from '../../controllers/AlertRuleController.js'; - -const router = express.Router(); - -router.use(authenticate); - -router.post('/:addressId/alerts', AlertRuleController.create); - -router.get('/:addressId/alerts', AlertRuleController.listByAddress); - -router.patch('/:alertId', AlertRuleController.updateStatus); - -router.delete('/:alertId', AlertRuleController.remove); - -export default router; - diff --git a/backend/api/routes/notificationConfig.js b/backend/api/routes/notificationConfig.js deleted file mode 100644 index 080f306..0000000 --- a/backend/api/routes/notificationConfig.js +++ /dev/null @@ -1,16 +0,0 @@ -import express from 'express'; -import { authenticate } from '../../middleware/authenticate.js'; -import * as NotificationConfigController from '../../controllers/NotificationConfigController.js'; - -const router = express.Router(); - -router.use(authenticate); - -router.get('/', NotificationConfigController.getConfig); - -router.put('/', NotificationConfigController.updateConfig); - -router.delete('/', NotificationConfigController.deleteConfig); - -export default router; - diff --git a/backend/api/routes/status.js b/backend/api/routes/status.js deleted file mode 100644 index 3618512..0000000 --- a/backend/api/routes/status.js +++ /dev/null @@ -1,17 +0,0 @@ -import express from 'express'; - -const router = express.Router(); - -// GET /status - System Status (mock) -router.get('/', (req, res) => { - // TEMP - Mock response until poller service is complete - res.json({ - latestBlock: 0, - lag: 0, - status: 'healthy', - timestamp: new Date().toISOString() - }); -}); - -export default router; - diff --git a/backend/api/server.js b/backend/api/server.js deleted file mode 100644 index 1031070..0000000 --- a/backend/api/server.js +++ /dev/null @@ -1,47 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import dotenv from 'dotenv'; -import addressesRouter from './routes/addresses.js'; -import alertsRouter from './routes/alerts.js'; -import alertEventsRouter from './routes/alertEvents.js'; -import statusRouter from './routes/status.js'; -import notificationConfigRouter from './routes/notificationConfig.js'; - -dotenv.config(); - -const app = express(); -const PORT = process.env.PORT || 3001; - -app.use(cors()); -app.use(express.json()); - -// Health check -app.get('/api/health', (req, res) => { - res.json({ - status: 'ok', - timestamp: new Date().toISOString(), - service: 'koin-ping-backend' - }); -}); - - -app.use('/api/addresses', addressesRouter); -app.use('/api/addresses', alertsRouter); // Alert routes include addressId in path -app.use('/api/alerts', alertsRouter); // Direct alert operations (PATCH, DELETE) -app.use('/api/alert-events', alertEventsRouter); -app.use('/api/status', statusRouter); -app.use('/api/notification-config', notificationConfigRouter); - -app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).json({ - error: 'Something went wrong!', - message: process.env.NODE_ENV === 'development' ? err.message : undefined - }); -}); - -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); - console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); -}); - diff --git a/backend/controllers/AddressController.js b/backend/controllers/AddressController.js deleted file mode 100644 index b0d4ace..0000000 --- a/backend/controllers/AddressController.js +++ /dev/null @@ -1,82 +0,0 @@ -import * as AddressModel from '../models/AddressModel.js'; - -/** - * Create a new address - */ -export async function create(req, res) { - const { address, label } = req.body; - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} creating address: ${address}`); - - if (!address) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Address is required' - }); - } - - // Validation: ETH address format - if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid Ethereum address format' - }); - } - - try { - const newAddress = await AddressModel.create(user_id, address, label || null); - console.log(`Address created with ID: ${newAddress.id}`); - return res.status(201).json(newAddress); - } catch (error) { - // Handle duplicate address (unique constraint violation) - if (error.code === '23505') { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'You are already tracking this address' - }); - } - throw error; - } -} - - -export async function list(req, res) { - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} listing addresses`); - - const addresses = await AddressModel.listByUser(user_id); - - console.log(`Found ${addresses.length} addresses for user`); - - return res.json(addresses); -} - -export async function remove(req, res) { - const addressId = parseInt(req.params.addressId); - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} deleting address ID: ${addressId}`); - - if (isNaN(addressId)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid address ID' - }); - } - - const deleted = await AddressModel.remove(addressId, user_id); - - if (!deleted) { - console.log(`Address ${addressId} not found or not owned by user`); - return res.status(404).json({ - error: 'NOT_FOUND', - message: 'Address not found' - }); - } - - console.log(`Address ${addressId} deleted`); - return res.status(204).send(); -} - diff --git a/backend/controllers/AlertEventController.js b/backend/controllers/AlertEventController.js deleted file mode 100644 index 7be4a88..0000000 --- a/backend/controllers/AlertEventController.js +++ /dev/null @@ -1,54 +0,0 @@ -import * as AlertEventModel from '../models/AlertEventModel.js'; - -// Mock data for when DB is empty (MVP scaffolding) -const MOCK_EVENTS = [ - { - id: 1, - alert_rule_id: 1, - message: 'Incoming transaction detected: 5.5 ETH received', - address_label: 'Treasury Wallet', - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString() // 2 hours ago - }, - { - id: 2, - alert_rule_id: 2, - message: 'Balance dropped below threshold: Current balance 8.2 ETH', - address_label: 'Treasury Wallet', - timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString() // 5 hours ago - }, - { - id: 3, - alert_rule_id: 3, - message: 'Outgoing transaction detected: 2.0 ETH sent', - address_label: 'Cold Storage', - timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() // 1 day ago - } -]; - - -export async function list(req, res) { - const limit = parseInt(req.query.limit) || 20; - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} listing alert events (limit: ${limit})`); - - - if (limit < 1 || limit > 100) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Limit must be between 1 and 100' - }); - } - - const events = await AlertEventModel.listRecentByUser(user_id, limit); - - console.log(`Found ${events.length} alert events for user`); - - // Return mock data if DB is empty (MVP scaffolding) - if (events.length === 0) { - return res.json(MOCK_EVENTS.slice(0, limit)); - } - - return res.json(events); -} - diff --git a/backend/controllers/AlertRuleController.js b/backend/controllers/AlertRuleController.js deleted file mode 100644 index 0f5bf63..0000000 --- a/backend/controllers/AlertRuleController.js +++ /dev/null @@ -1,185 +0,0 @@ -import * as AlertRuleModel from '../models/AlertRuleModel.js'; -import * as AddressModel from '../models/AddressModel.js'; - -const VALID_ALERT_TYPES = ['incoming_tx', 'outgoing_tx', 'large_transfer', 'balance_below']; -const THRESHOLD_REQUIRED_TYPES = ['large_transfer', 'balance_below']; - -/** - * Create a new alert rule - */ -export async function create(req, res) { - const addressId = parseInt(req.params.addressId); - const { type, threshold } = req.body; - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} creating alert for address ID: ${addressId}`); - - if (isNaN(addressId)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid address ID' - }); - } - - - if (!type) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Alert type is required' - }); - } - - // Validation: type must be valid - if (!VALID_ALERT_TYPES.includes(type)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: `Invalid alert type. Must be one of: ${VALID_ALERT_TYPES.join(', ')}` - }); - } - - - if (THRESHOLD_REQUIRED_TYPES.includes(type)) { - if (!threshold || threshold === '') { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: `Alert type '${type}' requires a threshold value` - }); - } - - - const thresholdNum = parseFloat(threshold); - if (isNaN(thresholdNum) || thresholdNum <= 0) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Threshold must be a positive number' - }); - } - } - - try { - // Verify address exists AND belongs to this user - const address = await AddressModel.findById(addressId, user_id); - if (!address) { - console.log(`Address ${addressId} not found or not owned by user`); - return res.status(404).json({ - error: 'NOT_FOUND', - message: 'Address not found' - }); - } - - const newAlert = await AlertRuleModel.create( - addressId, - type, - threshold || null - ); - - console.log(`Alert rule created with ID: ${newAlert.id}`); - return res.status(201).json(newAlert); - } catch (error) { - console.error(error); - return res.status(500).json({ - error: 'INTERNAL_ERROR', - message: 'Failed to create alert rule' - }); - } -} - - -export async function listByAddress(req, res) { - const addressId = parseInt(req.params.addressId); - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} listing alerts for address ID: ${addressId}`); - - if (isNaN(addressId)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid address ID' - }); - } - - - const address = await AddressModel.findById(addressId, user_id); - if (!address) { - console.log(`Address ${addressId} not found or not owned by user`); - return res.status(404).json({ - error: 'NOT_FOUND', - message: 'Address not found' - }); - } - - const alerts = await AlertRuleModel.listByAddress(addressId); - console.log(`Found ${alerts.length} alert rules`); - return res.json(alerts); -} - - -export async function updateStatus(req, res) { - const alertId = parseInt(req.params.alertId); - const { enabled } = req.body; - const user_id = req.user_id; - - console.log(`User ${user_id} updating alert ID: ${alertId}`); - - // Validation: alertId must be valid - if (isNaN(alertId)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid alert ID' - }); - } - - // Validation: enabled must be boolean - if (typeof enabled !== 'boolean') { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'enabled must be a boolean value' - }); - } - - // Verify user owns this alert (through address ownership) - const alert = await AlertRuleModel.findById(alertId, user_id); - if (!alert) { - console.log(`Alert ${alertId} not found or not owned by user`); - return res.status(404).json({ - error: 'NOT_FOUND', - message: 'Alert rule not found' - }); - } - - const updatedAlert = await AlertRuleModel.updateEnabled(alertId, enabled); - - console.log(`Alert ${alertId} updated: enabled=${enabled}`); - return res.json(updatedAlert); -} - - -export async function remove(req, res) { - const alertId = parseInt(req.params.alertId); - const user_id = req.user_id; - - console.log(`User ${user_id} deleting alert ID: ${alertId}`); - - if (isNaN(alertId)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid alert ID' - }); - } - - - const alert = await AlertRuleModel.findById(alertId, user_id); - if (!alert) { - console.log(`Alert ${alertId} not found or not owned by user`); - return res.status(404).json({ - error: 'NOT_FOUND', - message: 'Alert rule not found' - }); - } - - const deleted = await AlertRuleModel.remove(alertId); - - console.log(`Alert ${alertId} deleted`); - return res.status(204).send(); -} - diff --git a/backend/controllers/NotificationConfigController.js b/backend/controllers/NotificationConfigController.js deleted file mode 100644 index ce8b276..0000000 --- a/backend/controllers/NotificationConfigController.js +++ /dev/null @@ -1,105 +0,0 @@ -import * as NotificationConfigModel from '../models/NotificationConfigModel.js'; - -/** - * Get notification config for the authenticated user - */ -export async function getConfig(req, res) { - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} getting notification config`); - - const config = await NotificationConfigModel.getConfig(user_id); - - if (!config) { - // Return empty config if not set - return res.json({ - user_id, - discord_webhook_url: null, - telegram_chat_id: null, - email: null, - notification_enabled: true - }); - } - - console.log(`Config found`); - return res.json(config); -} - -/** - * Update notification config for the authenticated user - */ -export async function updateConfig(req, res) { - const user_id = req.user_id; // From authenticate middleware - const { discord_webhook_url, telegram_chat_id, email, notification_enabled } = req.body; - - console.log(`User ${user_id} updating notification config`); - - // Validation: at least one field should be provided - if ( - discord_webhook_url === undefined && - telegram_chat_id === undefined && - email === undefined && - notification_enabled === undefined - ) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'At least one configuration field must be provided' - }); - } - - // Validation: Discord webhook URL format (basic check) - if (discord_webhook_url && !discord_webhook_url.startsWith('https://discord.com/api/webhooks/')) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid Discord webhook URL format' - }); - } - - // Validation: Email format (basic check) - if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - return res.status(400).json({ - error: 'VALIDATION_ERROR', - message: 'Invalid email address format' - }); - } - - try { - const updatedConfig = await NotificationConfigModel.upsertConfig(user_id, { - discord_webhook_url, - telegram_chat_id, - email, - notification_enabled - }); - - console.log(`Notification config updated`); - return res.json(updatedConfig); - } catch (error) { - console.error('Error updating notification config:', error); - return res.status(500).json({ - error: 'INTERNAL_ERROR', - message: 'Failed to update notification configuration' - }); - } -} - -/** - * Delete notification config for the authenticated user - */ -export async function deleteConfig(req, res) { - const user_id = req.user_id; // From authenticate middleware - - console.log(`User ${user_id} deleting notification config`); - - const deleted = await NotificationConfigModel.remove(user_id); - - if (!deleted) { - return res.status(404).json({ - error: 'NOT_FOUND', - message: 'No notification configuration found' - }); - } - - console.log(`Notification config deleted`); - return res.status(204).send(); -} - diff --git a/backend/domain/NormalizedTx.js b/backend/domain/NormalizedTx.js deleted file mode 100644 index 2c55239..0000000 --- a/backend/domain/NormalizedTx.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * NormalizedTx - Standardized transaction representation - * - * Defines the minimal, normalized transaction data relevant - * for tracking blockchain activity. - */ - -/** - * @typedef {Object} NormalizedTx - * @property {string} hash - Transaction hash (0x prefixed) - * @property {string} from - Sender address (lowercase, 0x prefixed) - * @property {string|null} to - Recipient address (lowercase, 0x prefixed, null for contract creation) - * @property {string} value - Value transferred in Wei (as string to preserve precision) - * @property {number} blockNumber - Block number where transaction was included - * @property {number} blockTimestamp - Unix timestamp of the block - */ - -export default {}; - diff --git a/backend/domain/ObservedTx.js b/backend/domain/ObservedTx.js deleted file mode 100644 index 6c14924..0000000 --- a/backend/domain/ObservedTx.js +++ /dev/null @@ -1,17 +0,0 @@ - -/** - * @typedef {import('./NormalizedTx.js').NormalizedTx} NormalizedTx - */ - -/** - * @typedef {Object} ObservedTxMetadata - * @property {number} addressId - ID of the tracked address (from addresses table) - * @property {'incoming'|'outgoing'} direction - Direction relative to tracked address - */ - -/** - * @typedef {NormalizedTx & ObservedTxMetadata} ObservedTx - */ - -export default {}; - diff --git a/backend/firebase/admin.js b/backend/firebase/admin.js deleted file mode 100644 index ed78292..0000000 --- a/backend/firebase/admin.js +++ /dev/null @@ -1,20 +0,0 @@ - - -import admin from 'firebase-admin'; -import dotenv from 'dotenv'; - -dotenv.config(); - - -if (!admin.apps.length) { - admin.initializeApp({ - projectId: process.env.FIREBASE_PROJECT_ID, - // For local development, you can use application default credentials - // or provide a service account key file - }); -} - -export const auth = admin.auth(); - -export default admin; - diff --git a/backend/infra/README.md b/backend/infra/README.md deleted file mode 100644 index 461f7f6..0000000 --- a/backend/infra/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Database Setup - -## Prerequisites - -- PostgreSQL installed locally -- Database created (e.g., `koin_ping_dev`) - -## Setup Steps - -### 1. Create Database - -```bash -# Using psql -createdb koin_ping_dev - -# Or via psql command line -psql -U postgres -CREATE DATABASE koin_ping_dev; -\q -``` - -### 2. Set Environment Variables - -Create a `.env` file in the `backend/` directory: - -```env -DATABASE_URL=postgresql://your_user:your_password@localhost:5432/koin_ping_dev -``` - -Or if using individual variables (for `infra/database.js`): - -```env -DB_HOST=localhost -DB_PORT=5432 -DB_USER=your_user -DB_PASSWORD=your_password -DB_NAME=koin_ping_dev -``` - -### 3. Run Schema - -```bash -# From the backend -psql -d koin_ping_dev -f db/schema.sql - -# Or the full connection string -psql postgresql://your_user:your_password@localhost:5432/koin_ping_dev -f db/schema.sql -``` - -### 4. Verify Tables - -```bash -psql -d koin_ping_dev - -# In psql: -\dt # List tables -\d addresses # Describe addresses table -\d alert_rules # Describe alert_rules table -\d alert_events # Describe alert_events table -``` - -## Tables - -### addresses -- `id` - Primary key -- `address` - Ethereum address (unique) -- `label` - Optional display name -- `created_at` - Timestamp - -### alert_rules -- `id` - Primary key -- `address_id` - Foreign key to addresses -- `type` - Alert type: `incoming_tx`, `outgoing_tx`, `large_transfer`, `balance_below` -- `threshold` - ETH amount (nullable) -- `enabled` - Active status -- `created_at` - Timestamp - -### alert_events -- `id` - Primary key -- `alert_rule_id` - Foreign key to alert_rules -- `message` - Alert description -- `address_label` - Denormalized label for display -- `timestamp` - When alert fired - -## Reset Database - -To start fresh: - -```bash -psql -d koin_ping_dev -f db/schema.sql -``` - -The schema includes `DROP TABLE IF EXISTS` statements for clean reruns. - diff --git a/backend/infra/add_notification_config.sql b/backend/infra/add_notification_config.sql deleted file mode 100644 index 0442dec..0000000 --- a/backend/infra/add_notification_config.sql +++ /dev/null @@ -1,21 +0,0 @@ - --- Run: psql -d koin_ping_dev -f add_notification_config.sql - --- Create user notification configuration tableStores notification preferences and webhook URLs for each user -CREATE TABLE user_notification_configs ( - user_id VARCHAR(128) PRIMARY KEY, - discord_webhook_url TEXT, -- Discord webhook URL (nullable) - telegram_chat_id VARCHAR(128), -- Telegram chat ID (nullable) - telegram_bot_token VARCHAR(255), -- Telegram bot token (nullable, future use) - email VARCHAR(255), -- Email for notifications (nullable) - notification_enabled BOOLEAN DEFAULT TRUE, -- Master on/off switch - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - --- Create index for active notifications lookup -CREATE INDEX idx_notification_configs_enabled ON user_notification_configs(notification_enabled); - --- Note: No foreign key to a users table (doesn't exist yet) --- user_id is the Firebase UID, validated by backend middleware - diff --git a/backend/infra/add_user_id_migration.sql b/backend/infra/add_user_id_migration.sql deleted file mode 100644 index c2890cc..0000000 --- a/backend/infra/add_user_id_migration.sql +++ /dev/null @@ -1,16 +0,0 @@ - -ALTER TABLE addresses -ADD COLUMN user_id VARCHAR(128); - -UPDATE addresses -SET user_id = 'legacy_user' -WHERE user_id IS NULL; - -ALTER TABLE addresses -ALTER COLUMN user_id SET NOT NULL; - -CREATE INDEX idx_addresses_user_id ON addresses(user_id); - -ALTER TABLE addresses DROP CONSTRAINT IF EXISTS addresses_address_key; - -ALTER TABLE addresses ADD CONSTRAINT addresses_user_address_unique UNIQUE (user_id, address); diff --git a/backend/infra/database.js b/backend/infra/database.js deleted file mode 100644 index b37354f..0000000 --- a/backend/infra/database.js +++ /dev/null @@ -1,32 +0,0 @@ -import pg from "pg"; -import dotenv from "dotenv"; - -dotenv.config(); - -const { Pool } = pg; - -const pool = new Pool( - process.env.DATABASE_URL - ? { connectionString: process.env.DATABASE_URL } - : { - host: process.env.DB_HOST || "localhost", - port: process.env.DB_PORT || 5432, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - max: 20, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, - } -); - -pool.on("connect", () => { - console.log("Connected to PostgreSQL database"); -}); - -pool.on("error", (err) => { - console.error("Unexpected error on idle client", err); - process.exit(1); -}); - -export default pool; diff --git a/backend/infra/schema.sql b/backend/infra/schema.sql deleted file mode 100644 index 92cf66e..0000000 --- a/backend/infra/schema.sql +++ /dev/null @@ -1,71 +0,0 @@ - -DROP TABLE IF EXISTS alert_events CASCADE; -DROP TABLE IF EXISTS alert_rules CASCADE; -DROP TABLE IF EXISTS address_checkpoints CASCADE; -DROP TABLE IF EXISTS user_notification_configs CASCADE; -DROP TABLE IF EXISTS addresses CASCADE; - -CREATE TABLE addresses ( - id SERIAL PRIMARY KEY, - user_id VARCHAR(128) NOT NULL, -- Firebase user ID (multi-user support) - address VARCHAR(42) NOT NULL, -- Ethereum address format (0x + 40 hex chars) - label VARCHAR(255), -- Optional human-readable label - created_at TIMESTAMP DEFAULT NOW(), - - -- Unique users can track the same address independently - UNIQUE(user_id, address) -); - - -CREATE TABLE alert_rules ( - id SERIAL PRIMARY KEY, - address_id INTEGER NOT NULL REFERENCES addresses(id) ON DELETE CASCADE, - type VARCHAR(50) NOT NULL, -- 'incoming_tx', 'outgoing_tx', 'large_transfer', 'balance_below' - threshold DECIMAL(20, 6), -- ETH amount threshold (nullable for tx types that don't need it) - enabled BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT NOW(), - - CONSTRAINT valid_alert_type CHECK ( - type IN ('incoming_tx', 'outgoing_tx', 'large_transfer', 'balance_below') - ), - - CONSTRAINT positive_threshold CHECK ( - threshold IS NULL OR threshold > 0 - ) -); - - -CREATE TABLE alert_events ( - id SERIAL PRIMARY KEY, - alert_rule_id INTEGER NOT NULL REFERENCES alert_rules(id) ON DELETE CASCADE, - message TEXT NOT NULL, -- Human-readable alert message - address_label VARCHAR(255), -- Denormalized for display (avoids joins) - tx_hash VARCHAR(66), -- Transaction hash that triggered alert (nullable for balance_below) - timestamp TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE address_checkpoints ( - address_id INTEGER PRIMARY KEY REFERENCES addresses(id) ON DELETE CASCADE, - last_checked_block INTEGER NOT NULL, -- Last block number that was checked for this address - last_checked_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE user_notification_configs ( - user_id VARCHAR(128) PRIMARY KEY, - discord_webhook_url TEXT, -- Discord webhook URL (nullable) - telegram_chat_id VARCHAR(128), -- Telegram chat ID (nullable) - telegram_bot_token VARCHAR(255), -- Telegram bot token (nullable, future use) - email VARCHAR(255), -- Email for notifications (nullable) - notification_enabled BOOLEAN DEFAULT TRUE, -- Master on/off switch - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - --- Create indexes for common queries -CREATE INDEX idx_addresses_user_id ON addresses(user_id); -CREATE INDEX idx_alert_rules_address_id ON alert_rules(address_id); -CREATE INDEX idx_alert_rules_enabled ON alert_rules(enabled); -CREATE INDEX idx_alert_events_timestamp ON alert_events(timestamp DESC); -CREATE INDEX idx_alert_events_alert_rule_id ON alert_events(alert_rule_id); -CREATE INDEX idx_address_checkpoints_last_checked_at ON address_checkpoints(last_checked_at); -CREATE INDEX idx_notification_configs_enabled ON user_notification_configs(notification_enabled); diff --git a/backend/middleware/authenticate.js b/backend/middleware/authenticate.js deleted file mode 100644 index 4670d0d..0000000 --- a/backend/middleware/authenticate.js +++ /dev/null @@ -1,68 +0,0 @@ - -import { auth } from '../firebase/admin.js'; - - -export async function authenticate(req, res, next) { - try { - // Get Authorization header - const authHeader = req.headers.authorization; - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - console.log('No Authorization header or invalid format'); - return res.status(401).json({ - error: 'UNAUTHORIZED', - message: 'No authentication token provided' - }); - } - - const token = authHeader.split('Bearer ')[1]; - - if (!token) { - console.log('Empty token'); - return res.status(401).json({ - error: 'UNAUTHORIZED', - message: 'Invalid token format' - }); - } - - console.log('Verifying Firebase token...'); - const decodedToken = await auth.verifyIdToken(token); - - const user_id = decodedToken.uid; - - // SMOKE TEST: Console log the user_id - console.log('Token verified!'); - console.log(' User ID:', user_id); - console.log(' Email:', decodedToken.email); - console.log(' Token issued at:', new Date(decodedToken.iat * 1000).toISOString()); - - req.user_id = user_id; - req.user_email = decodedToken.email; - - next(); - - } catch (error) { - console.error('Token verification failed:', error.message); - - // Handle specific Firebase errors - if (error.code === 'auth/id-token-expired') { - return res.status(401).json({ - error: 'TOKEN_EXPIRED', - message: 'Authentication token has expired' - }); - } - - if (error.code === 'auth/argument-error') { - return res.status(401).json({ - error: 'INVALID_TOKEN', - message: 'Invalid authentication token format' - }); - } - - return res.status(401).json({ - error: 'UNAUTHORIZED', - message: 'Failed to verify authentication token' - }); - } -} - diff --git a/backend/models/AddressCheckpointModel.js b/backend/models/AddressCheckpointModel.js deleted file mode 100644 index 8f0724b..0000000 --- a/backend/models/AddressCheckpointModel.js +++ /dev/null @@ -1,76 +0,0 @@ -import pool from '../infra/database.js'; - - -/** - * Get the last checked block number for an address - * @param {number} addressId - Address ID from the addresses table - * @returns {Promise} Last checked block number, or null if never checked - */ -export async function getLastCheckedBlock(addressId) { - const result = await pool.query( - `SELECT last_checked_block - FROM address_checkpoints - WHERE address_id = $1`, - [addressId] - ); - - return result.rows[0]?.last_checked_block || null; -} - -/** - * Update the last checked block number for an address - * Creates a new checkpoint record if one doesn't exist - * @param {number} addressId - Address ID from the addresses table - * @param {number} blockNumber - Block number that was just checked - * @returns {Promise} Updated checkpoint record - */ -export async function updateLastCheckedBlock(addressId, blockNumber) { - const result = await pool.query( - `INSERT INTO address_checkpoints (address_id, last_checked_block, last_checked_at) - VALUES ($1, $2, NOW()) - ON CONFLICT (address_id) - DO UPDATE SET - last_checked_block = $2, - last_checked_at = NOW() - RETURNING address_id, last_checked_block, last_checked_at`, - [addressId, blockNumber] - ); - - return result.rows[0]; -} - -/** - * Get all address checkpoints (useful for debugging/monitoring) - * @returns {Promise} Array of checkpoint objects - */ -export async function listAll() { - const result = await pool.query( - `SELECT - ac.address_id, - a.address, - a.label, - ac.last_checked_block, - ac.last_checked_at - FROM address_checkpoints ac - JOIN addresses a ON a.id = ac.address_id - ORDER BY ac.last_checked_at DESC` - ); - - return result.rows; -} - -/** - * Delete checkpoint for an address (useful when address is removed) - * Note: This should happen automatically via CASCADE when address is deleted - * @param {number} addressId - Address ID - * @returns {Promise} True if deleted, false if not found - */ -export async function remove(addressId) { - const result = await pool.query( - `DELETE FROM address_checkpoints WHERE address_id = $1`, - [addressId] - ); - - return result.rowCount > 0; -} - diff --git a/backend/models/AddressModel.js b/backend/models/AddressModel.js deleted file mode 100644 index 1802b25..0000000 --- a/backend/models/AddressModel.js +++ /dev/null @@ -1,84 +0,0 @@ -import pool from '../infra/database.js'; - -/** - * Create a new address - * @param {string} user_id - Firebase user ID - * @param {string} address - Ethereum address - * @param {string|null} label - Optional label - * @returns {Promise} Created address object - */ -export async function create(user_id, address, label) { - const result = await pool.query( - `INSERT INTO addresses (user_id, address, label) - VALUES ($1, $2, $3) - RETURNING id, user_id, address, label, created_at`, - [user_id, address, label] - ); - return result.rows[0]; -} - -/** - * List all addresses for a specific user - * @param {string} user_id - Firebase user ID - * @returns {Promise} Array of address objects - */ -export async function listByUser(user_id) { - const result = await pool.query( - `SELECT id, address, label, created_at - FROM addresses - WHERE user_id = $1 - ORDER BY created_at DESC`, - [user_id] - ); - return result.rows; -} - -/** - * List all addresses (for system use only, e.g., poller) - * @returns {Promise} Array of address objects - */ -export async function list() { - const result = await pool.query( - `SELECT id, user_id, address, label, created_at - FROM addresses - ORDER BY created_at DESC` - ); - return result.rows; -} - -/** - * Find address by ID (for specific user) - * @param {number} id - Address ID - * @param {string} user_id - Firebase user ID (for ownership verification) - * @returns {Promise} Address object or null if not found or not owned by user - */ -export async function findById(id, user_id = null) { - const query = user_id - ? `SELECT id, user_id, address, label, created_at - FROM addresses - WHERE id = $1 AND user_id = $2` - : `SELECT id, user_id, address, label, created_at - FROM addresses - WHERE id = $1`; - - const params = user_id ? [id, user_id] : [id]; - - const result = await pool.query(query, params); - return result.rows[0] || null; -} - -/** - * Remove address by ID (for specific user) - * @param {number} id - Address ID - * @param {string} user_id - Firebase user ID (for ownership verification) - * @returns {Promise} True if deleted, false if not found or not owned by user - */ -export async function remove(id, user_id) { - const result = await pool.query( - `DELETE FROM addresses - WHERE id = $1 AND user_id = $2`, - [id, user_id] - ); - return result.rowCount > 0; -} - diff --git a/backend/models/AlertEventModel.js b/backend/models/AlertEventModel.js deleted file mode 100644 index c2860c3..0000000 --- a/backend/models/AlertEventModel.js +++ /dev/null @@ -1,56 +0,0 @@ -import pool from '../infra/database.js'; - -/** - * List recent alert events for a specific user - * @param {string} user_id - Firebase user ID - * @param {number} limit - Maximum number of events to return - * @returns {Promise} Array of alert event objects - */ -export async function listRecentByUser(user_id, limit = 20) { - const result = await pool.query( - `SELECT ae.id, ae.alert_rule_id, ae.message, ae.address_label, ae.tx_hash, ae.timestamp - FROM alert_events ae - JOIN alert_rules ar ON ar.id = ae.alert_rule_id - JOIN addresses a ON a.id = ar.address_id - WHERE a.user_id = $1 - ORDER BY ae.timestamp DESC - LIMIT $2`, - [user_id, limit] - ); - return result.rows; -} - -/** - * List recent alert events - * @param {number} limit - Maximum number of event - * @returns {Promise} Array of alert event objects - */ -export async function listRecent(limit = 20) { - const result = await pool.query( - `SELECT id, alert_rule_id, message, address_label, tx_hash, timestamp - FROM alert_events - ORDER BY timestamp DESC - LIMIT $1`, - [limit] - ); - return result.rows; -} - -/** - * Create a new alert event - * @param {number} alertRuleId - Alert rule ID - * @param {string} message - Alert message - * @param {string|null} addressLabel - Address label (denormalized) - * @param {string|null} txHash - Transaction hash (optional, null for non-tx alerts) - * @returns {Promise} Created alert event object - */ -export async function create(alertRuleId, message, addressLabel, txHash = null) { - const result = await pool.query( - `INSERT INTO alert_events (alert_rule_id, message, address_label, tx_hash) - VALUES ($1, $2, $3, $4) - RETURNING id, alert_rule_id, message, address_label, tx_hash, timestamp`, - [alertRuleId, message, addressLabel, txHash] - ); - return result.rows[0]; -} - diff --git a/backend/models/AlertRuleModel.js b/backend/models/AlertRuleModel.js deleted file mode 100644 index d4df924..0000000 --- a/backend/models/AlertRuleModel.js +++ /dev/null @@ -1,92 +0,0 @@ -import pool from '../infra/database.js'; - -/** - * Create new alertrule - * @param {number} addressId - Address ID - * @param {string} type - Alert type - * @param {string|null} threshold - Threshold value (nullable) - * @returns {Promise} Created alert rule object - */ -export async function create(addressId, type, threshold) { - const result = await pool.query( - `INSERT INTO alert_rules (address_id, type, threshold, enabled) - VALUES ($1, $2, $3, TRUE) - RETURNING id, address_id, type, threshold, enabled, created_at`, - [addressId, type, threshold] - ); - return result.rows[0]; -} - -/** - * List alert rules by address ID - * @param {number} addressId - Address ID - * @returns {Promise} Array of alert rule objects - */ -export async function listByAddress(addressId) { - const result = await pool.query( - `SELECT id, address_id, type, threshold, enabled, created_at - FROM alert_rules - WHERE address_id = $1 - ORDER BY created_at DESC`, - [addressId] - ); - return result.rows; -} - -/** - * Find alert rule by ID - * @param {number} id - Alert rule ID - * @param {string} user_id - Optional user ID for ownership verification - * @returns {Promise} Alert rule object or null if not found/not owned - */ -export async function findById(id, user_id = null) { - let query, params; - - if (user_id) { - // Verify ownership - query = `SELECT ar.id, ar.address_id, ar.type, ar.threshold, ar.enabled, ar.created_at - FROM alert_rules ar - JOIN addresses a ON a.id = ar.address_id - WHERE ar.id = $1 AND a.user_id = $2`; - params = [id, user_id]; - } else { - query = `SELECT id, address_id, type, threshold, enabled, created_at - FROM alert_rules - WHERE id = $1`; - params = [id]; - } - - const result = await pool.query(query, params); - return result.rows[0] || null; -} - -/** - * Update alert rule enabled status - * @param {number} id - Alert rule ID - * @param {boolean} enabled - Enabled status - * @returns {Promise} Updated alert rule or null if not found - */ -export async function updateEnabled(id, enabled) { - const result = await pool.query( - `UPDATE alert_rules - SET enabled = $2 - WHERE id = $1 - RETURNING id, address_id, type, threshold, enabled, created_at`, - [id, enabled] - ); - return result.rows[0] || null; -} - -/** - * Remove alert rule by ID - * @param {number} id - Alert rule ID - * @returns {Promise} True if deleted, false if not found - */ -export async function remove(id) { - const result = await pool.query( - `DELETE FROM alert_rules WHERE id = $1`, - [id] - ); - return result.rowCount > 0; -} - diff --git a/backend/models/NotificationConfigModel.js b/backend/models/NotificationConfigModel.js deleted file mode 100644 index 80cf81f..0000000 --- a/backend/models/NotificationConfigModel.js +++ /dev/null @@ -1,86 +0,0 @@ -import pool from '../infra/database.js'; - -/** - * Get notification config for a user - * @param {string} user_id - Firebase user ID - * @returns {Promise} Notification config or null if not set - */ -export async function getConfig(user_id) { - const result = await pool.query( - `SELECT user_id, discord_webhook_url, telegram_chat_id, telegram_bot_token, - email, notification_enabled, created_at, updated_at - FROM user_notification_configs - WHERE user_id = $1`, - [user_id] - ); - - return result.rows[0] || null; -} - -/** - * Create or update notification config for a user - * @param {string} user_id - Firebase user ID - * @param {Object} config - Notification configuration - * @param {string} [config.discord_webhook_url] - Discord webhook URL - * @param {string} [config.telegram_chat_id] - Telegram chat ID - * @param {string} [config.email] - Email address - * @param {boolean} [config.notification_enabled] - Enable/disable notifications - * @returns {Promise} Updated config - */ -export async function upsertConfig(user_id, config) { - const { - discord_webhook_url, - telegram_chat_id, - telegram_bot_token, - email, - notification_enabled = true - } = config; - - const result = await pool.query( - `INSERT INTO user_notification_configs - (user_id, discord_webhook_url, telegram_chat_id, telegram_bot_token, email, notification_enabled, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, NOW()) - ON CONFLICT (user_id) - DO UPDATE SET - discord_webhook_url = COALESCE($2, user_notification_configs.discord_webhook_url), - telegram_chat_id = COALESCE($3, user_notification_configs.telegram_chat_id), - telegram_bot_token = COALESCE($4, user_notification_configs.telegram_bot_token), - email = COALESCE($5, user_notification_configs.email), - notification_enabled = $6, - updated_at = NOW() - RETURNING user_id, discord_webhook_url, telegram_chat_id, telegram_bot_token, - email, notification_enabled, created_at, updated_at`, - [user_id, discord_webhook_url, telegram_chat_id, telegram_bot_token, email, notification_enabled] - ); - - return result.rows[0]; -} - -/** - * Delete notification config for a user - * @param {string} user_id - Firebase user ID - * @returns {Promise} True if deleted, false if not found - */ -export async function remove(user_id) { - const result = await pool.query( - `DELETE FROM user_notification_configs WHERE user_id = $1`, - [user_id] - ); - - return result.rowCount > 0; -} - -/** - * Get all users with notifications enabled (for system use) - * @returns {Promise} Array of user configs - */ -export async function listEnabled() { - const result = await pool.query( - `SELECT user_id, discord_webhook_url, telegram_chat_id, email - FROM user_notification_configs - WHERE notification_enabled = TRUE` - ); - - return result.rows; -} - diff --git a/backend/notifications/discordNotifier.js b/backend/notifications/discordNotifier.js deleted file mode 100644 index cf38fca..0000000 --- a/backend/notifications/discordNotifier.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * https://discord.com/developers/docs/resources/webhook#execute-webhook - */ - -/** - * Send an alert notification to webhook - * @param {string} webhookUrl - Discord webhook URL - * @param {string} message - Alert message - * @param {Object} metadata - Additional alert metadata - * @param {string} metadata.txHash - Transaction hash (nullable) - * @param {string} metadata.addressLabel - Address label - * @param {string} metadata.alertType - Alert type - * @param {string} metadata.address - Blockchain address - * @returns {Promise} True if sent successfully, false otherwise - */ -export async function sendDiscordNotification(webhookUrl, message, metadata) { - const { txHash, addressLabel, alertType, address } = metadata; - - try { - const payload = { - content: null, - embeds: [ - { - title: 'Koin Ping Alert', - description: message, - color: getColorForAlertType(alertType), - fields: [ - { - name: 'Address', - value: addressLabel || 'Unknown', - inline: true, - }, - { - name: 'Blockchain Address', - value: `\`${address}\``, - inline: false, - }, - ], - timestamp: new Date().toISOString(), - footer: { - text: 'Koin Ping', - }, - }, - ], - }; - - if (txHash) { - payload.embeds[0].fields.push({ - name: 'Transaction', - value: `[View on Etherscan](https://etherscan.io/tx/${txHash})`, - inline: false, - }); - } - - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error( - `Discord webhook failed: HTTP ${response.status} - ${errorText}` - ); - } - - return true; - } catch (error) { - console.error('Failed to send Discord notification:', error.message); - return false; - } -} - -/** - * Get Discord embed color based on alert type - * @param {string} alertType - Alert type - * @returns {number} - */ -function getColorForAlertType(alertType) { - const colors = { - incoming_tx: 0x00ff00, // Green - incoming money - outgoing_tx: 0xff9900, // Orange - outgoing money - large_transfer: 0xff0000, // Red - large movement - balance_below: 0xff0000, // Red - low balance warning - }; - - return colors[alertType] || 0x0099ff; -} - -/** - * Sends a test message to verify webhook works - * @param {string} webhookUrl - * @returns {Promise} - */ -export async function testDiscordWebhook(webhookUrl) { - try { - const payload = { - content: 'Koin Ping test notification - Your Discord webhook is configured correctly!', - }; - - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - return response.ok; - } catch (error) { - console.error('Discord webhook test failed:', error.message); - return false; - } -} - diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index 3ba1cc9..0000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,3225 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^17.2.3", - "express": "^5.2.1", - "firebase-admin": "^13.6.0", - "pg": "^8.16.3" - }, - "devDependencies": { - "concurrently": "^9.2.1", - "nodemon": "^3.1.11" - } - }, - "node_modules/@fastify/busboy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", - "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", - "license": "MIT" - }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", - "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-types": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", - "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", - "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/component": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", - "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", - "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-check-interop-types": "0.3.3", - "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.7.0", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-compat": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", - "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/component": "0.7.0", - "@firebase/database": "1.1.0", - "@firebase/database-types": "1.0.16", - "@firebase/logger": "0.5.0", - "@firebase/util": "1.13.0", - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/database-types": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", - "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", - "license": "Apache-2.0", - "dependencies": { - "@firebase/app-types": "0.9.3", - "@firebase/util": "1.13.0" - } - }, - "node_modules/@firebase/logger": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", - "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@firebase/util": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", - "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@google-cloud/firestore": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", - "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@opentelemetry/api": "^1.3.0", - "fast-deep-equal": "^3.1.1", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^4.3.3", - "protobufjs": "^7.2.6" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/paginator": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", - "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", - "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", - "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.18.0.tgz", - "integrity": "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@google-cloud/paginator": "^5.0.0", - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "<4.1.0", - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "duplexify": "^4.1.3", - "fast-xml-parser": "^4.4.1", - "gaxios": "^6.0.2", - "google-auth-library": "^9.6.3", - "html-entities": "^2.5.2", - "mime": "^3.0.0", - "p-limit": "^3.0.1", - "retry-request": "^7.0.0", - "teeny-request": "^9.0.0", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/caseless": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", - "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "license": "MIT" - }, - "node_modules/@types/request": { - "version": "2.48.13", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", - "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.5" - } - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "license": "MIT", - "optional": true - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "optional": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "retry": "0.13.1" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT", - "optional": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "optional": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.8.3", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/concurrently/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "optional": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/farmhash-modern": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", - "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "strnum": "^1.1.1" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/firebase-admin": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.0.tgz", - "integrity": "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==", - "license": "Apache-2.0", - "dependencies": { - "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "^2.0.0", - "@firebase/database-types": "^1.0.6", - "@types/node": "^22.8.7", - "farmhash-modern": "^1.1.0", - "fast-deep-equal": "^3.1.1", - "google-auth-library": "^9.14.2", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.1.0", - "node-forge": "^1.3.1", - "uuid": "^11.0.2" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@google-cloud/firestore": "^7.11.0", - "@google-cloud/storage": "^7.14.0" - } - }, - "node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "license": "MIT", - "optional": true - }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gaxios/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "devOptional": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", - "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@grpc/grpc-js": "^1.10.9", - "@grpc/proto-loader": "^0.7.13", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "google-auth-library": "^9.3.0", - "node-fetch": "^2.7.0", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.2", - "protobufjs": "^7.3.2", - "retry-request": "^7.0.0", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "optional": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", - "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jwks-rsa": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", - "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", - "license": "MIT", - "dependencies": { - "@types/express": "^4.17.20", - "@types/jsonwebtoken": "^9.0.4", - "debug": "^4.3.4", - "jose": "^4.15.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT", - "optional": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lru-memoizer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", - "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", - "license": "MIT", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "6.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "license": "MIT", - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", - "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "peer": true, - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/proto3-json-serializer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", - "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/retry-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", - "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/request": "^2.48.8", - "extend": "^3.0.2", - "teeny-request": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "license": "MIT", - "optional": true, - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT", - "optional": true - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", - "license": "MIT", - "optional": true - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/teeny-request": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", - "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.9", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/teeny-request/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/teeny-request/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "optional": true - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "devOptional": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "devOptional": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index ab00be4..0000000 --- a/backend/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "node api/server.js", - "dev": "nodemon api/server.js", - "poller": "node poller/observerPoller.js", - "poller:dev": "nodemon poller/observerPoller.js", - "dev:all": "concurrently \"npm run dev\" \"npm run poller:dev\" --names \"API,POLLER\" --prefix-colors \"cyan,magenta\"" - }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "module", - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^17.2.3", - "express": "^5.2.1", - "firebase-admin": "^13.6.0", - "pg": "^8.16.3" - }, - "devDependencies": { - "concurrently": "^9.2.1", - "nodemon": "^3.1.11" - } -} diff --git a/backend/poller/observerPoller.js b/backend/poller/observerPoller.js deleted file mode 100644 index 2fe1180..0000000 --- a/backend/poller/observerPoller.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * pure scheduling - no business logic, no DB, no blockchain details. - */ - -import dotenv from 'dotenv'; -import { ObserverService } from '../services/observerService.js'; -import { EvaluatorService } from '../services/evaluatorService.js'; -import { JsonRpcEthereum } from '../protocols/ethereum/JsonRpcEthereum.js'; - - -dotenv.config(); - -const ETH_RPC_URL = process.env.ETH_RPC_URL; -const POLL_INTERVAL_MS = parseInt(process.env.POLL_INTERVAL_MS || '60000', 10); - -if (!ETH_RPC_URL) { - console.error('ERROR: ETH_RPC_URL environment variable is required'); - console.error('Please set it in your .env file'); - process.exit(1); -} - -if (isNaN(POLL_INTERVAL_MS) || POLL_INTERVAL_MS < 1000) { - console.error('ERROR: POLL_INTERVAL_MS must be a number >= 1000'); - process.exit(1); -} - -const eth = new JsonRpcEthereum(ETH_RPC_URL); -const observer = new ObserverService(eth); -const evaluator = new EvaluatorService(eth); - -let isRunning = false; - - -/** - * Sleep for specified milliseconds - * @param {number} ms - Milliseconds to sleep - * @returns {Promise} - */ -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Run observation cycle with error handling - * - * @returns {Promise} - */ -async function runCycle() { - try { - const startTime = Date.now(); - console.log(`[${new Date().toISOString()}] Starting observation cycle...`); - const observations = await observer.runOnce(); - const alertsFired = await evaluator.evaluate(observations); - - const duration = Date.now() - startTime; - console.log( - `[${new Date().toISOString()}] Cycle complete: ` + - `${observations.length} observations, ${alertsFired} alerts fired in ${duration}ms` - ); - } catch (error) { - console.error(`[${new Date().toISOString()}] Observation cycle failed:`, error.message); - } -} - -async function startPolling() { - if (isRunning) { - console.warn('Poller is already running'); - return; - } - - isRunning = true; - console.log('='.repeat(60)); - console.log('Koin Ping Observer Poller Starting'); - console.log('='.repeat(60)); - console.log(`RPC URL: ${ETH_RPC_URL}`); - console.log(`Poll Interval: ${POLL_INTERVAL_MS}ms (${POLL_INTERVAL_MS / 1000}s)`); - console.log('='.repeat(60)); - - await runCycle(); - - while (isRunning) { - await sleep(POLL_INTERVAL_MS); - - if (isRunning) { - await runCycle(); - } - } - - console.log('Poller stopped'); -} - -function stopPolling() { - if (!isRunning) { - return; - } - - console.log('\n' + '='.repeat(60)); - console.log('Shutting down poller gracefully...'); - console.log('='.repeat(60)); - isRunning = false; -} - -// ------------------------- -// Graceful Shutdown Handlers -// ------------------------- - -process.on('SIGINT', () => { - stopPolling(); - setTimeout(() => process.exit(0), 2000); -}); - -process.on('SIGTERM', () => { - stopPolling(); - setTimeout(() => process.exit(0), 2000); -}); - -if (import.meta.url === `file://${process.argv[1]}`) { - startPolling().catch((error) => { - console.error('Fatal error in poller:', error); - process.exit(1); - }); -} - -export { startPolling, stopPolling }; - diff --git a/backend/protocols/ethereum/EthereumObserver.js b/backend/protocols/ethereum/EthereumObserver.js deleted file mode 100644 index 2bf9b85..0000000 --- a/backend/protocols/ethereum/EthereumObserver.js +++ /dev/null @@ -1,61 +0,0 @@ - -/** - * @typedef {import('../../domain/NormalizedTx.js').NormalizedTx} NormalizedTx - */ - -/** - * @typedef {Object} EthereumObserver - * @property {() => Promise} getLatestBlockNumber - Get the most recent block number on the chain - * @property {(blockNumber: number) => Promise} getBlockTransactions - Get all transactions in a specific block - * @property {(address: string) => Promise} getBalance - Get the current balance of an address (in Wei as string) - */ - -/** - * Validates that an object implements the EthereumObserver interface - * @param {any} obj - Object to validate - * @returns {boolean} True if object implements the interface - */ -export function isEthereumObserver(obj) { - if (!obj || typeof obj !== 'object') { - return false; - } - - return ( - typeof obj.getLatestBlockNumber === 'function' && - typeof obj.getBlockTransactions === 'function' && - typeof obj.getBalance === 'function' - ); -} - -/** - * Asserts that an object implements the EthereumObserver interface - * Throws an error if validation fails - * @param {any} obj - Object to validate - * @param {string} [name='object'] - Name for error messages - * @throws {Error} If object doesn't implement the interface - */ -export function assertEthereumObserver(obj, name = 'object') { - if (!isEthereumObserver(obj)) { - throw new Error( - `${name} must implement EthereumObserver interface ` + - `(getLatestBlockNumber, getBlockTransactions, getBalance)` - ); - } -} - -/** - * Example usage - * - * import { assertEthereumObserver } from './protocols/ethereum/EthereumObserver.js'; - * - * export function createObserverService(ethereumObserver) { - * assertEthereumObserver(ethereumObserver, 'ethereumObserver'); - * // ... use ethereumObserver safely - * } - */ - -export default { - isEthereumObserver, - assertEthereumObserver, -}; - diff --git a/backend/protocols/ethereum/JsonRpcEthereum.js b/backend/protocols/ethereum/JsonRpcEthereum.js deleted file mode 100644 index 4dde32a..0000000 --- a/backend/protocols/ethereum/JsonRpcEthereum.js +++ /dev/null @@ -1,159 +0,0 @@ - -import { assertEthereumObserver } from './EthereumObserver.js'; - -/** - * @typedef {import('../../domain/NormalizedTx.js').NormalizedTx} NormalizedTx - */ - -const RPC_TIMEOUT_MS = 30000; // 30 seconds - -/** - * JsonRpcEthereum - Concrete implementation of EthereumObserver using JSON-RPC - * @implements {EthereumObserver} - */ -export class JsonRpcEthereum { - /** - * @param {string} rpcUrl - Ethereum JSON-RPC endpoint URL - */ - constructor(rpcUrl) { - if (!rpcUrl || typeof rpcUrl !== 'string') { - throw new Error('JsonRpcEthereum requires a valid RPC URL'); - } - this.rpcUrl = rpcUrl; - } - /** - * Make a JSON-RPC call to the Ethereum node - * @private - * @param {string} method - RPC method name (e.g., 'eth_blockNumber') - * @param {any[]} params - Method parameters - * @returns {Promise} RPC result - * @throws {Error} If RPC call fails or returns an error - */ - async _callRpc(method, params = []) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), RPC_TIMEOUT_MS); - - try { - const response = await fetch(this.rpcUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: Date.now(), - method, - params, - }), - signal: controller.signal, - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error( - `HTTP ${response.status}: ${response.statusText} for ${method}` - ); - } - - const payload = await response.json(); - - if (payload.error) { - throw new Error( - `RPC Error [${method}]: ${payload.error.message} (code: ${payload.error.code})` - ); - } - - return payload.result; - } catch (error) { - clearTimeout(timeoutId); - - if (error.name === 'AbortError') { - throw new Error(`RPC timeout after ${RPC_TIMEOUT_MS}ms for ${method}`); - } - - throw new Error(`RPC call failed [${method}]: ${error.message}`); - } - } - - /** - * Get the latest block number on the chain - * @returns {Promise} Current block number - */ - async getLatestBlockNumber() { - const hexBlock = await this._callRpc('eth_blockNumber'); - return parseInt(hexBlock, 16); - } - - /** - * Get all transactions in a specific block - * @param {number} blockNumber - Block number to fetch - * @returns {Promise} Array of normalized transactions - */ - async getBlockTransactions(blockNumber) { - const hexBlockNumber = '0x' + blockNumber.toString(16); - - const block = await this._callRpc('eth_getBlockByNumber', [ - hexBlockNumber, - true, // true = return full transaction objects, not just hashes - ]); - - if (!block || !block.transactions) { - return []; - } - - const blockTimestamp = parseInt(block.timestamp, 16); - - return block.transactions.map((tx) => ({ - hash: tx.hash, - from: tx.from?.toLowerCase() || '', - to: tx.to?.toLowerCase() || null, // null for contract creation - value: this._hexToDecimalString(tx.value), - blockNumber: parseInt(tx.blockNumber, 16), - blockTimestamp: blockTimestamp, - })); - } - - /** - * Get the current balance of an address - * @param {string} address - Ethereum address - * @returns {Promise} Balance in Wei as decimal string - */ - async getBalance(address) { - const balanceHex = await this._callRpc('eth_getBalance', [ - address, - 'latest', - ]); - - return this._hexToDecimalString(balanceHex); - } - - /** - * Convert hex string to decimal string (for large numbers) - * @private - * @param {string} hexValue - Hex value (with or without 0x prefix) - * @returns {string} - */ - _hexToDecimalString(hexValue) { - if (!hexValue || hexValue === '0x' || hexValue === '0x0') { - return '0'; - } - - try { - return BigInt(hexValue).toString(); - } catch (error) { - throw new Error(`Invalid hex value: ${hexValue}`); - } - } -} - -/** - * @param {string} rpcUrl - Ethereum JSON-RPC endpoint URL - * @returns {JsonRpcEthereum} - */ -export function createJsonRpcEthereum(rpcUrl) { - const instance = new JsonRpcEthereum(rpcUrl); - - // Validate that it implements the interface - assertEthereumObserver(instance, 'JsonRpcEthereum'); - - return instance; -} diff --git a/backend/services/evaluatorService.js b/backend/services/evaluatorService.js deleted file mode 100644 index 54fae1a..0000000 --- a/backend/services/evaluatorService.js +++ /dev/null @@ -1,258 +0,0 @@ - -import { assertEthereumObserver } from '../protocols/ethereum/EthereumObserver.js'; -import * as AlertRuleModel from '../models/AlertRuleModel.js'; -import * as AlertEventModel from '../models/AlertEventModel.js'; -import * as AddressModel from '../models/AddressModel.js'; -import * as NotificationConfigModel from '../models/NotificationConfigModel.js'; -import { ethToWei, weiGreaterThanOrEqual, weiLessThan, formatWeiAsEth } from '../shared/weiConverter.js'; -import { sendDiscordNotification } from '../notifications/discordNotifier.js'; - -/** - * @typedef {import('../domain/ObservedTx.js').ObservedTx} ObservedTx - * @typedef {import('../protocols/ethereum/EthereumObserver.js').EthereumObserver} EthereumObserver - */ - -export class EvaluatorService { - /** - * @param {EthereumObserver} ethObserver - Implementation of EthereumObserver interface - */ - constructor(ethObserver) { - assertEthereumObserver(ethObserver, 'ethObserver'); - this.eth = ethObserver; - } - - /** - * Evaluate all observations against their associated alert rules - * @param {ObservedTx[]} observations - * @returns {Promise} - */ - async evaluate(observations) { - let alertsFired = 0; - - for (const obs of observations) { - try { - const fired = await this._evaluateObservation(obs); - alertsFired += fired; - } catch (error) { - console.error( - `Error evaluating observation for address ID ${obs.addressId}:`, - error.message - ); - // Continue processing other observations - tbd - } - } - - return alertsFired; - } - - /** - * Evaluate a single observation against all rules - * @private - * @param {ObservedTx} obs - * @returns {Promise} - */ - async _evaluateObservation(obs) { - const rules = await AlertRuleModel.listByAddress(obs.addressId); - let alertsFired = 0; - - for (const rule of rules) { - // Skip disabled - if (!rule.enabled) { - continue; - } - - const matches = await this._ruleMatches(rule, obs); - - if (matches) { - await this._fireAlert(rule, obs); - alertsFired++; - } - } - - return alertsFired; - } - - /** - * Fire an alert by creating an alert event - * @private - * @param {Object} rule - Alert rule that matched - * @param {ObservedTx} obs - Observed transaction that triggered it - * @returns {Promise} - */ - async _fireAlert(rule, obs) { - const address = await AddressModel.findById(obs.addressId); - const addressLabel = address?.label || address?.address || 'Unknown'; - - const message = this._buildMessage(rule, obs); - const txHash = obs.hash; - await AlertEventModel.create(rule.id, message, addressLabel, txHash); - - console.log( - `[ALERT FIRED] Rule ${rule.id} (${rule.type}) - ${message} - TX: ${txHash}` - ); - - // Send Discord notification if configured - try { - const user_id = address.user_id; - const notificationConfig = await NotificationConfigModel.getConfig(user_id); - - if ( - notificationConfig?.discord_webhook_url && - notificationConfig.notification_enabled - ) { - const sent = await sendDiscordNotification( - notificationConfig.discord_webhook_url, - message, - { - txHash, - addressLabel, - alertType: rule.type, - address: address.address, - } - ); - - if (sent) { - console.log(`Discord notification sent to user ${user_id}`); - } else { - console.warn(`Discord notification failed for user ${user_id}`); - } - } - } catch (notificationError) { - // Don't fail the alert if notification fails - console.error( - `Failed to send Discord notification:`, - notificationError.message - ); - } - } - - /** - * Check if a rule matches an observation - * @private - * @param {Object} rule - Alert rule to evaluate - * @param {ObservedTx} obs - Observed transaction - * @returns {Promise} True if rule conditions are met - */ - async _ruleMatches(rule, obs) { - switch (rule.type) { - case 'incoming_tx': - return this._matchesIncomingTx(obs); - - case 'outgoing_tx': - return this._matchesOutgoingTx(obs); - - case 'large_transfer': - return this._matchesLargeTransfer(rule, obs); - - case 'balance_below': - return await this._matchesBalanceBelow(rule, obs); - - default: - console.warn(`Unknown rule type: ${rule.type}`); - return false; - } - } - - /** - * Match incoming transaction rule - * @private - */ - _matchesIncomingTx(obs) { - return obs.direction === 'incoming'; - } - - /** - * Match outgoing transaction rule - * @private - */ - _matchesOutgoingTx(obs) { - return obs.direction === 'outgoing'; - } - - /** - * Match large transfer rule - * @private - * - * IMPORTANT: Threshold is stored as ETH in DB, but we compare in Wei - * for precision. Both incoming and outgoing transfers are checked. - */ - _matchesLargeTransfer(rule, obs) { - if (!rule.threshold) { - return false; - } - - const thresholdWei = ethToWei(Number(rule.threshold)); - const transferValueWei = obs.value; - - return weiGreaterThanOrEqual(transferValueWei, thresholdWei); - } - - /** - * balance below threshold rule - * @private - * - * OPTIMIZATION: Only check balance after outgoing transactions - * (incoming transactions increase balance, so can't trigger this alert) - * - * IMPORTANT: Threshold stored as ETH in DB, but compared in Wei. - */ - async _matchesBalanceBelow(rule, obs) { - if (!rule.threshold) { - return false; - } - - if (obs.direction !== 'outgoing') { - return false; - } - - const address = await AddressModel.findById(obs.addressId); - if (!address) { - console.warn(`Address ID ${obs.addressId} not found`); - return false; - } - - const balanceWei = await this.eth.getBalance(address.address); - - // Convert ETH threshold from DB to Wei for comparison - const thresholdWei = ethToWei(Number(rule.threshold)); - - - return weiLessThan(balanceWei, thresholdWei); - } - - /** - * Build a human-readable alert message - * @private - * @param {Object} rule - Alert rule that matched - * @param {ObservedTx} obs - Observed transaction - * @returns {string} Human-readable message - */ - _buildMessage(rule, obs) { - switch (rule.type) { - case 'incoming_tx': - return `Incoming transaction: ${formatWeiAsEth(obs.value)} received`; - - case 'outgoing_tx': - return `Outgoing transaction: ${formatWeiAsEth(obs.value)} sent`; - - case 'large_transfer': - return `Large transfer detected: ${formatWeiAsEth(obs.value)} (threshold: ${rule.threshold} ETH)`; - - case 'balance_below': - return `Balance dropped below threshold of ${rule.threshold} ETH`; - - default: - return 'Alert triggered'; - } - } -} - -/** - * Factory function to create an EvaluatorService instance - * @param {EthereumObserver} ethObserver - Implementation of EthereumObserver interface - * @returns {EvaluatorService} - */ -export function createEvaluatorService(ethObserver) { - return new EvaluatorService(ethObserver); -} - diff --git a/backend/services/observerService.js b/backend/services/observerService.js deleted file mode 100644 index 1693dbe..0000000 --- a/backend/services/observerService.js +++ /dev/null @@ -1,158 +0,0 @@ - -import { assertEthereumObserver } from '../protocols/ethereum/EthereumObserver.js'; -import * as AddressModel from '../models/AddressModel.js'; -import * as CheckpointModel from '../models/AddressCheckpointModel.js'; - -/** - * @typedef {import('../domain/NormalizedTx.js').NormalizedTx} NormalizedTx - * @typedef {import('../domain/ObservedTx.js').ObservedTx} ObservedTx - * @typedef {import('../protocols/ethereum/EthereumObserver.js').EthereumObserver} EthereumObserver - */ - -// Safety limit: maximum blocks to process per address per run -// Prevents overwhelming RPC endpoints and ensures bounded execution time -const MAX_BLOCKS_PER_RUN = 100; - - -export class ObserverService { - /** - * @param {EthereumObserver} ethObserver - Implementation of EthereumObserver interface - */ - constructor(ethObserver) { - assertEthereumObserver(ethObserver, 'ethObserver'); - this.eth = ethObserver; - } - - /** - * Run one observation cycle across all tracked addresses - * - * - * @returns {Promise} All observations from this cycle - */ - async runOnce() { - const addresses = await AddressModel.list(); - - if (addresses.length === 0) { - return []; - } - - const latestBlock = await this.eth.getLatestBlockNumber(); - const observations = []; - - for (const address of addresses) { - try { - const addressObservations = await this._observeAddress(address, latestBlock); - observations.push(...addressObservations); - } catch (error) { - console.error(`Error observing address ${address.address}:`, error); - - } - } - - return observations; - } - - /** - * Observe a single address for activity - * @private - * @param {Object} address - Address record from database - * @param {number} latestBlock - Current latest block number - * @returns {Promise} Observations for this address - */ - async _observeAddress(address, latestBlock) { - const lastChecked = await CheckpointModel.getLastCheckedBlock(address.id); - - const startBlock = this._getStartBlock(lastChecked, latestBlock); - const endBlock = this._getEndBlock(startBlock, latestBlock); - - // No new blocks to check - if (startBlock > endBlock) { - return []; - } - - const observations = []; - - for (let blockNumber = startBlock; blockNumber <= endBlock; blockNumber++) { - const blockTxs = await this.eth.getBlockTransactions(blockNumber); - const relevantTxs = this._filterRelevantTransactions(blockTxs, address.address); - - for (const tx of relevantTxs) { - observations.push(this._createObservedTx(tx, address)); - } - } - - await CheckpointModel.updateLastCheckedBlock(address.id, endBlock); - - return observations; - } - - /** - * Determine starting block for observation - * @private - * @param {number|null} lastChecked - Last checked block, or null if never checked - * @param {number} latestBlock - Current latest block - * @returns {number} Block number to start from - */ - _getStartBlock(lastChecked, latestBlock) { - if (lastChecked === null) { - return latestBlock; - } - return lastChecked + 1; - } - - /** - * Determine ending block for observation (respects MAX_BLOCKS_PER_RUN) - * @private - * @param {number} startBlock - * @param {number} latestBlock - * @returns {number} - */ - _getEndBlock(startBlock, latestBlock) { - return Math.min(startBlock + MAX_BLOCKS_PER_RUN - 1, latestBlock); - } - - /** - * Filter transactions to only those involving the tracked address - * @private - * @param {NormalizedTx[]} transactions - * @param {string} trackedAddress - * @returns {NormalizedTx[]} - */ - _filterRelevantTransactions(transactions, trackedAddress) { - const addressLower = trackedAddress.toLowerCase(); - - return transactions.filter((tx) => { - const fromMatch = tx.from?.toLowerCase() === addressLower; - const toMatch = tx.to?.toLowerCase() === addressLower; - return fromMatch || toMatch; - }); - } - - /** - * Create an ObservedTx with direction metadata - * @private - * @param {NormalizedTx} tx - Normalized transaction - * @param {Object} address - Address record from database - * @returns {ObservedTx} Transaction with observation metadata - */ - _createObservedTx(tx, address) { - const addressLower = address.address.toLowerCase(); - const direction = tx.to?.toLowerCase() === addressLower ? 'incoming' : 'outgoing'; - - return { - ...tx, - addressId: address.id, - direction, - }; - } -} - -/** - * Factory to create an ObserverService instance - * @param {EthereumObserver} ethObserver - Implementation of EthereumObserver interface - * @returns {ObserverService} - */ -export function createObserverService(ethObserver) { - return new ObserverService(ethObserver); -} - diff --git a/backend/shared/weiConverter.js b/backend/shared/weiConverter.js deleted file mode 100644 index 3d091a7..0000000 --- a/backend/shared/weiConverter.js +++ /dev/null @@ -1,95 +0,0 @@ - - -const WEI_PER_ETH = 1000000000000000000n; // 10^18 Wei = 1 ETH - -/** - * Convert Wei (as string) to ETH (as number) - * Use for display purposes only, not for calculations - * @param {string} weiString - Wei value as string - * @returns {number} ETH value as number - */ -export function weiToEth(weiString) { - if (!weiString || weiString === '0') { - return 0; - } - - try { - const wei = BigInt(weiString); - // Convert to number (safe for display values) - return Number(wei) / Number(WEI_PER_ETH); - } catch (error) { - throw new Error(`Invalid Wei value: ${weiString}`); - } -} - -/** - * Convert ETH (as number) to Wei (as string) - * Use when converting DB thresholds for comparison - * @param {number} eth - ETH value as number - * @returns {string} Wei value as string - */ -export function ethToWei(eth) { - if (typeof eth !== 'number' || isNaN(eth)) { - throw new Error(`Invalid ETH value: ${eth}`); - } - - if (eth === 0) { - return '0'; - } - - // Convert to string with fixed precision to avoid floating point issues - const ethString = eth.toFixed(18); // Max precision - const [whole, decimal = ''] = ethString.split('.'); - - - const wholePart = BigInt(whole) * WEI_PER_ETH; - const decimalPart = BigInt(decimal.padEnd(18, '0')); - - return (wholePart + decimalPart).toString(); -} - -/** - * Compare two Wei values - * @param {string} weiA - * @param {string} weiB - * @returns {number} - */ -export function compareWei(weiA, weiB) { - const a = BigInt(weiA); - const b = BigInt(weiB); - - if (a < b) return -1; - if (a > b) return 1; - return 0; -} - -/** - * Check if Wei value A is greater than or equal to Wei value B - * @param {string} weiA - First Wei value as string - * @param {string} weiB - Second Wei value as string - * @returns {boolean} - */ -export function weiGreaterThanOrEqual(weiA, weiB) { - return BigInt(weiA) >= BigInt(weiB); -} - -/** - * Check if Wei value A is less than Wei value B - * @param {string} weiA - * @param {string} weiB - * @returns {boolean} - */ -export function weiLessThan(weiA, weiB) { - return BigInt(weiA) < BigInt(weiB); -} - -/** - * @param {string} weiString - * @param {number} decimals - Number of decimal places (default: 4) - * @returns {string} - */ -export function formatWeiAsEth(weiString, decimals = 4) { - const eth = weiToEth(weiString); - return `${eth.toFixed(decimals)} ETH`; -} - diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e6bc3ce..055bf76 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "frontend", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { "firebase": "^12.7.0", "react": "^19.2.3", diff --git a/frontend/package.json b/frontend/package.json index 5334ddd..902032e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "version": "1.0.0", - "description": "", + "description": "koin_ping blockchain monoitor UI", "main": "index.js", "scripts": { "dev": "vite", @@ -10,7 +10,8 @@ }, "keywords": [ "cryptocurrency", - "event moitoring" + "blockchain", + "blockchain event moitoring" ], "author": "Steven Jannette", "license": "MIT", diff --git a/frontend/src/api/addresses.jsx b/frontend/src/api/addresses.jsx index a404db0..15ef07a 100644 --- a/frontend/src/api/addresses.jsx +++ b/frontend/src/api/addresses.jsx @@ -1,8 +1,7 @@ // API client for address management import { getAuthHeaders, getAuthHeadersSimple } from './authHeaders'; - -const API_BASE = '/api'; +import { API_BASE } from './config'; /** * Create a new blockchain address to monitor diff --git a/frontend/src/api/alertEvents.jsx b/frontend/src/api/alertEvents.jsx index 2ddc811..86576d0 100644 --- a/frontend/src/api/alertEvents.jsx +++ b/frontend/src/api/alertEvents.jsx @@ -1,8 +1,7 @@ // API client for alert event history import { getAuthHeadersSimple } from './authHeaders'; - -const API_BASE = '/api'; +import { API_BASE } from './config'; /** * Get all alert events (history) diff --git a/frontend/src/api/alerts.jsx b/frontend/src/api/alerts.jsx index 14eadbe..310f7cc 100644 --- a/frontend/src/api/alerts.jsx +++ b/frontend/src/api/alerts.jsx @@ -1,8 +1,7 @@ // API client for alert rule management import { getAuthHeaders, getAuthHeadersSimple } from './authHeaders'; - -const API_BASE = '/api'; +import { API_BASE } from './config'; /** * Alert types supported by the system diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js new file mode 100644 index 0000000..5e945c8 --- /dev/null +++ b/frontend/src/api/config.js @@ -0,0 +1 @@ +export const API_BASE = import.meta.env.VITE_API_BASE || '/v1'; diff --git a/frontend/src/api/notificationConfig.jsx b/frontend/src/api/notificationConfig.jsx index b96b5b7..73010fb 100644 --- a/frontend/src/api/notificationConfig.jsx +++ b/frontend/src/api/notificationConfig.jsx @@ -1,8 +1,7 @@ // API client for notification configuration import { getAuthHeaders } from './authHeaders'; - -const API_BASE = '/api'; +import { API_BASE } from './config'; /** * Get notification configuration for current user diff --git a/frontend/src/api/status.jsx b/frontend/src/api/status.jsx index 921cc6a..3d49bd4 100644 --- a/frontend/src/api/status.jsx +++ b/frontend/src/api/status.jsx @@ -1,6 +1,6 @@ // API client for system status -const API_BASE = '/api'; +import { API_BASE } from './config'; /** * Get system status including latest processed block and health diff --git a/frontend/src/components/AlertForm.jsx b/frontend/src/components/AlertForm.jsx index 63ad455..11b687b 100644 --- a/frontend/src/components/AlertForm.jsx +++ b/frontend/src/components/AlertForm.jsx @@ -28,7 +28,7 @@ export default function AlertForm({ onSubmit }) { onSubmit({ type, - threshold: needsThreshold ? threshold : undefined, + threshold: needsThreshold ? Number(threshold) : undefined, }); setThreshold(""); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 56948e6..81d80e1 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,7 +7,7 @@ export default defineConfig({ server: { port: 3000, proxy: { - '/api': { + '/v1': { target: 'http://localhost:3001', changeOrigin: true, },