require('dotenv').config(); const logger = require('../utils/logger'); /** * Required environment variables for core operation. * The app will refuse to start if these are missing. */ const REQUIRED_ENV_VARS = ['WEBEX_ACCESS_TOKEN', 'MERAKI_API_KEY', 'MERAKI_ORG_ID', 'WS_TOKEN']; /** * Recommended environment variables for full functionality. * Warnings will be logged but startup will continue. */ const RECOMMENDED_ENV_VARS = [ 'SIW_BASE_URL', 'SIW_USERNAME', 'SIW_PASSWORD', 'WS1_BASE_URL', 'WS1_TOKEN_URL', 'WS1_CLIENT_ID', 'WS1_CLIENT_SECRET', 'WS1_TENANT_CODE', // Webex Service App (only needed for `st phone`). Missing values // surface as an unavailable banner in phone mode rather than a fatal error. 'WEBEX_CLIENT_ID', 'WEBEX_CLIENT_SECRET', // Atlas / Xyte (only needed for `st av`). Missing key surfaces as // an inline banner in AV mode; MDM-side AV devices still render. 'ATLAS_AUTH_KEY', ]; function validateEnvironment() { const missingRequired = REQUIRED_ENV_VARS.filter( key => !process.env[key] || process.env[key].trim() === '' ); const missingRecommended = RECOMMENDED_ENV_VARS.filter( key => !process.env[key] || process.env[key].trim() === '' ); if (missingRequired.length > 0) { logger.error('Missing required environment variables', { missing: missingRequired }); throw new Error(`Missing required environment variables: ${missingRequired.join(', ')}`); } if (missingRecommended.length > 0) { logger.warn('Optional environment variables not set; some features may be limited', { missing: missingRecommended, }); } const wsPort = parseInt(process.env.WS_PORT, 10); if (process.env.WS_PORT && (isNaN(wsPort) || wsPort < 1 || wsPort > 65535)) { throw new Error('WS_PORT must be a valid port number between 1 and 65535'); } logger.info('Environment validation passed'); } // Skip validation when running unit tests so tests don't require a fully // populated .env. Integration tests can opt in by unsetting JEST_WORKER_ID. if (!process.env.JEST_WORKER_ID) { validateEnvironment(); } module.exports = { logLevel: process.env.LOG_LEVEL || 'info', webex: { token: process.env.WEBEX_ACCESS_TOKEN, name: process.env.BOT_NAME || 'StoreHealthAnalyzer', }, meraki: { baseUrl: 'https://api.meraki.com/api/v1', apiKey: process.env.MERAKI_API_KEY, orgId: process.env.MERAKI_ORG_ID, }, ws: { port: parseInt(process.env.WS_PORT) || 8080, token: process.env.WS_TOKEN, }, siw: { baseUrl: process.env.SIW_BASE_URL, username: process.env.SIW_USERNAME, password: process.env.SIW_PASSWORD, }, mdm: { baseUrl: process.env.WS1_BASE_URL, tokenUrl: process.env.WS1_TOKEN_URL, clientId: process.env.WS1_CLIENT_ID, clientSecret: process.env.WS1_CLIENT_SECRET, tenantCode: process.env.WS1_TENANT_CODE, }, webexServiceApp: { clientId: process.env.WEBEX_CLIENT_ID, clientSecret: process.env.WEBEX_CLIENT_SECRET, // Absolute path resolved by WebexServiceAppAuth at construction time. tokensPath: process.env.WEBEX_TOKENS_PATH || './tokens/webex-service-tokens.json', }, atlas: { // Long-lived API key; sent verbatim as the Authorization header value // (no Bearer prefix). The Atlas client throws AtlasUnavailableError // when this is unset so the AV renderer can show a banner. authKey: process.env.ATLAS_AUTH_KEY, baseUrl: process.env.ATLAS_BASE_URL || 'https://hub.xyte.io/core/v1', }, };