/** * Scaffold Controller for SentryAgent.ai AgentIdP. * HTTP handler for the GET /sdk/scaffold/:agentId endpoint. */ import { Request, Response, NextFunction } from 'express'; import { AgentRepository } from '../repositories/AgentRepository.js'; import { CredentialRepository } from '../repositories/CredentialRepository.js'; import { AuditService } from '../services/AuditService.js'; import { ScaffoldService, SCAFFOLD_LANGUAGES } from '../services/ScaffoldService.js'; import { ScaffoldLanguage } from '../types/scaffold.js'; import { AgentNotFoundError, AuthorizationError, ValidationError } from '../utils/errors.js'; /** * Controller for the scaffold generator endpoint. * Validates request, fetches agent credentials, delegates ZIP generation to ScaffoldService. */ export class ScaffoldController { /** * @param scaffoldService - The scaffold generation service. * @param agentRepo - Agent repository for agent lookup. * @param credentialRepo - Credential repository for active credential lookup. * @param auditService - Audit log service. */ constructor( private readonly scaffoldService: ScaffoldService, private readonly agentRepo: AgentRepository, private readonly credentialRepo: CredentialRepository, private readonly auditService: AuditService, ) {} /** * Handles GET /sdk/scaffold/:agentId — generates and streams a scaffold ZIP. * * @param req - Express request with agentId path param and language query param. * @param res - Express response streaming the ZIP file. * @param next - Express next function. */ getScaffold = async (req: Request, res: Response, next: NextFunction): Promise => { try { if (!req.user) { throw new AuthorizationError(); } const { agentId } = req.params; const rawLanguage = req.query['language'] as string | undefined; const language: ScaffoldLanguage = (rawLanguage as ScaffoldLanguage) ?? 'typescript'; if (!SCAFFOLD_LANGUAGES.includes(language)) { throw new ValidationError( `Unsupported language '${language}'. Choose: ${SCAFFOLD_LANGUAGES.join(', ')}`, { code: 'INVALID_LANGUAGE' }, ); } const tenantId = req.user.organization_id ?? 'org_system'; // Fetch agent and verify it belongs to the authenticated tenant const agent = await this.agentRepo.findById(agentId); if (agent === null) { throw new AgentNotFoundError(agentId); } if (agent.organizationId !== tenantId) { throw new AuthorizationError('You do not own this agent.'); } // Fetch the agent's active credential client_id const activeClientId = await this.credentialRepo.findActiveClientId(agentId); const clientId = activeClientId ?? agentId; const apiUrl = process.env['API_URL'] ?? process.env['NEXT_PUBLIC_API_URL'] ?? 'https://api.sentryagent.ai'; const { stream, filename } = await this.scaffoldService.generateScaffold({ agentId, agentName: agent.email.split('@')[0] ?? agentId, clientId, language, apiUrl, }); const ipAddress = req.ip ?? '0.0.0.0'; const userAgent = req.headers['user-agent'] ?? 'unknown'; await this.auditService.logEvent( req.user.sub, 'scaffold.generated', 'success', ipAddress, userAgent, { agentId, language }, tenantId, ); res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); stream.pipe(res); } catch (err) { next(err); } }; }