@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory store for deployment progress events.
|
|
3
|
+
*
|
|
4
|
+
* Each deployment gets a ring buffer of events (capped at MAX_EVENTS).
|
|
5
|
+
* SSE clients subscribe via addListener / removeListener, and the store
|
|
6
|
+
* pushes new events as they arrive from the envoy's progress callback.
|
|
7
|
+
*
|
|
8
|
+
* Cleanup happens automatically after a deployment completes —
|
|
9
|
+
* the buffer is retained for CLEANUP_DELAY_MS to let late-connecting
|
|
10
|
+
* clients catch up, then purged.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface ProgressEvent {
|
|
14
|
+
id?: number;
|
|
15
|
+
deploymentId: string;
|
|
16
|
+
type:
|
|
17
|
+
| "step-started"
|
|
18
|
+
| "step-completed"
|
|
19
|
+
| "step-failed"
|
|
20
|
+
| "rollback-started"
|
|
21
|
+
| "rollback-completed"
|
|
22
|
+
| "deployment-completed";
|
|
23
|
+
stepIndex: number;
|
|
24
|
+
stepDescription: string;
|
|
25
|
+
status: "in_progress" | "completed" | "failed";
|
|
26
|
+
output?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
timestamp: string;
|
|
29
|
+
overallProgress: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ProgressListener = (event: ProgressEvent) => void;
|
|
33
|
+
|
|
34
|
+
const MAX_EVENTS = 100;
|
|
35
|
+
const CLEANUP_DELAY_MS = 60_000; // 1 minute after completion
|
|
36
|
+
|
|
37
|
+
export class ProgressEventStore {
|
|
38
|
+
private buffers = new Map<string, ProgressEvent[]>();
|
|
39
|
+
private listeners = new Map<string, Set<ProgressListener>>();
|
|
40
|
+
private cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
41
|
+
private nextEventId = 1;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Push a new event for a deployment. Assigns a sequential ID,
|
|
45
|
+
* stores in the ring buffer, and notifies all subscribed SSE listeners.
|
|
46
|
+
*/
|
|
47
|
+
push(event: ProgressEvent): void {
|
|
48
|
+
const { deploymentId } = event;
|
|
49
|
+
|
|
50
|
+
// Assign sequential event ID for SSE Last-Event-ID replay
|
|
51
|
+
event.id = this.nextEventId++;
|
|
52
|
+
|
|
53
|
+
// Ensure buffer exists
|
|
54
|
+
if (!this.buffers.has(deploymentId)) {
|
|
55
|
+
this.buffers.set(deploymentId, []);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const buffer = this.buffers.get(deploymentId)!;
|
|
59
|
+
|
|
60
|
+
// Ring buffer: drop oldest if at capacity
|
|
61
|
+
if (buffer.length >= MAX_EVENTS) {
|
|
62
|
+
buffer.shift();
|
|
63
|
+
}
|
|
64
|
+
buffer.push(event);
|
|
65
|
+
|
|
66
|
+
// Notify listeners
|
|
67
|
+
const listeners = this.listeners.get(deploymentId);
|
|
68
|
+
if (listeners) {
|
|
69
|
+
for (const listener of listeners) {
|
|
70
|
+
try {
|
|
71
|
+
listener(event);
|
|
72
|
+
} catch {
|
|
73
|
+
// Don't let a broken listener stop others
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Schedule cleanup if deployment completed
|
|
79
|
+
if (event.type === "deployment-completed") {
|
|
80
|
+
this.scheduleCleanup(deploymentId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get all buffered events for a deployment (for catch-up on SSE connect).
|
|
86
|
+
*/
|
|
87
|
+
getEvents(deploymentId: string): ProgressEvent[] {
|
|
88
|
+
return this.buffers.get(deploymentId) ?? [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get buffered events after a given event ID (for reconnect replay).
|
|
93
|
+
* Returns only events with id > afterId.
|
|
94
|
+
*/
|
|
95
|
+
getEventsSince(deploymentId: string, afterId: number): ProgressEvent[] {
|
|
96
|
+
const buffer = this.buffers.get(deploymentId) ?? [];
|
|
97
|
+
return buffer.filter((e) => (e.id ?? 0) > afterId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Subscribe to new events for a deployment.
|
|
102
|
+
*/
|
|
103
|
+
addListener(deploymentId: string, listener: ProgressListener): void {
|
|
104
|
+
if (!this.listeners.has(deploymentId)) {
|
|
105
|
+
this.listeners.set(deploymentId, new Set());
|
|
106
|
+
}
|
|
107
|
+
this.listeners.get(deploymentId)!.add(listener);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Unsubscribe from events.
|
|
112
|
+
*/
|
|
113
|
+
removeListener(deploymentId: string, listener: ProgressListener): void {
|
|
114
|
+
const set = this.listeners.get(deploymentId);
|
|
115
|
+
if (set) {
|
|
116
|
+
set.delete(listener);
|
|
117
|
+
if (set.size === 0) {
|
|
118
|
+
this.listeners.delete(deploymentId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Schedule buffer cleanup after deployment completion.
|
|
125
|
+
* Gives late-connecting clients time to catch up.
|
|
126
|
+
*/
|
|
127
|
+
private scheduleCleanup(deploymentId: string): void {
|
|
128
|
+
// Clear any existing timer
|
|
129
|
+
const existing = this.cleanupTimers.get(deploymentId);
|
|
130
|
+
if (existing) clearTimeout(existing);
|
|
131
|
+
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
this.buffers.delete(deploymentId);
|
|
134
|
+
this.listeners.delete(deploymentId);
|
|
135
|
+
this.cleanupTimers.delete(deploymentId);
|
|
136
|
+
}, CLEANUP_DELAY_MS);
|
|
137
|
+
|
|
138
|
+
this.cleanupTimers.set(deploymentId, timer);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Immediately clean up all data for a deployment (for testing).
|
|
143
|
+
*/
|
|
144
|
+
clear(deploymentId: string): void {
|
|
145
|
+
this.buffers.delete(deploymentId);
|
|
146
|
+
this.listeners.delete(deploymentId);
|
|
147
|
+
const timer = this.cleanupTimers.get(deploymentId);
|
|
148
|
+
if (timer) {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
this.cleanupTimers.delete(deploymentId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// --- Partitions ---
|
|
4
|
+
|
|
5
|
+
export const CreatePartitionSchema = z.object({
|
|
6
|
+
name: z.string().min(1),
|
|
7
|
+
variables: z.record(z.string().max(10_000, "Variable value must not exceed 10,000 characters"))
|
|
8
|
+
.refine((v) => Object.keys(v).length <= 200, {
|
|
9
|
+
message: "Maximum 200 variables per entity",
|
|
10
|
+
})
|
|
11
|
+
.optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const UpdatePartitionSchema = z.object({
|
|
15
|
+
name: z.string().min(1).optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const SetVariablesSchema = z.object({
|
|
19
|
+
variables: z.record(z.string().max(10_000, "Variable value must not exceed 10,000 characters"))
|
|
20
|
+
.refine((v) => Object.keys(v).length <= 200, {
|
|
21
|
+
message: "Maximum 200 variables per entity",
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// --- Artifacts ---
|
|
26
|
+
|
|
27
|
+
export const CreateArtifactSchema = z.object({
|
|
28
|
+
name: z.string().min(1),
|
|
29
|
+
type: z.string().min(1),
|
|
30
|
+
source: z.string().optional(),
|
|
31
|
+
metadata: z.record(z.string()).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const AddAnnotationSchema = z.object({
|
|
35
|
+
field: z.string().min(1),
|
|
36
|
+
correction: z.string().min(1),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const AddArtifactVersionSchema = z.object({
|
|
40
|
+
version: z.string().min(1),
|
|
41
|
+
source: z.string(),
|
|
42
|
+
metadata: z.record(z.string()).optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// --- Security Boundaries ---
|
|
46
|
+
|
|
47
|
+
export const SetSecurityBoundariesSchema = z.object({
|
|
48
|
+
boundaries: z.array(z.object({
|
|
49
|
+
boundaryType: z.enum(["filesystem", "service", "network", "credential", "execution"]),
|
|
50
|
+
config: z.record(z.unknown()),
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// --- Environments ---
|
|
55
|
+
|
|
56
|
+
export const CreateEnvironmentSchema = z.object({
|
|
57
|
+
name: z.string().min(1),
|
|
58
|
+
variables: z.record(z.string().max(10_000, "Variable value must not exceed 10,000 characters"))
|
|
59
|
+
.refine((v) => Object.keys(v).length <= 200, {
|
|
60
|
+
message: "Maximum 200 variables per entity",
|
|
61
|
+
})
|
|
62
|
+
.optional(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const UpdateEnvironmentSchema = z.object({
|
|
66
|
+
name: z.string().min(1).optional(),
|
|
67
|
+
variables: z.record(z.string().max(10_000, "Variable value must not exceed 10,000 characters"))
|
|
68
|
+
.refine((v) => Object.keys(v).length <= 200, {
|
|
69
|
+
message: "Maximum 200 variables per entity",
|
|
70
|
+
})
|
|
71
|
+
.optional(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// --- SSRF Prevention ---
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* SSRF-safe URL validator. Blocks private/internal IP ranges and
|
|
78
|
+
* restricts to http/https protocols.
|
|
79
|
+
*/
|
|
80
|
+
function isSsrfSafeUrl(url: string): boolean {
|
|
81
|
+
let parsed: URL;
|
|
82
|
+
try {
|
|
83
|
+
parsed = new URL(url);
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Only allow http and https
|
|
89
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hostname = parsed.hostname;
|
|
94
|
+
|
|
95
|
+
// Block localhost variants
|
|
96
|
+
if (hostname === "localhost" || hostname === "[::1]") {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Block IPv6 loopback
|
|
101
|
+
if (hostname === "::1") {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check IPv4 private ranges
|
|
106
|
+
const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
107
|
+
if (ipv4Match) {
|
|
108
|
+
const [, a, b] = ipv4Match.map(Number);
|
|
109
|
+
// 127.0.0.0/8 — loopback
|
|
110
|
+
if (a === 127) return false;
|
|
111
|
+
// 10.0.0.0/8 — private
|
|
112
|
+
if (a === 10) return false;
|
|
113
|
+
// 172.16.0.0/12 — private
|
|
114
|
+
if (a === 172 && b >= 16 && b <= 31) return false;
|
|
115
|
+
// 192.168.0.0/16 — private
|
|
116
|
+
if (a === 192 && b === 168) return false;
|
|
117
|
+
// 169.254.0.0/16 — link-local (AWS metadata)
|
|
118
|
+
if (a === 169 && b === 254) return false;
|
|
119
|
+
// 0.0.0.0
|
|
120
|
+
if (a === 0) return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Settings ---
|
|
127
|
+
|
|
128
|
+
const LlmProviderEnum = z.enum(["claude", "openai", "gemini", "grok", "deepseek", "ollama", "custom"]);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* LLM base URL validator. Allows localhost/private IPs for local providers
|
|
132
|
+
* like Ollama, but validates URL format.
|
|
133
|
+
*/
|
|
134
|
+
function isValidLlmBaseUrl(url: string): boolean {
|
|
135
|
+
try {
|
|
136
|
+
const parsed = new URL(url);
|
|
137
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const LlmFallbackConfigSchema = z.object({
|
|
144
|
+
provider: LlmProviderEnum,
|
|
145
|
+
apiKeyConfigured: z.boolean().optional(),
|
|
146
|
+
baseUrl: z.string().refine(isValidLlmBaseUrl, {
|
|
147
|
+
message: "Must be a valid http or https URL",
|
|
148
|
+
}).optional(),
|
|
149
|
+
model: z.string().min(1),
|
|
150
|
+
timeoutMs: z.number().int().positive({ message: "Timeout must be a positive number" }),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const LlmProviderConfigSchema = z.object({
|
|
154
|
+
provider: LlmProviderEnum,
|
|
155
|
+
apiKeyConfigured: z.boolean().optional(),
|
|
156
|
+
apiKey: z.string().optional(),
|
|
157
|
+
baseUrl: z.string().refine(isValidLlmBaseUrl, {
|
|
158
|
+
message: "Must be a valid http or https URL",
|
|
159
|
+
}).optional(),
|
|
160
|
+
reasoningModel: z.string().min(1),
|
|
161
|
+
classificationModel: z.string().min(1),
|
|
162
|
+
timeoutMs: z.number().int().positive({ message: "Timeout must be a positive number" }),
|
|
163
|
+
rateLimitPerMin: z.number().int().positive({ message: "Rate limit must be a positive number" }),
|
|
164
|
+
fallbacks: z.array(LlmFallbackConfigSchema).optional(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export { LlmProviderConfigSchema };
|
|
168
|
+
|
|
169
|
+
const TaskModelConfigSchema = z.object({
|
|
170
|
+
logClassification: z.string().optional(),
|
|
171
|
+
diagnosticSynthesis: z.string().optional(),
|
|
172
|
+
postmortemGeneration: z.string().optional(),
|
|
173
|
+
queryAnswering: z.string().optional(),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export { TaskModelConfigSchema };
|
|
177
|
+
|
|
178
|
+
export const VerifyTaskModelSchema = z.object({
|
|
179
|
+
task: z.enum(["logClassification", "diagnosticSynthesis", "postmortemGeneration", "queryAnswering"]),
|
|
180
|
+
model: z.string().min(1),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export const UpdateSettingsSchema = z.object({
|
|
184
|
+
environmentsEnabled: z.boolean().optional(),
|
|
185
|
+
defaultTheme: z.enum(["dark", "light", "system"]).optional(),
|
|
186
|
+
agent: z.object({
|
|
187
|
+
defaultHealthCheckRetries: z.number().int().nonnegative().optional(),
|
|
188
|
+
defaultTimeoutMs: z.number().int().positive().optional(),
|
|
189
|
+
conflictPolicy: z.enum(["permissive", "strict"]).optional(),
|
|
190
|
+
defaultVerificationStrategy: z.enum(["basic", "full", "none"]).optional(),
|
|
191
|
+
llmEntityExposure: z.enum(["names", "none"]).optional(),
|
|
192
|
+
llmOverride: LlmProviderConfigSchema.partial().optional(),
|
|
193
|
+
taskModels: TaskModelConfigSchema.optional(),
|
|
194
|
+
}).optional(),
|
|
195
|
+
envoy: z.object({
|
|
196
|
+
url: z.string().refine(isSsrfSafeUrl, {
|
|
197
|
+
message: "URL must not point to private/internal IP ranges (SSRF prevention)",
|
|
198
|
+
}).optional(),
|
|
199
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
200
|
+
}).optional(),
|
|
201
|
+
coBranding: z.object({
|
|
202
|
+
operatorName: z.string(),
|
|
203
|
+
logoUrl: z.string(),
|
|
204
|
+
accentColor: z.string().optional(),
|
|
205
|
+
}).optional().nullable(),
|
|
206
|
+
mcpServers: z.array(z.object({
|
|
207
|
+
name: z.string(),
|
|
208
|
+
url: z.string().url().refine(isSsrfSafeUrl, {
|
|
209
|
+
message: "URL must not point to private/internal IP ranges (SSRF prevention)",
|
|
210
|
+
}),
|
|
211
|
+
description: z.string().optional(),
|
|
212
|
+
})).optional(),
|
|
213
|
+
llm: LlmProviderConfigSchema.partial().optional(),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// --- Artifacts (update) ---
|
|
217
|
+
|
|
218
|
+
export const UpdateArtifactSchema = z.object({
|
|
219
|
+
name: z.string().min(1).optional(),
|
|
220
|
+
type: z.string().min(1).optional(),
|
|
221
|
+
source: z.string().optional(),
|
|
222
|
+
metadata: z.record(z.string()).optional(),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// --- Deployments ---
|
|
226
|
+
|
|
227
|
+
export const CreateDeploymentSchema = z.object({
|
|
228
|
+
artifactId: z.string().min(1),
|
|
229
|
+
environmentId: z.string().min(1).optional(),
|
|
230
|
+
partitionId: z.string().optional(),
|
|
231
|
+
envoyId: z.string().optional(),
|
|
232
|
+
version: z.string().optional(),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
export const ApproveDeploymentSchema = z.object({
|
|
236
|
+
approvedBy: z.string().min(1),
|
|
237
|
+
modifications: z.string().optional(),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
export const RejectDeploymentSchema = z.object({
|
|
241
|
+
reason: z.string().min(1),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
export const ModifyDeploymentPlanSchema = z.object({
|
|
245
|
+
steps: z.array(z.object({
|
|
246
|
+
description: z.string().min(1),
|
|
247
|
+
action: z.string().min(1),
|
|
248
|
+
target: z.string().min(1),
|
|
249
|
+
reversible: z.boolean(),
|
|
250
|
+
rollbackAction: z.string().optional(),
|
|
251
|
+
})).min(1, "Plan must contain at least one step"),
|
|
252
|
+
reason: z.string().min(1),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
export const SubmitPlanSchema = z.object({
|
|
256
|
+
plan: z.object({
|
|
257
|
+
steps: z.array(z.object({
|
|
258
|
+
description: z.string().min(1),
|
|
259
|
+
action: z.string().min(1),
|
|
260
|
+
target: z.string().min(1),
|
|
261
|
+
reversible: z.boolean(),
|
|
262
|
+
rollbackAction: z.string().optional(),
|
|
263
|
+
})).min(1),
|
|
264
|
+
reasoning: z.string().min(1),
|
|
265
|
+
diffFromCurrent: z.array(z.object({ key: z.string(), from: z.string(), to: z.string() })).optional(),
|
|
266
|
+
diffFromPreviousPlan: z.string().optional(),
|
|
267
|
+
}),
|
|
268
|
+
rollbackPlan: z.object({
|
|
269
|
+
steps: z.array(z.object({
|
|
270
|
+
description: z.string().min(1),
|
|
271
|
+
action: z.string().min(1),
|
|
272
|
+
target: z.string().min(1),
|
|
273
|
+
reversible: z.boolean(),
|
|
274
|
+
rollbackAction: z.string().optional(),
|
|
275
|
+
})),
|
|
276
|
+
reasoning: z.string().min(1),
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
export const DeploymentListQuerySchema = z.object({
|
|
281
|
+
partitionId: z.string().optional(),
|
|
282
|
+
artifactId: z.string().optional(),
|
|
283
|
+
envoyId: z.string().optional(),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
export const ReplanDeploymentSchema = z.object({
|
|
287
|
+
feedback: z.string().min(1),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
export const DebriefQuerySchema = z.object({
|
|
291
|
+
limit: z.coerce.number().int().positive().optional(),
|
|
292
|
+
partitionId: z.string().optional(),
|
|
293
|
+
decisionType: z.string().optional(),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// --- Progress Events (from envoy callback) ---
|
|
297
|
+
|
|
298
|
+
export const ProgressEventSchema = z.object({
|
|
299
|
+
deploymentId: z.string(),
|
|
300
|
+
type: z.enum([
|
|
301
|
+
"step-started",
|
|
302
|
+
"step-completed",
|
|
303
|
+
"step-failed",
|
|
304
|
+
"rollback-started",
|
|
305
|
+
"rollback-completed",
|
|
306
|
+
"deployment-completed",
|
|
307
|
+
]),
|
|
308
|
+
stepIndex: z.number().int().nonnegative(),
|
|
309
|
+
stepDescription: z.string(),
|
|
310
|
+
status: z.enum(["in_progress", "completed", "failed"]),
|
|
311
|
+
output: z.string().optional(),
|
|
312
|
+
error: z.string().optional(),
|
|
313
|
+
timestamp: z.string(),
|
|
314
|
+
overallProgress: z.number().min(0).max(100),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// --- Telemetry ---
|
|
318
|
+
|
|
319
|
+
export const TelemetryQuerySchema = z.object({
|
|
320
|
+
actor: z.string().optional(),
|
|
321
|
+
action: z.string().optional(),
|
|
322
|
+
from: z.string().optional(),
|
|
323
|
+
to: z.string().optional(),
|
|
324
|
+
limit: z.coerce.number().int().positive().max(200).optional(),
|
|
325
|
+
offset: z.coerce.number().int().nonnegative().optional(),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// --- Agent ---
|
|
329
|
+
|
|
330
|
+
export const QueryRequestSchema = z.object({
|
|
331
|
+
query: z.string().min(1),
|
|
332
|
+
conversationId: z.string().optional(),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// --- Auth ---
|
|
336
|
+
|
|
337
|
+
export const LoginSchema = z.object({
|
|
338
|
+
email: z.string().email(),
|
|
339
|
+
password: z.string().min(1),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
export const RegisterSchema = z.object({
|
|
343
|
+
email: z.string().email(),
|
|
344
|
+
name: z.string().min(1),
|
|
345
|
+
password: z.string().min(8),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
export const RefreshTokenSchema = z.object({
|
|
349
|
+
refreshToken: z.string().min(1),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
export const CreateUserSchema = z.object({
|
|
353
|
+
email: z.string().email(),
|
|
354
|
+
name: z.string().min(1),
|
|
355
|
+
password: z.string().min(8),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
export const UpdateUserSchema = z.object({
|
|
359
|
+
email: z.string().email().optional(),
|
|
360
|
+
name: z.string().min(1).optional(),
|
|
361
|
+
password: z.string().min(8).optional(),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
export const AssignRolesSchema = z.object({
|
|
365
|
+
roleIds: z.array(z.string().min(1)),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
export const CreateRoleSchema = z.object({
|
|
369
|
+
name: z.string().min(1),
|
|
370
|
+
permissions: z.array(z.string().min(1)),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
export const UpdateRoleSchema = z.object({
|
|
374
|
+
name: z.string().min(1).optional(),
|
|
375
|
+
permissions: z.array(z.string().min(1)).optional(),
|
|
376
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import type { ISecurityBoundaryStore, ITelemetryStore } from "@synth-deploy/core";
|
|
4
|
+
import { SetSecurityBoundariesSchema } from "./schemas.js";
|
|
5
|
+
import { requirePermission } from "../middleware/permissions.js";
|
|
6
|
+
|
|
7
|
+
export function registerSecurityBoundaryRoutes(
|
|
8
|
+
app: FastifyInstance,
|
|
9
|
+
securityBoundaryStore: ISecurityBoundaryStore,
|
|
10
|
+
telemetry: ITelemetryStore,
|
|
11
|
+
): void {
|
|
12
|
+
// Get boundaries for envoy
|
|
13
|
+
app.get<{ Params: { envoyId: string } }>(
|
|
14
|
+
"/api/envoys/:envoyId/security-boundaries",
|
|
15
|
+
{ preHandler: [requirePermission("envoy.view")] },
|
|
16
|
+
async (request) => {
|
|
17
|
+
const boundaries = securityBoundaryStore.get(request.params.envoyId);
|
|
18
|
+
return { boundaries };
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Set/replace boundaries for envoy
|
|
23
|
+
app.put<{ Params: { envoyId: string } }>(
|
|
24
|
+
"/api/envoys/:envoyId/security-boundaries",
|
|
25
|
+
{ preHandler: [requirePermission("envoy.configure")] },
|
|
26
|
+
async (request, reply) => {
|
|
27
|
+
const parsed = SetSecurityBoundariesSchema.safeParse(request.body);
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
return reply.status(400).send({ error: parsed.error.message });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const boundaries = parsed.data.boundaries.map((b) => ({
|
|
33
|
+
id: crypto.randomUUID(),
|
|
34
|
+
envoyId: request.params.envoyId,
|
|
35
|
+
boundaryType: b.boundaryType,
|
|
36
|
+
config: b.config,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
securityBoundaryStore.set(request.params.envoyId, boundaries);
|
|
40
|
+
telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "security-boundary.updated", target: { type: "envoy", id: request.params.envoyId }, details: { boundaryCount: boundaries.length } });
|
|
41
|
+
return { boundaries };
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Remove all boundaries for envoy
|
|
46
|
+
app.delete<{ Params: { envoyId: string } }>(
|
|
47
|
+
"/api/envoys/:envoyId/security-boundaries",
|
|
48
|
+
{ preHandler: [requirePermission("envoy.configure")] },
|
|
49
|
+
async (request, reply) => {
|
|
50
|
+
securityBoundaryStore.delete(request.params.envoyId);
|
|
51
|
+
return reply.status(204).send();
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { ISettingsStore, ITelemetryStore, AppSettings, LlmProviderConfig } from "@synth-deploy/core";
|
|
3
|
+
import { UpdateSettingsSchema } from "./schemas.js";
|
|
4
|
+
import { requirePermission } from "../middleware/permissions.js";
|
|
5
|
+
import { requireEnterprise, getEdition, getLicenseInfo, getMaxEnvoys, isPartnership, ENTERPRISE_FEATURES } from "@synth-deploy/core";
|
|
6
|
+
import { invalidateLlmHealthCache } from "./health.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Strips API key from LLM settings before returning to the frontend.
|
|
10
|
+
* The apiKeyConfigured field tells the UI whether a key is set without exposing it.
|
|
11
|
+
*/
|
|
12
|
+
function sanitizeLlmSettings(settings: AppSettings): AppSettings {
|
|
13
|
+
const sanitized = structuredClone(settings);
|
|
14
|
+
|
|
15
|
+
if (sanitized.llm) {
|
|
16
|
+
// Remove any raw apiKey that leaked into the config
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
delete (sanitized.llm as any)["apiKey"];
|
|
19
|
+
// Ensure apiKeyConfigured reflects whether an env var key is set
|
|
20
|
+
sanitized.llm.apiKeyConfigured =
|
|
21
|
+
typeof process.env.SYNTH_LLM_API_KEY === "string" &&
|
|
22
|
+
process.env.SYNTH_LLM_API_KEY.length > 0;
|
|
23
|
+
|
|
24
|
+
if (sanitized.llm.fallbacks) {
|
|
25
|
+
for (const fb of sanitized.llm.fallbacks) {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
delete (fb as any)["apiKey"];
|
|
28
|
+
fb.apiKeyConfigured =
|
|
29
|
+
typeof process.env.SYNTH_LLM_API_KEY === "string" &&
|
|
30
|
+
process.env.SYNTH_LLM_API_KEY.length > 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return sanitized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Strips API key from incoming LLM provider config before persisting.
|
|
40
|
+
* API keys are stored in environment variables only — never in the settings store.
|
|
41
|
+
*/
|
|
42
|
+
function stripApiKeyFromConfig(
|
|
43
|
+
llmConfig: LlmProviderConfig & { apiKey?: string },
|
|
44
|
+
): LlmProviderConfig {
|
|
45
|
+
const { apiKey: _apiKey, ...rest } = llmConfig;
|
|
46
|
+
return {
|
|
47
|
+
...rest,
|
|
48
|
+
apiKeyConfigured:
|
|
49
|
+
typeof process.env.SYNTH_LLM_API_KEY === "string" &&
|
|
50
|
+
process.env.SYNTH_LLM_API_KEY.length > 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerSettingsRoutes(
|
|
55
|
+
app: FastifyInstance,
|
|
56
|
+
settings: ISettingsStore,
|
|
57
|
+
telemetry: ITelemetryStore,
|
|
58
|
+
): void {
|
|
59
|
+
// Get all settings
|
|
60
|
+
app.get("/api/settings", { preHandler: [requirePermission("settings.manage")] }, async () => {
|
|
61
|
+
return { settings: sanitizeLlmSettings(settings.get()) };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Update settings (partial merge)
|
|
65
|
+
app.put("/api/settings", { preHandler: [requirePermission("settings.manage")] }, async (request, reply) => {
|
|
66
|
+
const parsed = UpdateSettingsSchema.safeParse(request.body);
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
const msg = parsed.error.issues.map(i => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
69
|
+
return reply.status(400).send({ error: msg || "Invalid input" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Gate enterprise-only settings
|
|
73
|
+
const data = parsed.data as Partial<AppSettings> & { llm?: LlmProviderConfig & { apiKey?: string } };
|
|
74
|
+
if (data.coBranding) requireEnterprise("co-branding");
|
|
75
|
+
if (data.mcpServers && data.mcpServers.length > 0) requireEnterprise("mcp-servers");
|
|
76
|
+
|
|
77
|
+
// Persist API key encrypted in DB and apply to process env, then strip before storing settings
|
|
78
|
+
if (data.llm) {
|
|
79
|
+
if (data.llm.apiKey && data.llm.apiKey.length > 0) {
|
|
80
|
+
settings.setSecret("llm_api_key", data.llm.apiKey);
|
|
81
|
+
process.env.SYNTH_LLM_API_KEY = data.llm.apiKey;
|
|
82
|
+
invalidateLlmHealthCache();
|
|
83
|
+
}
|
|
84
|
+
data.llm = stripApiKeyFromConfig(data.llm);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const updated = settings.update(data as Partial<AppSettings>);
|
|
88
|
+
telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "settings.updated", target: { type: "settings", id: "app" }, details: { fields: Object.keys(parsed.data) } });
|
|
89
|
+
return { settings: sanitizeLlmSettings(updated) };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Edition info — public (no auth required), used by UI to render edition badge and gate features
|
|
93
|
+
app.get("/api/edition", async () => {
|
|
94
|
+
const edition = getEdition();
|
|
95
|
+
const license = getLicenseInfo();
|
|
96
|
+
return {
|
|
97
|
+
edition,
|
|
98
|
+
maxEnvoys: getMaxEnvoys(),
|
|
99
|
+
partnership: isPartnership(),
|
|
100
|
+
license,
|
|
101
|
+
features: ENTERPRISE_FEATURES,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Read-only command info
|
|
106
|
+
app.get("/api/settings/command-info", { preHandler: [requirePermission("settings.manage")] }, async () => {
|
|
107
|
+
return {
|
|
108
|
+
info: {
|
|
109
|
+
version: "0.1.0",
|
|
110
|
+
host: process.env.HOST ?? "0.0.0.0",
|
|
111
|
+
port: parseInt(process.env.PORT ?? "9410", 10),
|
|
112
|
+
startedAt: commandStartTime,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const commandStartTime = new Date().toISOString();
|