fase(16): integrations module

This commit is contained in:
Claude
2026-03-06 07:22:00 -05:00
committed by debian
parent cffa1aeea9
commit 1f1678af17
49 changed files with 2558 additions and 13 deletions

View File

@@ -0,0 +1,111 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createIntegrationsRouter = createIntegrationsRouter;
const express_1 = require("express");
const Integration_1 = require("../../domain/entities/Integration");
const WebhookEndpoint_1 = require("../../domain/entities/WebhookEndpoint");
const IntegrationType_1 = require("../../domain/value-objects/IntegrationType");
function createIntegrationsRouter(deps) {
const router = (0, express_1.Router)();
const { integrationRepo, webhookRepo } = deps;
// ─── Integrations CRUD ──────────────────────────────────────────────────────
router.get('/', async (_req, res) => {
const items = await integrationRepo.findAll();
res.json(items.map(serializeIntegration));
});
router.post('/', async (req, res) => {
const { name, type, config } = req.body;
if (!name || !type) {
res.status(400).json({ error: 'name and type are required' });
return;
}
let intType;
try {
intType = IntegrationType_1.IntegrationType.fromString(type);
}
catch {
res.status(400).json({ error: `Invalid integration type: ${type}` });
return;
}
const integration = Integration_1.Integration.create({ name, type: intType, config: config ?? {} });
await integrationRepo.save(integration);
res.status(201).json(serializeIntegration(integration));
});
router.get('/:id', async (req, res) => {
const item = await integrationRepo.findById(req.params['id']);
if (!item) {
res.status(404).json({ error: 'Integration not found' });
return;
}
res.json(serializeIntegration(item));
});
router.patch('/:id', async (req, res) => {
const item = await integrationRepo.findById(req.params['id']);
if (!item) {
res.status(404).json({ error: 'Integration not found' });
return;
}
const { enabled, config } = req.body;
if (enabled === true)
item.enable();
else if (enabled === false)
item.disable();
if (config)
item.updateConfig(config);
await integrationRepo.update(item);
res.json(serializeIntegration(item));
});
router.delete('/:id', async (req, res) => {
await integrationRepo.delete(req.params['id']);
res.status(204).end();
});
// ─── Webhook Endpoints ───────────────────────────────────────────────────────
router.get('/webhooks/endpoints', async (_req, res) => {
const endpoints = await webhookRepo.findAll();
res.json(endpoints.map(serializeWebhook));
});
router.post('/webhooks/endpoints', async (req, res) => {
const { url } = req.body;
if (!url) {
res.status(400).json({ error: 'url is required' });
return;
}
const endpoint = WebhookEndpoint_1.WebhookEndpoint.create({ url });
await webhookRepo.save(endpoint);
res.status(201).json(serializeWebhook(endpoint));
});
router.delete('/webhooks/endpoints/:id', async (req, res) => {
await webhookRepo.delete(req.params['id']);
res.status(204).end();
});
return router;
}
function serializeIntegration(i) {
return {
id: i.id.toString(),
name: i.name,
type: i.type.value,
enabled: i.enabled,
config: maskSecrets(i.config),
createdAt: i.createdAt.toISOString(),
};
}
function serializeWebhook(ep) {
return {
id: ep.id.toString(),
url: ep.url,
enabled: ep.enabled,
createdAt: ep.createdAt.toISOString(),
lastDeliveredAt: ep.lastDeliveredAt?.toISOString() ?? null,
lastStatus: ep.lastStatus ?? null,
// Return the secret only once at creation (caller can see it from the first POST)
};
}
function maskSecrets(config) {
const masked = { ...config };
for (const key of ['token', 'secret', 'password', 'apiKey']) {
if (masked[key])
masked[key] = '***';
}
return masked;
}

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitHubIssuesProvider = void 0;
const rest_1 = require("@octokit/rest");
class GitHubIssuesProvider {
constructor(token, repo) {
this.octokit = new rest_1.Octokit({ auth: token });
const [owner, repoName] = repo.split('/');
this.owner = owner;
this.repo = repoName;
}
async sendFinding(finding) {
const stepsSection = finding.steps && finding.steps.length > 0
? `\n\n## Reproduction Steps\n${finding.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`
: '';
const body = `## ABE Security Finding
**Severity:** ${finding.severity.toUpperCase()}
**Type:** ${finding.type}
**Session:** ${finding.sessionId}
## Description
${finding.description}${stepsSection}
---
*Generated by [ABE — Autonomous Bug Explorer](https://github.com/your-org/abe)*`;
await this.octokit.issues.create({
owner: this.owner,
repo: this.repo,
title: `[ABE] [${finding.severity.toUpperCase()}] ${finding.title}`,
body,
labels: ['bug', 'abe-finding', `severity:${finding.severity}`],
});
}
}
exports.GitHubIssuesProvider = GitHubIssuesProvider;

