/** * Express application factory for SentryAgent.ai AgentIdP. * Creates and configures the Express app with all middleware and routes. * Exported as a factory function — does NOT call listen (testable). */ import express, { Application } from 'express'; import helmet from 'helmet'; import cors from 'cors'; import morgan from 'morgan'; import Stripe from 'stripe'; import { getPool } from './db/pool.js'; import { getRedisClient } from './cache/redis.js'; import { AgentRepository } from './repositories/AgentRepository.js'; import { CredentialRepository } from './repositories/CredentialRepository.js'; import { TokenRepository } from './repositories/TokenRepository.js'; import { AuditRepository } from './repositories/AuditRepository.js'; import { OrgRepository } from './repositories/OrgRepository.js'; import { AuditService } from './services/AuditService.js'; import { AgentService } from './services/AgentService.js'; import { AnalyticsService } from './services/AnalyticsService.js'; import { MarketplaceService } from './services/MarketplaceService.js'; import { BillingService } from './services/BillingService.js'; import { UsageService } from './services/UsageService.js'; import { CredentialService } from './services/CredentialService.js'; import { OAuth2Service } from './services/OAuth2Service.js'; import { OrgService } from './services/OrgService.js'; import { DIDService } from './services/DIDService.js'; import { OIDCKeyService } from './services/OIDCKeyService.js'; import { IDTokenService } from './services/IDTokenService.js'; import { FederationService } from './services/FederationService.js'; import { WebhookService } from './services/WebhookService.js'; import { EventPublisher } from './services/EventPublisher.js'; import { WebhookDeliveryWorker } from './workers/WebhookDeliveryWorker.js'; import { createKafkaProducer } from './adapters/KafkaAdapter.js'; import { AnalyticsController } from './controllers/AnalyticsController.js'; import { AgentController } from './controllers/AgentController.js'; import { MarketplaceController } from './controllers/MarketplaceController.js'; import { BillingController } from './controllers/BillingController.js'; import { OIDCTrustPolicyController } from './controllers/OIDCTrustPolicyController.js'; import { OIDCTokenExchangeController } from './controllers/OIDCTokenExchangeController.js'; import { TokenController } from './controllers/TokenController.js'; import { CredentialController } from './controllers/CredentialController.js'; import { AuditController } from './controllers/AuditController.js'; import { OrgController } from './controllers/OrgController.js'; import { DIDController } from './controllers/DIDController.js'; import { OIDCController } from './controllers/OIDCController.js'; import { FederationController } from './controllers/FederationController.js'; import { WebhookController } from './controllers/WebhookController.js'; import { ComplianceController } from './controllers/ComplianceController.js'; import { ComplianceService } from './services/ComplianceService.js'; import { createAnalyticsRouter } from './routes/analytics.js'; import { createAgentsRouter } from './routes/agents.js'; import { createMarketplaceRouter } from './routes/marketplace.js'; import { createBillingRouter } from './routes/billing.js'; import { createOIDCTrustPoliciesRouter } from './routes/oidcTrustPolicies.js'; import { createOIDCTokenExchangeRouter } from './routes/oidcTokenExchange.js'; import { OIDCTrustPolicyService } from './services/OIDCTrustPolicyService.js'; import { createTokenRouter } from './routes/token.js'; import { createCredentialsRouter } from './routes/credentials.js'; import { createAuditRouter } from './routes/audit.js'; import { createHealthRouter } from './routes/health.js'; import { createMetricsRouter } from './routes/metrics.js'; import { createOrgsRouter } from './routes/organizations.js'; import { createDIDRouter } from './routes/did.js'; import { createOIDCRouter } from './routes/oidc.js'; import { createFederationRouter } from './routes/federation.js'; import { createWebhooksRouter } from './routes/webhooks.js'; import { createComplianceRouter } from './routes/compliance.js'; import { createDelegationRouter } from './routes/delegation.js'; import { DelegationService } from './services/DelegationService.js'; import { DelegationController } from './controllers/DelegationController.js'; import { createScaffoldRouter } from './routes/scaffold.js'; import { ScaffoldService } from './services/ScaffoldService.js'; import { ScaffoldController } from './controllers/ScaffoldController.js'; import { TierService } from './services/TierService.js'; import { TierController } from './controllers/TierController.js'; import { createTiersRouter } from './routes/tiers.js'; import { errorHandler } from './middleware/errorHandler.js'; import { createOpaMiddleware } from './middleware/opa.js'; import { metricsMiddleware } from './middleware/metrics.js'; import { createOrgContextMiddleware } from './middleware/orgContext.js'; import { authMiddleware } from './middleware/auth.js'; import { createUsageMeteringMiddleware, startUsageMeteringFlush } from './middleware/usageMeteringMiddleware.js'; import { createTierEnforcementMiddleware } from './middleware/tierEnforcement.js'; import { tlsEnforcementMiddleware } from './middleware/TLSEnforcementMiddleware.js'; import { createVaultClientFromEnv } from './vault/VaultClient.js'; import { getEncryptionService } from './services/EncryptionService.js'; import { getAuditVerificationService } from './services/AuditVerificationService.js'; import { startSecretsRotationJob } from './jobs/SecretsRotationJob.js'; import { startAuditChainVerificationJob } from './jobs/AuditChainVerificationJob.js'; import { RedisClientType } from 'redis'; import path from 'path'; /** * Creates and returns a configured Express application. * All infrastructure dependencies (DB pool, Redis) are initialised here. * * @returns Promise resolving to the configured Express Application. * @throws Error if required environment variables are missing. */ export async function createApp(): Promise { const app = express(); // ──────────────────────────────────────────────────────────────── // TLS enforcement — MUST be first middleware (SOC 2 CC6.7) // In production, redirects all non-HTTPS requests to HTTPS. // ──────────────────────────────────────────────────────────────── app.use(tlsEnforcementMiddleware); // ──────────────────────────────────────────────────────────────── // Security headers // ──────────────────────────────────────────────────────────────── app.use(helmet()); // ──────────────────────────────────────────────────────────────── // CORS // ──────────────────────────────────────────────────────────────── const corsOrigin = process.env['CORS_ORIGIN'] ?? '*'; app.use(cors({ origin: corsOrigin })); // ──────────────────────────────────────────────────────────────── // Request logging // ──────────────────────────────────────────────────────────────── if (process.env['NODE_ENV'] !== 'test') { app.use(morgan('combined')); } // ──────────────────────────────────────────────────────────────── // Body parsers // JSON body parser for most routes // urlencoded parser for token endpoint (application/x-www-form-urlencoded) // ──────────────────────────────────────────────────────────────── app.use(express.json()); app.use(express.urlencoded({ extended: false })); // ──────────────────────────────────────────────────────────────── // Prometheus HTTP metrics middleware — must be before all routes // ──────────────────────────────────────────────────────────────── app.use(metricsMiddleware); // ──────────────────────────────────────────────────────────────── // Infrastructure singletons // ──────────────────────────────────────────────────────────────── const pool = getPool(); const redis = await getRedisClient(); // ──────────────────────────────────────────────────────────────── // Repository layer // ──────────────────────────────────────────────────────────────── const agentRepo = new AgentRepository(pool); const credentialRepo = new CredentialRepository(pool); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const tokenRepo = new TokenRepository(pool, redis as RedisClientType); const auditRepo = new AuditRepository(pool); const orgRepo = new OrgRepository(pool); // ──────────────────────────────────────────────────────────────── // Optional integrations // ──────────────────────────────────────────────────────────────── // Vault is optional. When VAULT_ADDR + VAULT_TOKEN are set, new credentials // are stored in Vault KV v2. When not set, bcrypt is used (Phase 1 behaviour). const vaultClient = createVaultClientFromEnv(); if (vaultClient !== null) { console.log('[AgentIdP] Vault integration enabled — new credentials will use Vault KV v2'); } // ──────────────────────────────────────────────────────────────── // Kafka (optional) — activated when KAFKA_BROKERS env var is set // ──────────────────────────────────────────────────────────────── const kafkaProducer = await createKafkaProducer(); if (kafkaProducer !== null) { console.log('[AgentIdP] Kafka integration enabled — events will be produced to agentidp-events'); } // ──────────────────────────────────────────────────────────────── // Encryption service — column-level AES-256-CBC (SOC 2 CC6.1) // Only initialised when Vault is configured (key stored in Vault). // ──────────────────────────────────────────────────────────────── const encryptionService = vaultClient !== null ? getEncryptionService(vaultClient) : null; if (encryptionService !== null) { console.log('[AgentIdP] EncryptionService enabled — sensitive columns encrypted at rest (SOC 2 CC6.1)'); } // ──────────────────────────────────────────────────────────────── // Webhook infrastructure // ──────────────────────────────────────────────────────────────── const redisUrl = process.env['REDIS_URL'] ?? 'redis://localhost:6379'; const webhookService = new WebhookService(pool, vaultClient, redis as RedisClientType, encryptionService); const webhookWorker = new WebhookDeliveryWorker(pool, vaultClient, redis as RedisClientType, redisUrl); webhookWorker.start(); const eventPublisher = new EventPublisher(webhookWorker, pool, kafkaProducer); // ──────────────────────────────────────────────────────────────── // Stripe client + TierService — created early so both BillingService // and AgentService can receive TierService via constructor injection. // ──────────────────────────────────────────────────────────────── const stripe = new Stripe(process.env['STRIPE_SECRET_KEY'] ?? '', { apiVersion: '2026-03-25.dahlia' }); const tierService = new TierService(pool, redis as RedisClientType, stripe); // ──────────────────────────────────────────────────────────────── // Service layer // ──────────────────────────────────────────────────────────────── const auditService = new AuditService(auditRepo); const didService = new DIDService(pool, vaultClient, redis as RedisClientType, encryptionService); // ──────────────────────────────────────────────────────────────── // Phase 6 WS3: Analytics Service // ──────────────────────────────────────────────────────────────── const analyticsService = new AnalyticsService(pool); const agentService = new AgentService(agentRepo, credentialRepo, auditService, didService, eventPublisher, analyticsService, tierService); const marketplaceService = new MarketplaceService(agentRepo); const credentialService = new CredentialService(credentialRepo, agentRepo, auditService, vaultClient, eventPublisher, encryptionService); const orgService = new OrgService(orgRepo, agentRepo); const privateKey = process.env['JWT_PRIVATE_KEY']; const publicKey = process.env['JWT_PUBLIC_KEY']; if (!privateKey || !publicKey) { throw new Error('JWT_PRIVATE_KEY and JWT_PUBLIC_KEY environment variables are required'); } // OIDC services — initialised after DB pool is ready const oidcKeyService = new OIDCKeyService(pool, redis as RedisClientType); await oidcKeyService.ensureCurrentKey(); const idTokenService = new IDTokenService(oidcKeyService); const oauth2Service = new OAuth2Service( tokenRepo, credentialRepo, agentRepo, auditService, privateKey, publicKey, vaultClient, idTokenService, eventPublisher, encryptionService, analyticsService, ); // ──────────────────────────────────────────────────────────────── // OPA authorization middleware (created once — shared across all routers) // ──────────────────────────────────────────────────────────────── const opaMiddleware = await createOpaMiddleware(); // ──────────────────────────────────────────────────────────────── // Controller layer // ──────────────────────────────────────────────────────────────── const agentController = new AgentController(agentService); const analyticsController = new AnalyticsController(analyticsService); const tokenController = new TokenController(oauth2Service); const credentialController = new CredentialController(credentialService); const auditController = new AuditController(auditService); const orgController = new OrgController(orgService); const didController = new DIDController(didService, agentRepo); const oidcController = new OIDCController(oidcKeyService, agentRepo); const federationService = new FederationService(pool, redis as RedisClientType); const federationController = new FederationController(federationService); const webhookController = new WebhookController(webhookService); const marketplaceController = new MarketplaceController(marketplaceService); // ──────────────────────────────────────────────────────────────── // Billing & Usage Metering (WS6) // ──────────────────────────────────────────────────────────────── const billingService = new BillingService(pool, stripe, tierService); const usageService = new UsageService(pool); const billingController = new BillingController(billingService, usageService); // Start periodic flush of in-memory usage counters to DB (every 60s) startUsageMeteringFlush(pool); // OIDC trust policy management + GitHub Actions token exchange const oidcTrustPolicyService = new OIDCTrustPolicyService(pool); const oidcTrustPolicyController = new OIDCTrustPolicyController(oidcTrustPolicyService); const oidcTokenExchangeController = new OIDCTokenExchangeController(oidcTrustPolicyService, privateKey); // ──────────────────────────────────────────────────────────────── // Compliance services and background jobs (SOC 2 Type II) // ──────────────────────────────────────────────────────────────── const auditVerificationService = getAuditVerificationService(pool); const complianceService = new ComplianceService(pool, redis as RedisClientType); const complianceController = new ComplianceController(auditVerificationService, complianceService); // Start background compliance monitoring jobs (non-blocking) startSecretsRotationJob(pool); startAuditChainVerificationJob(auditVerificationService); // ──────────────────────────────────────────────────────────────── // Org context middleware — sets PostgreSQL session variable app.organization_id // Must run after auth (so req.user is populated) and before route handlers. // Applied globally here; routes that don't require auth skip it gracefully. // ──────────────────────────────────────────────────────────────── app.use(createOrgContextMiddleware(pool)); // ──────────────────────────────────────────────────────────────── // Usage metering — records per-tenant API call counts in-memory // Applied after auth middleware so req.user is populated. // ──────────────────────────────────────────────────────────────── app.use(createUsageMeteringMiddleware(pool)); // ──────────────────────────────────────────────────────────────── // Tier enforcement — Redis-backed daily API call rate limits per // tenant tier (free/pro/enterprise). Runs after auth; skipped when // TIER_ENFORCEMENT=false or for enterprise tenants. Supersedes // the legacy freeTierEnforcementMiddleware (removed Phase 6 WS4). // ──────────────────────────────────────────────────────────────── app.use(createTierEnforcementMiddleware(pool, redis as RedisClientType)); // ──────────────────────────────────────────────────────────────── // Routes // ──────────────────────────────────────────────────────────────── const API_BASE = '/api/v1'; // Health check — unauthenticated, no OPA app.use('/health', createHealthRouter(pool, redis as RedisClientType)); // Prometheus metrics — unauthenticated, internal scraping only app.use('/metrics', createMetricsRouter()); // Well-known DID Document for the AgentIdP instance — unauthenticated app.get('/.well-known/did.json', (req, res, next) => { void didController.getInstanceDIDDocument(req, res, next); }); // OIDC well-known endpoints and agent-info — mounted at root so /.well-known/* paths resolve app.use('/', createOIDCRouter(oidcController, authMiddleware)); app.use(`${API_BASE}/agents`, createAgentsRouter(agentController, opaMiddleware)); app.use(`${API_BASE}`, createDIDRouter(didController, authMiddleware, opaMiddleware)); app.use( `${API_BASE}/agents/:agentId/credentials`, createCredentialsRouter(credentialController, opaMiddleware), ); app.use(`${API_BASE}/token`, createTokenRouter(tokenController, opaMiddleware)); app.use(`${API_BASE}/audit`, createAuditRouter(auditController, opaMiddleware)); app.use(`${API_BASE}/organizations`, createOrgsRouter(orgController, opaMiddleware)); app.use(`${API_BASE}`, createFederationRouter(federationController, authMiddleware, opaMiddleware)); app.use(`${API_BASE}/webhooks`, createWebhooksRouter(webhookController, authMiddleware, opaMiddleware)); app.use(`${API_BASE}`, createComplianceRouter(complianceController)); app.use(`${API_BASE}/marketplace`, createMarketplaceRouter(marketplaceController)); // Billing & Usage Metering — checkout, webhook, usage summary app.use(`${API_BASE}/billing`, createBillingRouter(billingController, authMiddleware)); // ──────────────────────────────────────────────────────────────── // Phase 6 WS4: Tier management — status and upgrade endpoints // ──────────────────────────────────────────────────────────────── const tierController = new TierController(tierService); app.use(`${API_BASE}/tiers`, createTiersRouter(tierController, authMiddleware)); // OIDC trust-policy management (authenticated) and token exchange (unauthenticated) // Both routers mount under ${API_BASE}/oidc — trust-policy routes use /trust-policies prefix, // token exchange uses /token, so there are no path conflicts. app.use(`${API_BASE}/oidc`, createOIDCTrustPoliciesRouter(oidcTrustPolicyController, authMiddleware)); app.use(`${API_BASE}/oidc`, createOIDCTokenExchangeRouter(oidcTokenExchangeController)); // ──────────────────────────────────────────────────────────────── // Phase 5 WS2: A2A Delegation (guarded by A2A_ENABLED flag) // ──────────────────────────────────────────────────────────────── if (process.env['A2A_ENABLED'] !== 'false') { const delegationService = new DelegationService(pool, auditService); const delegationController = new DelegationController(delegationService); app.use(`${API_BASE}`, createDelegationRouter(delegationController, authMiddleware)); } // ──────────────────────────────────────────────────────────────── // Phase 6 WS3: Analytics (guarded by ANALYTICS_ENABLED flag) // When disabled, all /api/v1/analytics/* routes return 404. // ──────────────────────────────────────────────────────────────── if (process.env['ANALYTICS_ENABLED'] !== 'false') { app.use(`${API_BASE}/analytics`, createAnalyticsRouter(analyticsController, authMiddleware)); } // ──────────────────────────────────────────────────────────────── // Phase 5 WS5: Scaffold Generator // ──────────────────────────────────────────────────────────────── const scaffoldService = new ScaffoldService(); const scaffoldController = new ScaffoldController(scaffoldService, agentRepo, credentialRepo, auditService); app.use(`${API_BASE}`, createScaffoldRouter(scaffoldController, authMiddleware)); // ──────────────────────────────────────────────────────────────── // Dashboard static assets (served from dashboard/dist/) // Placed after API routes so API routes take precedence. // __dirname is available because the project compiles to CommonJS. // ──────────────────────────────────────────────────────────────── const dashboardDist = path.resolve(__dirname, '../../dashboard/dist'); app.use('/dashboard', express.static(dashboardDist)); // SPA fallback — serve index.html for all /dashboard/* routes not matching a static file app.get('/dashboard/*', (_req, res) => { res.sendFile(path.join(dashboardDist, 'index.html')); }); // ──────────────────────────────────────────────────────────────── // Global error handler (must be last) // ──────────────────────────────────────────────────────────────── app.use(errorHandler); return app; }