learning_ai_invt_trdg/backend/src/config/index.ts
Saravana Achu Mac aaa516122e feat(backend): wire Azure Key Vault secret resolution at startup
- Add bootstrap.ts as new entry point — resolves Key Vault secrets via
  DefaultAzureCredential before config/index.ts is evaluated, so all
  process.env reads pick up KV values (Azure CLI in dev, Managed Identity
  in prod). Falls back to .env if AZURE_KEYVAULT_URL is not set.
- Define INVTTRDG_SECRETS mappings for Cosmos, Azure OpenAI, product-id
- Add AZURE_OPENAI_ENDPOINT / KEY / DEPLOYMENT to config
- aiClient: prefer AzureOpenAIProvider (AI Foundry) when Azure OpenAI
  config is present; falls back to direct OpenAI if not configured
- Add @azure/identity, @azure/keyvault-secrets, @bytelyst/config deps
- Update dev/start scripts to use bootstrap.ts entry point
- Document AZURE_KEYVAULT_URL and Azure OpenAI vars in .env.example

Key Vault: https://kv-mywisprai.vault.azure.net/
Secrets prefix: invttrdg-*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 18:28:47 -07:00

478 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as dotenv from 'dotenv';
import logger from '../utils/logger.js';
import { listDynamicConfigEntries } from '../services/dynamicConfigRepository.js';
dotenv.config({ override: true });
export const config = {
PRODUCT_ID: process.env.PRODUCT_ID || 'invttrdg',
PLATFORM_API_URL: process.env.PLATFORM_API_URL || 'http://localhost:4003/api',
PLATFORM_AUTH_ENABLED: process.env.PLATFORM_AUTH_ENABLED !== 'false',
PLATFORM_JWT_ISSUER: process.env.PLATFORM_JWT_ISSUER || 'bytelyst-platform',
PLATFORM_JWT_PUBLIC_KEY: process.env.PLATFORM_JWT_PUBLIC_KEY || '',
PLATFORM_JWT_JWKS_URL: process.env.PLATFORM_JWT_JWKS_URL || '',
JWT_SECRET: process.env.JWT_SECRET || '',
COSMOS_ENDPOINT: process.env.COSMOS_ENDPOINT || '',
COSMOS_KEY: process.env.COSMOS_KEY || '',
COSMOS_DATABASE: process.env.COSMOS_DATABASE || 'invttrdg',
// Azure OpenAI (AI Foundry) — populated from Key Vault via bootstrap
AZURE_OPENAI_ENDPOINT: process.env.AZURE_OPENAI_ENDPOINT || '',
AZURE_OPENAI_KEY: process.env.AZURE_OPENAI_KEY || '',
AZURE_OPENAI_DEPLOYMENT: process.env.AZURE_OPENAI_DEPLOYMENT || '',
AZURE_KEYVAULT_URL: process.env.AZURE_KEYVAULT_URL || '',
// Plug-and-Play Provider Selection
PROVIDER: process.env.PROVIDER || 'alpaca',
DATA_PROVIDER: process.env.DATA_PROVIDER || process.env.PROVIDER || 'alpaca',
EXECUTION_PROVIDER: process.env.EXECUTION_PROVIDER || process.env.PROVIDER || 'alpaca',
// Asset Details
SYMBOL: process.env.SYMBOL || 'BTC/USD', // Legacy single symbol
SYMBOLS: (process.env.SYMBOLS || process.env.SYMBOL || 'BTC/USD').split(',').map(s => s.trim()), // Multi-asset support
TIMEFRAME: process.env.TIMEFRAME || '1Min',
POLLING_INTERVAL: parseInt(process.env.POLLING_INTERVAL || '60000', 10),
// Alpaca Specific
ALPACA_API_KEY: (process.env.ALPACA_API_KEY === 'your_key' ? '' : process.env.ALPACA_API_KEY) || '',
ALPACA_API_SECRET: (process.env.ALPACA_API_SECRET === 'your_secret' ? '' : process.env.ALPACA_API_SECRET) || '',
PAPER_TRADING: process.env.PAPER_TRADING === 'true',
ASSET_CLASS: (process.env.ASSET_CLASS || 'crypto') as 'crypto' | 'us_equity',
// CCXT Specific
EXCHANGE: process.env.EXCHANGE || 'binance',
CCXT_API_KEY: (process.env.CCXT_API_KEY === 'your_key' ? '' : process.env.CCXT_API_KEY) || '',
CCXT_API_SECRET: (process.env.CCXT_API_SECRET === 'your_secret' ? '' : process.env.CCXT_API_SECRET) || '',
// Notifications
WEBHOOK_URL: process.env.WEBHOOK_URL || '',
NOTIFICATION_PHONE_NUMBERS: (process.env.NOTIFICATION_PHONE_NUMBERS || '').split(',').map(s => s.trim()).filter(Boolean),
NOTIFICATION_API_HOST: process.env.NOTIFICATION_API_HOST || 'www.zenhustles.com',
NOTIFICATION_API_PATH: process.env.NOTIFICATION_API_PATH || '/api/whatsapp/send',
// Server
API_PORT: parseInt(process.env.PORT || process.env.API_PORT || '4018', 10),
ALLOWED_ORIGINS: (process.env.CORS_ALLOWED_ORIGINS
|| process.env.ALLOWED_ORIGINS
|| 'http://localhost:3048,http://localhost:5173,http://localhost:8081')
.split(',')
.map(s => s.trim())
.filter(Boolean),
// Supabase
SUPABASE_URL: process.env.SUPABASE_URL || '',
SUPABASE_KEY: process.env.SUPABASE_KEY || process.env.SUPABASE_ANON_KEY || '',
SUPABASE_JWT_ISSUER: process.env.SUPABASE_JWT_ISSUER || '',
SUPABASE_JWT_AUDIENCE: process.env.SUPABASE_JWT_AUDIENCE || '',
SNAPSHOT_USER_ID: process.env.SNAPSHOT_USER_ID || '',
// AI Configuration (Phase 2.3)
AI: {
PROVIDER: process.env.AI_PROVIDER || 'openai', // Default/Primary provider
OPENAI_API_KEY: process.env.OPENAI_API_KEY || process.env.AI_API_KEY || '',
GEMINI_API_KEY: process.env.GEMINI_API_KEY || process.env.AI_API_KEY || '',
PERPLEXITY_API_KEY: process.env.PERPLEXITY_API_KEY || '',
MODEL: process.env.AI_MODEL || 'gpt-4o', // Default model for primary provider
CONFIDENCE_THRESHOLD: parseInt(process.env.AI_CONFIDENCE_THRESHOLD || '70', 10),
FALLBACK_LIST: (process.env.AI_FALLBACK_LIST || 'openai,perplexity,gemini').split(',').map(s => s.trim().toLowerCase()),
CACHE_HOURS: parseInt(process.env.AI_CACHE_HOURS || '4', 10),
FAIL_OPEN: process.env.AI_FAIL_OPEN !== 'false',
},
// Low-Stress Mode (Toggleable Feature)
LOW_STRESS_MODE: process.env.LOW_STRESS_MODE === 'true',
// Alert Toggles
ENABLE_TREND_ALERTS: process.env.ENABLE_TREND_ALERTS !== 'false', // Default to true
ENABLE_PULSE_ALERTS: process.env.ENABLE_PULSE_ALERTS !== 'false', // Default to true
// Execution / Trading (Phase 5)
ENABLE_TRADING: process.env.ENABLE_TRADING === 'true',
TOTAL_CAPITAL: parseFloat(process.env.TOTAL_CAPITAL || '1000'),
MAX_OPEN_TRADES: parseInt(process.env.MAX_OPEN_TRADES || '3', 10),
MAX_OPEN_TRADES_PER_ACCOUNT: parseInt(process.env.MAX_OPEN_TRADES_PER_ACCOUNT || '6', 10),
COOLDOWN_MS: parseInt(process.env.COOLDOWN_MS || '3600000', 10), // Default 1 hour
PROFIT_EXIT_PERCENT: parseFloat(process.env.PROFIT_EXIT_PERCENT || '1.0'), // Default 1%
TRAILING_STOP_PERCENT: parseFloat(process.env.TRAILING_STOP_PERCENT || '0.001'), // Default 0.1%
PROFILE_SYNC_INTERVAL_MS: parseInt(process.env.PROFILE_SYNC_INTERVAL_MS || '60000', 10), // Default 1 min
MONITOR_INTERVAL_MS: parseInt(process.env.MONITOR_INTERVAL_MS || '60000', 10), // Default 1 min
ORDER_SYNC_INTERVAL_MS: parseInt(process.env.ORDER_SYNC_INTERVAL_MS || '60000', 10), // Default 1 min
STALE_ORDER_THRESHOLD_MINUTES: parseInt(process.env.STALE_ORDER_THRESHOLD_MINUTES || '2', 10), // Default 2 min
ORDER_SYNC_MISSING_GRACE_MINUTES: parseInt(process.env.ORDER_SYNC_MISSING_GRACE_MINUTES || '5', 10),
ORDER_SYNC_MISSING_CONFIRMATION_COUNT: parseInt(process.env.ORDER_SYNC_MISSING_CONFIRMATION_COUNT || '2', 10),
ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES: parseInt(process.env.ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES || '30', 10),
SYMBOL_DELAY_MS: parseInt(process.env.SYMBOL_DELAY_MS || '2000', 10), // Delay between symbols
ENABLE_BACKTEST: process.env.ENABLE_BACKTEST === 'true',
BACKTEST_CUSTOMER_ENABLED: process.env.BACKTEST_CUSTOMER_ENABLED === 'true',
BACKTEST_MAX_CSV_BYTES: parseInt(process.env.BACKTEST_MAX_CSV_BYTES || '5242880', 10), // 5MB
BACKTEST_MAX_ROWS: parseInt(process.env.BACKTEST_MAX_ROWS || '200000', 10),
CAPITAL_WATCHDOG_INTERVAL_MS: parseInt(process.env.CAPITAL_WATCHDOG_INTERVAL_MS || '60000', 10), // Default 1 min
DB_SNAPSHOT_INTERVAL_MS: parseInt(process.env.DB_SNAPSHOT_INTERVAL_MS || '300000', 10), // Default 5 min
ENABLE_DB_SNAPSHOTS: process.env.ENABLE_DB_SNAPSHOTS !== 'false', // Default true
ACCOUNT_SNAPSHOT_INTERVAL_MS: parseInt(process.env.ACCOUNT_SNAPSHOT_INTERVAL_MS || '60000', 10), // Default 1 min
DYNAMIC_CONFIG_REFRESH_MS: parseInt(process.env.DYNAMIC_CONFIG_REFRESH_MS || '60000', 10), // Default 1 min
EXCHANGE_STATE_MISMATCH_THROTTLE_MS: parseInt(process.env.EXCHANGE_STATE_MISMATCH_THROTTLE_MS || '300000', 10), // 5 min
REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE: process.env.REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE !== 'false',
// Order Execution Safety
ORDER_POLL_INTERVAL_MS: parseInt(process.env.ORDER_POLL_INTERVAL_MS || '3000', 10), // Poll every 3s
ORDER_POLL_MAX_ATTEMPTS: parseInt(process.env.ORDER_POLL_MAX_ATTEMPTS || '10', 10), // Max 10 polls (30s)
LIMIT_ORDER_TIMEOUT_MS: parseInt(process.env.LIMIT_ORDER_TIMEOUT_MS || '300000', 10), // 5 min timeout
MAX_SLIPPAGE_PERCENT: parseFloat(process.env.MAX_SLIPPAGE_PERCENT || '1.0'), // 1% max slippage
ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH: process.env.ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH !== 'false',
MIN_POSITION_QTY: parseFloat(process.env.MIN_POSITION_QTY || '0.0001'),
MAX_POSITION_QTY: parseFloat(process.env.MAX_POSITION_QTY || '1000000'),
QUANTITY_PRECISION: parseInt(process.env.QUANTITY_PRECISION || '6', 10),
MIN_NOTIONAL_USD: parseFloat(process.env.MIN_NOTIONAL_USD || '10'),
MAX_NOTIONAL_USD: parseFloat(process.env.MAX_NOTIONAL_USD || '100000'),
CAPITAL_RESERVE_PERCENT: parseFloat(process.env.CAPITAL_RESERVE_PERCENT || '0'),
ENTRY_CAPITAL_BUFFER_PCT: parseFloat(process.env.ENTRY_CAPITAL_BUFFER_PCT || '0.25'),
ENABLE_STRICT_CAPITAL_GUARD: process.env.ENABLE_STRICT_CAPITAL_GUARD !== 'false',
STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT: parseFloat(process.env.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT || process.env.MAX_SLIPPAGE_PERCENT || '1.0'),
STRICT_CAPITAL_FEE_BUFFER_PCT: parseFloat(process.env.STRICT_CAPITAL_FEE_BUFFER_PCT || '0.15'),
STRICT_CAPITAL_MIN_RESERVE_USD: parseFloat(process.env.STRICT_CAPITAL_MIN_RESERVE_USD || '0'),
ENTRY_AUTO_REDUCE_ALERT_MIN_PCT: parseFloat(process.env.ENTRY_AUTO_REDUCE_ALERT_MIN_PCT || '0.05'),
ENTRY_AUTO_REDUCE_ALERT_MIN_USD: parseFloat(process.env.ENTRY_AUTO_REDUCE_ALERT_MIN_USD || '25'),
ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS: parseInt(process.env.ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS || '1800000', 10),
CAPITAL_INVARIANT_EPSILON_USD: parseFloat(process.env.CAPITAL_INVARIANT_EPSILON_USD || '2'),
CAPITAL_INVARIANT_EPSILON_PCT: parseFloat(process.env.CAPITAL_INVARIANT_EPSILON_PCT || '0.00005'),
CAPITAL_INVARIANT_ALERT_THROTTLE_MS: parseInt(process.env.CAPITAL_INVARIANT_ALERT_THROTTLE_MS || '600000', 10),
CAPITAL_LEDGER_DRIFT_ALERT_PCT: parseFloat(process.env.CAPITAL_LEDGER_DRIFT_ALERT_PCT || '10'),
CAPITAL_LEDGER_DRIFT_MIN_USD: parseFloat(process.env.CAPITAL_LEDGER_DRIFT_MIN_USD || '10'),
CAPITAL_LEDGER_DRIFT_SCOPE: String(process.env.CAPITAL_LEDGER_DRIFT_SCOPE || 'auto').trim().toLowerCase(),
OPERATIONAL_EVENTS_MAX_BUFFER: parseInt(process.env.OPERATIONAL_EVENTS_MAX_BUFFER || '2000', 10),
// Alpaca omnibus sub-tagging
ENABLE_ALPACA_SUBTAG: process.env.ENABLE_ALPACA_SUBTAG !== 'false',
SUBTAG_OMNIBUS_ONLY: process.env.SUBTAG_OMNIBUS_ONLY === 'true',
ALPACA_SUBTAG_ENV: process.env.ALPACA_SUBTAG_ENV || '',
ALPACA_SUBTAG_MAX_LENGTH: parseInt(process.env.ALPACA_SUBTAG_MAX_LENGTH || '48', 10),
ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE: (process.env.ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE || '')
.split(',')
.map(s => s.trim())
.filter(Boolean),
ALPACA_OMNIBUS_PROFILE_ALLOWLIST: (process.env.ALPACA_OMNIBUS_PROFILE_ALLOWLIST || '')
.split(',')
.map(s => s.trim())
.filter(Boolean),
// Reconciliation EXIT Backfill (Data-only safety path)
ENABLE_RECON_EXIT_BACKFILL: process.env.ENABLE_RECON_EXIT_BACKFILL !== 'false',
RECON_EXIT_BACKFILL_DRY_RUN: process.env.RECON_EXIT_BACKFILL_DRY_RUN === 'true',
RECON_EXIT_BACKFILL_REQUIRE_PAUSE: process.env.RECON_EXIT_BACKFILL_REQUIRE_PAUSE !== 'false',
RECON_EXIT_BACKFILL_DUST_ABS_QTY: parseFloat(process.env.RECON_EXIT_BACKFILL_DUST_ABS_QTY || '0.001'),
RECON_EXIT_BACKFILL_DUST_REL_PCT: parseFloat(process.env.RECON_EXIT_BACKFILL_DUST_REL_PCT || '0.002'),
RECON_EXIT_BACKFILL_LOOKBACK_HOURS: parseInt(process.env.RECON_EXIT_BACKFILL_LOOKBACK_HOURS || '72', 10),
RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION: process.env.RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION !== 'false',
RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH: process.env.RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH === 'true',
RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES: parseInt(process.env.RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES || '5', 10),
RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST: (process.env.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST || '')
.split(',')
.map(s => s.trim())
.filter(Boolean),
// Reconciliation missing-order coverage sync (data-only safety path)
ENABLE_RECON_ORDER_COVERAGE_SYNC: process.env.ENABLE_RECON_ORDER_COVERAGE_SYNC !== 'false',
RECON_ORDER_COVERAGE_DRY_RUN: process.env.RECON_ORDER_COVERAGE_DRY_RUN === 'true',
RECON_ORDER_COVERAGE_REQUIRE_PAUSE: process.env.RECON_ORDER_COVERAGE_REQUIRE_PAUSE === 'true',
RECON_ORDER_COVERAGE_LOOKBACK_HOURS: parseInt(process.env.RECON_ORDER_COVERAGE_LOOKBACK_HOURS || '72', 10),
RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE: parseInt(process.env.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE || '500', 10),
RECON_ORDER_COVERAGE_MAX_FETCH_PAGES: parseInt(process.env.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES || '25', 10),
RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE: parseInt(process.env.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE || '200', 10),
RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS: parseInt(process.env.RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS || '2000', 10),
RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION: process.env.RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION !== 'false',
RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS: process.env.RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS !== 'false',
RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT: parseInt(process.env.RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT || '1', 10),
RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS: parseInt(process.env.RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS || '0', 10),
// Reconciliation sub-tag repair (data-only; fills missing legacy sub_tag for traceability)
ENABLE_RECON_SUBTAG_REPAIR: process.env.ENABLE_RECON_SUBTAG_REPAIR !== 'false',
RECON_SUBTAG_REPAIR_DRY_RUN: process.env.RECON_SUBTAG_REPAIR_DRY_RUN === 'true',
RECON_SUBTAG_REPAIR_LOOKBACK_HOURS: parseInt(process.env.RECON_SUBTAG_REPAIR_LOOKBACK_HOURS || '720', 10),
RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE: parseInt(process.env.RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE || '500', 10),
// Canonical lifecycle API sizing and truncation visibility
CANONICAL_LIFECYCLE_MAX_ROWS: parseInt(process.env.CANONICAL_LIFECYCLE_MAX_ROWS || '200000', 10),
CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS: parseInt(process.env.CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS || '600000', 10),
// Reconciliation SLO alerts
RECONCILIATION_SLO_ALERT_STREAK: parseInt(process.env.RECONCILIATION_SLO_ALERT_STREAK || '2', 10),
RECONCILIATION_SLO_ALERT_THROTTLE_MS: parseInt(process.env.RECONCILIATION_SLO_ALERT_THROTTLE_MS || '600000', 10),
RECONCILIATION_SLO_MISMATCH_THRESHOLD: parseInt(process.env.RECONCILIATION_SLO_MISMATCH_THRESHOLD || '1', 10),
RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD: parseInt(process.env.RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD || '1', 10),
RECONCILIATION_SLO_MISSING_DB_THRESHOLD: parseInt(process.env.RECONCILIATION_SLO_MISSING_DB_THRESHOLD || '1', 10),
// Reconciliation integrity watchdog (hard alerts for lifecycle drift risks)
ENABLE_RECON_INTEGRITY_WATCHDOG: process.env.ENABLE_RECON_INTEGRITY_WATCHDOG !== 'false',
RECON_INTEGRITY_WATCHDOG_THROTTLE_MS: parseInt(process.env.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS || '600000', 10),
RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD: parseInt(process.env.RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD || '1', 10),
RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD: parseInt(process.env.RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD || '1', 10),
ENABLE_RECON_WATCHDOG_AUTO_RESUME: process.env.ENABLE_RECON_WATCHDOG_AUTO_RESUME !== 'false',
RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS: parseInt(process.env.RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS || '900000', 10),
RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES: parseInt(process.env.RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES || '2', 10),
RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS: parseInt(process.env.RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS || '1800000', 10),
// Reconciliation position-parity heartbeat (automated ghost self-healing path)
ENABLE_RECON_POSITION_PARITY_HEARTBEAT: process.env.ENABLE_RECON_POSITION_PARITY_HEARTBEAT !== 'false',
RECON_POSITION_PARITY_DRY_RUN: process.env.RECON_POSITION_PARITY_DRY_RUN === 'true',
RECON_POSITION_PARITY_CONFIRMATIONS: parseInt(process.env.RECON_POSITION_PARITY_CONFIRMATIONS || '3', 10),
RECON_POSITION_PARITY_DUST_ABS_QTY: parseFloat(process.env.RECON_POSITION_PARITY_DUST_ABS_QTY || '0.0001'),
RECON_POSITION_PARITY_MAX_NOTIONAL_PCT: parseFloat(process.env.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT || '0.5'),
RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION: process.env.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION !== 'false',
RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION: process.env.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION !== 'false',
// Pro Strategy Configuration (Modular Rules)
PRO_STRATEGY: {
ENABLED_RULES: (process.env.ENABLED_RULES || 'trend_bias,momentum,zone,session,entry_trigger,ai_analysis,risk_management').split(',').map(s => s.trim()),
PARAMETERS: {
// Trend Bias (4H)
TREND_TIMEFRAME: process.env.R_TREND_TIMEFRAME || '4h',
// Execution (15m) -- Phase 2 Upgrade
EXECUTION_TIMEFRAME: process.env.R_EXECUTION_TIMEFRAME || (process.env.LOW_STRESS_MODE === 'true' ? '1h' : '15m'),
TREND_EMA_FAST: parseInt(process.env.R_TREND_EMA_FAST || '50', 10),
TREND_EMA_SLOW: parseInt(process.env.R_TREND_EMA_SLOW || '200', 10),
// Momentum (15m)
MOMENTUM_TIMEFRAME: process.env.R_MOMENTUM_TIMEFRAME || '15m',
RSI_PERIOD: parseInt(process.env.R_RSI_PERIOD || '14', 10),
RSI_OVERBOUGHT: parseInt(process.env.R_RSI_OVERBOUGHT || '70', 10),
RSI_OVERSOLD: parseInt(process.env.R_RSI_OVERSOLD || '30', 10),
// Zone / Location
ZONE_EMA_PERIOD: parseInt(process.env.R_ZONE_EMA_PERIOD || '20', 10),
// Session (UTC Hours) — JSON-configurable
SESSION_WINDOWS: JSON.parse(process.env.R_SESSION_WINDOWS || JSON.stringify([
{ start: 0, end: 9 }, // Tokyo (TOK)
{ start: 22, end: 7 }, // Sydney (SYD)
{ start: 7, end: 16 }, // London (LDN)
{ start: 13, end: 22 } // New York (NY)
])),
// Risk
ATR_PERIOD: parseInt(process.env.R_ATR_PERIOD || '14', 10),
RISK_PER_TRADE: parseFloat(process.env.R_RISK_PER_TRADE || '0.01'),
RISK_REWARD_RATIO: parseFloat(process.env.R_RISK_REWARD_RATIO || '1.5'),
SL_MULTIPLIER: parseFloat(process.env.R_SL_MULTIPLIER || '1.5'),
MAX_TP_CAP: parseFloat(process.env.R_MAX_TP_CAP || '0.01'),
}
},
};
const toNumber = (value: unknown, fallback: number): number => {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string') {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return fallback;
};
const toBoolean = (value: unknown, fallback: boolean): boolean => {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
if (['false', '0', 'no', 'off'].includes(normalized)) return false;
}
return fallback;
};
const toCsvArray = (value: unknown, fallback: string[]): string[] => {
if (Array.isArray(value)) {
return value.map(v => String(v).trim()).filter(Boolean);
}
if (typeof value === 'string') {
return value.split(',').map(s => s.trim()).filter(Boolean);
}
return fallback;
};
const dynamicConfigParsers: Record<string, (value: unknown) => unknown> = {
SYMBOLS: (value) => toCsvArray(value, config.SYMBOLS),
POLLING_INTERVAL: (value) => toNumber(value, config.POLLING_INTERVAL),
ENABLE_TRADING: (value) => toBoolean(value, config.ENABLE_TRADING),
TOTAL_CAPITAL: (value) => toNumber(value, config.TOTAL_CAPITAL),
MAX_OPEN_TRADES: (value) => toNumber(value, config.MAX_OPEN_TRADES),
MAX_OPEN_TRADES_PER_ACCOUNT: (value) => toNumber(value, config.MAX_OPEN_TRADES_PER_ACCOUNT),
COOLDOWN_MS: (value) => toNumber(value, config.COOLDOWN_MS),
PROFIT_EXIT_PERCENT: (value) => toNumber(value, config.PROFIT_EXIT_PERCENT),
TRAILING_STOP_PERCENT: (value) => toNumber(value, config.TRAILING_STOP_PERCENT),
PROFILE_SYNC_INTERVAL_MS: (value) => toNumber(value, config.PROFILE_SYNC_INTERVAL_MS),
MONITOR_INTERVAL_MS: (value) => toNumber(value, config.MONITOR_INTERVAL_MS),
ORDER_SYNC_INTERVAL_MS: (value) => toNumber(value, config.ORDER_SYNC_INTERVAL_MS),
STALE_ORDER_THRESHOLD_MINUTES: (value) => toNumber(value, config.STALE_ORDER_THRESHOLD_MINUTES),
ORDER_SYNC_MISSING_GRACE_MINUTES: (value) => toNumber(value, config.ORDER_SYNC_MISSING_GRACE_MINUTES),
ORDER_SYNC_MISSING_CONFIRMATION_COUNT: (value) => toNumber(value, config.ORDER_SYNC_MISSING_CONFIRMATION_COUNT),
ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES: (value) => toNumber(value, config.ORDER_SYNC_RECENT_CLOSED_LOOKBACK_MINUTES),
SYMBOL_DELAY_MS: (value) => toNumber(value, config.SYMBOL_DELAY_MS),
ENABLE_BACKTEST: (value) => toBoolean(value, config.ENABLE_BACKTEST),
BACKTEST_CUSTOMER_ENABLED: (value) => toBoolean(value, config.BACKTEST_CUSTOMER_ENABLED),
BACKTEST_MAX_CSV_BYTES: (value) => toNumber(value, config.BACKTEST_MAX_CSV_BYTES),
BACKTEST_MAX_ROWS: (value) => toNumber(value, config.BACKTEST_MAX_ROWS),
ORDER_POLL_INTERVAL_MS: (value) => toNumber(value, config.ORDER_POLL_INTERVAL_MS),
ORDER_POLL_MAX_ATTEMPTS: (value) => toNumber(value, config.ORDER_POLL_MAX_ATTEMPTS),
LIMIT_ORDER_TIMEOUT_MS: (value) => toNumber(value, config.LIMIT_ORDER_TIMEOUT_MS),
MAX_SLIPPAGE_PERCENT: (value) => toNumber(value, config.MAX_SLIPPAGE_PERCENT),
ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH: (value) => toBoolean(value, config.ENABLE_AUTO_PAUSE_ON_SLIPPAGE_BREACH),
MIN_POSITION_QTY: (value) => toNumber(value, config.MIN_POSITION_QTY),
MAX_POSITION_QTY: (value) => toNumber(value, config.MAX_POSITION_QTY),
QUANTITY_PRECISION: (value) => toNumber(value, config.QUANTITY_PRECISION),
MIN_NOTIONAL_USD: (value) => toNumber(value, config.MIN_NOTIONAL_USD),
MAX_NOTIONAL_USD: (value) => toNumber(value, config.MAX_NOTIONAL_USD),
CAPITAL_RESERVE_PERCENT: (value) => toNumber(value, config.CAPITAL_RESERVE_PERCENT),
ENTRY_CAPITAL_BUFFER_PCT: (value) => toNumber(value, config.ENTRY_CAPITAL_BUFFER_PCT),
ENABLE_STRICT_CAPITAL_GUARD: (value) => toBoolean(value, config.ENABLE_STRICT_CAPITAL_GUARD),
STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT: (value) => toNumber(value, config.STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT),
STRICT_CAPITAL_FEE_BUFFER_PCT: (value) => toNumber(value, config.STRICT_CAPITAL_FEE_BUFFER_PCT),
STRICT_CAPITAL_MIN_RESERVE_USD: (value) => toNumber(value, config.STRICT_CAPITAL_MIN_RESERVE_USD),
ENTRY_AUTO_REDUCE_ALERT_MIN_PCT: (value) => toNumber(value, config.ENTRY_AUTO_REDUCE_ALERT_MIN_PCT),
ENTRY_AUTO_REDUCE_ALERT_MIN_USD: (value) => toNumber(value, config.ENTRY_AUTO_REDUCE_ALERT_MIN_USD),
ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS: (value) => toNumber(value, config.ENTRY_AUTO_REDUCE_ALERT_THROTTLE_MS),
CAPITAL_INVARIANT_EPSILON_USD: (value) => toNumber(value, config.CAPITAL_INVARIANT_EPSILON_USD),
CAPITAL_INVARIANT_EPSILON_PCT: (value) => toNumber(value, config.CAPITAL_INVARIANT_EPSILON_PCT),
CAPITAL_INVARIANT_ALERT_THROTTLE_MS: (value) => toNumber(value, config.CAPITAL_INVARIANT_ALERT_THROTTLE_MS),
CAPITAL_LEDGER_DRIFT_ALERT_PCT: (value) => toNumber(value, config.CAPITAL_LEDGER_DRIFT_ALERT_PCT),
CAPITAL_LEDGER_DRIFT_MIN_USD: (value) => toNumber(value, config.CAPITAL_LEDGER_DRIFT_MIN_USD),
CAPITAL_LEDGER_DRIFT_SCOPE: (value) => String(value ?? config.CAPITAL_LEDGER_DRIFT_SCOPE).trim().toLowerCase(),
OPERATIONAL_EVENTS_MAX_BUFFER: (value) => toNumber(value, config.OPERATIONAL_EVENTS_MAX_BUFFER),
ENABLE_ALPACA_SUBTAG: (value) => toBoolean(value, config.ENABLE_ALPACA_SUBTAG),
SUBTAG_OMNIBUS_ONLY: (value) => toBoolean(value, config.SUBTAG_OMNIBUS_ONLY),
ALPACA_SUBTAG_ENV: (value) => String(value ?? config.ALPACA_SUBTAG_ENV),
ALPACA_SUBTAG_MAX_LENGTH: (value) => toNumber(value, config.ALPACA_SUBTAG_MAX_LENGTH),
ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE: (value) => toCsvArray(value, config.ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE),
ALPACA_OMNIBUS_PROFILE_ALLOWLIST: (value) => toCsvArray(value, config.ALPACA_OMNIBUS_PROFILE_ALLOWLIST),
ALLOWED_ORIGINS: (value) => toCsvArray(value, config.ALLOWED_ORIGINS),
DB_SNAPSHOT_INTERVAL_MS: (value) => toNumber(value, config.DB_SNAPSHOT_INTERVAL_MS),
ENABLE_DB_SNAPSHOTS: (value) => toBoolean(value, config.ENABLE_DB_SNAPSHOTS),
ACCOUNT_SNAPSHOT_INTERVAL_MS: (value) => toNumber(value, config.ACCOUNT_SNAPSHOT_INTERVAL_MS),
DYNAMIC_CONFIG_REFRESH_MS: (value) => toNumber(value, config.DYNAMIC_CONFIG_REFRESH_MS),
EXCHANGE_STATE_MISMATCH_THROTTLE_MS: (value) => toNumber(value, config.EXCHANGE_STATE_MISMATCH_THROTTLE_MS),
REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE: (value) => toBoolean(value, config.REQUIRE_EXCHANGE_FILL_EVIDENCE_FOR_AUTO_CLOSE),
ENABLE_RECON_EXIT_BACKFILL: (value) => toBoolean(value, config.ENABLE_RECON_EXIT_BACKFILL),
RECON_EXIT_BACKFILL_DRY_RUN: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_DRY_RUN),
RECON_EXIT_BACKFILL_REQUIRE_PAUSE: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_REQUIRE_PAUSE),
RECON_EXIT_BACKFILL_DUST_ABS_QTY: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_DUST_ABS_QTY),
RECON_EXIT_BACKFILL_DUST_REL_PCT: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_DUST_REL_PCT),
RECON_EXIT_BACKFILL_LOOKBACK_HOURS: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_LOOKBACK_HOURS),
RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION),
RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH: (value) => toBoolean(value, config.RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH),
RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES: (value) => toNumber(value, config.RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES),
RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST: (value) => toCsvArray(value, config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST),
ENABLE_RECON_ORDER_COVERAGE_SYNC: (value) => toBoolean(value, config.ENABLE_RECON_ORDER_COVERAGE_SYNC),
RECON_ORDER_COVERAGE_DRY_RUN: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_DRY_RUN),
RECON_ORDER_COVERAGE_REQUIRE_PAUSE: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_REQUIRE_PAUSE),
RECON_ORDER_COVERAGE_LOOKBACK_HOURS: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS),
RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE),
RECON_ORDER_COVERAGE_MAX_FETCH_PAGES: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES),
RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE),
RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_TRADE_ID_LOOKBACK_ROWS),
RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_REQUIRE_SUBTAG_ATTRIBUTION),
RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS: (value) => toBoolean(value, config.RECON_ORDER_COVERAGE_AUTO_PAUSE_ON_UNATTRIBUTED_FILLS),
RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_UNATTRIBUTED_PAUSE_MIN_COUNT),
RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS: (value) => toNumber(value, config.RECON_ORDER_COVERAGE_UNATTRIBUTED_BASELINE_MS),
ENABLE_RECON_SUBTAG_REPAIR: (value) => toBoolean(value, config.ENABLE_RECON_SUBTAG_REPAIR),
RECON_SUBTAG_REPAIR_DRY_RUN: (value) => toBoolean(value, config.RECON_SUBTAG_REPAIR_DRY_RUN),
RECON_SUBTAG_REPAIR_LOOKBACK_HOURS: (value) => toNumber(value, config.RECON_SUBTAG_REPAIR_LOOKBACK_HOURS),
RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE: (value) => toNumber(value, config.RECON_SUBTAG_REPAIR_MAX_UPDATES_PER_PROFILE),
CANONICAL_LIFECYCLE_MAX_ROWS: (value) => toNumber(value, config.CANONICAL_LIFECYCLE_MAX_ROWS),
CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS: (value) => toNumber(value, config.CANONICAL_LIFECYCLE_TRUNCATION_ALERT_MS),
RECONCILIATION_SLO_ALERT_STREAK: (value) => toNumber(value, config.RECONCILIATION_SLO_ALERT_STREAK),
RECONCILIATION_SLO_ALERT_THROTTLE_MS: (value) => toNumber(value, config.RECONCILIATION_SLO_ALERT_THROTTLE_MS),
RECONCILIATION_SLO_MISMATCH_THRESHOLD: (value) => toNumber(value, config.RECONCILIATION_SLO_MISMATCH_THRESHOLD),
RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD: (value) => toNumber(value, config.RECONCILIATION_SLO_MISSING_EXCHANGE_THRESHOLD),
RECONCILIATION_SLO_MISSING_DB_THRESHOLD: (value) => toNumber(value, config.RECONCILIATION_SLO_MISSING_DB_THRESHOLD),
ENABLE_RECON_INTEGRITY_WATCHDOG: (value) => toBoolean(value, config.ENABLE_RECON_INTEGRITY_WATCHDOG),
RECON_INTEGRITY_WATCHDOG_THROTTLE_MS: (value) => toNumber(value, config.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS),
RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD: (value) => toNumber(value, config.RECON_INTEGRITY_WATCHDOG_MISSING_DB_THRESHOLD),
RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD: (value) => toNumber(value, config.RECON_INTEGRITY_WATCHDOG_NO_GO_THRESHOLD),
ENABLE_RECON_WATCHDOG_AUTO_RESUME: (value) => toBoolean(value, config.ENABLE_RECON_WATCHDOG_AUTO_RESUME),
RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS: (value) => toNumber(value, config.RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS),
RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES: (value) => toNumber(value, config.RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES),
RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS: (value) => toNumber(value, config.RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS),
ENABLE_RECON_POSITION_PARITY_HEARTBEAT: (value) => toBoolean(value, config.ENABLE_RECON_POSITION_PARITY_HEARTBEAT),
RECON_POSITION_PARITY_DRY_RUN: (value) => toBoolean(value, config.RECON_POSITION_PARITY_DRY_RUN),
RECON_POSITION_PARITY_CONFIRMATIONS: (value) => toNumber(value, config.RECON_POSITION_PARITY_CONFIRMATIONS),
RECON_POSITION_PARITY_DUST_ABS_QTY: (value) => toNumber(value, config.RECON_POSITION_PARITY_DUST_ABS_QTY),
RECON_POSITION_PARITY_MAX_NOTIONAL_PCT: (value) => toNumber(value, config.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT),
RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION: (value) => toBoolean(value, config.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION),
RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION: (value) => toBoolean(value, config.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION),
};
const aiConfigParsers: Record<string, (value: unknown) => unknown> = {
CONFIDENCE_THRESHOLD: (value) => toNumber(value, config.AI.CONFIDENCE_THRESHOLD),
CACHE_HOURS: (value) => toNumber(value, config.AI.CACHE_HOURS),
FALLBACK_LIST: (value) => toCsvArray(value, config.AI.FALLBACK_LIST),
FAIL_OPEN: (value) => toBoolean(value, config.AI.FAIL_OPEN),
};
export function applyDynamicConfigEntries(data: Array<{ key: string; value: unknown }>) {
if (!Array.isArray(data) || data.length === 0) {
return [] as string[];
}
const loadedKeys: string[] = [];
data.forEach((item: any) => {
const { key, value } = item;
if (key in config) {
const parser = dynamicConfigParsers[key];
(config as any)[key] = parser ? parser(value) : value;
loadedKeys.push(key);
} else if (key in config.AI) {
const parser = aiConfigParsers[key];
(config.AI as any)[key] = parser ? parser(value) : value;
loadedKeys.push(`AI.${key}`);
}
});
return loadedKeys;
}
/**
* Loads global configuration from Cosmos-backed dynamic config storage to override .env defaults.
*/
export const loadDynamicConfig = async () => {
try {
logger.info('--- Loading Dynamic Global Config from control-plane storage ---');
const data = await listDynamicConfigEntries();
if (data && data.length > 0) {
const loadedKeys = applyDynamicConfigEntries(data);
logger.info(`✅ Dynamic Config Loaded: ${loadedKeys.join(', ')}`);
} else {
logger.info(' No dynamic config overrides found in control-plane storage. Using .env defaults.');
}
} catch (err: any) {
logger.error(`[Config] Unexpected error loading dynamic config: ${err.message}`);
}
};
export const validateConfig = () => {
// Treat "your_key" as empty
const isPlaceholder = (val: string) => !val || val === 'your_key' || val === 'your_secret';
const hasPerUserStoreConfigured =
Boolean(config.COSMOS_ENDPOINT && config.COSMOS_KEY) ||
Boolean(config.SUPABASE_URL && config.SUPABASE_KEY);
if (config.PROVIDER === 'alpaca') {
if (isPlaceholder(config.ALPACA_API_KEY) || isPlaceholder(config.ALPACA_API_SECRET)) {
if (hasPerUserStoreConfigured) {
logger.warn(
'⚠️ Alpaca keys in .env are placeholders/missing. Bot will attempt to use keys from the configured user store (Cosmos or legacy Supabase).'
);
} else {
logger.error('❌ Missing Alpaca API credentials and no user store (Cosmos or Supabase) configured!');
process.exit(1);
}
}
} else if (config.PROVIDER === 'ccxt') {
if (!config.EXCHANGE || config.EXCHANGE === 'binance') { // binance is default but check for keys
if (isPlaceholder(config.CCXT_API_KEY) && !hasPerUserStoreConfigured) {
logger.warn('⚠️ CCXT keys are placeholders/missing and no user store (Cosmos or Supabase) configured.');
}
}
}
};