View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JiraProvider = void 0;
const PRIORITY_MAP = {
critical: 'Highest',
high: 'High',
medium: 'Medium',
low: 'Low',
};
class JiraProvider {
constructor(host, token, username, projectKey) {
this.host = host;
this.token = token;
this.username = username;
this.projectKey = projectKey;
}
async sendFinding(finding) {
const stepsSection = finding.steps && finding.steps.length > 0
? `\n\nReproduction Steps:\n${finding.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`
: '';
const body = {
fields: {
project: { key: this.projectKey },
summary: `[ABE] [${finding.severity.toUpperCase()}] ${finding.title}`,
description: {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: `${finding.description}${stepsSection}` }],
},
],
},
issuetype: { name: 'Bug' },
priority: { name: PRIORITY_MAP[finding.severity] ?? 'Medium' },
labels: ['abe-finding', `severity-${finding.severity}`],
},
};
const auth = Buffer.from(`${this.username}:${this.token}`).toString('base64');
const url = `${this.host.replace(/\/$/, '')}/rest/api/3/issue`;
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${auth}`,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(15000),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Jira API error ${res.status}: ${text}`);
}
}
}
exports.JiraProvider = JiraProvider;

View File

@@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SlackProvider = void 0;
const SEVERITY_COLORS = {
critical: '#dc2626',
high: '#ea580c',
medium: '#ca8a04',
low: '#2563eb',
};
class SlackProvider {
constructor(webhookUrl) {
this.webhookUrl = webhookUrl;
}
async sendFinding(finding) {
const color = SEVERITY_COLORS[finding.severity] ?? '#6b7280';
const payload = {
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: `ABE Finding: ${finding.title}`, emoji: true },
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Severity:*\n${finding.severity.toUpperCase()}` },
{ type: 'mrkdwn', text: `*Type:*\n${finding.type}` },
],
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*Description:*\n${finding.description}` },
},
{
type: 'context',
elements: [
{ type: 'mrkdwn', text: `Session: ${finding.sessionId}` },
],
},
],
attachments: [{ color, fallback: `${finding.severity.toUpperCase()} finding: ${finding.description}` }],
};
const res = await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
throw new Error(`Slack webhook failed: ${res.status} ${await res.text()}`);
}
}
}
exports.SlackProvider = SlackProvider;

View File

@@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KyselyIntegrationRepository = void 0;
const Integration_1 = require("../../domain/entities/Integration");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const IntegrationType_1 = require("../../domain/value-objects/IntegrationType");
class KyselyIntegrationRepository {
constructor(db) {
this.db = db;
}
async save(integration) {
const row = {
id: integration.id.toString(),
name: integration.name,
type: integration.type.value,
enabled: integration.enabled ? 1 : 0,
config_json: JSON.stringify(integration.config),
created_at: integration.createdAt.getTime(),
};
await this.db.insertInto('integrations').values(row).execute();
}
async findById(id) {
const row = await this.db
.selectFrom('integrations')
.selectAll()
.where('id', '=', id)
.executeTakeFirst();
return row ? this.toDomain(row) : undefined;
}
async findAll() {
const rows = await this.db
.selectFrom('integrations')
.selectAll()
.orderBy('created_at', 'desc')
.execute();
return rows.map(r => this.toDomain(r));
}
async findEnabled() {
const rows = await this.db
.selectFrom('integrations')
.selectAll()
.where('enabled', '=', 1)
.execute();
return rows.map(r => this.toDomain(r));
}
async update(integration) {
await this.db
.updateTable('integrations')
.set({
name: integration.name,
enabled: integration.enabled ? 1 : 0,
config_json: JSON.stringify(integration.config),
})
.where('id', '=', integration.id.toString())
.execute();
}
async delete(id) {
await this.db.deleteFrom('integrations').where('id', '=', id).execute();
}
toDomain(row) {
const config = JSON.parse(row.config_json);
const props = {
name: row.name,
type: IntegrationType_1.IntegrationType.fromString(row.type),
enabled: row.enabled === 1,
config,
createdAt: new Date(row.created_at),
};
return Integration_1.Integration.reconstitute(props, UniqueId_1.UniqueId.from(row.id));
}
}
exports.KyselyIntegrationRepository = KyselyIntegrationRepository;

