treliq 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +540 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/cache.d.ts +29 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +64 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/concurrency.d.ts +16 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +60 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/db.d.ts +127 -0
- package/dist/core/db.d.ts.map +1 -0
- package/dist/core/db.js +490 -0
- package/dist/core/db.js.map +1 -0
- package/dist/core/dedup.d.ts +18 -0
- package/dist/core/dedup.d.ts.map +1 -0
- package/dist/core/dedup.js +159 -0
- package/dist/core/dedup.js.map +1 -0
- package/dist/core/graphql.d.ts +30 -0
- package/dist/core/graphql.d.ts.map +1 -0
- package/dist/core/graphql.js +243 -0
- package/dist/core/graphql.js.map +1 -0
- package/dist/core/notifications.d.ts +37 -0
- package/dist/core/notifications.d.ts.map +1 -0
- package/dist/core/notifications.js +174 -0
- package/dist/core/notifications.js.map +1 -0
- package/dist/core/provider.d.ts +45 -0
- package/dist/core/provider.d.ts.map +1 -0
- package/dist/core/provider.js +147 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/ratelimit.d.ts +40 -0
- package/dist/core/ratelimit.d.ts.map +1 -0
- package/dist/core/ratelimit.js +77 -0
- package/dist/core/ratelimit.js.map +1 -0
- package/dist/core/reputation.d.ts +16 -0
- package/dist/core/reputation.d.ts.map +1 -0
- package/dist/core/reputation.js +59 -0
- package/dist/core/reputation.js.map +1 -0
- package/dist/core/scanner.d.ts +58 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +635 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/scoring.d.ts +36 -0
- package/dist/core/scoring.d.ts.map +1 -0
- package/dist/core/scoring.js +360 -0
- package/dist/core/scoring.js.map +1 -0
- package/dist/core/types.d.ts +89 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/vectorstore.d.ts +42 -0
- package/dist/core/vectorstore.d.ts.map +1 -0
- package/dist/core/vectorstore.js +149 -0
- package/dist/core/vectorstore.js.map +1 -0
- package/dist/core/vision.d.ts +16 -0
- package/dist/core/vision.d.ts.map +1 -0
- package/dist/core/vision.js +41 -0
- package/dist/core/vision.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/server/app.d.ts +21 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +284 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +117 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/scheduler.d.ts +26 -0
- package/dist/server/scheduler.d.ts.map +1 -0
- package/dist/server/scheduler.js +136 -0
- package/dist/server/scheduler.js.map +1 -0
- package/dist/server/sse.d.ts +33 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +80 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/webhooks.d.ts +23 -0
- package/dist/server/webhooks.d.ts.map +1 -0
- package/dist/server/webhooks.js +175 -0
- package/dist/server/webhooks.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Treliq Server Module
|
|
4
|
+
*
|
|
5
|
+
* Provides REST API, webhook handling, and scheduled scanning
|
|
6
|
+
* for continuous PR monitoring and triage.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { startServer } from './server';
|
|
10
|
+
* await startServer({ port: 8000, ... });
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.startScheduler = exports.registerWebhooks = exports.createServer = void 0;
|
|
14
|
+
exports.startServer = startServer;
|
|
15
|
+
var app_1 = require("./app");
|
|
16
|
+
Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return app_1.createServer; } });
|
|
17
|
+
var webhooks_1 = require("./webhooks");
|
|
18
|
+
Object.defineProperty(exports, "registerWebhooks", { enumerable: true, get: function () { return webhooks_1.registerWebhooks; } });
|
|
19
|
+
var scheduler_1 = require("./scheduler");
|
|
20
|
+
Object.defineProperty(exports, "startScheduler", { enumerable: true, get: function () { return scheduler_1.startScheduler; } });
|
|
21
|
+
const app_2 = require("./app");
|
|
22
|
+
const scheduler_2 = require("./scheduler");
|
|
23
|
+
const notifications_1 = require("../core/notifications");
|
|
24
|
+
const db_1 = require("../core/db");
|
|
25
|
+
/**
|
|
26
|
+
* Start Treliq server with all components:
|
|
27
|
+
* - REST API
|
|
28
|
+
* - Webhook handler (if webhookSecret provided)
|
|
29
|
+
* - Scheduler (if scheduledRepos provided)
|
|
30
|
+
* - Notifications (if webhook URLs provided)
|
|
31
|
+
*/
|
|
32
|
+
async function startServer(config) {
|
|
33
|
+
console.error('🚀 Starting Treliq Server...\n');
|
|
34
|
+
// Initialize notifications if configured
|
|
35
|
+
let notifications;
|
|
36
|
+
if (config.slackWebhook || config.discordWebhook) {
|
|
37
|
+
notifications = new notifications_1.NotificationDispatcher({
|
|
38
|
+
slackWebhook: config.slackWebhook,
|
|
39
|
+
discordWebhook: config.discordWebhook,
|
|
40
|
+
});
|
|
41
|
+
console.error('📢 Notifications enabled');
|
|
42
|
+
if (config.slackWebhook)
|
|
43
|
+
console.error(' - Slack webhook configured');
|
|
44
|
+
if (config.discordWebhook)
|
|
45
|
+
console.error(' - Discord webhook configured');
|
|
46
|
+
}
|
|
47
|
+
// Create and start Fastify server
|
|
48
|
+
const fastify = await (0, app_2.createServer)(config);
|
|
49
|
+
try {
|
|
50
|
+
await fastify.listen({
|
|
51
|
+
port: config.port,
|
|
52
|
+
host: config.host,
|
|
53
|
+
});
|
|
54
|
+
console.error(`\n✅ Server listening on http://${config.host}:${config.port}`);
|
|
55
|
+
console.error(` Health check: http://${config.host}:${config.port}/health`);
|
|
56
|
+
if (config.webhookSecret) {
|
|
57
|
+
console.error(` Webhook endpoint: http://${config.host}:${config.port}/webhooks`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error('❌ Failed to start server:', error);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
// Start scheduler if configured
|
|
65
|
+
let schedulerHandle;
|
|
66
|
+
if (config.scheduledRepos && config.scheduledRepos.length > 0) {
|
|
67
|
+
if (!config.cronExpression) {
|
|
68
|
+
console.error('⚠️ scheduledRepos provided but no cronExpression - scheduler not started');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
try {
|
|
72
|
+
const db = new db_1.TreliqDB(config.dbPath);
|
|
73
|
+
const schedulerConfig = {
|
|
74
|
+
cronExpression: config.cronExpression,
|
|
75
|
+
repos: config.scheduledRepos,
|
|
76
|
+
treliqConfig: config.treliqConfig,
|
|
77
|
+
db,
|
|
78
|
+
notifications,
|
|
79
|
+
};
|
|
80
|
+
schedulerHandle = (0, scheduler_2.startScheduler)(schedulerConfig);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('❌ Failed to start scheduler:', error.message);
|
|
84
|
+
// Continue without scheduler
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Log startup summary
|
|
89
|
+
console.error('\n📋 Server Configuration:');
|
|
90
|
+
console.error(` Port: ${config.port}`);
|
|
91
|
+
console.error(` Host: ${config.host}`);
|
|
92
|
+
console.error(` Database: ${config.dbPath}`);
|
|
93
|
+
console.error(` Repository: ${config.treliqConfig.repo}`);
|
|
94
|
+
console.error(` Webhooks: ${config.webhookSecret ? 'Enabled' : 'Disabled'}`);
|
|
95
|
+
console.error(` Scheduler: ${schedulerHandle ? 'Enabled' : 'Disabled'}`);
|
|
96
|
+
console.error(` Notifications: ${notifications?.hasChannels ? 'Enabled' : 'Disabled'}`);
|
|
97
|
+
console.error('\n🎯 Server ready to accept requests\n');
|
|
98
|
+
// Handle graceful shutdown
|
|
99
|
+
const shutdown = async (signal) => {
|
|
100
|
+
console.error(`\n🛑 Received ${signal}, shutting down...`);
|
|
101
|
+
if (schedulerHandle) {
|
|
102
|
+
schedulerHandle.stop();
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await fastify.close();
|
|
106
|
+
console.error('✅ Server shutdown complete');
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error('❌ Error during shutdown:', error);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
115
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduled Repository Scanner for Treliq
|
|
3
|
+
*
|
|
4
|
+
* Uses node-cron to periodically scan multiple repositories,
|
|
5
|
+
* save results to database, and optionally send notifications.
|
|
6
|
+
*/
|
|
7
|
+
import { TreliqDB } from '../core/db';
|
|
8
|
+
import { NotificationDispatcher } from '../core/notifications';
|
|
9
|
+
import type { TreliqConfig } from '../core/types';
|
|
10
|
+
export interface SchedulerConfig {
|
|
11
|
+
cronExpression: string;
|
|
12
|
+
repos: string[];
|
|
13
|
+
treliqConfig: TreliqConfig;
|
|
14
|
+
db: TreliqDB;
|
|
15
|
+
notifications?: NotificationDispatcher;
|
|
16
|
+
}
|
|
17
|
+
export interface SchedulerHandle {
|
|
18
|
+
stop: () => void;
|
|
19
|
+
isRunning: () => boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Start a cron scheduler that scans repositories on schedule
|
|
23
|
+
* Returns a handle to stop the scheduler
|
|
24
|
+
*/
|
|
25
|
+
export declare function startScheduler(config: SchedulerConfig): SchedulerHandle;
|
|
26
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/server/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,EAAE,YAAY,CAAC;IAC3B,EAAE,EAAE,QAAQ,CAAC;IACb,aAAa,CAAC,EAAE,sBAAsB,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAsDvE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Scheduled Repository Scanner for Treliq
|
|
4
|
+
*
|
|
5
|
+
* Uses node-cron to periodically scan multiple repositories,
|
|
6
|
+
* save results to database, and optionally send notifications.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.startScheduler = startScheduler;
|
|
13
|
+
const node_cron_1 = __importDefault(require("node-cron"));
|
|
14
|
+
const scanner_1 = require("../core/scanner");
|
|
15
|
+
const db_1 = require("../core/db");
|
|
16
|
+
/**
|
|
17
|
+
* Start a cron scheduler that scans repositories on schedule
|
|
18
|
+
* Returns a handle to stop the scheduler
|
|
19
|
+
*/
|
|
20
|
+
function startScheduler(config) {
|
|
21
|
+
if (config.repos.length === 0) {
|
|
22
|
+
throw new Error('No repositories configured for scheduled scanning');
|
|
23
|
+
}
|
|
24
|
+
// Validate cron expression
|
|
25
|
+
if (!node_cron_1.default.validate(config.cronExpression)) {
|
|
26
|
+
throw new Error(`Invalid cron expression: ${config.cronExpression}`);
|
|
27
|
+
}
|
|
28
|
+
console.error(`⏰ Scheduler configured for ${config.repos.length} repositories`);
|
|
29
|
+
console.error(` Schedule: ${config.cronExpression}`);
|
|
30
|
+
console.error(` Repositories: ${config.repos.join(', ')}`);
|
|
31
|
+
let isRunning = false;
|
|
32
|
+
const task = node_cron_1.default.schedule(config.cronExpression, async () => {
|
|
33
|
+
if (isRunning) {
|
|
34
|
+
console.error('⏭️ Previous scan still running, skipping this cycle');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
isRunning = true;
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
console.error(`\n🕐 ${new Date().toISOString()} - Starting scheduled scan...`);
|
|
40
|
+
try {
|
|
41
|
+
await scanAllRepositories(config);
|
|
42
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
43
|
+
console.error(`✅ Scheduled scan complete (${duration}s)\n`);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error(`❌ Scheduled scan failed:`, error.message);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
isRunning = false;
|
|
50
|
+
}
|
|
51
|
+
}, {
|
|
52
|
+
timezone: 'UTC',
|
|
53
|
+
});
|
|
54
|
+
task.start();
|
|
55
|
+
console.error('✅ Scheduler started');
|
|
56
|
+
return {
|
|
57
|
+
stop: () => {
|
|
58
|
+
task.stop();
|
|
59
|
+
console.error('🛑 Scheduler stopped');
|
|
60
|
+
},
|
|
61
|
+
isRunning: () => isRunning,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Scan all configured repositories sequentially
|
|
66
|
+
*/
|
|
67
|
+
async function scanAllRepositories(config) {
|
|
68
|
+
const results = {
|
|
69
|
+
successful: 0,
|
|
70
|
+
failed: 0,
|
|
71
|
+
totalPRs: 0,
|
|
72
|
+
totalSpam: 0,
|
|
73
|
+
};
|
|
74
|
+
for (const repo of config.repos) {
|
|
75
|
+
try {
|
|
76
|
+
console.error(`\n📡 Scanning ${repo}...`);
|
|
77
|
+
const scanConfig = {
|
|
78
|
+
...config.treliqConfig,
|
|
79
|
+
repo,
|
|
80
|
+
dbPath: config.db instanceof db_1.TreliqDB ? config.db.db.name : undefined,
|
|
81
|
+
};
|
|
82
|
+
const scanner = new scanner_1.TreliqScanner(scanConfig);
|
|
83
|
+
const result = await scanner.scan();
|
|
84
|
+
results.successful++;
|
|
85
|
+
results.totalPRs += result.totalPRs;
|
|
86
|
+
results.totalSpam += result.spamCount;
|
|
87
|
+
// Send scan completion notification
|
|
88
|
+
if (config.notifications?.hasChannels) {
|
|
89
|
+
await config.notifications.send({
|
|
90
|
+
type: 'scan_complete',
|
|
91
|
+
repo,
|
|
92
|
+
message: `Scanned ${result.totalPRs} PRs, found ${result.spamCount} spam, ${result.duplicateClusters.length} duplicate clusters`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Send notifications for high-priority PRs (score >= 90)
|
|
96
|
+
if (config.notifications?.hasChannels) {
|
|
97
|
+
const highPriorityPRs = result.rankedPRs.filter(pr => pr.totalScore >= 90 && !pr.isSpam);
|
|
98
|
+
for (const pr of highPriorityPRs.slice(0, 3)) {
|
|
99
|
+
// Limit to top 3 to avoid spam
|
|
100
|
+
await config.notifications.send({
|
|
101
|
+
type: 'high_priority_pr',
|
|
102
|
+
repo,
|
|
103
|
+
message: `High priority PR: ${pr.title}`,
|
|
104
|
+
prNumber: pr.number,
|
|
105
|
+
score: Math.round(pr.totalScore),
|
|
106
|
+
url: `https://github.com/${repo}/pull/${pr.number}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error(`❌ Failed to scan ${repo}:`, error.message);
|
|
113
|
+
results.failed++;
|
|
114
|
+
// Send error notification
|
|
115
|
+
if (config.notifications?.hasChannels) {
|
|
116
|
+
try {
|
|
117
|
+
await config.notifications.send({
|
|
118
|
+
type: 'spam_detected', // Reuse spam type for errors (red color)
|
|
119
|
+
repo,
|
|
120
|
+
message: `Scan failed: ${error.message}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (notifError) {
|
|
124
|
+
console.error(`⚠️ Failed to send error notification:`, notifError.message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Log summary
|
|
130
|
+
console.error(`\n📊 Scan Summary:`);
|
|
131
|
+
console.error(` Successful: ${results.successful}`);
|
|
132
|
+
console.error(` Failed: ${results.failed}`);
|
|
133
|
+
console.error(` Total PRs: ${results.totalPRs}`);
|
|
134
|
+
console.error(` Total Spam: ${results.totalSpam}`);
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/server/scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE (Server-Sent Events) Broadcaster for Treliq
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time updates to connected dashboard clients.
|
|
5
|
+
* No additional dependencies — uses native HTTP streaming via Fastify reply.
|
|
6
|
+
*/
|
|
7
|
+
import type { FastifyReply } from 'fastify';
|
|
8
|
+
export type SSEEvent = 'scan_start' | 'scan_complete' | 'pr_scored' | 'pr_closed';
|
|
9
|
+
export declare class SSEBroadcaster {
|
|
10
|
+
private clients;
|
|
11
|
+
/**
|
|
12
|
+
* Register a new SSE client connection.
|
|
13
|
+
* Sets appropriate headers and handles cleanup on disconnect.
|
|
14
|
+
*/
|
|
15
|
+
addClient(reply: FastifyReply): void;
|
|
16
|
+
/**
|
|
17
|
+
* Broadcast an event to all connected clients.
|
|
18
|
+
*/
|
|
19
|
+
broadcast(event: SSEEvent, data: any): void;
|
|
20
|
+
/**
|
|
21
|
+
* Send a keepalive ping to all clients (prevents proxy timeouts).
|
|
22
|
+
*/
|
|
23
|
+
ping(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Number of connected clients.
|
|
26
|
+
*/
|
|
27
|
+
get clientCount(): number;
|
|
28
|
+
/**
|
|
29
|
+
* Close all client connections.
|
|
30
|
+
*/
|
|
31
|
+
closeAll(): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/server/sse.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,eAAe,GAAG,WAAW,GAAG,WAAW,CAAC;AAElF,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAA2B;IAE1C;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAmBpC;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAa3C;;OAEG;IACH,IAAI,IAAI,IAAI;IAWZ;;OAEG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;OAEG;IACH,QAAQ,IAAI,IAAI;CAQjB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SSE (Server-Sent Events) Broadcaster for Treliq
|
|
4
|
+
*
|
|
5
|
+
* Provides real-time updates to connected dashboard clients.
|
|
6
|
+
* No additional dependencies — uses native HTTP streaming via Fastify reply.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SSEBroadcaster = void 0;
|
|
10
|
+
class SSEBroadcaster {
|
|
11
|
+
clients = new Set();
|
|
12
|
+
/**
|
|
13
|
+
* Register a new SSE client connection.
|
|
14
|
+
* Sets appropriate headers and handles cleanup on disconnect.
|
|
15
|
+
*/
|
|
16
|
+
addClient(reply) {
|
|
17
|
+
reply.raw.writeHead(200, {
|
|
18
|
+
'Content-Type': 'text/event-stream',
|
|
19
|
+
'Cache-Control': 'no-cache',
|
|
20
|
+
'Connection': 'keep-alive',
|
|
21
|
+
'Access-Control-Allow-Origin': '*',
|
|
22
|
+
});
|
|
23
|
+
// Send initial connection event
|
|
24
|
+
reply.raw.write(`event: connected\ndata: ${JSON.stringify({ timestamp: new Date().toISOString() })}\n\n`);
|
|
25
|
+
this.clients.add(reply);
|
|
26
|
+
// Clean up on disconnect
|
|
27
|
+
reply.raw.on('close', () => {
|
|
28
|
+
this.clients.delete(reply);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Broadcast an event to all connected clients.
|
|
33
|
+
*/
|
|
34
|
+
broadcast(event, data) {
|
|
35
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
36
|
+
for (const client of this.clients) {
|
|
37
|
+
try {
|
|
38
|
+
client.raw.write(payload);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Client disconnected — remove
|
|
42
|
+
this.clients.delete(client);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Send a keepalive ping to all clients (prevents proxy timeouts).
|
|
48
|
+
*/
|
|
49
|
+
ping() {
|
|
50
|
+
const payload = `: keepalive ${new Date().toISOString()}\n\n`;
|
|
51
|
+
for (const client of this.clients) {
|
|
52
|
+
try {
|
|
53
|
+
client.raw.write(payload);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
this.clients.delete(client);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Number of connected clients.
|
|
62
|
+
*/
|
|
63
|
+
get clientCount() {
|
|
64
|
+
return this.clients.size;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Close all client connections.
|
|
68
|
+
*/
|
|
69
|
+
closeAll() {
|
|
70
|
+
for (const client of this.clients) {
|
|
71
|
+
try {
|
|
72
|
+
client.raw.end();
|
|
73
|
+
}
|
|
74
|
+
catch { /* ignore */ }
|
|
75
|
+
}
|
|
76
|
+
this.clients.clear();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.SSEBroadcaster = SSEBroadcaster;
|
|
80
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/server/sse.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAMH,MAAa,cAAc;IACjB,OAAO,GAAG,IAAI,GAAG,EAAgB,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,KAAmB;QAC3B,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YACvB,cAAc,EAAE,mBAAmB;YACnC,eAAe,EAAE,UAAU;YAC3B,YAAY,EAAE,YAAY;YAC1B,6BAA6B,EAAE,GAAG;SACnC,CAAC,CAAC;QAEH,gCAAgC;QAChC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,2BAA2B,IAAI,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;QAE1G,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAExB,yBAAyB;QACzB,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,KAAe,EAAE,IAAS;QAClC,MAAM,OAAO,GAAG,UAAU,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;QAErE,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,+BAA+B;gBAC/B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,IAAI;QACF,MAAM,OAAO,GAAG,eAAe,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC;QAC9D,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,QAAQ;QACN,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;YACnB,CAAC;YAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF;AA1ED,wCA0EC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Webhook Handler for Treliq
|
|
3
|
+
*
|
|
4
|
+
* Handles incoming webhook events from GitHub:
|
|
5
|
+
* - pull_request.opened: Score new PRs
|
|
6
|
+
* - pull_request.synchronize: Re-score updated PRs
|
|
7
|
+
* - pull_request.closed: Update PR state in DB
|
|
8
|
+
*/
|
|
9
|
+
import type { FastifyInstance } from 'fastify';
|
|
10
|
+
import { TreliqDB } from '../core/db';
|
|
11
|
+
import type { TreliqConfig } from '../core/types';
|
|
12
|
+
import type { SSEBroadcaster } from './sse';
|
|
13
|
+
export interface WebhookConfig {
|
|
14
|
+
secret: string;
|
|
15
|
+
treliqConfig: TreliqConfig;
|
|
16
|
+
db: TreliqDB;
|
|
17
|
+
broadcaster?: SSEBroadcaster;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Register webhook endpoint with GitHub signature verification
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerWebhooks(fastify: FastifyInstance, config: WebhookConfig): void;
|
|
23
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/server/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAG7E,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAE5C,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,EAAE,EAAE,QAAQ,CAAC;IACb,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAwCD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,aAAa,GACpB,IAAI,CA2DN"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Webhook Handler for Treliq
|
|
4
|
+
*
|
|
5
|
+
* Handles incoming webhook events from GitHub:
|
|
6
|
+
* - pull_request.opened: Score new PRs
|
|
7
|
+
* - pull_request.synchronize: Re-score updated PRs
|
|
8
|
+
* - pull_request.closed: Update PR state in DB
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.registerWebhooks = registerWebhooks;
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const scanner_1 = require("../core/scanner");
|
|
14
|
+
const db_1 = require("../core/db");
|
|
15
|
+
/**
|
|
16
|
+
* Verify GitHub webhook signature using HMAC-SHA256
|
|
17
|
+
*/
|
|
18
|
+
function verifySignature(payload, signature, secret) {
|
|
19
|
+
if (!signature)
|
|
20
|
+
return false;
|
|
21
|
+
const hmac = (0, crypto_1.createHmac)('sha256', secret);
|
|
22
|
+
hmac.update(payload, 'utf8');
|
|
23
|
+
const digest = `sha256=${hmac.digest('hex')}`;
|
|
24
|
+
// Constant-time comparison to prevent timing attacks
|
|
25
|
+
if (signature.length !== digest.length)
|
|
26
|
+
return false;
|
|
27
|
+
return signature === digest;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Register webhook endpoint with GitHub signature verification
|
|
31
|
+
*/
|
|
32
|
+
function registerWebhooks(fastify, config) {
|
|
33
|
+
fastify.post('/webhooks', {
|
|
34
|
+
config: {
|
|
35
|
+
// Raw body needed for signature verification
|
|
36
|
+
rawBody: true,
|
|
37
|
+
},
|
|
38
|
+
}, async (request, reply) => {
|
|
39
|
+
const signature = request.headers['x-hub-signature-256'];
|
|
40
|
+
const event = request.headers['x-github-event'];
|
|
41
|
+
const deliveryId = request.headers['x-github-delivery'];
|
|
42
|
+
console.error(`📨 Webhook received: ${event} (delivery: ${deliveryId})`);
|
|
43
|
+
// Get raw body for signature verification
|
|
44
|
+
let rawBody;
|
|
45
|
+
try {
|
|
46
|
+
rawBody = JSON.stringify(request.body);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return reply.code(400).send({
|
|
50
|
+
error: 'Invalid JSON payload',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Verify signature
|
|
54
|
+
if (!signature || !verifySignature(rawBody, signature, config.secret)) {
|
|
55
|
+
console.error('⚠️ Invalid webhook signature');
|
|
56
|
+
return reply.code(401).send({
|
|
57
|
+
error: 'Invalid signature',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const payload = request.body;
|
|
61
|
+
// Handle different webhook events
|
|
62
|
+
try {
|
|
63
|
+
if (event === 'pull_request') {
|
|
64
|
+
await handlePullRequestEvent(payload, config);
|
|
65
|
+
return reply.code(200).send({ status: 'processed' });
|
|
66
|
+
}
|
|
67
|
+
else if (event === 'ping') {
|
|
68
|
+
console.error('🏓 Webhook ping received');
|
|
69
|
+
return reply.code(200).send({ status: 'pong' });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.error(`⏭️ Ignoring event: ${event}`);
|
|
73
|
+
return reply.code(200).send({ status: 'ignored' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(`❌ Webhook processing error:`, error);
|
|
78
|
+
return reply.code(500).send({
|
|
79
|
+
error: 'Webhook processing failed',
|
|
80
|
+
message: error.message,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
console.error('✅ Webhook endpoint registered at POST /webhooks');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Handle pull_request webhook events
|
|
88
|
+
*/
|
|
89
|
+
async function handlePullRequestEvent(payload, config) {
|
|
90
|
+
const { action, pull_request, repository } = payload;
|
|
91
|
+
if (!pull_request || !repository) {
|
|
92
|
+
console.error('⚠️ Missing pull_request or repository in payload');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const owner = repository.owner.login;
|
|
96
|
+
const repo = repository.name;
|
|
97
|
+
const prNumber = pull_request.number;
|
|
98
|
+
const repoFullName = `${owner}/${repo}`;
|
|
99
|
+
console.error(`📋 Processing ${action} for ${repoFullName}#${prNumber}`);
|
|
100
|
+
const repoId = config.db.upsertRepository(owner, repo);
|
|
101
|
+
try {
|
|
102
|
+
switch (action) {
|
|
103
|
+
case 'opened':
|
|
104
|
+
// New PR opened - score it
|
|
105
|
+
await scorePR(repoFullName, prNumber, config);
|
|
106
|
+
console.error(`✅ Scored new PR ${repoFullName}#${prNumber}`);
|
|
107
|
+
break;
|
|
108
|
+
case 'synchronize':
|
|
109
|
+
// PR updated (new commits) - re-score
|
|
110
|
+
await scorePR(repoFullName, prNumber, config);
|
|
111
|
+
console.error(`✅ Re-scored updated PR ${repoFullName}#${prNumber}`);
|
|
112
|
+
break;
|
|
113
|
+
case 'closed':
|
|
114
|
+
// PR closed - update state
|
|
115
|
+
const newState = pull_request.merged ? 'merged' : 'closed';
|
|
116
|
+
config.db.updatePRState(repoId, prNumber, newState);
|
|
117
|
+
config.broadcaster?.broadcast('pr_closed', {
|
|
118
|
+
repo: repoFullName,
|
|
119
|
+
prNumber,
|
|
120
|
+
state: newState,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
console.error(`✅ Updated PR ${repoFullName}#${prNumber} state to ${newState}`);
|
|
124
|
+
break;
|
|
125
|
+
case 'reopened':
|
|
126
|
+
// PR reopened - update state and re-score
|
|
127
|
+
config.db.updatePRState(repoId, prNumber, 'open');
|
|
128
|
+
await scorePR(repoFullName, prNumber, config);
|
|
129
|
+
console.error(`✅ Re-opened and re-scored PR ${repoFullName}#${prNumber}`);
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
console.error(`⏭️ Ignoring action: ${action}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error(`❌ Failed to process PR ${repoFullName}#${prNumber}:`, error.message);
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Score a specific PR and save to database
|
|
142
|
+
*/
|
|
143
|
+
async function scorePR(repoFullName, prNumber, config) {
|
|
144
|
+
const [owner, repo] = repoFullName.split('/');
|
|
145
|
+
// Create scanner with repository-specific config
|
|
146
|
+
const scanConfig = {
|
|
147
|
+
...config.treliqConfig,
|
|
148
|
+
repo: repoFullName,
|
|
149
|
+
dbPath: config.db instanceof db_1.TreliqDB ? config.db.db.name : undefined,
|
|
150
|
+
};
|
|
151
|
+
const scanner = new scanner_1.TreliqScanner(scanConfig);
|
|
152
|
+
// Fetch and score the specific PR
|
|
153
|
+
const prs = await scanner.fetchPRDetails([prNumber]);
|
|
154
|
+
if (prs.length === 0) {
|
|
155
|
+
throw new Error(`PR #${prNumber} not found`);
|
|
156
|
+
}
|
|
157
|
+
const pr = prs[0];
|
|
158
|
+
// Score the PR
|
|
159
|
+
const scoredPR = await scanner.scoring.score(pr);
|
|
160
|
+
// Save to database
|
|
161
|
+
const repoId = config.db.upsertRepository(owner, repo);
|
|
162
|
+
const configHash = 'webhook'; // Use a special hash for webhook-triggered scores
|
|
163
|
+
config.db.upsertPR(repoId, scoredPR, configHash);
|
|
164
|
+
console.error(` Score: ${scoredPR.totalScore}/100 ${scoredPR.isSpam ? '(SPAM)' : ''}`);
|
|
165
|
+
// Broadcast to connected dashboard clients
|
|
166
|
+
config.broadcaster?.broadcast('pr_scored', {
|
|
167
|
+
repo: repoFullName,
|
|
168
|
+
prNumber,
|
|
169
|
+
title: pr.title,
|
|
170
|
+
totalScore: scoredPR.totalScore,
|
|
171
|
+
isSpam: scoredPR.isSpam,
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/server/webhooks.ts"],"names":[],"mappings":"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"}
|