kachow 0.1.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/README.md +77 -0
- package/_server/dist/app.js +130 -0
- package/_server/dist/db/index.js +50 -0
- package/_server/dist/db/schema.js +247 -0
- package/_server/dist/queues/ingestQueue.js +49 -0
- package/_server/dist/queues/redis.js +58 -0
- package/_server/dist/routes/agents.js +162 -0
- package/_server/dist/routes/architecture.js +88 -0
- package/_server/dist/routes/config.js +24 -0
- package/_server/dist/routes/github.js +158 -0
- package/_server/dist/routes/graph.js +112 -0
- package/_server/dist/routes/healing.js +137 -0
- package/_server/dist/routes/impact.js +100 -0
- package/_server/dist/routes/ingest.js +182 -0
- package/_server/dist/routes/manager.js +179 -0
- package/_server/dist/routes/notifications.js +85 -0
- package/_server/dist/routes/qa.js +68 -0
- package/_server/dist/routes/scanner.js +221 -0
- package/_server/dist/routes/stream.js +179 -0
- package/_server/dist/routes/webhooks.js +168 -0
- package/_server/dist/server.js +46 -0
- package/_server/dist/services/agentService.js +715 -0
- package/_server/dist/services/architectureService.js +172 -0
- package/_server/dist/services/demoSeed.js +181 -0
- package/_server/dist/services/graphLayout.js +102 -0
- package/_server/dist/services/graphService.js +532 -0
- package/_server/dist/services/healingService.js +253 -0
- package/_server/dist/services/impactService.js +304 -0
- package/_server/dist/services/ingestService.js +129 -0
- package/_server/dist/services/managerService.js +260 -0
- package/_server/dist/services/notificationService.js +283 -0
- package/_server/dist/services/qaService.js +413 -0
- package/_server/dist/services/scannerService.js +748 -0
- package/_server/dist/services/seedService.js +215 -0
- package/_server/dist/sse/sseManager.js +101 -0
- package/_server/dist/types/index.js +38 -0
- package/_server/dist/workers/ingestWorker.js +274 -0
- package/_server/public/assets/index-BTkbB_YF.js +4546 -0
- package/_server/public/assets/index-Bmh3jWBm.css +1 -0
- package/_server/public/favicon.ico +0 -0
- package/_server/public/images/glass-waves-bg.png +0 -0
- package/_server/public/index.html +29 -0
- package/_server/public/placeholder.svg +1 -0
- package/_server/public/robots.txt +14 -0
- package/dist/config.js +133 -0
- package/dist/index.js +510 -0
- package/dist/setup.js +223 -0
- package/package.json +62 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent routes — Phase 10: Deep Analysis Engine.
|
|
4
|
+
*
|
|
5
|
+
* POST /api/agents/analyze — Trigger deep analysis on a repo
|
|
6
|
+
* GET /api/agents/knowledge-graph — Get latest knowledge graph
|
|
7
|
+
* GET /api/agents/analyses — Get agent analysis history
|
|
8
|
+
* GET /api/agents/status — Quick status check
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.agentRouter = void 0;
|
|
12
|
+
const express_1 = require("express");
|
|
13
|
+
const zod_1 = require("zod");
|
|
14
|
+
const crypto_1 = require("crypto");
|
|
15
|
+
const agentService_js_1 = require("../services/agentService.js");
|
|
16
|
+
const scannerService_js_1 = require("../services/scannerService.js");
|
|
17
|
+
const graphService_js_1 = require("../services/graphService.js");
|
|
18
|
+
const index_js_1 = require("../db/index.js");
|
|
19
|
+
const index_js_2 = require("../types/index.js");
|
|
20
|
+
const router = (0, express_1.Router)();
|
|
21
|
+
exports.agentRouter = router;
|
|
22
|
+
// ── POST /api/agents/analyze ──────────────────────────────────────────────────
|
|
23
|
+
const AnalyzeBodySchema = zod_1.z.object({
|
|
24
|
+
repoPath: zod_1.z.string().min(1, 'repoPath is required'),
|
|
25
|
+
repoUrl: zod_1.z.string().url().optional(),
|
|
26
|
+
team: zod_1.z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
router.post('/analyze', async (req, res) => {
|
|
29
|
+
const parse = AnalyzeBodySchema.safeParse(req.body);
|
|
30
|
+
if (!parse.success) {
|
|
31
|
+
res.status(400).json((0, index_js_2.err)(parse.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const { repoPath, repoUrl, team } = parse.data;
|
|
35
|
+
try {
|
|
36
|
+
// Step 1: Run the normal scanner first to discover services
|
|
37
|
+
console.log('[Agents] Step 1: Running scanner...');
|
|
38
|
+
const scanResult = (0, scannerService_js_1.scanMonorepo)(repoPath, repoUrl, team);
|
|
39
|
+
// Persist scanner results to DB (same as scanner route)
|
|
40
|
+
const db = (0, index_js_1.getDb)();
|
|
41
|
+
const toTier = (s) => s >= 75 ? 'healthy' : s >= 50 ? 'warning' : 'critical';
|
|
42
|
+
const upsertService = db.prepare(`
|
|
43
|
+
INSERT INTO services (id, name, repo_url, repo_subpath, team, language,
|
|
44
|
+
health_score, health_tier, doc_coverage, test_coverage, last_commit, last_ingested)
|
|
45
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
46
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
47
|
+
name=excluded.name, repo_url=excluded.repo_url, repo_subpath=excluded.repo_subpath,
|
|
48
|
+
team=excluded.team, language=excluded.language, health_score=excluded.health_score,
|
|
49
|
+
health_tier=excluded.health_tier, doc_coverage=excluded.doc_coverage,
|
|
50
|
+
test_coverage=excluded.test_coverage, last_commit=excluded.last_commit,
|
|
51
|
+
last_ingested=excluded.last_ingested, updated_at=datetime('now')
|
|
52
|
+
`);
|
|
53
|
+
const upsertEndpoint = db.prepare(`
|
|
54
|
+
INSERT INTO endpoints (id, service_id, path, method, deprecated)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?)
|
|
56
|
+
ON CONFLICT(service_id, path, method) DO UPDATE SET deprecated=excluded.deprecated, updated_at=datetime('now')
|
|
57
|
+
`);
|
|
58
|
+
for (const svc of scanResult.services) {
|
|
59
|
+
upsertService.run(svc.id, svc.name, svc.repoUrl, svc.repoSubpath, svc.team ?? team ?? null, svc.language, svc.healthScore, toTier(svc.healthScore), svc.docCoverage, svc.testCoverage, svc.lastCommit);
|
|
60
|
+
for (const ep of svc.endpoints) {
|
|
61
|
+
upsertEndpoint.run((0, crypto_1.randomUUID)(), svc.id, ep.path, ep.method, ep.deprecated ? 1 : 0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Build service roots for deep analysis
|
|
65
|
+
const serviceRoots = scanResult.services
|
|
66
|
+
.filter((s) => s.language !== 'infra') // skip infra stubs from scanner
|
|
67
|
+
.map((s) => ({
|
|
68
|
+
id: s.id,
|
|
69
|
+
name: s.name,
|
|
70
|
+
language: s.language,
|
|
71
|
+
dir: s.repoPath,
|
|
72
|
+
}));
|
|
73
|
+
if (serviceRoots.length === 0) {
|
|
74
|
+
res.status(400).json((0, index_js_2.err)('No services found to analyze. Run /api/scanner/scan first.'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Step 2: Run deep analysis with LLM agents
|
|
78
|
+
console.log(`[Agents] Step 2: Deep analysis on ${serviceRoots.length} services...`);
|
|
79
|
+
const analysisResult = await (0, agentService_js_1.runDeepAnalysis)(repoPath, serviceRoots);
|
|
80
|
+
// Save a fresh snapshot
|
|
81
|
+
try {
|
|
82
|
+
(0, graphService_js_1.saveSnapshot)('latest');
|
|
83
|
+
}
|
|
84
|
+
catch { /* non-fatal */ }
|
|
85
|
+
res.json((0, index_js_2.ok)({
|
|
86
|
+
analysisId: analysisResult.analysisId,
|
|
87
|
+
servicesAnalyzed: analysisResult.servicesAnalyzed,
|
|
88
|
+
totalFindings: analysisResult.totalFindings,
|
|
89
|
+
knowledgeGraph: {
|
|
90
|
+
nodes: analysisResult.orchestratorResult.nodes.length,
|
|
91
|
+
edges: analysisResult.orchestratorResult.edges.length,
|
|
92
|
+
layers: analysisResult.orchestratorResult.architecture.layers.length,
|
|
93
|
+
dataFlows: analysisResult.orchestratorResult.architecture.dataFlows.length,
|
|
94
|
+
selfHealingSuggestions: analysisResult.orchestratorResult.selfHealingSuggestions.length,
|
|
95
|
+
},
|
|
96
|
+
agentBreakdown: analysisResult.agentResults.map((r) => ({
|
|
97
|
+
agent: r.agentName,
|
|
98
|
+
service: r.serviceName,
|
|
99
|
+
findings: r.findings.length,
|
|
100
|
+
durationMs: r.durationMs,
|
|
101
|
+
})),
|
|
102
|
+
totalDurationMs: analysisResult.totalDurationMs,
|
|
103
|
+
summary: analysisResult.orchestratorResult.summary,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
const msg = e instanceof Error ? e.message : 'Deep analysis failed';
|
|
108
|
+
console.error('[Agents] Error:', msg);
|
|
109
|
+
res.status(500).json((0, index_js_2.err)(msg));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// ── GET /api/agents/knowledge-graph ───────────────────────────────────────────
|
|
113
|
+
router.get('/knowledge-graph', (_req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const kg = (0, agentService_js_1.getLatestKnowledgeGraph)();
|
|
116
|
+
if (!kg) {
|
|
117
|
+
res.json((0, index_js_2.ok)({
|
|
118
|
+
nodes: [], edges: [],
|
|
119
|
+
architecture: { layers: [], dataFlows: [] },
|
|
120
|
+
selfHealingSuggestions: [],
|
|
121
|
+
summary: 'No deep analysis has been run yet. POST /api/agents/analyze to start.',
|
|
122
|
+
}));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
res.json((0, index_js_2.ok)(kg));
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
res.status(500).json((0, index_js_2.err)(e instanceof Error ? e.message : 'Failed to get knowledge graph'));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// ── GET /api/agents/analyses ──────────────────────────────────────────────────
|
|
132
|
+
router.get('/analyses', (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const analysisId = req.query.analysisId;
|
|
135
|
+
const analyses = (0, agentService_js_1.getAgentAnalyses)(analysisId);
|
|
136
|
+
res.json((0, index_js_2.ok)(analyses));
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
res.status(500).json((0, index_js_2.err)(e instanceof Error ? e.message : 'Failed to get analyses'));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// ── GET /api/agents/status ────────────────────────────────────────────────────
|
|
143
|
+
router.get('/status', (_req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const hasKey = !!(process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY !== 'your_openai_key_here');
|
|
146
|
+
const kg = (0, agentService_js_1.getLatestKnowledgeGraph)();
|
|
147
|
+
res.json((0, index_js_2.ok)({
|
|
148
|
+
openaiConfigured: hasKey,
|
|
149
|
+
hasKnowledgeGraph: !!kg,
|
|
150
|
+
nodeCount: kg?.nodes.length ?? 0,
|
|
151
|
+
edgeCount: kg?.edges.length ?? 0,
|
|
152
|
+
agents: [
|
|
153
|
+
'db-agent', 'routing-agent', 'dependency-agent', 'infra-agent',
|
|
154
|
+
'cicd-agent', 'schema-agent', 'security-agent', 'test-agent',
|
|
155
|
+
],
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
res.status(500).json((0, index_js_2.err)(e instanceof Error ? e.message : 'Failed to get status'));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Architecture diagram routes — Phase 9.
|
|
4
|
+
*
|
|
5
|
+
* GET /api/architecture/flow — Services in layers (INGRESS/CORE/DOMAIN/INFRA)
|
|
6
|
+
* GET /api/architecture/blueprint/:id — Full service blueprint
|
|
7
|
+
* POST /api/architecture/propose — AI-proposed new service (brownie points)
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.architectureRouter = void 0;
|
|
14
|
+
const express_1 = require("express");
|
|
15
|
+
const zod_1 = require("zod");
|
|
16
|
+
const openai_1 = __importDefault(require("openai"));
|
|
17
|
+
const architectureService_js_1 = require("../services/architectureService.js");
|
|
18
|
+
const index_js_1 = require("../types/index.js");
|
|
19
|
+
const router = (0, express_1.Router)();
|
|
20
|
+
exports.architectureRouter = router;
|
|
21
|
+
// ── GET /api/architecture/flow ────────────────────────────────────────────────
|
|
22
|
+
router.get('/flow', (_req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
res.json((0, index_js_1.ok)((0, architectureService_js_1.getArchitectureFlow)()));
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
res.status(500).json((0, index_js_1.err)(e instanceof Error ? e.message : 'Failed to get architecture flow'));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// ── GET /api/architecture/blueprint/:serviceId ────────────────────────────────
|
|
31
|
+
router.get('/blueprint/:serviceId', (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const blueprint = (0, architectureService_js_1.getServiceBlueprint)(req.params.serviceId);
|
|
34
|
+
if (!blueprint) {
|
|
35
|
+
res.status(404).json((0, index_js_1.err)(`Service ${req.params.serviceId} not found`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
res.json((0, index_js_1.ok)(blueprint));
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
res.status(500).json((0, index_js_1.err)(e instanceof Error ? e.message : 'Failed to get blueprint'));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
// ── POST /api/architecture/propose (brownie points) ───────────────────────────
|
|
45
|
+
const ProposeSchema = zod_1.z.object({
|
|
46
|
+
requirement: zod_1.z.string().min(10, 'Requirement must be at least 10 characters').max(500),
|
|
47
|
+
});
|
|
48
|
+
router.post('/propose', async (req, res) => {
|
|
49
|
+
const parse = ProposeSchema.safeParse(req.body);
|
|
50
|
+
if (!parse.success) {
|
|
51
|
+
res.status(400).json((0, index_js_1.err)(parse.error.errors.map(e => e.message).join('; ')));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
55
|
+
if (!apiKey) {
|
|
56
|
+
res.status(503).json((0, index_js_1.err)('OPENAI_API_KEY not set'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const openai = new openai_1.default({ apiKey });
|
|
61
|
+
const completion = await openai.chat.completions.create({
|
|
62
|
+
model: 'gpt-4o',
|
|
63
|
+
messages: [{
|
|
64
|
+
role: 'user',
|
|
65
|
+
content: `You are a senior software architect. Design a new microservice to fulfil this requirement: "${parse.data.requirement}"
|
|
66
|
+
|
|
67
|
+
Respond with ONLY valid JSON:
|
|
68
|
+
{
|
|
69
|
+
"name": "kebab-case-service-name",
|
|
70
|
+
"description": "what this service does",
|
|
71
|
+
"dependencies": ["existing-service-name"],
|
|
72
|
+
"techStack": ["Node.js", "TypeScript", "PostgreSQL"],
|
|
73
|
+
"folderStructure": ["src/index.ts", "src/routes.ts", "src/service.ts"],
|
|
74
|
+
"adraft": "one paragraph ADR context for this decision"
|
|
75
|
+
}`,
|
|
76
|
+
}],
|
|
77
|
+
response_format: { type: 'json_object' },
|
|
78
|
+
temperature: 0.4,
|
|
79
|
+
max_tokens: 600,
|
|
80
|
+
});
|
|
81
|
+
const proposed = JSON.parse(completion.choices[0]?.message?.content ?? '{}');
|
|
82
|
+
res.json((0, index_js_1.ok)({ proposedService: proposed, requirement: parse.data.requirement }));
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
res.status(500).json((0, index_js_1.err)(e instanceof Error ? e.message : 'Proposal generation failed'));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
//# sourceMappingURL=architecture.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Config router — exposes runtime configuration to the frontend.
|
|
4
|
+
* GET /api/config returns which integrations are active and the default team.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.configRouter = void 0;
|
|
8
|
+
const express_1 = require("express");
|
|
9
|
+
exports.configRouter = (0, express_1.Router)();
|
|
10
|
+
exports.configRouter.get('/', (_req, res) => {
|
|
11
|
+
res.json({
|
|
12
|
+
success: true,
|
|
13
|
+
data: {
|
|
14
|
+
defaultTeamId: process.env.DEFAULT_TEAM_ID || null,
|
|
15
|
+
hasGitHub: !!process.env.GITHUB_TOKEN,
|
|
16
|
+
hasSlack: !!process.env.SLACK_BOT_TOKEN,
|
|
17
|
+
hasJira: !!process.env.JIRA_BASE_URL,
|
|
18
|
+
hasOpenAI: !!process.env.OPENAI_API_KEY,
|
|
19
|
+
},
|
|
20
|
+
error: null,
|
|
21
|
+
meta: { timestamp: new Date().toISOString(), version: '1.0.0' },
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub webhook handler.
|
|
4
|
+
*
|
|
5
|
+
* POST /api/github/webhook
|
|
6
|
+
*
|
|
7
|
+
* GitHub sends a signed POST on every push event.
|
|
8
|
+
* We verify the HMAC-SHA256 signature using GITHUB_WEBHOOK_SECRET,
|
|
9
|
+
* then enqueue a commit-triggered partial re-scan via ingestService.
|
|
10
|
+
*
|
|
11
|
+
* Register this webhook at:
|
|
12
|
+
* https://github.com/organizations/<org>/settings/hooks
|
|
13
|
+
* or per-repo at Settings → Webhooks
|
|
14
|
+
*
|
|
15
|
+
* Required env vars:
|
|
16
|
+
* GITHUB_WEBHOOK_SECRET — must match the "Secret" field in GitHub's UI
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.githubRouter = void 0;
|
|
53
|
+
const crypto_1 = require("crypto");
|
|
54
|
+
const express_1 = __importStar(require("express"));
|
|
55
|
+
const ingestService_js_1 = require("../services/ingestService.js");
|
|
56
|
+
exports.githubRouter = (0, express_1.Router)();
|
|
57
|
+
// ── Signature verification ────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Verifies the X-Hub-Signature-256 header sent by GitHub.
|
|
60
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
61
|
+
*
|
|
62
|
+
* @param secret - GITHUB_WEBHOOK_SECRET from env
|
|
63
|
+
* @param payload - Raw request body as a Buffer
|
|
64
|
+
* @param header - Value of X-Hub-Signature-256 header
|
|
65
|
+
* @returns true if the signature matches
|
|
66
|
+
*/
|
|
67
|
+
function verifySignature(secret, payload, header) {
|
|
68
|
+
if (!header)
|
|
69
|
+
return false;
|
|
70
|
+
const expected = 'sha256=' + (0, crypto_1.createHmac)('sha256', secret).update(payload).digest('hex');
|
|
71
|
+
try {
|
|
72
|
+
return (0, crypto_1.timingSafeEqual)(Buffer.from(expected), Buffer.from(header));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// ── POST /api/github/webhook ──────────────────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Receives GitHub push events and enqueues a commit-triggered re-scan.
|
|
81
|
+
*
|
|
82
|
+
* GitHub push payload shape (relevant fields only):
|
|
83
|
+
* { ref, after, repository: { id, full_name, clone_url }, commits: [{ id, added, removed, modified }] }
|
|
84
|
+
*/
|
|
85
|
+
exports.githubRouter.post('/webhook',
|
|
86
|
+
// Capture raw body for HMAC verification BEFORE JSON parsing strips it.
|
|
87
|
+
// We register this route with express.raw() so the body is a Buffer here.
|
|
88
|
+
express_1.default.raw({ type: 'application/json' }), async (req, res) => {
|
|
89
|
+
const secret = process.env.GITHUB_WEBHOOK_SECRET;
|
|
90
|
+
if (!secret) {
|
|
91
|
+
console.error('[GitHub Webhook] GITHUB_WEBHOOK_SECRET is not set');
|
|
92
|
+
res.status(500).json({ success: false, error: 'Webhook secret not configured' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// ── Signature check ───────────────────────────────────────────────────────
|
|
96
|
+
const signature = req.headers['x-hub-signature-256'];
|
|
97
|
+
const rawBody = req.body;
|
|
98
|
+
if (!verifySignature(secret, rawBody, signature)) {
|
|
99
|
+
console.warn('[GitHub Webhook] Invalid signature — request rejected');
|
|
100
|
+
res.status(401).json({ success: false, error: 'Invalid webhook signature' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// ── Parse payload ─────────────────────────────────────────────────────────
|
|
104
|
+
let payload;
|
|
105
|
+
try {
|
|
106
|
+
payload = JSON.parse(rawBody.toString('utf-8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
res.status(400).json({ success: false, error: 'Could not parse JSON payload' });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Only process push events (not tag deletions, etc.)
|
|
113
|
+
const event = req.headers['x-github-event'];
|
|
114
|
+
if (event !== 'push') {
|
|
115
|
+
res.json({ success: true, data: { skipped: true, reason: `event '${String(event)}' not handled` } });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const commitHash = payload.after;
|
|
119
|
+
const repoFullName = payload.repository?.full_name ?? 'unknown';
|
|
120
|
+
const repoId = String(payload.repository?.id ?? '');
|
|
121
|
+
if (!commitHash || commitHash === '0000000000000000000000000000000000000000') {
|
|
122
|
+
// Branch deletion — nothing to scan
|
|
123
|
+
res.json({ success: true, data: { skipped: true, reason: 'branch deletion event' } });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Collect all changed files across every commit in this push
|
|
127
|
+
const changedFiles = [
|
|
128
|
+
...new Set((payload.commits ?? []).flatMap((c) => [
|
|
129
|
+
...(c.added ?? []),
|
|
130
|
+
...(c.modified ?? []),
|
|
131
|
+
...(c.removed ?? []),
|
|
132
|
+
])),
|
|
133
|
+
];
|
|
134
|
+
if (changedFiles.length === 0) {
|
|
135
|
+
res.json({ success: true, data: { skipped: true, reason: 'no files changed' } });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// ── Enqueue re-scan ───────────────────────────────────────────────────────
|
|
139
|
+
try {
|
|
140
|
+
const result = await (0, ingestService_js_1.enqueueCommitIngest)({ repoId, commitHash, changedFiles });
|
|
141
|
+
console.log(`[GitHub Webhook] Enqueued commit job for ${repoFullName}@${commitHash.slice(0, 7)}`);
|
|
142
|
+
res.status(201).json({ success: true, data: result });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const e = err;
|
|
146
|
+
if (e.statusCode === 404) {
|
|
147
|
+
// Repo not yet ingested — that's fine, just skip
|
|
148
|
+
res.status(404).json({
|
|
149
|
+
success: false,
|
|
150
|
+
error: `Repository '${repoFullName}' (id: ${repoId}) not found in KA-CHOW. Run a full ingest first.`,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.error('[GitHub Webhook] Enqueue error:', e.message);
|
|
155
|
+
res.status(500).json({ success: false, error: e.message ?? 'Internal error' });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Graph router — Phase 2 read-only endpoints.
|
|
4
|
+
*
|
|
5
|
+
* GET /api/graph/nodes → all service nodes with layout coords
|
|
6
|
+
* GET /api/graph/edges → all dependency edges
|
|
7
|
+
* GET /api/graph/node/:id → full node detail for sidebar panel
|
|
8
|
+
* GET /api/graph/health → system-wide health aggregate (30-s poll)
|
|
9
|
+
* GET /api/graph/snapshot/:version → snapshot at a specific version/tag
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.graphRouter = void 0;
|
|
13
|
+
const express_1 = require("express");
|
|
14
|
+
const graphService_js_1 = require("../services/graphService.js");
|
|
15
|
+
exports.graphRouter = (0, express_1.Router)();
|
|
16
|
+
// ── GET /nodes ────────────────────────────────────────────────────────────────
|
|
17
|
+
exports.graphRouter.get('/nodes', (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const team = typeof req.query.team === 'string' && req.query.team ? req.query.team : undefined;
|
|
20
|
+
const teamId = typeof req.query.teamId === 'string' && req.query.teamId ? req.query.teamId : undefined;
|
|
21
|
+
const nodes = (0, graphService_js_1.getNodes)(team, teamId);
|
|
22
|
+
res.json({ success: true, data: nodes, meta: { count: nodes.length } });
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
26
|
+
res.status(500).json({ success: false, error: message });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
// ── GET /edges ────────────────────────────────────────────────────────────────
|
|
30
|
+
exports.graphRouter.get('/edges', (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const team = typeof req.query.team === 'string' && req.query.team ? req.query.team : undefined;
|
|
33
|
+
const teamId = typeof req.query.teamId === 'string' && req.query.teamId ? req.query.teamId : undefined;
|
|
34
|
+
const edges = (0, graphService_js_1.getEdges)(team, teamId);
|
|
35
|
+
res.json({ success: true, data: edges, meta: { count: edges.length } });
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
39
|
+
res.status(500).json({ success: false, error: message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// ── GET /health ───────────────────────────────────────────────────────────────
|
|
43
|
+
exports.graphRouter.get('/health', (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const teamId = typeof req.query.teamId === 'string' && req.query.teamId ? req.query.teamId : undefined;
|
|
46
|
+
const health = (0, graphService_js_1.getSystemHealth)(teamId);
|
|
47
|
+
res.json({ success: true, data: health });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
51
|
+
res.status(500).json({ success: false, error: message });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// ── GET /snapshot/:version ────────────────────────────────────────────────────
|
|
55
|
+
exports.graphRouter.get('/snapshot/:version', (req, res) => {
|
|
56
|
+
const { version } = req.params;
|
|
57
|
+
if (!version || version.trim().length === 0) {
|
|
58
|
+
res.status(400).json({ success: false, error: 'version is required' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const snapshot = (0, graphService_js_1.getSnapshot)(version);
|
|
63
|
+
if (!snapshot) {
|
|
64
|
+
res.status(404).json({
|
|
65
|
+
success: false,
|
|
66
|
+
error: `Snapshot '${version}' not found`,
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
res.json({ success: true, data: snapshot });
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
74
|
+
res.status(500).json({ success: false, error: message });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
// ── GET /critical-issues ──────────────────────────────────────────────────────
|
|
78
|
+
exports.graphRouter.get('/critical-issues', (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const teamId = typeof req.query.teamId === 'string' && req.query.teamId ? req.query.teamId : undefined;
|
|
81
|
+
const issues = (0, graphService_js_1.getCriticalIssues)(teamId);
|
|
82
|
+
res.json({ success: true, data: issues, meta: { count: issues.length } });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
86
|
+
res.status(500).json({ success: false, error: message });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// ── GET /node/:id (most specific last) ──────────────────────────────────────
|
|
90
|
+
exports.graphRouter.get('/node/:id', (req, res) => {
|
|
91
|
+
const { id } = req.params;
|
|
92
|
+
if (!id || id.trim().length === 0) {
|
|
93
|
+
res.status(400).json({ success: false, error: 'id is required' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const detail = (0, graphService_js_1.getNodeDetail)(id);
|
|
98
|
+
if (!detail) {
|
|
99
|
+
res.status(404).json({
|
|
100
|
+
success: false,
|
|
101
|
+
error: `Service node '${id}' not found`,
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
res.json({ success: true, data: detail });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
109
|
+
res.status(500).json({ success: false, error: message });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=graph.js.map
|