()
| 576 | } |
| 577 | |
| 578 | private setupMiddleware(): void { |
| 579 | // Trust the first proxy (Fly.io) for accurate client IP detection |
| 580 | // Required for express-rate-limit and other middleware that use req.ip |
| 581 | this.app.set('trust proxy', 1); |
| 582 | |
| 583 | // Serve JSON schemas (aliases + static files + discovery) before body-parsing, |
| 584 | // cookie, and CSRF middleware so these high-traffic reads stay cheap. |
| 585 | const distPath = process.env.NODE_ENV === 'production' |
| 586 | ? __dirname |
| 587 | : path.join(__dirname, "../../dist"); |
| 588 | mountSchemasRoutes(this.app, path.join(distPath, 'schemas')); |
| 589 | mountComplianceRoutes(this.app, path.join(distPath, 'compliance')); |
| 590 | mountProtocolRoutes(this.app, path.join(distPath, 'protocol')); |
| 591 | |
| 592 | // Track slow API responses and alert ops |
| 593 | this.app.use(slowResponseTracker); |
| 594 | |
| 595 | // Capture request duration metrics for all API calls |
| 596 | this.app.use(requestMetrics); |
| 597 | |
| 598 | // Use JSON parser for all routes EXCEPT those that need raw body for signature verification |
| 599 | // Limit increased to 10MB to support base64-encoded logo uploads in member profiles |
| 600 | this.app.use((req, res, next) => { |
| 601 | // Skip global JSON parser for routes that need raw body capture: |
| 602 | // - Stripe webhooks: need raw body for webhook signature verification |
| 603 | // - Resend inbound webhooks: need raw body for Svix signature verification |
| 604 | // - WorkOS webhooks: need raw body for WorkOS signature verification |
| 605 | // - Zoom webhooks: need raw body for HMAC signature verification |
| 606 | // - Slack routes: need raw body for Slack signature verification |
| 607 | // (both JSON for events and URL-encoded for commands) |
| 608 | if (req.path === '/api/webhooks/stripe' || |
| 609 | req.path === '/api/webhooks/resend-inbound' || |
| 610 | req.path === '/api/webhooks/resend-tracking' || |
| 611 | req.path === '/api/webhooks/workos' || |
| 612 | req.path === '/api/webhooks/zoom' || |
| 613 | req.path.startsWith('/api/slack/')) { |
| 614 | next(); |
| 615 | } else { |
| 616 | // `verify` captures raw body bytes before JSON parses them — required |
| 617 | // for RFC 9421 request-signature verification on the training-agent |
| 618 | // `/mcp` endpoint, which rehashes the exact bytes the signer signed. |
| 619 | // Cheap (one utf-8 decode per request) and unused elsewhere. |
| 620 | express.json({ |
| 621 | limit: '10mb', |
| 622 | verify: (req, _res, buf) => { |
| 623 | (req as unknown as { rawBody?: string }).rawBody = buf.toString('utf8'); |
| 624 | }, |
| 625 | })(req, res, next); |
| 626 | } |
| 627 | }); |
| 628 | this.app.use(cookieParser()); |
| 629 | this.app.use(csrfProtection); |
| 630 | |
| 631 | // Serve brand.json for both AAO domains. |
| 632 | // AdCP domain redirects to the AAO house. AAO domain redirects to the DB-managed hosted brand. |
| 633 | this.app.get('/.well-known/brand.json', (req, res) => { |
| 634 | res.setHeader('Cache-Control', 'public, max-age=3600'); |
| 635 | if (this.isAdcpDomain(req)) { |
no test coverage detected