@synth-deploy/server 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/dist/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseWebhook, parseGitHubActionsWebhook, parseAzureDevOpsWebhook, parseJenkinsWebhook, parseGitLabCIWebhook, parseCircleCIWebhook, parseGenericWebhook } from "./webhook-handlers.js";
|
|
2
|
+
export type { WebhookPayload } from "./webhook-handlers.js";
|
|
3
|
+
export { RegistryPoller } from "./registry-poller.js";
|
|
4
|
+
export { IntakeProcessor } from "./intake-processor.js";
|
|
5
|
+
export { IntakeChannelStore, IntakeEventStore } from "./intake-store.js";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intake processor — takes a normalized webhook payload and creates
|
|
3
|
+
* or updates artifacts and versions in the artifact store.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IArtifactStore } from "@synth-deploy/core";
|
|
7
|
+
import type { ArtifactAnalyzer } from "../artifact-analyzer.js";
|
|
8
|
+
import { detectArtifactType } from "../artifact-analyzer.js";
|
|
9
|
+
import type { WebhookPayload } from "./webhook-handlers.js";
|
|
10
|
+
|
|
11
|
+
export class IntakeProcessor {
|
|
12
|
+
constructor(
|
|
13
|
+
private artifactStore: IArtifactStore,
|
|
14
|
+
private analyzer?: ArtifactAnalyzer,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
async process(
|
|
18
|
+
payload: WebhookPayload,
|
|
19
|
+
channelId: string,
|
|
20
|
+
): Promise<{ artifactId: string; versionId: string }> {
|
|
21
|
+
// 1. Find existing artifact by name, or create a new one
|
|
22
|
+
const existing = this.artifactStore
|
|
23
|
+
.list()
|
|
24
|
+
.find((a) => a.name === payload.artifactName);
|
|
25
|
+
|
|
26
|
+
let artifactId: string;
|
|
27
|
+
|
|
28
|
+
if (existing) {
|
|
29
|
+
artifactId = existing.id;
|
|
30
|
+
} else {
|
|
31
|
+
const artifact = this.artifactStore.create({
|
|
32
|
+
name: payload.artifactName,
|
|
33
|
+
type: payload.artifactType,
|
|
34
|
+
analysis: {
|
|
35
|
+
summary: `Auto-ingested artifact from ${payload.source}`,
|
|
36
|
+
dependencies: [],
|
|
37
|
+
configurationExpectations: {},
|
|
38
|
+
deploymentIntent: undefined,
|
|
39
|
+
confidence: 0.1,
|
|
40
|
+
},
|
|
41
|
+
annotations: [],
|
|
42
|
+
learningHistory: [
|
|
43
|
+
{
|
|
44
|
+
timestamp: new Date(),
|
|
45
|
+
event: "intake-created",
|
|
46
|
+
details: `Created via intake channel ${channelId} from ${payload.source}`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
artifactId = artifact.id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Add new version
|
|
54
|
+
const stringMetadata: Record<string, string> = {};
|
|
55
|
+
for (const [k, v] of Object.entries(payload.metadata)) {
|
|
56
|
+
stringMetadata[k] = String(v ?? "");
|
|
57
|
+
}
|
|
58
|
+
if (payload.downloadUrl) {
|
|
59
|
+
stringMetadata["downloadUrl"] = payload.downloadUrl;
|
|
60
|
+
}
|
|
61
|
+
stringMetadata["intakeChannel"] = channelId;
|
|
62
|
+
|
|
63
|
+
const version = this.artifactStore.addVersion({
|
|
64
|
+
artifactId,
|
|
65
|
+
version: payload.version,
|
|
66
|
+
source: payload.source,
|
|
67
|
+
metadata: stringMetadata,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 3. Trigger analysis if analyzer is available (best-effort, non-blocking)
|
|
71
|
+
if (this.analyzer) {
|
|
72
|
+
try {
|
|
73
|
+
const result = await this.analyzer.analyze({
|
|
74
|
+
name: payload.artifactName,
|
|
75
|
+
type: payload.artifactType,
|
|
76
|
+
source: payload.source,
|
|
77
|
+
content: payload.content,
|
|
78
|
+
metadata: stringMetadata,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Resolve the best type: prefer the detected type over "unknown"
|
|
82
|
+
const detectedType = detectArtifactType({
|
|
83
|
+
name: payload.artifactName,
|
|
84
|
+
type: payload.artifactType !== "unknown" ? payload.artifactType : undefined,
|
|
85
|
+
source: payload.source,
|
|
86
|
+
content: payload.content,
|
|
87
|
+
metadata: stringMetadata,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Update the artifact with the new analysis (and corrected type)
|
|
91
|
+
this.artifactStore.update(artifactId, {
|
|
92
|
+
type: detectedType !== "unknown" ? detectedType : payload.artifactType,
|
|
93
|
+
analysis: result.analysis,
|
|
94
|
+
learningHistory: [
|
|
95
|
+
...(existing?.learningHistory ?? []),
|
|
96
|
+
{
|
|
97
|
+
timestamp: new Date(),
|
|
98
|
+
event: "intake-analysis",
|
|
99
|
+
details: `Re-analyzed via intake (method: ${result.method}, confidence: ${result.analysis.confidence})`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Analysis failure should not block intake
|
|
105
|
+
console.error(`[IntakeProcessor] Analysis failed for ${payload.artifactName}:`, err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { artifactId, versionId: version.id };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports persistent stores for intake channels and events from @synth-deploy/core.
|
|
3
|
+
* These stores are now SQLite-backed to survive server restarts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { PersistentIntakeChannelStore as IntakeChannelStore } from "@synth-deploy/core";
|
|
7
|
+
export { PersistentIntakeEventStore as IntakeEventStore } from "@synth-deploy/core";
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry poller — periodically checks container and package registries
|
|
3
|
+
* for new versions and emits intake events when new versions are found.
|
|
4
|
+
*
|
|
5
|
+
* Known versions are persisted via PersistentRegistryPollerVersionStore
|
|
6
|
+
* so that server restarts don't re-trigger deployments for already-seen versions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IntakeChannel, RegistryConfig } from "@synth-deploy/core";
|
|
10
|
+
import type { PersistentRegistryPollerVersionStore } from "@synth-deploy/core";
|
|
11
|
+
import type { WebhookPayload } from "./webhook-handlers.js";
|
|
12
|
+
|
|
13
|
+
export class RegistryPoller {
|
|
14
|
+
private intervals = new Map<string, NodeJS.Timeout>();
|
|
15
|
+
private knownVersions = new Map<string, Set<string>>();
|
|
16
|
+
private onNewVersion: (channelId: string, payload: WebhookPayload) => Promise<void>;
|
|
17
|
+
private versionStore?: PersistentRegistryPollerVersionStore;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
onNewVersion: (channelId: string, payload: WebhookPayload) => Promise<void>,
|
|
21
|
+
versionStore?: PersistentRegistryPollerVersionStore,
|
|
22
|
+
) {
|
|
23
|
+
this.onNewVersion = onNewVersion;
|
|
24
|
+
this.versionStore = versionStore;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
startPolling(channel: IntakeChannel): void {
|
|
28
|
+
// Stop existing polling for this channel if any
|
|
29
|
+
this.stopPolling(channel.id);
|
|
30
|
+
|
|
31
|
+
const config = channel.config as unknown as RegistryConfig;
|
|
32
|
+
const intervalMs = config.pollIntervalMs || 300_000; // Default: 5 minutes
|
|
33
|
+
|
|
34
|
+
// Load persisted known versions, or initialize empty set
|
|
35
|
+
if (!this.knownVersions.has(channel.id)) {
|
|
36
|
+
if (this.versionStore) {
|
|
37
|
+
this.knownVersions.set(channel.id, this.versionStore.getKnownVersions(channel.id));
|
|
38
|
+
} else {
|
|
39
|
+
this.knownVersions.set(channel.id, new Set());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Poll immediately on start
|
|
44
|
+
this.poll(channel).catch((err) => {
|
|
45
|
+
console.error(`[RegistryPoller] Initial poll failed for channel ${channel.id}:`, err);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Set up interval
|
|
49
|
+
const interval = setInterval(() => {
|
|
50
|
+
this.poll(channel).catch((err) => {
|
|
51
|
+
console.error(`[RegistryPoller] Poll failed for channel ${channel.id}:`, err);
|
|
52
|
+
});
|
|
53
|
+
}, intervalMs);
|
|
54
|
+
|
|
55
|
+
this.intervals.set(channel.id, interval);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
stopPolling(channelId: string): void {
|
|
59
|
+
const interval = this.intervals.get(channelId);
|
|
60
|
+
if (interval) {
|
|
61
|
+
clearInterval(interval);
|
|
62
|
+
this.intervals.delete(channelId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
stopAll(): void {
|
|
67
|
+
for (const [id] of this.intervals) {
|
|
68
|
+
this.stopPolling(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Record a version as known, both in memory and in the persistent store.
|
|
74
|
+
* Returns true if the version was newly seen (not previously known).
|
|
75
|
+
*/
|
|
76
|
+
private recordVersion(channelId: string, versionKey: string): boolean {
|
|
77
|
+
const known = this.knownVersions.get(channelId) ?? new Set();
|
|
78
|
+
if (known.has(versionKey)) return false;
|
|
79
|
+
|
|
80
|
+
known.add(versionKey);
|
|
81
|
+
this.knownVersions.set(channelId, known);
|
|
82
|
+
this.versionStore?.addVersion(channelId, versionKey);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async poll(channel: IntakeChannel): Promise<void> {
|
|
87
|
+
const config = channel.config as unknown as RegistryConfig;
|
|
88
|
+
switch (config.type) {
|
|
89
|
+
case "docker":
|
|
90
|
+
await this.pollDockerRegistry(channel, config);
|
|
91
|
+
break;
|
|
92
|
+
case "npm":
|
|
93
|
+
await this.pollNpmRegistry(channel, config);
|
|
94
|
+
break;
|
|
95
|
+
case "nuget":
|
|
96
|
+
await this.pollNuGetRegistry(channel, config);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Poll a Docker registry for new image tags.
|
|
103
|
+
* Uses the Docker Registry HTTP API V2.
|
|
104
|
+
*/
|
|
105
|
+
private async pollDockerRegistry(channel: IntakeChannel, config: RegistryConfig): Promise<void> {
|
|
106
|
+
const trackedImages = config.trackedImages ?? [];
|
|
107
|
+
if (trackedImages.length === 0) return;
|
|
108
|
+
|
|
109
|
+
const baseUrl = config.url.replace(/\/$/, "");
|
|
110
|
+
const headers: Record<string, string> = {};
|
|
111
|
+
if (config.credentials) {
|
|
112
|
+
const auth = Buffer.from(`${config.credentials.username}:${config.credentials.password}`).toString("base64");
|
|
113
|
+
headers["Authorization"] = `Basic ${auth}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const image of trackedImages) {
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch(`${baseUrl}/v2/${image}/tags/list`, { headers });
|
|
119
|
+
if (!res.ok) continue;
|
|
120
|
+
|
|
121
|
+
const data = await res.json() as { tags?: string[] };
|
|
122
|
+
const tags = data.tags ?? [];
|
|
123
|
+
const known = this.knownVersions.get(channel.id) ?? new Set();
|
|
124
|
+
const isFirstPoll = known.size === 0;
|
|
125
|
+
|
|
126
|
+
for (const tag of tags) {
|
|
127
|
+
const key = `${image}:${tag}`;
|
|
128
|
+
const isNew = this.recordVersion(channel.id, key);
|
|
129
|
+
|
|
130
|
+
// Don't emit events on first poll (seed phase)
|
|
131
|
+
if (isNew && !isFirstPoll) {
|
|
132
|
+
await this.onNewVersion(channel.id, {
|
|
133
|
+
artifactName: image,
|
|
134
|
+
artifactType: "docker",
|
|
135
|
+
version: tag,
|
|
136
|
+
source: `docker-registry:${baseUrl}`,
|
|
137
|
+
downloadUrl: `${baseUrl}/v2/${image}/manifests/${tag}`,
|
|
138
|
+
metadata: { registry: baseUrl, image, tag },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(`[RegistryPoller] Docker poll error for ${image}:`, err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Poll the npm registry for new package versions.
|
|
150
|
+
*/
|
|
151
|
+
private async pollNpmRegistry(channel: IntakeChannel, config: RegistryConfig): Promise<void> {
|
|
152
|
+
const trackedPackages = config.trackedPackages ?? [];
|
|
153
|
+
if (trackedPackages.length === 0) return;
|
|
154
|
+
|
|
155
|
+
const baseUrl = config.url.replace(/\/$/, "") || "https://registry.npmjs.org";
|
|
156
|
+
|
|
157
|
+
for (const pkg of trackedPackages) {
|
|
158
|
+
try {
|
|
159
|
+
const res = await fetch(`${baseUrl}/${encodeURIComponent(pkg)}`);
|
|
160
|
+
if (!res.ok) continue;
|
|
161
|
+
|
|
162
|
+
const data = await res.json() as { versions?: Record<string, unknown> };
|
|
163
|
+
const versions = Object.keys(data.versions ?? {});
|
|
164
|
+
const known = this.knownVersions.get(channel.id) ?? new Set();
|
|
165
|
+
const isFirstPoll = known.size === 0;
|
|
166
|
+
|
|
167
|
+
for (const version of versions) {
|
|
168
|
+
const key = `${pkg}@${version}`;
|
|
169
|
+
const isNew = this.recordVersion(channel.id, key);
|
|
170
|
+
|
|
171
|
+
if (isNew && !isFirstPoll) {
|
|
172
|
+
await this.onNewVersion(channel.id, {
|
|
173
|
+
artifactName: pkg,
|
|
174
|
+
artifactType: "npm",
|
|
175
|
+
version,
|
|
176
|
+
source: `npm-registry:${baseUrl}`,
|
|
177
|
+
downloadUrl: `${baseUrl}/${encodeURIComponent(pkg)}/-/${pkg}-${version}.tgz`,
|
|
178
|
+
metadata: { registry: baseUrl, package: pkg, version },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error(`[RegistryPoller] npm poll error for ${pkg}:`, err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Poll a NuGet feed for new package versions.
|
|
190
|
+
* Uses the NuGet V3 API.
|
|
191
|
+
*/
|
|
192
|
+
private async pollNuGetRegistry(channel: IntakeChannel, config: RegistryConfig): Promise<void> {
|
|
193
|
+
const trackedPackages = config.trackedPackages ?? [];
|
|
194
|
+
if (trackedPackages.length === 0) return;
|
|
195
|
+
|
|
196
|
+
const baseUrl = config.url.replace(/\/$/, "") || "https://api.nuget.org/v3";
|
|
197
|
+
|
|
198
|
+
for (const pkg of trackedPackages) {
|
|
199
|
+
try {
|
|
200
|
+
// NuGet V3: flat container for version listing
|
|
201
|
+
const res = await fetch(
|
|
202
|
+
`${baseUrl}/flatcontainer/${pkg.toLowerCase()}/index.json`,
|
|
203
|
+
);
|
|
204
|
+
if (!res.ok) continue;
|
|
205
|
+
|
|
206
|
+
const data = await res.json() as { versions?: string[] };
|
|
207
|
+
const versions = data.versions ?? [];
|
|
208
|
+
const known = this.knownVersions.get(channel.id) ?? new Set();
|
|
209
|
+
const isFirstPoll = known.size === 0;
|
|
210
|
+
|
|
211
|
+
for (const version of versions) {
|
|
212
|
+
const key = `${pkg}@${version}`;
|
|
213
|
+
const isNew = this.recordVersion(channel.id, key);
|
|
214
|
+
|
|
215
|
+
if (isNew && !isFirstPoll) {
|
|
216
|
+
await this.onNewVersion(channel.id, {
|
|
217
|
+
artifactName: pkg,
|
|
218
|
+
artifactType: "nuget",
|
|
219
|
+
version,
|
|
220
|
+
source: `nuget-registry:${baseUrl}`,
|
|
221
|
+
metadata: { registry: baseUrl, package: pkg, version },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`[RegistryPoller] NuGet poll error for ${pkg}:`, err);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook payload parsers for CI/CD system integrations.
|
|
5
|
+
*
|
|
6
|
+
* Each parser extracts a normalized WebhookPayload from the raw
|
|
7
|
+
* webhook body sent by the respective CI/CD platform.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface WebhookPayload {
|
|
11
|
+
artifactName: string;
|
|
12
|
+
artifactType: string;
|
|
13
|
+
version: string;
|
|
14
|
+
source: string;
|
|
15
|
+
downloadUrl?: string;
|
|
16
|
+
metadata: Record<string, unknown>;
|
|
17
|
+
/** Raw file content — passed through to the analyzer for deterministic extraction */
|
|
18
|
+
content?: Buffer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// GitHub Actions — workflow_run completed event
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export function parseGitHubActionsWebhook(body: unknown): WebhookPayload | null {
|
|
26
|
+
if (!body || typeof body !== "object") return null;
|
|
27
|
+
const data = body as Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
// GitHub sends workflow_run events with action: "completed"
|
|
30
|
+
const workflowRun = data.workflow_run as Record<string, unknown> | undefined;
|
|
31
|
+
if (!workflowRun) return null;
|
|
32
|
+
|
|
33
|
+
const conclusion = workflowRun.conclusion as string | undefined;
|
|
34
|
+
if (conclusion !== "success") return null;
|
|
35
|
+
|
|
36
|
+
const repo = data.repository as Record<string, unknown> | undefined;
|
|
37
|
+
const repoName = (repo?.name as string) ?? "unknown";
|
|
38
|
+
const headSha = (workflowRun.head_sha as string) ?? "unknown";
|
|
39
|
+
const headBranch = (workflowRun.head_branch as string) ?? "unknown";
|
|
40
|
+
const runNumber = workflowRun.run_number as number | undefined;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
artifactName: repoName,
|
|
44
|
+
artifactType: "github-actions-build",
|
|
45
|
+
version: runNumber ? `build-${runNumber}` : headSha.slice(0, 8),
|
|
46
|
+
source: "github-actions",
|
|
47
|
+
downloadUrl: (workflowRun.artifacts_url as string) ?? undefined,
|
|
48
|
+
metadata: {
|
|
49
|
+
sha: headSha,
|
|
50
|
+
branch: headBranch,
|
|
51
|
+
workflow: (workflowRun.name as string) ?? "unknown",
|
|
52
|
+
runId: workflowRun.id,
|
|
53
|
+
runNumber,
|
|
54
|
+
htmlUrl: workflowRun.html_url,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Azure DevOps — build.complete event
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export function parseAzureDevOpsWebhook(body: unknown): WebhookPayload | null {
|
|
64
|
+
if (!body || typeof body !== "object") return null;
|
|
65
|
+
const data = body as Record<string, unknown>;
|
|
66
|
+
|
|
67
|
+
const eventType = data.eventType as string | undefined;
|
|
68
|
+
if (eventType !== "build.complete") return null;
|
|
69
|
+
|
|
70
|
+
const resource = data.resource as Record<string, unknown> | undefined;
|
|
71
|
+
if (!resource) return null;
|
|
72
|
+
|
|
73
|
+
const result = resource.result as string | undefined;
|
|
74
|
+
if (result !== "succeeded") return null;
|
|
75
|
+
|
|
76
|
+
const definition = resource.definition as Record<string, unknown> | undefined;
|
|
77
|
+
const buildNumber = (resource.buildNumber as string) ?? "unknown";
|
|
78
|
+
const sourceVersion = (resource.sourceVersion as string) ?? "unknown";
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
artifactName: (definition?.name as string) ?? "unknown",
|
|
82
|
+
artifactType: "azure-devops-build",
|
|
83
|
+
version: buildNumber,
|
|
84
|
+
source: "azure-devops",
|
|
85
|
+
downloadUrl: (resource.url as string) ?? undefined,
|
|
86
|
+
metadata: {
|
|
87
|
+
buildId: resource.id,
|
|
88
|
+
buildNumber,
|
|
89
|
+
sourceVersion,
|
|
90
|
+
sourceBranch: resource.sourceBranch,
|
|
91
|
+
definitionName: definition?.name,
|
|
92
|
+
project: (data.resourceContainers as Record<string, unknown>)?.project,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Jenkins — build notification
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
export function parseJenkinsWebhook(body: unknown): WebhookPayload | null {
|
|
102
|
+
if (!body || typeof body !== "object") return null;
|
|
103
|
+
const data = body as Record<string, unknown>;
|
|
104
|
+
|
|
105
|
+
const build = data.build as Record<string, unknown> | undefined;
|
|
106
|
+
if (!build) {
|
|
107
|
+
// Some Jenkins plugins send flat payloads
|
|
108
|
+
if (!data.name && !data.job_name) return null;
|
|
109
|
+
return {
|
|
110
|
+
artifactName: (data.name as string) ?? (data.job_name as string) ?? "unknown",
|
|
111
|
+
artifactType: "jenkins-build",
|
|
112
|
+
version: String(data.build_number ?? data.number ?? "unknown"),
|
|
113
|
+
source: "jenkins",
|
|
114
|
+
downloadUrl: (data.build_url as string) ?? (data.url as string) ?? undefined,
|
|
115
|
+
metadata: {
|
|
116
|
+
status: data.status ?? data.build_status,
|
|
117
|
+
url: data.build_url ?? data.url,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const phase = build.phase as string | undefined;
|
|
123
|
+
const status = build.status as string | undefined;
|
|
124
|
+
|
|
125
|
+
// Only process completed successful builds
|
|
126
|
+
if (phase !== "COMPLETED" && phase !== "FINALIZED") return null;
|
|
127
|
+
if (status && status !== "SUCCESS") return null;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
artifactName: (data.name as string) ?? "unknown",
|
|
131
|
+
artifactType: "jenkins-build",
|
|
132
|
+
version: String(build.number ?? "unknown"),
|
|
133
|
+
source: "jenkins",
|
|
134
|
+
downloadUrl: (build.full_url as string) ?? (build.url as string) ?? undefined,
|
|
135
|
+
metadata: {
|
|
136
|
+
buildNumber: build.number,
|
|
137
|
+
phase,
|
|
138
|
+
status,
|
|
139
|
+
url: build.full_url ?? build.url,
|
|
140
|
+
scmInfo: build.scm,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// GitLab CI — pipeline event
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export function parseGitLabCIWebhook(body: unknown): WebhookPayload | null {
|
|
150
|
+
if (!body || typeof body !== "object") return null;
|
|
151
|
+
const data = body as Record<string, unknown>;
|
|
152
|
+
|
|
153
|
+
const objectKind = data.object_kind as string | undefined;
|
|
154
|
+
if (objectKind !== "pipeline") return null;
|
|
155
|
+
|
|
156
|
+
const attrs = data.object_attributes as Record<string, unknown> | undefined;
|
|
157
|
+
if (!attrs) return null;
|
|
158
|
+
|
|
159
|
+
const status = attrs.status as string | undefined;
|
|
160
|
+
if (status !== "success") return null;
|
|
161
|
+
|
|
162
|
+
const project = data.project as Record<string, unknown> | undefined;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
artifactName: (project?.name as string) ?? "unknown",
|
|
166
|
+
artifactType: "gitlab-ci-build",
|
|
167
|
+
version: `pipeline-${attrs.id ?? "unknown"}`,
|
|
168
|
+
source: "gitlab-ci",
|
|
169
|
+
downloadUrl: (project?.web_url as string) ?? undefined,
|
|
170
|
+
metadata: {
|
|
171
|
+
pipelineId: attrs.id,
|
|
172
|
+
ref: attrs.ref,
|
|
173
|
+
sha: attrs.sha,
|
|
174
|
+
source: attrs.source,
|
|
175
|
+
projectName: project?.name,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// CircleCI — workflow-completed event
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
export function parseCircleCIWebhook(body: unknown): WebhookPayload | null {
|
|
185
|
+
if (!body || typeof body !== "object") return null;
|
|
186
|
+
const data = body as Record<string, unknown>;
|
|
187
|
+
|
|
188
|
+
const type = data.type as string | undefined;
|
|
189
|
+
if (type !== "workflow-completed") return null;
|
|
190
|
+
|
|
191
|
+
const workflow = data.workflow as Record<string, unknown> | undefined;
|
|
192
|
+
if (!workflow) return null;
|
|
193
|
+
|
|
194
|
+
const status = workflow.status as string | undefined;
|
|
195
|
+
if (status !== "success") return null;
|
|
196
|
+
|
|
197
|
+
const pipeline = data.pipeline as Record<string, unknown> | undefined;
|
|
198
|
+
const project = data.project as Record<string, unknown> | undefined;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
artifactName: (project?.name as string) ?? "unknown",
|
|
202
|
+
artifactType: "circleci-build",
|
|
203
|
+
version: `workflow-${workflow.id ?? "unknown"}`,
|
|
204
|
+
source: "circleci",
|
|
205
|
+
metadata: {
|
|
206
|
+
workflowId: workflow.id,
|
|
207
|
+
workflowName: workflow.name,
|
|
208
|
+
pipelineId: pipeline?.id,
|
|
209
|
+
pipelineNumber: pipeline?.number,
|
|
210
|
+
projectSlug: project?.slug,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Generic — expects normalized payload
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
export function parseGenericWebhook(body: unknown): WebhookPayload | null {
|
|
220
|
+
if (!body || typeof body !== "object") return null;
|
|
221
|
+
const data = body as Record<string, unknown>;
|
|
222
|
+
|
|
223
|
+
const artifactName = data.artifactName as string | undefined;
|
|
224
|
+
const version = data.version as string | undefined;
|
|
225
|
+
|
|
226
|
+
if (!artifactName || !version) return null;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
artifactName,
|
|
230
|
+
artifactType: (data.type as string) ?? "generic",
|
|
231
|
+
version,
|
|
232
|
+
source: (data.source as string) ?? "generic",
|
|
233
|
+
downloadUrl: (data.downloadUrl as string) ?? undefined,
|
|
234
|
+
metadata: (data.metadata as Record<string, unknown>) ?? {},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Webhook signature verification
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
export interface WebhookVerificationResult {
|
|
243
|
+
verified: boolean;
|
|
244
|
+
error?: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Verify a webhook signature from GitHub (X-Hub-Signature-256) or
|
|
249
|
+
* GitLab (X-Gitlab-Token). Returns { verified: true } if the signature
|
|
250
|
+
* is valid, or { verified: false, error } if not.
|
|
251
|
+
*
|
|
252
|
+
* If no secretToken is configured for the channel, verification is skipped
|
|
253
|
+
* (returns verified: true) to allow backwards-compatible operation.
|
|
254
|
+
*/
|
|
255
|
+
export function verifyWebhookSignature(
|
|
256
|
+
source: string,
|
|
257
|
+
secretToken: string | undefined,
|
|
258
|
+
headers: Record<string, string | string[] | undefined>,
|
|
259
|
+
rawBody: string,
|
|
260
|
+
): WebhookVerificationResult {
|
|
261
|
+
// Unauthenticated webhooks allowed when no secret is configured
|
|
262
|
+
if (!secretToken) {
|
|
263
|
+
return { verified: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
switch (source) {
|
|
267
|
+
case "github-actions": {
|
|
268
|
+
const signature = headers["x-hub-signature-256"] as string | undefined;
|
|
269
|
+
if (!signature) {
|
|
270
|
+
return { verified: false, error: "Missing X-Hub-Signature-256 header" };
|
|
271
|
+
}
|
|
272
|
+
const expected = "sha256=" + crypto
|
|
273
|
+
.createHmac("sha256", secretToken)
|
|
274
|
+
.update(rawBody)
|
|
275
|
+
.digest("hex");
|
|
276
|
+
const valid = crypto.timingSafeEqual(
|
|
277
|
+
Buffer.from(signature),
|
|
278
|
+
Buffer.from(expected),
|
|
279
|
+
);
|
|
280
|
+
return valid
|
|
281
|
+
? { verified: true }
|
|
282
|
+
: { verified: false, error: "Invalid GitHub webhook signature" };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case "gitlab-ci": {
|
|
286
|
+
const token = headers["x-gitlab-token"] as string | undefined;
|
|
287
|
+
if (!token) {
|
|
288
|
+
return { verified: false, error: "Missing X-Gitlab-Token header" };
|
|
289
|
+
}
|
|
290
|
+
// GitLab uses a shared secret token (not HMAC) — compare in constant time
|
|
291
|
+
if (token.length !== secretToken.length) {
|
|
292
|
+
return { verified: false, error: "Invalid GitLab webhook token" };
|
|
293
|
+
}
|
|
294
|
+
const valid = crypto.timingSafeEqual(
|
|
295
|
+
Buffer.from(token),
|
|
296
|
+
Buffer.from(secretToken),
|
|
297
|
+
);
|
|
298
|
+
return valid
|
|
299
|
+
? { verified: true }
|
|
300
|
+
: { verified: false, error: "Invalid GitLab webhook token" };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
default:
|
|
304
|
+
// No standard signature mechanism for this source type
|
|
305
|
+
return { verified: true };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Router — dispatch to the correct parser based on source
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
export function parseWebhook(source: string, body: unknown): WebhookPayload | null {
|
|
314
|
+
switch (source) {
|
|
315
|
+
case "github-actions":
|
|
316
|
+
return parseGitHubActionsWebhook(body);
|
|
317
|
+
case "azure-devops":
|
|
318
|
+
return parseAzureDevOpsWebhook(body);
|
|
319
|
+
case "jenkins":
|
|
320
|
+
return parseJenkinsWebhook(body);
|
|
321
|
+
case "gitlab-ci":
|
|
322
|
+
return parseGitLabCIWebhook(body);
|
|
323
|
+
case "circleci":
|
|
324
|
+
return parseCircleCIWebhook(body);
|
|
325
|
+
default:
|
|
326
|
+
return parseGenericWebhook(body);
|
|
327
|
+
}
|
|
328
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SynthLogger } from "@synth-deploy/core";
|
|
2
|
+
|
|
3
|
+
let _logger: SynthLogger | null = null;
|
|
4
|
+
|
|
5
|
+
export function initServerLogger(dataDir: string): void {
|
|
6
|
+
_logger = new SynthLogger(dataDir, "server");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function serverLog(label: string, data?: unknown): void {
|
|
10
|
+
_logger?.log(label, data);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function serverWarn(label: string, data?: unknown): void {
|
|
14
|
+
_logger?.warn(label, data);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function serverError(label: string, data?: unknown): void {
|
|
18
|
+
_logger?.error(label, data);
|
|
19
|
+
}
|