fase(16): integrations module
This commit is contained in:
61
dist/modules/integrations/application/event-handlers/OnFindingCreated.js
vendored
Normal file
61
dist/modules/integrations/application/event-handlers/OnFindingCreated.js
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OnFindingCreated = void 0;
|
||||
const SlackProvider_1 = require("../../infrastructure/providers/SlackProvider");
|
||||
const GitHubIssuesProvider_1 = require("../../infrastructure/providers/GitHubIssuesProvider");
|
||||
const JiraProvider_1 = require("../../infrastructure/providers/JiraProvider");
|
||||
class OnFindingCreated {
|
||||
constructor(integrationRepo, webhookRepo, dispatcher, logger) {
|
||||
this.integrationRepo = integrationRepo;
|
||||
this.webhookRepo = webhookRepo;
|
||||
this.dispatcher = dispatcher;
|
||||
this.logger = logger;
|
||||
}
|
||||
async handle(event) {
|
||||
const payload = event.payload;
|
||||
const finding = {
|
||||
id: payload.findingId,
|
||||
title: `${payload.type} finding`,
|
||||
severity: payload.severity,
|
||||
type: payload.type,
|
||||
description: payload.description,
|
||||
sessionId: payload.sessionId,
|
||||
};
|
||||
// Dispatch to custom webhooks
|
||||
await this.dispatcher.dispatchFinding(finding);
|
||||
// Dispatch to named integrations (Slack, GitHub, Jira)
|
||||
const integrations = await this.integrationRepo.findEnabled();
|
||||
for (const integration of integrations) {
|
||||
try {
|
||||
const minSev = integration.config.minSeverity ?? 'low';
|
||||
if (!severityMeetsThreshold(payload.severity, minSev))
|
||||
continue;
|
||||
const type = integration.type.value;
|
||||
if (type === 'slack' && integration.config.webhookUrl) {
|
||||
const provider = new SlackProvider_1.SlackProvider(integration.config.webhookUrl);
|
||||
await provider.sendFinding(finding);
|
||||
}
|
||||
else if (type === 'github' && integration.config.token && integration.config.repo) {
|
||||
const provider = new GitHubIssuesProvider_1.GitHubIssuesProvider(integration.config.token, integration.config.repo);
|
||||
await provider.sendFinding(finding);
|
||||
}
|
||||
else if (type === 'jira' &&
|
||||
integration.config.host &&
|
||||
integration.config.token &&
|
||||
integration.config.username &&
|
||||
integration.config.projectKey) {
|
||||
const provider = new JiraProvider_1.JiraProvider(integration.config.host, integration.config.token, integration.config.username, integration.config.projectKey);
|
||||
await provider.sendFinding(finding);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.warn({ integrationId: integration.id.toString(), err }, 'Integration dispatch failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.OnFindingCreated = OnFindingCreated;
|
||||
const SEVERITY_ORDER = ['low', 'medium', 'high', 'critical'];
|
||||
function severityMeetsThreshold(severity, min) {
|
||||
return SEVERITY_ORDER.indexOf(severity) >= SEVERITY_ORDER.indexOf(min);
|
||||
}
|
||||
22
dist/modules/integrations/domain/entities/Integration.js
vendored
Normal file
22
dist/modules/integrations/domain/entities/Integration.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Integration = void 0;
|
||||
const Entity_1 = require("../../../../shared/domain/Entity");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
class Integration extends Entity_1.Entity {
|
||||
static create(props, id) {
|
||||
return new Integration({ ...props, enabled: true, createdAt: new Date() }, id ?? UniqueId_1.UniqueId.create());
|
||||
}
|
||||
static reconstitute(props, id) {
|
||||
return new Integration(props, id);
|
||||
}
|
||||
get name() { return this.props.name; }
|
||||
get type() { return this.props.type; }
|
||||
get enabled() { return this.props.enabled; }
|
||||
get config() { return this.props.config; }
|
||||
get createdAt() { return this.props.createdAt; }
|
||||
enable() { this.props.enabled = true; }
|
||||
disable() { this.props.enabled = false; }
|
||||
updateConfig(config) { this.props.config = config; }
|
||||
}
|
||||
exports.Integration = Integration;
|
||||
27
dist/modules/integrations/domain/entities/WebhookEndpoint.js
vendored
Normal file
27
dist/modules/integrations/domain/entities/WebhookEndpoint.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WebhookEndpoint = void 0;
|
||||
const Entity_1 = require("../../../../shared/domain/Entity");
|
||||
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
|
||||
const WebhookSecret_1 = require("../value-objects/WebhookSecret");
|
||||
class WebhookEndpoint extends Entity_1.Entity {
|
||||
static create(props, id) {
|
||||
return new WebhookEndpoint({ ...props, secret: WebhookSecret_1.WebhookSecret.generate(), enabled: true, createdAt: new Date() }, id ?? UniqueId_1.UniqueId.create());
|
||||
}
|
||||
static reconstitute(props, id) {
|
||||
return new WebhookEndpoint(props, id);
|
||||
}
|
||||
get url() { return this.props.url; }
|
||||
get secret() { return this.props.secret; }
|
||||
get enabled() { return this.props.enabled; }
|
||||
get createdAt() { return this.props.createdAt; }
|
||||
get lastDeliveredAt() { return this.props.lastDeliveredAt; }
|
||||
get lastStatus() { return this.props.lastStatus; }
|
||||
recordDelivery(statusCode) {
|
||||
this.props.lastDeliveredAt = new Date();
|
||||
this.props.lastStatus = statusCode;
|
||||
}
|
||||
enable() { this.props.enabled = true; }
|
||||
disable() { this.props.enabled = false; }
|
||||
}
|
||||
exports.WebhookEndpoint = WebhookEndpoint;
|
||||
2
dist/modules/integrations/domain/ports/IIntegrationProvider.js
vendored
Normal file
2
dist/modules/integrations/domain/ports/IIntegrationProvider.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
2
dist/modules/integrations/domain/ports/IIntegrationRepository.js
vendored
Normal file
2
dist/modules/integrations/domain/ports/IIntegrationRepository.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
2
dist/modules/integrations/domain/ports/IWebhookEndpointRepository.js
vendored
Normal file
2
dist/modules/integrations/domain/ports/IWebhookEndpointRepository.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
18
dist/modules/integrations/domain/value-objects/IntegrationType.js
vendored
Normal file
18
dist/modules/integrations/domain/value-objects/IntegrationType.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.IntegrationType = void 0;
|
||||
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
|
||||
class IntegrationType extends ValueObject_1.ValueObject {
|
||||
get value() { return this.props.value; }
|
||||
static fromString(s) {
|
||||
if (s === 'jira' || s === 'slack' || s === 'github' || s === 'webhook') {
|
||||
return new IntegrationType({ value: s });
|
||||
}
|
||||
throw new Error(`Invalid integration type: ${s}`);
|
||||
}
|
||||
static jira() { return new IntegrationType({ value: 'jira' }); }
|
||||
static slack() { return new IntegrationType({ value: 'slack' }); }
|
||||
static github() { return new IntegrationType({ value: 'github' }); }
|
||||
static webhook() { return new IntegrationType({ value: 'webhook' }); }
|
||||
}
|
||||
exports.IntegrationType = IntegrationType;
|
||||
21
dist/modules/integrations/domain/value-objects/WebhookSecret.js
vendored
Normal file
21
dist/modules/integrations/domain/value-objects/WebhookSecret.js
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.WebhookSecret = void 0;
|
||||
const ValueObject_1 = require("../../../../shared/domain/ValueObject");
|
||||
const crypto_1 = require("crypto");
|
||||
class WebhookSecret extends ValueObject_1.ValueObject {
|
||||
get value() { return this.props.value; }
|
||||
static generate() {
|
||||
const secret = (0, crypto_1.randomBytes)(32).toString('hex');
|
||||
return new WebhookSecret({ value: secret });
|
||||
}
|
||||
static fromString(s) {
|
||||
if (!s || s.length < 16)
|
||||
throw new Error('Webhook secret must be at least 16 characters');
|
||||
return new WebhookSecret({ value: s });
|
||||
}
|
||||
sign(payload) {
|
||||
return (0, crypto_1.createHmac)('sha256', this.props.value).update(payload).digest('hex');
|
||||
}
|
||||
}
|
||||
exports.WebhookSecret = WebhookSecret;
|
||||
22
dist/modules/integrations/index.js
vendored
Normal file
22
dist/modules/integrations/index.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
// Integrations module — public facade
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createIntegrationsRouter = exports.OnFindingCreated = exports.WebhookDispatcher = exports.KyselyWebhookEndpointRepository = exports.KyselyIntegrationRepository = exports.WebhookSecret = exports.IntegrationType = exports.WebhookEndpoint = exports.Integration = void 0;
|
||||
var Integration_1 = require("./domain/entities/Integration");
|
||||
Object.defineProperty(exports, "Integration", { enumerable: true, get: function () { return Integration_1.Integration; } });
|
||||
var WebhookEndpoint_1 = require("./domain/entities/WebhookEndpoint");
|
||||
Object.defineProperty(exports, "WebhookEndpoint", { enumerable: true, get: function () { return WebhookEndpoint_1.WebhookEndpoint; } });
|
||||
var IntegrationType_1 = require("./domain/value-objects/IntegrationType");
|
||||
Object.defineProperty(exports, "IntegrationType", { enumerable: true, get: function () { return IntegrationType_1.IntegrationType; } });
|
||||
var WebhookSecret_1 = require("./domain/value-objects/WebhookSecret");
|
||||
Object.defineProperty(exports, "WebhookSecret", { enumerable: true, get: function () { return WebhookSecret_1.WebhookSecret; } });
|
||||
var KyselyIntegrationRepository_1 = require("./infrastructure/repositories/KyselyIntegrationRepository");
|
||||
Object.defineProperty(exports, "KyselyIntegrationRepository", { enumerable: true, get: function () { return KyselyIntegrationRepository_1.KyselyIntegrationRepository; } });
|
||||
var KyselyWebhookEndpointRepository_1 = require("./infrastructure/repositories/KyselyWebhookEndpointRepository");
|
||||
Object.defineProperty(exports, "KyselyWebhookEndpointRepository", { enumerable: true, get: function () { return KyselyWebhookEndpointRepository_1.KyselyWebhookEndpointRepository; } });
|
||||
var WebhookDispatcher_1 = require("./infrastructure/webhooks/WebhookDispatcher");
|
||||
Object.defineProperty(exports, "WebhookDispatcher", { enumerable: true, get: function () { return WebhookDispatcher_1.WebhookDispatcher; } });
|
||||
var OnFindingCreated_1 = require("./application/event-handlers/OnFindingCreated");
|
||||
Object.defineProperty(exports, "OnFindingCreated", { enumerable: true, get: function () { return OnFindingCreated_1.OnFindingCreated; } });
|
||||
var IntegrationsController_1 = require("./infrastructure/http/IntegrationsController");
|
||||
Object.defineProperty(exports, "createIntegrationsRouter", { enumerable: true, get: function () { return IntegrationsController_1.createIntegrationsRouter; } });
|
||||
111
dist/modules/integrations/infrastructure/http/IntegrationsController.js
vendored
Normal file
111
dist/modules/integrations/infrastructure/http/IntegrationsController.js
vendored
Normal 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;
|
||||
}
|
||||
36
dist/modules/integrations/infrastructure/providers/GitHubIssuesProvider.js
vendored
Normal file
36
dist/modules/integrations/infrastructure/providers/GitHubIssuesProvider.js
vendored
Normal 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;
|
||||
57
dist/modules/integrations/infrastructure/providers/JiraProvider.js
vendored
Normal file
57
dist/modules/integrations/infrastructure/providers/JiraProvider.js
vendored
Normal 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;
|
||||
53
dist/modules/integrations/infrastructure/providers/SlackProvider.js
vendored
Normal file
53
dist/modules/integrations/infrastructure/providers/SlackProvider.js
vendored
Normal 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;
|
||||
72
dist/modules/integrations/infrastructure/repositories/KyselyIntegrationRepository.js
vendored
Normal file
72
dist/modules/integrations/infrastructure/repositories/KyselyIntegrationRepository.js
vendored
Normal 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;
|
||||
73
dist/modules/integrations/infrastructure/repositories/KyselyWebhookEndpointRepository.js
vendored
Normal file
73
dist/modules/integrations/infrastructure/repositories/KyselyWebhookEndpointRepository.js
vendored
Normal 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;
|
||||
81
dist/modules/integrations/infrastructure/webhooks/WebhookDispatcher.js
vendored
Normal file
81
dist/modules/integrations/infrastructure/webhooks/WebhookDispatcher.js
vendored
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user