@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.
Files changed (317) hide show
  1. package/dist/agent/debrief-retention.d.ts +12 -0
  2. package/dist/agent/debrief-retention.d.ts.map +1 -0
  3. package/dist/agent/debrief-retention.js +27 -0
  4. package/dist/agent/debrief-retention.js.map +1 -0
  5. package/dist/agent/envoy-client.d.ts +216 -0
  6. package/dist/agent/envoy-client.d.ts.map +1 -0
  7. package/dist/agent/envoy-client.js +266 -0
  8. package/dist/agent/envoy-client.js.map +1 -0
  9. package/dist/agent/envoy-registry.d.ts +102 -0
  10. package/dist/agent/envoy-registry.d.ts.map +1 -0
  11. package/dist/agent/envoy-registry.js +319 -0
  12. package/dist/agent/envoy-registry.js.map +1 -0
  13. package/dist/agent/health-checker.d.ts +39 -0
  14. package/dist/agent/health-checker.d.ts.map +1 -0
  15. package/dist/agent/health-checker.js +49 -0
  16. package/dist/agent/health-checker.js.map +1 -0
  17. package/dist/agent/mcp-client-manager.d.ts +36 -0
  18. package/dist/agent/mcp-client-manager.d.ts.map +1 -0
  19. package/dist/agent/mcp-client-manager.js +106 -0
  20. package/dist/agent/mcp-client-manager.js.map +1 -0
  21. package/dist/agent/stale-deployment-detector.d.ts +15 -0
  22. package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
  23. package/dist/agent/stale-deployment-detector.js +50 -0
  24. package/dist/agent/stale-deployment-detector.js.map +1 -0
  25. package/dist/agent/step-runner.d.ts +31 -0
  26. package/dist/agent/step-runner.d.ts.map +1 -0
  27. package/dist/agent/step-runner.js +80 -0
  28. package/dist/agent/step-runner.js.map +1 -0
  29. package/dist/agent/synth-agent.d.ts +168 -0
  30. package/dist/agent/synth-agent.d.ts.map +1 -0
  31. package/dist/agent/synth-agent.js +1195 -0
  32. package/dist/agent/synth-agent.js.map +1 -0
  33. package/dist/api/agent.d.ts +36 -0
  34. package/dist/api/agent.d.ts.map +1 -0
  35. package/dist/api/agent.js +867 -0
  36. package/dist/api/agent.js.map +1 -0
  37. package/dist/api/api-keys.d.ts +4 -0
  38. package/dist/api/api-keys.d.ts.map +1 -0
  39. package/dist/api/api-keys.js +118 -0
  40. package/dist/api/api-keys.js.map +1 -0
  41. package/dist/api/artifacts.d.ts +5 -0
  42. package/dist/api/artifacts.d.ts.map +1 -0
  43. package/dist/api/artifacts.js +142 -0
  44. package/dist/api/artifacts.js.map +1 -0
  45. package/dist/api/auth.d.ts +4 -0
  46. package/dist/api/auth.d.ts.map +1 -0
  47. package/dist/api/auth.js +280 -0
  48. package/dist/api/auth.js.map +1 -0
  49. package/dist/api/deployments.d.ts +11 -0
  50. package/dist/api/deployments.d.ts.map +1 -0
  51. package/dist/api/deployments.js +1098 -0
  52. package/dist/api/deployments.js.map +1 -0
  53. package/dist/api/environments.d.ts +5 -0
  54. package/dist/api/environments.d.ts.map +1 -0
  55. package/dist/api/environments.js +69 -0
  56. package/dist/api/environments.js.map +1 -0
  57. package/dist/api/envoy-reports.d.ts +17 -0
  58. package/dist/api/envoy-reports.d.ts.map +1 -0
  59. package/dist/api/envoy-reports.js +138 -0
  60. package/dist/api/envoy-reports.js.map +1 -0
  61. package/dist/api/envoys.d.ts +5 -0
  62. package/dist/api/envoys.d.ts.map +1 -0
  63. package/dist/api/envoys.js +192 -0
  64. package/dist/api/envoys.js.map +1 -0
  65. package/dist/api/fleet.d.ts +11 -0
  66. package/dist/api/fleet.d.ts.map +1 -0
  67. package/dist/api/fleet.js +394 -0
  68. package/dist/api/fleet.js.map +1 -0
  69. package/dist/api/graph.d.ts +8 -0
  70. package/dist/api/graph.d.ts.map +1 -0
  71. package/dist/api/graph.js +355 -0
  72. package/dist/api/graph.js.map +1 -0
  73. package/dist/api/health.d.ts +20 -0
  74. package/dist/api/health.d.ts.map +1 -0
  75. package/dist/api/health.js +248 -0
  76. package/dist/api/health.js.map +1 -0
  77. package/dist/api/idp-schemas.d.ts +41 -0
  78. package/dist/api/idp-schemas.d.ts.map +1 -0
  79. package/dist/api/idp-schemas.js +17 -0
  80. package/dist/api/idp-schemas.js.map +1 -0
  81. package/dist/api/idp.d.ts +6 -0
  82. package/dist/api/idp.d.ts.map +1 -0
  83. package/dist/api/idp.js +620 -0
  84. package/dist/api/idp.js.map +1 -0
  85. package/dist/api/intake.d.ts +10 -0
  86. package/dist/api/intake.d.ts.map +1 -0
  87. package/dist/api/intake.js +418 -0
  88. package/dist/api/intake.js.map +1 -0
  89. package/dist/api/partitions.d.ts +5 -0
  90. package/dist/api/partitions.d.ts.map +1 -0
  91. package/dist/api/partitions.js +113 -0
  92. package/dist/api/partitions.js.map +1 -0
  93. package/dist/api/progress-event-store.d.ts +62 -0
  94. package/dist/api/progress-event-store.d.ts.map +1 -0
  95. package/dist/api/progress-event-store.js +118 -0
  96. package/dist/api/progress-event-store.js.map +1 -0
  97. package/dist/api/schemas.d.ts +1000 -0
  98. package/dist/api/schemas.d.ts.map +1 -0
  99. package/dist/api/schemas.js +328 -0
  100. package/dist/api/schemas.js.map +1 -0
  101. package/dist/api/security-boundaries.d.ts +4 -0
  102. package/dist/api/security-boundaries.d.ts.map +1 -0
  103. package/dist/api/security-boundaries.js +32 -0
  104. package/dist/api/security-boundaries.js.map +1 -0
  105. package/dist/api/settings.d.ts +4 -0
  106. package/dist/api/settings.d.ts.map +1 -0
  107. package/dist/api/settings.js +99 -0
  108. package/dist/api/settings.js.map +1 -0
  109. package/dist/api/system.d.ts +75 -0
  110. package/dist/api/system.d.ts.map +1 -0
  111. package/dist/api/system.js +558 -0
  112. package/dist/api/system.js.map +1 -0
  113. package/dist/api/telemetry.d.ts +4 -0
  114. package/dist/api/telemetry.d.ts.map +1 -0
  115. package/dist/api/telemetry.js +24 -0
  116. package/dist/api/telemetry.js.map +1 -0
  117. package/dist/api/users.d.ts +4 -0
  118. package/dist/api/users.d.ts.map +1 -0
  119. package/dist/api/users.js +173 -0
  120. package/dist/api/users.js.map +1 -0
  121. package/dist/archive-unpacker.d.ts +24 -0
  122. package/dist/archive-unpacker.d.ts.map +1 -0
  123. package/dist/archive-unpacker.js +239 -0
  124. package/dist/archive-unpacker.js.map +1 -0
  125. package/dist/artifact-analyzer.d.ts +59 -0
  126. package/dist/artifact-analyzer.d.ts.map +1 -0
  127. package/dist/artifact-analyzer.js +334 -0
  128. package/dist/artifact-analyzer.js.map +1 -0
  129. package/dist/auth/idp/index.d.ts +9 -0
  130. package/dist/auth/idp/index.d.ts.map +1 -0
  131. package/dist/auth/idp/index.js +5 -0
  132. package/dist/auth/idp/index.js.map +1 -0
  133. package/dist/auth/idp/ldap.d.ts +56 -0
  134. package/dist/auth/idp/ldap.d.ts.map +1 -0
  135. package/dist/auth/idp/ldap.js +276 -0
  136. package/dist/auth/idp/ldap.js.map +1 -0
  137. package/dist/auth/idp/oidc.d.ts +27 -0
  138. package/dist/auth/idp/oidc.d.ts.map +1 -0
  139. package/dist/auth/idp/oidc.js +97 -0
  140. package/dist/auth/idp/oidc.js.map +1 -0
  141. package/dist/auth/idp/role-mapping.d.ts +9 -0
  142. package/dist/auth/idp/role-mapping.d.ts.map +1 -0
  143. package/dist/auth/idp/role-mapping.js +16 -0
  144. package/dist/auth/idp/role-mapping.js.map +1 -0
  145. package/dist/auth/idp/saml.d.ts +40 -0
  146. package/dist/auth/idp/saml.d.ts.map +1 -0
  147. package/dist/auth/idp/saml.js +117 -0
  148. package/dist/auth/idp/saml.js.map +1 -0
  149. package/dist/auth/idp/types.d.ts +23 -0
  150. package/dist/auth/idp/types.d.ts.map +1 -0
  151. package/dist/auth/idp/types.js +2 -0
  152. package/dist/auth/idp/types.js.map +1 -0
  153. package/dist/fleet/fleet-executor.d.ts +35 -0
  154. package/dist/fleet/fleet-executor.d.ts.map +1 -0
  155. package/dist/fleet/fleet-executor.js +228 -0
  156. package/dist/fleet/fleet-executor.js.map +1 -0
  157. package/dist/fleet/fleet-store.d.ts +13 -0
  158. package/dist/fleet/fleet-store.d.ts.map +1 -0
  159. package/dist/fleet/fleet-store.js +13 -0
  160. package/dist/fleet/fleet-store.js.map +1 -0
  161. package/dist/fleet/index.d.ts +5 -0
  162. package/dist/fleet/index.d.ts.map +1 -0
  163. package/dist/fleet/index.js +4 -0
  164. package/dist/fleet/index.js.map +1 -0
  165. package/dist/fleet/representative-selector.d.ts +15 -0
  166. package/dist/fleet/representative-selector.d.ts.map +1 -0
  167. package/dist/fleet/representative-selector.js +71 -0
  168. package/dist/fleet/representative-selector.js.map +1 -0
  169. package/dist/graph/graph-executor.d.ts +36 -0
  170. package/dist/graph/graph-executor.d.ts.map +1 -0
  171. package/dist/graph/graph-executor.js +348 -0
  172. package/dist/graph/graph-executor.js.map +1 -0
  173. package/dist/graph/graph-inference.d.ts +22 -0
  174. package/dist/graph/graph-inference.d.ts.map +1 -0
  175. package/dist/graph/graph-inference.js +149 -0
  176. package/dist/graph/graph-inference.js.map +1 -0
  177. package/dist/graph/graph-store.d.ts +12 -0
  178. package/dist/graph/graph-store.d.ts.map +1 -0
  179. package/dist/graph/graph-store.js +61 -0
  180. package/dist/graph/graph-store.js.map +1 -0
  181. package/dist/graph/index.d.ts +5 -0
  182. package/dist/graph/index.d.ts.map +1 -0
  183. package/dist/graph/index.js +4 -0
  184. package/dist/graph/index.js.map +1 -0
  185. package/dist/index.d.ts +2 -0
  186. package/dist/index.d.ts.map +1 -0
  187. package/dist/index.js +837 -0
  188. package/dist/index.js.map +1 -0
  189. package/dist/intake/index.d.ts +6 -0
  190. package/dist/intake/index.d.ts.map +1 -0
  191. package/dist/intake/index.js +5 -0
  192. package/dist/intake/index.js.map +1 -0
  193. package/dist/intake/intake-processor.d.ts +17 -0
  194. package/dist/intake/intake-processor.d.ts.map +1 -0
  195. package/dist/intake/intake-processor.js +99 -0
  196. package/dist/intake/intake-processor.js.map +1 -0
  197. package/dist/intake/intake-store.d.ts +7 -0
  198. package/dist/intake/intake-store.d.ts.map +1 -0
  199. package/dist/intake/intake-store.js +7 -0
  200. package/dist/intake/intake-store.js.map +1 -0
  201. package/dist/intake/registry-poller.d.ts +41 -0
  202. package/dist/intake/registry-poller.d.ts.map +1 -0
  203. package/dist/intake/registry-poller.js +202 -0
  204. package/dist/intake/registry-poller.js.map +1 -0
  205. package/dist/intake/webhook-handlers.d.ts +37 -0
  206. package/dist/intake/webhook-handlers.d.ts.map +1 -0
  207. package/dist/intake/webhook-handlers.js +268 -0
  208. package/dist/intake/webhook-handlers.js.map +1 -0
  209. package/dist/logger.d.ts +5 -0
  210. package/dist/logger.d.ts.map +1 -0
  211. package/dist/logger.js +15 -0
  212. package/dist/logger.js.map +1 -0
  213. package/dist/mcp/resources.d.ts +9 -0
  214. package/dist/mcp/resources.d.ts.map +1 -0
  215. package/dist/mcp/resources.js +72 -0
  216. package/dist/mcp/resources.js.map +1 -0
  217. package/dist/mcp/server.d.ts +15 -0
  218. package/dist/mcp/server.d.ts.map +1 -0
  219. package/dist/mcp/server.js +20 -0
  220. package/dist/mcp/server.js.map +1 -0
  221. package/dist/mcp/tools.d.ts +9 -0
  222. package/dist/mcp/tools.d.ts.map +1 -0
  223. package/dist/mcp/tools.js +88 -0
  224. package/dist/mcp/tools.js.map +1 -0
  225. package/dist/middleware/auth.d.ts +29 -0
  226. package/dist/middleware/auth.d.ts.map +1 -0
  227. package/dist/middleware/auth.js +76 -0
  228. package/dist/middleware/auth.js.map +1 -0
  229. package/dist/middleware/permissions.d.ts +13 -0
  230. package/dist/middleware/permissions.d.ts.map +1 -0
  231. package/dist/middleware/permissions.js +32 -0
  232. package/dist/middleware/permissions.js.map +1 -0
  233. package/dist/pattern-store.d.ts +104 -0
  234. package/dist/pattern-store.d.ts.map +1 -0
  235. package/dist/pattern-store.js +299 -0
  236. package/dist/pattern-store.js.map +1 -0
  237. package/package.json +54 -0
  238. package/src/agent/debrief-retention.ts +44 -0
  239. package/src/agent/envoy-client.ts +474 -0
  240. package/src/agent/envoy-registry.ts +384 -0
  241. package/src/agent/health-checker.ts +70 -0
  242. package/src/agent/mcp-client-manager.ts +131 -0
  243. package/src/agent/stale-deployment-detector.ts +79 -0
  244. package/src/agent/step-runner.ts +124 -0
  245. package/src/agent/synth-agent.ts +1567 -0
  246. package/src/api/agent.ts +1075 -0
  247. package/src/api/api-keys.ts +129 -0
  248. package/src/api/artifacts.ts +194 -0
  249. package/src/api/auth.ts +320 -0
  250. package/src/api/deployments.ts +1347 -0
  251. package/src/api/environments.ts +97 -0
  252. package/src/api/envoy-reports.ts +159 -0
  253. package/src/api/envoys.ts +237 -0
  254. package/src/api/fleet.ts +510 -0
  255. package/src/api/graph.ts +516 -0
  256. package/src/api/health.ts +311 -0
  257. package/src/api/idp-schemas.ts +19 -0
  258. package/src/api/idp.ts +735 -0
  259. package/src/api/intake.ts +537 -0
  260. package/src/api/partitions.ts +147 -0
  261. package/src/api/progress-event-store.ts +153 -0
  262. package/src/api/schemas.ts +376 -0
  263. package/src/api/security-boundaries.ts +54 -0
  264. package/src/api/settings.ts +118 -0
  265. package/src/api/system.ts +704 -0
  266. package/src/api/telemetry.ts +32 -0
  267. package/src/api/users.ts +210 -0
  268. package/src/archive-unpacker.ts +271 -0
  269. package/src/artifact-analyzer.ts +438 -0
  270. package/src/auth/idp/index.ts +8 -0
  271. package/src/auth/idp/ldap.ts +340 -0
  272. package/src/auth/idp/oidc.ts +117 -0
  273. package/src/auth/idp/role-mapping.ts +22 -0
  274. package/src/auth/idp/saml.ts +148 -0
  275. package/src/auth/idp/types.ts +22 -0
  276. package/src/fleet/fleet-executor.ts +309 -0
  277. package/src/fleet/fleet-store.ts +13 -0
  278. package/src/fleet/index.ts +4 -0
  279. package/src/fleet/representative-selector.ts +83 -0
  280. package/src/graph/graph-executor.ts +446 -0
  281. package/src/graph/graph-inference.ts +184 -0
  282. package/src/graph/graph-store.ts +75 -0
  283. package/src/graph/index.ts +4 -0
  284. package/src/index.ts +916 -0
  285. package/src/intake/index.ts +5 -0
  286. package/src/intake/intake-processor.ts +111 -0
  287. package/src/intake/intake-store.ts +7 -0
  288. package/src/intake/registry-poller.ts +230 -0
  289. package/src/intake/webhook-handlers.ts +328 -0
  290. package/src/logger.ts +19 -0
  291. package/src/mcp/resources.ts +98 -0
  292. package/src/mcp/server.ts +34 -0
  293. package/src/mcp/tools.ts +117 -0
  294. package/src/middleware/auth.ts +103 -0
  295. package/src/middleware/permissions.ts +35 -0
  296. package/src/pattern-store.ts +409 -0
  297. package/tests/agent-mode.test.ts +536 -0
  298. package/tests/api-handlers.test.ts +1245 -0
  299. package/tests/archive-unpacker.test.ts +179 -0
  300. package/tests/artifact-analyzer.test.ts +240 -0
  301. package/tests/auth-middleware.test.ts +189 -0
  302. package/tests/decision-diary.test.ts +957 -0
  303. package/tests/diary-reader.test.ts +782 -0
  304. package/tests/envoy-client.test.ts +342 -0
  305. package/tests/envoy-reports.test.ts +156 -0
  306. package/tests/mcp-tools.test.ts +213 -0
  307. package/tests/orchestration.test.ts +536 -0
  308. package/tests/partition-deletion.test.ts +143 -0
  309. package/tests/partition-isolation.test.ts +830 -0
  310. package/tests/pattern-store.test.ts +371 -0
  311. package/tests/rbac-enforcement.test.ts +409 -0
  312. package/tests/ssrf-validation.test.ts +56 -0
  313. package/tests/stale-deployment.test.ts +85 -0
  314. package/tests/step-runner.test.ts +308 -0
  315. package/tests/ui-journey.test.ts +330 -0
  316. package/tsconfig.json +11 -0
  317. 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();