import fs from 'fs'; import { loadConfig, loadRequests, loadWebexRooms, getMissingSecrets, describeJiraIdentity } from './lib/config.js'; import { createLogger } from './lib/logger.js'; import { createJiraClient } from './services/jiraClient.js'; import { createJiraService } from './services/jira.js'; import { createWebexService } from './services/webex.js'; import { createJiraProcessor } from './services/jiraProcessor.js'; import { createPendingApprovalsJob, cleanupOldLogs } from './jobs/pendingApprovals.js'; import { scheduleJobs } from './jobs/cron.js'; import { createJiraTicketsService } from './services/jiraTickets.js'; import { createWebexOAuthService } from './services/webexOAuth.js'; import { createWebexWebhookManager } from './services/webexWebhookManager.js'; import { createWebhookRoutes } from './routes/webhooks.js'; import { createWebexWebhookRoutes } from './routes/webexWebhooks.js'; import { createWebexOAuthRoutes } from './routes/webexOAuth.js'; import { createApp } from './app.js'; const OAUTH_START_URL = 'https://bot.joesjavajoint.com/jiracloud/webex/oauth/start'; const config = loadConfig(); const requests = loadRequests(); const webexRooms = loadWebexRooms(); const log = createLogger(config); if (!fs.existsSync('./logs')) { fs.mkdirSync('./logs', { recursive: true }); } const missingSecrets = getMissingSecrets(config); if (missingSecrets.length > 0) { log.writeLog('warn', 'startup', `Missing configuration — set env vars or config.json: ${missingSecrets.join(', ')}`); } if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') { log.writeLog('warn', 'startup', 'NODE_TLS_REJECT_UNAUTHORIZED=0 is set — TLS certificate verification is disabled'); } log.logger('startup', `Jira identity: ${describeJiraIdentity(config)}`); if (config.webex.inboundEnabled) { log.logger( 'startup', `Webex inbound enabled (dryRun=${config.webex.inboundDryRun}, rooms=${Object.keys(webexRooms).length})` ); if (!config.webex.webhookSecret) { log.writeLog('warn', 'startup', 'WEBEX_WEBHOOK_SECRET is not set — inbound Webex webhooks will be rejected'); } if (!config.webex.integration?.clientId || !config.webex.integration?.clientSecret) { log.writeLog('warn', 'startup', 'WEBEX_INTEGRATION_CLIENT_ID/SECRET not set — OAuth flow unavailable'); } } const jiraClient = createJiraClient(config); const jiraService = createJiraService(jiraClient, log); const jiraTicketsService = createJiraTicketsService(config, log); const webexService = createWebexService(config, log); const webexOAuth = createWebexOAuthService(config, log); const webexWebhookManager = createWebexWebhookManager({ webexOAuth, log }); const jiraProcessor = createJiraProcessor({ config, requests, jiraService, webexService, log }); const pendingApprovalsJob = createPendingApprovalsJob({ jiraService, webexService, log }); scheduleJobs({ runPendingApprovals: pendingApprovalsJob.runPendingApprovals, cleanupOldLogs: () => cleanupOldLogs(log), runWebexOAuthMaintenance: async () => { if (!config.webex.inboundEnabled || !webexOAuth.isAuthenticated()) { return; } await webexOAuth.refreshIfNeeded(); const accessToken = await webexOAuth.getAccessToken(); if (accessToken) { await webexWebhookManager.ensureWebhooks({ accessToken, webexRooms, config }); } }, log, }); async function initializeWebexInbound() { if (!config.webex.inboundEnabled) { return; } if (!webexOAuth.isAuthenticated()) { log.writeLog('warn', 'startup', `Webex integration not authenticated — visit ${OAUTH_START_URL}`); return; } try { await webexOAuth.refreshIfNeeded(); const accessToken = await webexOAuth.getAccessToken(); if (!accessToken) { log.writeLog('warn', 'startup', `Webex integration token unavailable — visit ${OAUTH_START_URL}`); return; } const results = await webexWebhookManager.ensureWebhooks({ accessToken, webexRooms, config }); log.logger('startup', `Webex webhooks ensured for ${results.length} room(s)`); } catch (error) { log.logError('startup', 'Failed to initialize Webex inbound OAuth/webhooks', error); } } const app = createApp(); const webhookRoutes = createWebhookRoutes({ config, jiraProcessor, webexService, log }); const webexWebhookRoutes = createWebexWebhookRoutes({ config, webexRooms, jiraTicketsService, webexService, webexOAuth, log, }); const webexOAuthRoutes = createWebexOAuthRoutes({ config, webexOAuth, webexWebhookManager, webexRooms, log, }); webhookRoutes.registerRoutes(app); webexWebhookRoutes.registerRoutes(app); webexOAuthRoutes.registerRoutes(app); webhookRoutes.registerErrorHandler(app); initializeWebexInbound().catch(error => { log.logError('startup', 'Webex inbound initialization failed', error); }); const server = app.listen(config.server.port, () => { log.logger('startup', `${config.server.name} running on port ${config.server.port}.`); }); process.on('SIGINT', () => { server.close(() => { log.logger('shutdown', `${config.server.name} stopped!`); process.exit(); }); }); process.on('unhandledRejection', (reason) => { log.logError('process', 'Unhandled promise rejection', reason); }); process.on('uncaughtException', (err) => { log.logError('process', 'Uncaught exception — shutting down', err); process.exit(1); });