View File

@@ -0,0 +1,73 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KyselyWebhookEndpointRepository = void 0;
const WebhookEndpoint_1 = require("../../domain/entities/WebhookEndpoint");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const WebhookSecret_1 = require("../../domain/value-objects/WebhookSecret");
class KyselyWebhookEndpointRepository {
constructor(db) {
this.db = db;
}
async save(endpoint) {
const row = {
id: endpoint.id.toString(),
url: endpoint.url,
secret: endpoint.secret.value,
enabled: endpoint.enabled ? 1 : 0,
created_at: endpoint.createdAt.getTime(),
last_delivered_at: endpoint.lastDeliveredAt ? endpoint.lastDeliveredAt.getTime() : null,
last_status: endpoint.lastStatus ?? null,
};
await this.db.insertInto('webhook_endpoints').values(row).execute();
}
async findById(id) {
const row = await this.db
.selectFrom('webhook_endpoints')
.selectAll()
.where('id', '=', id)
.executeTakeFirst();
return row ? this.toDomain(row) : undefined;
}
async findAll() {
const rows = await this.db
.selectFrom('webhook_endpoints')
.selectAll()
.orderBy('created_at', 'desc')
.execute();
return rows.map(r => this.toDomain(r));
}
async findEnabled() {
const rows = await this.db
.selectFrom('webhook_endpoints')
.selectAll()
.where('enabled', '=', 1)
.execute();
return rows.map(r => this.toDomain(r));
}
async update(endpoint) {
await this.db
.updateTable('webhook_endpoints')
.set({
enabled: endpoint.enabled ? 1 : 0,
last_delivered_at: endpoint.lastDeliveredAt ? endpoint.lastDeliveredAt.getTime() : null,
last_status: endpoint.lastStatus ?? null,
})
.where('id', '=', endpoint.id.toString())
.execute();
}
async delete(id) {
await this.db.deleteFrom('webhook_endpoints').where('id', '=', id).execute();
}
toDomain(row) {
const props = {
url: row.url,
secret: WebhookSecret_1.WebhookSecret.fromString(row.secret),
enabled: row.enabled === 1,
createdAt: new Date(row.created_at),
lastDeliveredAt: row.last_delivered_at ? new Date(row.last_delivered_at) : undefined,
lastStatus: row.last_status ?? undefined,
};
return WebhookEndpoint_1.WebhookEndpoint.reconstitute(props, UniqueId_1.UniqueId.from(row.id));
}
}
exports.KyselyWebhookEndpointRepository = KyselyWebhookEndpointRepository;

View File

@@ -0,0 +1,81 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebhookDispatcher = void 0;
const MAX_ATTEMPTS = 3;
const BASE_DELAY_MS = 1000;
class WebhookDispatcher {
constructor(endpointRepo, logger) {
this.endpointRepo = endpointRepo;
this.logger = logger;
}
async dispatchFinding(finding) {
const endpoints = await this.endpointRepo.findEnabled();
await Promise.allSettled(endpoints.map(ep => this.deliverWithRetry(ep.url, ep.secret.value, finding)));
}
async deliverWithRetry(url, secret, payload) {
const body = JSON.stringify({ event: 'finding.created', data: payload });
const { createHmac } = await Promise.resolve().then(() => __importStar(require('crypto')));
const signature = createHmac('sha256', secret).update(body).digest('hex');
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-ABE-Signature': `sha256=${signature}`,
'X-ABE-Event': 'finding.created',
'User-Agent': 'ABE-Webhook/1.0',
},
body,
signal: AbortSignal.timeout(10000),
});
this.logger.info({ url, status: res.status, attempt }, 'Webhook delivered');
return;
}
catch (err) {
this.logger.warn({ url, attempt, err }, 'Webhook delivery failed');
if (attempt < MAX_ATTEMPTS) {
await sleep(BASE_DELAY_MS * 2 ** (attempt - 1));
}
}
}
this.logger.error({ url }, 'Webhook delivery failed after max attempts');
}
}
exports.WebhookDispatcher = WebhookDispatcher;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}