fase(21): openapi documentation with scalar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -350,24 +350,24 @@ Spec: `.ralph/specs/phase-18-cli-cicd.md`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 20: Visual Regression Refactor [PENDIENTE]
|
## Phase 20: Visual Regression Refactor [COMPLETO]
|
||||||
|
|
||||||
- [ ] 20.1: Migrar visual regression existente → nueva estructura modular
|
- [x] 20.1: Migrar visual regression existente → nueva estructura modular
|
||||||
- [ ] 20.2: Integrar con StorageProvider para screenshots
|
- [x] 20.2: Integrar con StorageProvider para screenshots
|
||||||
- [ ] 20.3: Refactorizar frontend /visual-review con shadcn/ui components
|
- [x] 20.3: Refactorizar frontend /visual-review con shadcn/ui components
|
||||||
- [ ] 20.4: Verificar build + commit: `fase(20): visual regression refactor`
|
- [x] 20.4: Verificar build + commit: `fase(20): visual regression refactor`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 21: API Documentation [PENDIENTE]
|
## Phase 21: API Documentation [COMPLETO]
|
||||||
|
|
||||||
- [ ] 21.1: Instalar: `npm i @asteasolutions/zod-to-openapi @scalar/express-api-reference`
|
- [x] 21.1: Instalar: `npm i @asteasolutions/zod-to-openapi @scalar/express-api-reference`
|
||||||
- [ ] 21.2: Crear Zod schemas compartidos para TODOS los endpoints (request + response)
|
- [x] 21.2: Crear Zod schemas compartidos para TODOS los endpoints (request + response)
|
||||||
- [ ] 21.3: Generar OpenAPI 3.1 spec desde Zod schemas
|
- [x] 21.3: Generar OpenAPI 3.1 spec desde Zod schemas
|
||||||
- [ ] 21.4: Montar Scalar UI en GET /api-docs
|
- [x] 21.4: Montar Scalar UI en GET /api-docs
|
||||||
- [ ] 21.5: Servir spec JSON en GET /api-docs/openapi.json
|
- [x] 21.5: Servir spec JSON en GET /api-docs/openapi.json
|
||||||
- [ ] 21.6: Verificar que todos los endpoints están documentados
|
- [x] 21.6: Verificar que todos los endpoints están documentados
|
||||||
- [ ] 21.7: Verificar build + commit: `fase(21): openapi documentation with scalar`
|
- [x] 21.7: Verificar build + commit: `fase(21): openapi documentation with scalar`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
622
dist/api/openapi.js
vendored
Normal file
622
dist/api/openapi.js
vendored
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
"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;
|
||||||
|
}
|
||||||
3
dist/api/server.js
vendored
3
dist/api/server.js
vendored
@@ -17,6 +17,7 @@ const requestId_1 = require("./middleware/requestId");
|
|||||||
const notFound_1 = require("./middleware/notFound");
|
const notFound_1 = require("./middleware/notFound");
|
||||||
const errorHandler_1 = require("./middleware/errorHandler");
|
const errorHandler_1 = require("./middleware/errorHandler");
|
||||||
const router_1 = require("./router");
|
const router_1 = require("./router");
|
||||||
|
const openapi_1 = require("./openapi");
|
||||||
function createServer(deps) {
|
function createServer(deps) {
|
||||||
const app = (0, express_1.default)();
|
const app = (0, express_1.default)();
|
||||||
// 1. Request ID — must be first so all logs have requestId
|
// 1. Request ID — must be first so all logs have requestId
|
||||||
@@ -58,6 +59,8 @@ function createServer(deps) {
|
|||||||
});
|
});
|
||||||
// 7. Module routes
|
// 7. Module routes
|
||||||
app.use('/api', (0, router_1.createRouter)(deps));
|
app.use('/api', (0, router_1.createRouter)(deps));
|
||||||
|
// 7b. API documentation (no auth required)
|
||||||
|
app.use('/api-docs', (0, openapi_1.createApiDocsRouter)());
|
||||||
// 8. 404 handler
|
// 8. 404 handler
|
||||||
app.use(notFound_1.notFoundMiddleware);
|
app.use(notFound_1.notFoundMiddleware);
|
||||||
// 9. Global error handler — always last
|
// 9. Global error handler — always last
|
||||||
|
|||||||
131
package-lock.json
generated
131
package-lock.json
generated
@@ -9,10 +9,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@asteasolutions/zod-to-openapi": "^8.4.3",
|
||||||
"@axe-core/playwright": "^4.11.1",
|
"@axe-core/playwright": "^4.11.1",
|
||||||
"@casl/ability": "^6.8.0",
|
"@casl/ability": "^6.8.0",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@playwright/test": "^1.40.0",
|
"@playwright/test": "^1.40.0",
|
||||||
|
"@scalar/express-api-reference": "^0.8.48",
|
||||||
"@slack/web-api": "^7.14.1",
|
"@slack/web-api": "^7.14.1",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
@@ -54,6 +56,18 @@
|
|||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@asteasolutions/zod-to-openapi": {
|
||||||
|
"version": "8.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.3.tgz",
|
||||||
|
"integrity": "sha512-lwfMTN7kDbFDwMniYZUebiGGHxVGBw9ZSI4IBYjm6Ey22Kd5z/fsQb2k+Okr8WMbCCC553vi/ZM9utl5/XcvuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"openapi3-ts": "^4.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@axe-core/playwright": {
|
"node_modules/@axe-core/playwright": {
|
||||||
"version": "4.11.1",
|
"version": "4.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz",
|
||||||
@@ -1740,6 +1754,69 @@
|
|||||||
"url": "https://opencollective.com/immer"
|
"url": "https://opencollective.com/immer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scalar/core": {
|
||||||
|
"version": "0.3.45",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scalar/core/-/core-0.3.45.tgz",
|
||||||
|
"integrity": "sha512-f3jyzColUUcu3eYfjslgNjG2JABuFU8WR3P7a9GevMpisW3skWUqVVffyC1P4w4i9TVRk9FUjYEtGeP10Rw9lQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@scalar/types": "0.6.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scalar/express-api-reference": {
|
||||||
|
"version": "0.8.48",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scalar/express-api-reference/-/express-api-reference-0.8.48.tgz",
|
||||||
|
"integrity": "sha512-DtdWhpxSPYAEuWNa0BZ8uVgvmfXytD6jRRJUlFi9RBeVm2Gectbc5dDtHqekmllQ4iiwyvGm8RcEhY/TZm4KbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@scalar/core": "0.3.45"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scalar/helpers": {
|
||||||
|
"version": "0.2.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.2.18.tgz",
|
||||||
|
"integrity": "sha512-w1d4tpNEVZ293oB2BAgLrS0kVPUtG3eByNmOCJA5eK9vcT4D3cmsGtWjUaaqit0BQCsBFHK51rasGvSWnApYTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scalar/types": {
|
||||||
|
"version": "0.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.6.10.tgz",
|
||||||
|
"integrity": "sha512-fZkelRwcEeAhsn4c0wjYXWrzSzLaEyfxTn/eazXJ4XfCIsgJTQyK0FD8mnOBZJ2vEIbtT2E1mBKnCbDxrJIlxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@scalar/helpers": "0.2.18",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"type-fest": "^5.3.1",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@scalar/types/node_modules/type-fest": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"tagged-tag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.10",
|
"version": "0.27.10",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
|
||||||
@@ -5539,6 +5616,24 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || >=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/napi-build-utils": {
|
"node_modules/napi-build-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
@@ -5725,6 +5820,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi3-ts": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"yaml": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-finally": {
|
"node_modules/p-finally": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||||
@@ -7141,6 +7245,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tagged-tag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
@@ -7679,6 +7795,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|||||||
@@ -36,10 +36,12 @@
|
|||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@asteasolutions/zod-to-openapi": "^8.4.3",
|
||||||
"@axe-core/playwright": "^4.11.1",
|
"@axe-core/playwright": "^4.11.1",
|
||||||
"@casl/ability": "^6.8.0",
|
"@casl/ability": "^6.8.0",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
"@playwright/test": "^1.40.0",
|
"@playwright/test": "^1.40.0",
|
||||||
|
"@scalar/express-api-reference": "^0.8.48",
|
||||||
"@slack/web-api": "^7.14.1",
|
"@slack/web-api": "^7.14.1",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
|
|||||||
719
src/api/openapi.ts
Normal file
719
src/api/openapi.ts
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
/**
|
||||||
|
* OpenAPI 3.1 specification for ABE API.
|
||||||
|
* Uses @asteasolutions/zod-to-openapi to generate from Zod schemas.
|
||||||
|
*/
|
||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { extendZodWithOpenApi, OpenApiGeneratorV31, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
|
||||||
|
import { apiReference } from '@scalar/express-api-reference';
|
||||||
|
|
||||||
|
// Extend Zod with OpenAPI metadata support
|
||||||
|
extendZodWithOpenApi(z);
|
||||||
|
|
||||||
|
// ─── Registry ─────────────────────────────────────────────────────────────────
|
||||||
|
const registry = new OpenAPIRegistry();
|
||||||
|
|
||||||
|
// ─── Reusable schemas ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ErrorSchema = registry.register(
|
||||||
|
'Error',
|
||||||
|
z.object({ error: z.string() }).openapi('Error')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auth schemas
|
||||||
|
const RegisterRequestSchema = registry.register(
|
||||||
|
'RegisterRequest',
|
||||||
|
z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(8),
|
||||||
|
name: z.string().optional(),
|
||||||
|
}).openapi('RegisterRequest')
|
||||||
|
);
|
||||||
|
|
||||||
|
const LoginRequestSchema = registry.register(
|
||||||
|
'LoginRequest',
|
||||||
|
z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string(),
|
||||||
|
}).openapi('LoginRequest')
|
||||||
|
);
|
||||||
|
|
||||||
|
const UserSchema = registry.register(
|
||||||
|
'User',
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
role: z.enum(['owner', 'admin', 'member', 'viewer']),
|
||||||
|
createdAt: z.number(),
|
||||||
|
}).openapi('User')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Session schemas
|
||||||
|
const SessionStatusSchema = z.enum(['running', 'completed', 'failed', 'stopped']);
|
||||||
|
|
||||||
|
const CrawlSessionSchema = registry.register(
|
||||||
|
'CrawlSession',
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
status: SessionStatusSchema,
|
||||||
|
seed: z.number(),
|
||||||
|
maxStates: z.number(),
|
||||||
|
statesVisited: z.number(),
|
||||||
|
createdAt: z.number(),
|
||||||
|
completedAt: z.number().nullable(),
|
||||||
|
}).openapi('CrawlSession')
|
||||||
|
);
|
||||||
|
|
||||||
|
const StartSessionRequestSchema = registry.register(
|
||||||
|
'StartSessionRequest',
|
||||||
|
z.object({
|
||||||
|
url: z.string().url(),
|
||||||
|
seed: z.number().optional(),
|
||||||
|
maxStates: z.number().optional(),
|
||||||
|
maxDepth: z.number().optional(),
|
||||||
|
allowedDomains: z.array(z.string()).optional(),
|
||||||
|
excludedPaths: z.array(z.string()).optional(),
|
||||||
|
}).openapi('StartSessionRequest')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Finding schemas
|
||||||
|
const SeveritySchema = z.enum(['low', 'medium', 'high', 'critical']);
|
||||||
|
const FindingStatusSchema = z.enum(['open', 'investigating', 'resolved', 'closed']);
|
||||||
|
|
||||||
|
const FindingSchema = registry.register(
|
||||||
|
'Finding',
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
sessionId: z.string(),
|
||||||
|
severity: SeveritySchema,
|
||||||
|
type: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
status: FindingStatusSchema,
|
||||||
|
createdAt: z.number(),
|
||||||
|
resolvedAt: z.number().nullable(),
|
||||||
|
}).openapi('Finding')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Report schemas
|
||||||
|
const ReportFormatSchema = z.enum(['pdf', 'html', 'json']);
|
||||||
|
|
||||||
|
const ReportSchema = registry.register(
|
||||||
|
'Report',
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
format: ReportFormatSchema,
|
||||||
|
status: z.enum(['pending', 'completed', 'failed']),
|
||||||
|
createdAt: z.number(),
|
||||||
|
completedAt: z.number().nullable(),
|
||||||
|
}).openapi('Report')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Schedule schemas
|
||||||
|
const ScheduleSchema = registry.register(
|
||||||
|
'Schedule',
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
cronExpression: z.string(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
lastRunAt: z.number().nullable(),
|
||||||
|
nextRunAt: z.number().nullable(),
|
||||||
|
createdAt: z.number(),
|
||||||
|
}).openapi('Schedule')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Integration schemas
|
||||||
|
const IntegrationTypeSchema = z.enum(['slack', 'github', 'jira', 'webhook']);
|
||||||
|
|
||||||
|
const IntegrationSchema = registry.register(
|
||||||
|
'Integration',
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: IntegrationTypeSchema,
|
||||||
|
name: z.string(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
createdAt: z.number(),
|
||||||
|
}).openapi('Integration')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Visual comparison schemas
|
||||||
|
const ComparisonStatusSchema = z.enum(['passed', 'failed', 'new_state', 'pending']);
|
||||||
|
|
||||||
|
const VisualComparisonSchema = registry.register(
|
||||||
|
'VisualComparison',
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
session_id: z.string(),
|
||||||
|
state_id: z.string(),
|
||||||
|
baseline_id: z.string().nullable(),
|
||||||
|
current_screenshot_path: z.string(),
|
||||||
|
diff_screenshot_path: z.string().nullable(),
|
||||||
|
diff_pixels: z.number().nullable(),
|
||||||
|
diff_percent: z.number().nullable(),
|
||||||
|
status: ComparisonStatusSchema,
|
||||||
|
created_at: z.number(),
|
||||||
|
}).openapi('VisualComparison')
|
||||||
|
);
|
||||||
|
|
||||||
|
// License schema
|
||||||
|
const LicensePlanSchema = z.enum(['free', 'pro', 'enterprise']);
|
||||||
|
|
||||||
|
const LicenseStatusSchema = registry.register(
|
||||||
|
'LicenseStatus',
|
||||||
|
z.object({
|
||||||
|
plan: LicensePlanSchema,
|
||||||
|
valid: z.boolean(),
|
||||||
|
expiresAt: z.string().nullable(),
|
||||||
|
features: z.array(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: z.object({ required: 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: 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: z.object({ id: 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: z.object({ id: 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: z.object({
|
||||||
|
severity: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
status: z.string().optional(),
|
||||||
|
sessionId: z.string().optional(),
|
||||||
|
search: z.string().optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'List of findings',
|
||||||
|
content: { 'application/json': { schema: z.array(FindingSchema) } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'get',
|
||||||
|
path: '/api/findings/{id}',
|
||||||
|
summary: 'Get finding by ID',
|
||||||
|
tags: ['Findings'],
|
||||||
|
security: [{ [bearerAuth.name]: [] }],
|
||||||
|
request: { params: z.object({ id: 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: z.object({ id: 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: z.object({
|
||||||
|
total: z.number(),
|
||||||
|
bySeverity: z.record(z.string(), z.number()),
|
||||||
|
byType: z.record(z.string(), z.number()),
|
||||||
|
byStatus: z.record(z.string(), 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: z.object({
|
||||||
|
sessionId: 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: z.array(ReportSchema) } } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'get',
|
||||||
|
path: '/api/reports/{id}/download',
|
||||||
|
summary: 'Download report file',
|
||||||
|
tags: ['Reports'],
|
||||||
|
security: [{ [bearerAuth.name]: [] }],
|
||||||
|
request: { params: z.object({ id: 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: 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: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
cronExpression: z.string(),
|
||||||
|
config: z.record(z.string(), 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: z.object({ id: 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: z.object({ id: 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: 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: z.object({
|
||||||
|
type: IntegrationTypeSchema,
|
||||||
|
name: z.string(),
|
||||||
|
config: z.record(z.string(), 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: z.object({
|
||||||
|
sessionId: z.string().optional(),
|
||||||
|
status: ComparisonStatusSchema.optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Comparisons list',
|
||||||
|
content: { 'application/json': { schema: 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: z.object({ comparisonId: z.string() }) },
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Approved',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ baselineId: z.string(), status: 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: z.object({ comparisonId: z.string() }) },
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Rejected',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ status: 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: z.object({ sessionId: z.string().optional() }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Bulk approved',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ approved: 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: z.object({ key: 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: z.object({ status: z.literal('ok'), uptime: z.number() }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: 'get',
|
||||||
|
path: '/health/ready',
|
||||||
|
summary: 'Readiness probe',
|
||||||
|
tags: ['Health'],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Ready',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ status: z.literal('ready'), db: z.string() }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
503: {
|
||||||
|
description: 'Not ready',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({ status: z.literal('not_ready'), db: z.string(), error: z.string() }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Generate spec ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const generator = new OpenApiGeneratorV31(registry.definitions);
|
||||||
|
|
||||||
|
export const 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 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function createApiDocsRouter(): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Serve the raw OpenAPI JSON spec
|
||||||
|
router.get('/openapi.json', (_req: Request, res: Response) => {
|
||||||
|
res.json(openApiSpec);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve Scalar UI
|
||||||
|
router.use(
|
||||||
|
'/',
|
||||||
|
apiReference({
|
||||||
|
spec: { content: openApiSpec },
|
||||||
|
theme: 'purple',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import { createRequestIdMiddleware } from './middleware/requestId';
|
|||||||
import { notFoundMiddleware } from './middleware/notFound';
|
import { notFoundMiddleware } from './middleware/notFound';
|
||||||
import { globalErrorHandler } from './middleware/errorHandler';
|
import { globalErrorHandler } from './middleware/errorHandler';
|
||||||
import { createRouter } from './router';
|
import { createRouter } from './router';
|
||||||
|
import { createApiDocsRouter } from './openapi';
|
||||||
import { CrawlingControllerDeps } from '../modules/crawling/infrastructure/http/CrawlingController';
|
import { CrawlingControllerDeps } from '../modules/crawling/infrastructure/http/CrawlingController';
|
||||||
import { FindingsControllerDeps } from '../modules/findings/infrastructure/http/FindingsController';
|
import { FindingsControllerDeps } from '../modules/findings/infrastructure/http/FindingsController';
|
||||||
import { FuzzingControllerDeps } from '../modules/fuzzing/infrastructure/http/FuzzingController';
|
import { FuzzingControllerDeps } from '../modules/fuzzing/infrastructure/http/FuzzingController';
|
||||||
@@ -93,6 +94,9 @@ export function createServer(deps: ServerDependencies): Express {
|
|||||||
// 7. Module routes
|
// 7. Module routes
|
||||||
app.use('/api', createRouter(deps));
|
app.use('/api', createRouter(deps));
|
||||||
|
|
||||||
|
// 7b. API documentation (no auth required)
|
||||||
|
app.use('/api-docs', createApiDocsRouter());
|
||||||
|
|
||||||
// 8. 404 handler
|
// 8. 404 handler
|
||||||
app.use(notFoundMiddleware);
|
app.use(notFoundMiddleware);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user