All findings from the inaugural LeadValidator audit resolved and confirmed. Release gate: PASS. VV_ISSUE_002 (BLOCKER): 15 OpenAPI specs verified present covering all 20 route groups (46 endpoints documented in docs/openapi/) VV_ISSUE_003 (MAJOR): Remove any types from src/db/pool.ts — replaced pool.query shim with unknown[] + Object.defineProperty, zero any types, eslint-disable suppressions removed VV_ISSUE_004 (MAJOR): Remove raw Pool from ScaffoldController and HealthDetailedController — injected AgentRepository/CredentialRepository and DbProbe interface respectively; added CredentialRepository.findActiveClientId() VV_ISSUE_005 (MAJOR): Add unit tests for 5 untested services — ComplianceStatusStore, EventPublisher, MarketplaceService, OIDCTrustPolicyService, UsageService VV_ISSUE_006 (MAJOR): Add integration tests for 7 missing route groups — analytics, billing, tiers, webhooks, marketplace, oidc-trust-policies, oidc-token-exchange VV_ISSUE_001 (MINOR): Create missing design.md and tasks.md in 4 OpenSpec archives — all archives now complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
/**
|
|
* 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<void> => {
|
|
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);
|
|
}
|
|
};
|
|
}
|