/** * ComplianceController — SOC 2 Type II and AGNTCY compliance endpoints. * * Handles endpoints defined in docs/openapi/compliance.yaml: * GET /api/v1/audit/verify — Audit chain integrity verification (auth required) * GET /api/v1/compliance/controls — SOC 2 control status summary (public) * GET /api/v1/compliance/report — AGNTCY compliance report (auth required) * GET /api/v1/compliance/agent-cards — AGNTCY agent card export (auth required) */ import { Request, Response, NextFunction } from 'express'; import { AuditVerificationService } from '../services/AuditVerificationService.js'; import { ComplianceService } from '../services/ComplianceService.js'; import { getAllControlStatuses } from '../services/ComplianceStatusStore.js'; import { ValidationError } from '../utils/errors.js'; import { ITokenPayload } from '../types/index.js'; // ============================================================================ // Helpers // ============================================================================ /** * Returns `true` if the given string is a valid ISO 8601 date-time string. * Uses `Date.parse` — valid ISO 8601 strings produce a finite number; * invalid strings produce `NaN`. * * @param value - The string to validate. * @returns `true` if valid ISO 8601 date-time; `false` otherwise. */ function isValidIsoDateTime(value: string): boolean { const parsed = Date.parse(value); return !isNaN(parsed); } // ============================================================================ // Controller // ============================================================================ /** * Controller for SOC 2 Type II and AGNTCY compliance API endpoints. * Exposes audit chain verification, live control status reporting, * AGNTCY compliance report generation, and agent card export. */ export class ComplianceController { /** * @param auditVerificationService - Service for cryptographic audit chain verification. * @param complianceService - Service for AGNTCY compliance report and agent card generation. */ constructor( private readonly auditVerificationService: AuditVerificationService, private readonly complianceService: ComplianceService, ) {} // ────────────────────────────────────────────────────────────────────────── // Handlers // ────────────────────────────────────────────────────────────────────────── /** * GET /api/v1/audit/verify * * Verifies the cryptographic integrity of the audit event hash chain. * Accepts optional `fromDate` and `toDate` ISO 8601 query parameters to restrict * the verification window. Returns 200 regardless of whether the chain is intact — * check `verified` in the response body. * * Requires Bearer token with `audit:read` scope (enforced by route middleware). * * @param req - Express request; optional `fromDate` and `toDate` query params. * @param res - Express response. * @param next - Express next function. */ async verifyAuditChain(req: Request, res: Response, next: NextFunction): Promise { try { const { fromDate, toDate } = req.query as Record; // Validate fromDate if provided if (fromDate !== undefined && !isValidIsoDateTime(fromDate)) { throw new ValidationError('Invalid query parameter value.', { field: 'fromDate', reason: 'Must be a valid ISO 8601 date-time string (e.g. 2026-03-01T00:00:00.000Z).', }); } // Validate toDate if provided if (toDate !== undefined && !isValidIsoDateTime(toDate)) { throw new ValidationError('Invalid query parameter value.', { field: 'toDate', reason: 'Must be a valid ISO 8601 date-time string (e.g. 2026-03-31T23:59:59.999Z).', }); } // Validate date range ordering if (fromDate !== undefined && toDate !== undefined) { if (new Date(fromDate) > new Date(toDate)) { throw new ValidationError('Invalid date range.', { reason: 'fromDate must be before or equal to toDate.', }); } } const result = await this.auditVerificationService.verifyChain(fromDate, toDate); res.status(200).json(result); } catch (err) { next(err); } } /** * GET /api/v1/compliance/controls * * Returns a live status snapshot for all five in-scope SOC 2 Trust Services * Criteria controls. Status values are maintained by background jobs * (SecretsRotationJob, AuditChainVerificationJob) via ComplianceStatusStore. * * No authentication required — this is a public health-style endpoint. * Sets `Cache-Control: public, max-age=60` to permit 60-second downstream caching. * * @param _req - Express request (unused). * @param res - Express response. * @param next - Express next function. */ async getComplianceControls( _req: Request, res: Response, next: NextFunction, ): Promise { try { const controls = getAllControlStatuses(); res.setHeader('Cache-Control', 'public, max-age=60'); res.status(200).json({ controls }); } catch (err) { next(err); } } /** * GET /api/v1/compliance/report * * Generates and returns an AGNTCY compliance report for the authenticated tenant. * The report covers agent-identity verification and audit-trail integrity. * Reports are cached in Redis for 5 minutes; sets `X-Cache: HIT` when served from cache. * * Requires Bearer token authentication (tenant extracted from req.user.sub). * * @param req - Express request; tenant derived from authenticated user context. * @param res - Express response. * @param next - Express next function. */ async getComplianceReport(req: Request, res: Response, next: NextFunction): Promise { try { const user = req.user as ITokenPayload | undefined; const tenantId = user?.organization_id ?? user?.sub ?? ''; const report = await this.complianceService.generateReport(tenantId); if (report.from_cache === true) { res.setHeader('X-Cache', 'HIT'); } res.status(200).json(report); } catch (err) { next(err); } } /** * GET /api/v1/compliance/agent-cards * * Exports all active agents for the authenticated tenant as AGNTCY-standard * agent card JSON objects. * * Requires Bearer token authentication (tenant extracted from req.user.sub). * * @param req - Express request; tenant derived from authenticated user context. * @param res - Express response. * @param next - Express next function. */ async exportAgentCards(req: Request, res: Response, next: NextFunction): Promise { try { const user = req.user as ITokenPayload | undefined; const tenantId = user?.organization_id ?? user?.sub ?? ''; const cards = await this.complianceService.exportAgentCards(tenantId); res.status(200).json(cards); } catch (err) { next(err); } } }