"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.openApiSpec = void 0; exports.createApiDocsRouter = createApiDocsRouter; /** * OpenAPI 3.1 specification for ABE API. * Uses @asteasolutions/zod-to-openapi to generate from Zod schemas. */ const express_1 = require("express"); const zod_1 = require("zod"); const zod_to_openapi_1 = require("@asteasolutions/zod-to-openapi"); const express_api_reference_1 = require("@scalar/express-api-reference"); // Extend Zod with OpenAPI metadata support (0, zod_to_openapi_1.extendZodWithOpenApi)(zod_1.z); // ─── Registry ───────────────────────────────────────────────────────────────── const registry = new zod_to_openapi_1.OpenAPIRegistry(); // ─── Reusable schemas ───────────────────────────────────────────────────────── const ErrorSchema = registry.register('Error', zod_1.z.object({ error: zod_1.z.string() }).openapi('Error')); // Auth schemas const RegisterRequestSchema = registry.register('RegisterRequest', zod_1.z.object({ email: zod_1.z.string().email(), password: zod_1.z.string().min(8), name: zod_1.z.string().optional(), }).openapi('RegisterRequest')); const LoginRequestSchema = registry.register('LoginRequest', zod_1.z.object({ email: zod_1.z.string().email(), password: zod_1.z.string(), }).openapi('LoginRequest')); const UserSchema = registry.register('User', zod_1.z.object({ id: zod_1.z.string(), email: zod_1.z.string(), name: zod_1.z.string().nullable(), role: zod_1.z.enum(['owner', 'admin', 'member', 'viewer']), createdAt: zod_1.z.number(), }).openapi('User')); // Session schemas const SessionStatusSchema = zod_1.z.enum(['running', 'completed', 'failed', 'stopped']); const CrawlSessionSchema = registry.register('CrawlSession', zod_1.z.object({ id: zod_1.z.string(), url: zod_1.z.string().url(), status: SessionStatusSchema, seed: zod_1.z.number(), maxStates: zod_1.z.number(), statesVisited: zod_1.z.number(), createdAt: zod_1.z.number(), completedAt: zod_1.z.number().nullable(), }).openapi('CrawlSession')); const StartSessionRequestSchema = registry.register('StartSessionRequest', zod_1.z.object({ url: zod_1.z.string().url(), seed: zod_1.z.number().optional(), maxStates: zod_1.z.number().optional(), maxDepth: zod_1.z.number().optional(), allowedDomains: zod_1.z.array(zod_1.z.string()).optional(), excludedPaths: zod_1.z.array(zod_1.z.string()).optional(), }).openapi('StartSessionRequest')); // Finding schemas const SeveritySchema = zod_1.z.enum(['low', 'medium', 'high', 'critical']); const FindingStatusSchema = zod_1.z.enum(['open', 'investigating', 'resolved', 'closed']); const FindingSchema = registry.register('Finding', zod_1.z.object({ id: zod_1.z.string(), sessionId: zod_1.z.string(), severity: SeveritySchema, type: zod_1.z.string(), description: zod_1.z.string(), status: FindingStatusSchema, createdAt: zod_1.z.number(), resolvedAt: zod_1.z.number().nullable(), }).openapi('Finding')); // Report schemas const ReportFormatSchema = zod_1.z.enum(['pdf', 'html', 'json']); const ReportSchema = registry.register('Report', zod_1.z.object({ id: zod_1.z.string(), format: ReportFormatSchema, status: zod_1.z.enum(['pending', 'completed', 'failed']), createdAt: zod_1.z.number(), completedAt: zod_1.z.number().nullable(), }).openapi('Report')); // Schedule schemas const ScheduleSchema = registry.register('Schedule', zod_1.z.object({ id: zod_1.z.string(), name: zod_1.z.string(), url: zod_1.z.string(), cronExpression: zod_1.z.string(), enabled: zod_1.z.boolean(), lastRunAt: zod_1.z.number().nullable(), nextRunAt: zod_1.z.number().nullable(), createdAt: zod_1.z.number(), }).openapi('Schedule')); // Integration schemas const IntegrationTypeSchema = zod_1.z.enum(['slack', 'github', 'jira', 'webhook']); const IntegrationSchema = registry.register('Integration', zod_1.z.object({ id: zod_1.z.string(), type: IntegrationTypeSchema, name: zod_1.z.string(), enabled: zod_1.z.boolean(), createdAt: zod_1.z.number(), }).openapi('Integration')); // Visual comparison schemas const ComparisonStatusSchema = zod_1.z.enum(['passed', 'failed', 'new_state', 'pending']); const VisualComparisonSchema = registry.register('VisualComparison', zod_1.z.object({ id: zod_1.z.string(), session_id: zod_1.z.string(), state_id: zod_1.z.string(), baseline_id: zod_1.z.string().nullable(), current_screenshot_path: zod_1.z.string(), diff_screenshot_path: zod_1.z.string().nullable(), diff_pixels: zod_1.z.number().nullable(), diff_percent: zod_1.z.number().nullable(), status: ComparisonStatusSchema, created_at: zod_1.z.number(), }).openapi('VisualComparison')); // License schema const LicensePlanSchema = zod_1.z.enum(['free', 'pro', 'enterprise']); const LicenseStatusSchema = registry.register('LicenseStatus', zod_1.z.object({ plan: LicensePlanSchema, valid: zod_1.z.boolean(), expiresAt: zod_1.z.string().nullable(), features: zod_1.z.array(zod_1.z.string()), }).openapi('LicenseStatus')); // ─── Route registrations ─────────────────────────────────────────────────────── const bearerAuth = registry.registerComponent('securitySchemes', 'BearerAuth', { type: 'http', scheme: 'bearer', }); // Auth endpoints registry.registerPath({ method: 'post', path: '/api/auth/register', summary: 'Register a new user', tags: ['Auth'], request: { body: { content: { 'application/json': { schema: RegisterRequestSchema } } } }, responses: { 201: { description: 'User registered', content: { 'application/json': { schema: UserSchema } } }, 400: { description: 'Validation error', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'post', path: '/api/auth/login', summary: 'Login', tags: ['Auth'], request: { body: { content: { 'application/json': { schema: LoginRequestSchema } } } }, responses: { 200: { description: 'Login successful', content: { 'application/json': { schema: UserSchema } } }, 401: { description: 'Invalid credentials', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'post', path: '/api/auth/logout', summary: 'Logout', tags: ['Auth'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'Logged out' } }, }); registry.registerPath({ method: 'get', path: '/api/auth/me', summary: 'Get current user', tags: ['Auth'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'Current user', content: { 'application/json': { schema: UserSchema } } }, 401: { description: 'Not authenticated', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'get', path: '/api/auth/setup-required', summary: 'Check if first-run setup is required', tags: ['Auth'], responses: { 200: { description: 'Setup status', content: { 'application/json': { schema: zod_1.z.object({ required: zod_1.z.boolean() }), }, }, }, }, }); // Sessions endpoints registry.registerPath({ method: 'get', path: '/api/sessions', summary: 'List all crawl sessions', tags: ['Sessions'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'List of sessions', content: { 'application/json': { schema: zod_1.z.array(CrawlSessionSchema) } }, }, }, }); registry.registerPath({ method: 'post', path: '/api/sessions', summary: 'Start a new crawl session', tags: ['Sessions'], security: [{ [bearerAuth.name]: [] }], request: { body: { content: { 'application/json': { schema: StartSessionRequestSchema } } } }, responses: { 201: { description: 'Session started', content: { 'application/json': { schema: CrawlSessionSchema } } }, 400: { description: 'Validation error', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'get', path: '/api/sessions/{id}', summary: 'Get session by ID', tags: ['Sessions'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ id: zod_1.z.string() }) }, responses: { 200: { description: 'Session details', content: { 'application/json': { schema: CrawlSessionSchema } } }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'delete', path: '/api/sessions/{id}', summary: 'Stop a crawl session', tags: ['Sessions'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ id: zod_1.z.string() }) }, responses: { 200: { description: 'Session stopped' }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); // Findings endpoints registry.registerPath({ method: 'get', path: '/api/findings', summary: 'List findings', tags: ['Findings'], security: [{ [bearerAuth.name]: [] }], request: { query: zod_1.z.object({ severity: zod_1.z.string().optional(), type: zod_1.z.string().optional(), status: zod_1.z.string().optional(), sessionId: zod_1.z.string().optional(), search: zod_1.z.string().optional(), }), }, responses: { 200: { description: 'List of findings', content: { 'application/json': { schema: zod_1.z.array(FindingSchema) } }, }, }, }); registry.registerPath({ method: 'get', path: '/api/findings/{id}', summary: 'Get finding by ID', tags: ['Findings'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ id: zod_1.z.string() }) }, responses: { 200: { description: 'Finding details', content: { 'application/json': { schema: FindingSchema } } }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'post', path: '/api/findings/{id}/resolve', summary: 'Resolve a finding', tags: ['Findings'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ id: zod_1.z.string() }) }, responses: { 200: { description: 'Finding resolved', content: { 'application/json': { schema: FindingSchema } } }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'get', path: '/api/findings/stats', summary: 'Get finding statistics', tags: ['Findings'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'Statistics', content: { 'application/json': { schema: zod_1.z.object({ total: zod_1.z.number(), bySeverity: zod_1.z.record(zod_1.z.string(), zod_1.z.number()), byType: zod_1.z.record(zod_1.z.string(), zod_1.z.number()), byStatus: zod_1.z.record(zod_1.z.string(), zod_1.z.number()), }), }, }, }, }, }); // Reports endpoints registry.registerPath({ method: 'post', path: '/api/reports', summary: 'Generate a report', tags: ['Reports'], security: [{ [bearerAuth.name]: [] }], request: { body: { content: { 'application/json': { schema: zod_1.z.object({ sessionId: zod_1.z.string().optional(), format: ReportFormatSchema, }), }, }, }, }, responses: { 202: { description: 'Report generation started', content: { 'application/json': { schema: ReportSchema } } }, }, }); registry.registerPath({ method: 'get', path: '/api/reports', summary: 'List reports', tags: ['Reports'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'Reports list', content: { 'application/json': { schema: zod_1.z.array(ReportSchema) } } }, }, }); registry.registerPath({ method: 'get', path: '/api/reports/{id}/download', summary: 'Download report file', tags: ['Reports'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ id: zod_1.z.string() }) }, responses: { 200: { description: 'Report file (PDF, HTML or JSON)' }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); // Schedules endpoints registry.registerPath({ method: 'get', path: '/api/schedules', summary: 'List schedules', tags: ['Scheduling'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'Schedules list', content: { 'application/json': { schema: zod_1.z.array(ScheduleSchema) } } }, }, }); registry.registerPath({ method: 'post', path: '/api/schedules', summary: 'Create a schedule', tags: ['Scheduling'], security: [{ [bearerAuth.name]: [] }], request: { body: { content: { 'application/json': { schema: zod_1.z.object({ name: zod_1.z.string(), url: zod_1.z.string().url(), cronExpression: zod_1.z.string(), config: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(), }), }, }, }, }, responses: { 201: { description: 'Schedule created', content: { 'application/json': { schema: ScheduleSchema } } }, 400: { description: 'Invalid cron expression', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'patch', path: '/api/schedules/{id}/toggle', summary: 'Enable or disable a schedule', tags: ['Scheduling'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ id: zod_1.z.string() }) }, responses: { 200: { description: 'Toggled', content: { 'application/json': { schema: ScheduleSchema } } }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'delete', path: '/api/schedules/{id}', summary: 'Delete a schedule', tags: ['Scheduling'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ id: zod_1.z.string() }) }, responses: { 200: { description: 'Deleted' }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); // Integrations endpoints registry.registerPath({ method: 'get', path: '/api/integrations', summary: 'List integrations', tags: ['Integrations'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'Integrations list', content: { 'application/json': { schema: zod_1.z.array(IntegrationSchema) } } }, }, }); registry.registerPath({ method: 'post', path: '/api/integrations', summary: 'Create an integration', tags: ['Integrations'], security: [{ [bearerAuth.name]: [] }], request: { body: { content: { 'application/json': { schema: zod_1.z.object({ type: IntegrationTypeSchema, name: zod_1.z.string(), config: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()), }), }, }, }, }, responses: { 201: { description: 'Integration created', content: { 'application/json': { schema: IntegrationSchema } } }, }, }); // Visual regression endpoints registry.registerPath({ method: 'get', path: '/api/visual/comparisons', summary: 'List visual comparisons', tags: ['Visual Regression'], security: [{ [bearerAuth.name]: [] }], request: { query: zod_1.z.object({ sessionId: zod_1.z.string().optional(), status: ComparisonStatusSchema.optional(), }), }, responses: { 200: { description: 'Comparisons list', content: { 'application/json': { schema: zod_1.z.array(VisualComparisonSchema) } }, }, }, }); registry.registerPath({ method: 'post', path: '/api/visual/baselines/{comparisonId}/approve', summary: 'Approve a comparison as baseline', tags: ['Visual Regression'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ comparisonId: zod_1.z.string() }) }, responses: { 200: { description: 'Approved', content: { 'application/json': { schema: zod_1.z.object({ baselineId: zod_1.z.string(), status: zod_1.z.literal('approved') }), }, }, }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'post', path: '/api/visual/baselines/{comparisonId}/reject', summary: 'Reject a comparison', tags: ['Visual Regression'], security: [{ [bearerAuth.name]: [] }], request: { params: zod_1.z.object({ comparisonId: zod_1.z.string() }) }, responses: { 200: { description: 'Rejected', content: { 'application/json': { schema: zod_1.z.object({ status: zod_1.z.literal('rejected') }), }, }, }, 404: { description: 'Not found', content: { 'application/json': { schema: ErrorSchema } } }, }, }); registry.registerPath({ method: 'post', path: '/api/visual/baselines/approve-all', summary: 'Approve all new-state comparisons as baselines', tags: ['Visual Regression'], security: [{ [bearerAuth.name]: [] }], request: { body: { content: { 'application/json': { schema: zod_1.z.object({ sessionId: zod_1.z.string().optional() }), }, }, }, }, responses: { 200: { description: 'Bulk approved', content: { 'application/json': { schema: zod_1.z.object({ approved: zod_1.z.number() }), }, }, }, }, }); // License endpoints registry.registerPath({ method: 'get', path: '/api/license/status', summary: 'Get license status', tags: ['License'], security: [{ [bearerAuth.name]: [] }], responses: { 200: { description: 'License status', content: { 'application/json': { schema: LicenseStatusSchema } } }, }, }); registry.registerPath({ method: 'post', path: '/api/license/activate', summary: 'Activate a license key', tags: ['License'], security: [{ [bearerAuth.name]: [] }], request: { body: { content: { 'application/json': { schema: zod_1.z.object({ key: zod_1.z.string() }), }, }, }, }, responses: { 200: { description: 'License activated', content: { 'application/json': { schema: LicenseStatusSchema } } }, 400: { description: 'Invalid key', content: { 'application/json': { schema: ErrorSchema } } }, }, }); // Health endpoints registry.registerPath({ method: 'get', path: '/health/live', summary: 'Liveness probe', tags: ['Health'], responses: { 200: { description: 'Process alive', content: { 'application/json': { schema: zod_1.z.object({ status: zod_1.z.literal('ok'), uptime: zod_1.z.number() }), }, }, }, }, }); registry.registerPath({ method: 'get', path: '/health/ready', summary: 'Readiness probe', tags: ['Health'], responses: { 200: { description: 'Ready', content: { 'application/json': { schema: zod_1.z.object({ status: zod_1.z.literal('ready'), db: zod_1.z.string() }), }, }, }, 503: { description: 'Not ready', content: { 'application/json': { schema: zod_1.z.object({ status: zod_1.z.literal('not_ready'), db: zod_1.z.string(), error: zod_1.z.string() }), }, }, }, }, }); // ─── Generate spec ───────────────────────────────────────────────────────────── const generator = new zod_to_openapi_1.OpenApiGeneratorV31(registry.definitions); exports.openApiSpec = generator.generateDocument({ openapi: '3.1.0', info: { title: 'ABE — Autonomous Bug Explorer API', version: '1.0.0', description: 'ABE is an enterprise self-hosted platform for autonomous web application bug discovery. ' + 'This API allows you to manage crawl sessions, review findings, generate reports, and configure integrations.', }, servers: [{ url: 'http://localhost:3001', description: 'Local development' }], }); // ─── Express Router ──────────────────────────────────────────────────────────── function createApiDocsRouter() { const router = (0, express_1.Router)(); // Serve the raw OpenAPI JSON spec router.get('/openapi.json', (_req, res) => { res.json(exports.openApiSpec); }); // Serve Scalar UI router.use('/', (0, express_api_reference_1.apiReference)({ spec: { content: exports.openApiSpec }, theme: 'purple', })); return router; }