@xdelivered/emberflow 0.2.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 (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,125 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import type { WorkflowDefinition } from '../src/engine';
4
+
5
+ export interface FlowStoreOptions {
6
+ /**
7
+ * Project mode: scenarios live in <id>.scenarios.json next to the flow
8
+ * file (reviewable, hand-editable "stories"), not embedded in the flow.
9
+ */
10
+ scenarioSidecars?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Runner-owned store of workflow files. Each flow lives at `<dir>/<id>.json`,
15
+ * pretty-printed. The directory is the source of truth when the runner is
16
+ * online; the browser adopts it on first successful health check.
17
+ */
18
+ export class FlowStore {
19
+ readonly dir: string;
20
+ private readonly sidecars: boolean;
21
+
22
+ constructor(dir: string = resolve(process.cwd(), 'workflows'), options: FlowStoreOptions = {}) {
23
+ this.dir = dir;
24
+ this.sidecars = options.scenarioSidecars ?? false;
25
+ mkdirSync(this.dir, { recursive: true });
26
+ }
27
+
28
+ private sidecarPath(id: string): string {
29
+ return join(this.dir, `${id}.scenarios.json`);
30
+ }
31
+
32
+ /** Merge <id>.scenarios.json into the flow when sidecar mode is on. */
33
+ private withSidecar(flow: WorkflowDefinition): WorkflowDefinition {
34
+ if (!this.sidecars) return flow;
35
+ const path = this.sidecarPath(flow.id);
36
+ if (!existsSync(path)) return flow;
37
+ try {
38
+ const scenarios = JSON.parse(readFileSync(path, 'utf8')) as WorkflowDefinition['scenarios'];
39
+ return Array.isArray(scenarios) && scenarios.length > 0 ? { ...flow, scenarios } : flow;
40
+ } catch (err) {
41
+ console.warn(`[flowStore] skipping unparseable ${flow.id}.scenarios.json: ${String(err)}`);
42
+ return flow;
43
+ }
44
+ }
45
+
46
+ /** All parseable *.json flows. Unparseable files are skipped with a warning. */
47
+ list(): WorkflowDefinition[] {
48
+ const flows: WorkflowDefinition[] = [];
49
+ for (const file of readdirSync(this.dir)) {
50
+ if (!file.endsWith('.json') || file.endsWith('.scenarios.json')) continue;
51
+ const path = join(this.dir, file);
52
+ try {
53
+ const flow = JSON.parse(readFileSync(path, 'utf8')) as WorkflowDefinition;
54
+ flows.push(this.withSidecar(flow));
55
+ } catch (err) {
56
+ console.warn(`[flowStore] skipping unparseable ${file}: ${String(err)}`);
57
+ }
58
+ }
59
+ return flows;
60
+ }
61
+
62
+ /** Load a single flow by id, or undefined when its file is missing/unparseable. */
63
+ load(id: string): WorkflowDefinition | undefined {
64
+ const path = join(this.dir, `${id}.json`);
65
+ if (!existsSync(path)) return undefined;
66
+ try {
67
+ const flow = JSON.parse(readFileSync(path, 'utf8')) as WorkflowDefinition;
68
+ return this.withSidecar(flow);
69
+ } catch (err) {
70
+ console.warn(`[flowStore] skipping unparseable ${id}.json: ${String(err)}`);
71
+ return undefined;
72
+ }
73
+ }
74
+
75
+ /** Persist a flow. Its id must be a non-empty string and names the file. */
76
+ save(flow: WorkflowDefinition): void {
77
+ if (!flow || typeof flow.id !== 'string' || flow.id.length === 0) {
78
+ throw new Error('Flow must have a non-empty string id');
79
+ }
80
+ const path = join(this.dir, `${flow.id}.json`);
81
+ if (this.sidecars) {
82
+ const { scenarios, ...rest } = flow;
83
+ writeFileSync(path, `${JSON.stringify(rest, null, 2)}\n`, 'utf8');
84
+ const sidecar = this.sidecarPath(flow.id);
85
+ if (scenarios && scenarios.length > 0) {
86
+ writeFileSync(sidecar, `${JSON.stringify(scenarios, null, 2)}\n`, 'utf8');
87
+ } else if (existsSync(sidecar)) {
88
+ unlinkSync(sidecar);
89
+ }
90
+ return;
91
+ }
92
+ writeFileSync(path, `${JSON.stringify(flow, null, 2)}\n`, 'utf8');
93
+ }
94
+
95
+ /** Delete a flow file (and any scenario sidecar). Returns false when it did not exist. */
96
+ remove(id: string): boolean {
97
+ const sidecar = this.sidecarPath(id);
98
+ if (existsSync(sidecar)) unlinkSync(sidecar);
99
+ const path = join(this.dir, `${id}.json`);
100
+ if (!existsSync(path)) return false;
101
+ unlinkSync(path);
102
+ return true;
103
+ }
104
+
105
+ /** Seed the given flows only when the directory holds no json files yet. */
106
+ seedIfEmpty(flows: WorkflowDefinition[]): void {
107
+ const hasJson = readdirSync(this.dir).some(
108
+ (f) => f.endsWith('.json') && !f.endsWith('.scenarios.json'),
109
+ );
110
+ if (hasJson) return;
111
+ for (const flow of flows) this.save(flow);
112
+ }
113
+
114
+ /**
115
+ * Seed only flows the store doesn't know yet (by id). Existing copies —
116
+ * possibly user-edited — are never overwritten, so new built-in example
117
+ * flows appear after an upgrade without clobbering anything.
118
+ */
119
+ seedMissing(flows: WorkflowDefinition[]): void {
120
+ const known = new Set(this.list().map((f) => f.id));
121
+ for (const flow of flows) {
122
+ if (!known.has(flow.id)) this.save(flow);
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,377 @@
1
+ import express from 'express';
2
+ import type { Server } from 'node:http';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
+ import { makeOperationHandler, type OperationRunEnv } from './httpOperations';
5
+ import { extractResponse } from './operationResult';
6
+ import { RunRegistry } from './runRegistry';
7
+ import { createDefaultRegistry } from '../src/nodes';
8
+ import type { AuthPolicy, WorkflowDefinition } from '../src/engine';
9
+ import { createDefaultVerifierRegistry } from './auth/verifiers';
10
+
11
+ const base = { version: 1, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z' };
12
+
13
+ let server: Server | undefined;
14
+
15
+ /** Boot the op's handler on an ephemeral port; returns the base URL. */
16
+ function boot(op: WorkflowDefinition): Promise<string> {
17
+ const runs = new RunRegistry(() => undefined, createDefaultRegistry());
18
+ const app = express();
19
+ app.use(express.json());
20
+ const method = op.http!.method.toLowerCase() as 'post' | 'get';
21
+ app[method](op.http!.path, makeOperationHandler({ runs, op }));
22
+ return new Promise((resolve) => {
23
+ server = app.listen(0, '127.0.0.1', () => {
24
+ const addr = server!.address();
25
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
26
+ resolve(`http://127.0.0.1:${port}`);
27
+ });
28
+ });
29
+ }
30
+
31
+ afterEach(async () => {
32
+ if (!server) return;
33
+ await new Promise<void>((resolve) => server!.close(() => resolve()));
34
+ server = undefined;
35
+ });
36
+
37
+ describe('makeOperationHandler', () => {
38
+ it('400s when the body fails the input schema', async () => {
39
+ const op = {
40
+ ...base,
41
+ id: 'o',
42
+ name: 'O',
43
+ nodes: [],
44
+ edges: [],
45
+ http: { method: 'POST', path: '/things', inputSchema: { type: 'object', required: ['name'] } },
46
+ } as unknown as WorkflowDefinition;
47
+
48
+ const url = await boot(op);
49
+ const res = await fetch(`${url}/things`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({}),
53
+ });
54
+ expect(res.status).toBe(400);
55
+ const body = await res.json();
56
+ expect(body.error).toMatch(/name/);
57
+ });
58
+
59
+ it('runs the flow and returns the Response node status/body', async () => {
60
+ // Input node echoes the run's whole input ({params, query, body, headers});
61
+ // Response node's inputMap pulls `body` from it and pins status to 201.
62
+ const op = {
63
+ ...base,
64
+ id: 'echo',
65
+ name: 'Echo',
66
+ http: { method: 'POST', path: '/echo' },
67
+ nodes: [
68
+ {
69
+ id: 'input',
70
+ type: 'Input',
71
+ label: 'Input',
72
+ position: { x: 0, y: 0 },
73
+ config: { fields: [], defaults: {} },
74
+ },
75
+ {
76
+ id: 'response',
77
+ type: 'Response',
78
+ label: 'Response',
79
+ position: { x: 200, y: 0 },
80
+ config: { status: 201 },
81
+ inputMap: {
82
+ body: { sourceNodeId: 'input', sourceField: 'body' },
83
+ },
84
+ },
85
+ ],
86
+ edges: [{ id: 'e1', source: 'input', target: 'response' }],
87
+ } as unknown as WorkflowDefinition;
88
+
89
+ const url = await boot(op);
90
+ const res = await fetch(`${url}/echo`, {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({ id: 42, name: 'widget' }),
94
+ });
95
+ expect(res.status).toBe(201);
96
+ const body = await res.json();
97
+ expect(body).toEqual({ id: 42, name: 'widget' });
98
+ });
99
+ });
100
+
101
+ describe('run input promotes body fields to the top level', () => {
102
+ it('a flow node reading from(\'input\', \'x\') (a domain field posted in the body) resolves it, while input.body.x still works too', async () => {
103
+ // Input node echoes the run's whole input; Response node's inputMap
104
+ // pulls the top-level promoted field (`x`) straight off the Input
105
+ // node's output as the response body, proving `from('input','x')`
106
+ // resolves a field that only exists in the posted request body.
107
+ const op = {
108
+ ...base,
109
+ id: 'promote',
110
+ name: 'Promote',
111
+ http: { method: 'POST', path: '/promote' },
112
+ nodes: [
113
+ {
114
+ id: 'input',
115
+ type: 'Input',
116
+ label: 'Input',
117
+ position: { x: 0, y: 0 },
118
+ config: { fields: [], defaults: {} },
119
+ },
120
+ {
121
+ id: 'response',
122
+ type: 'Response',
123
+ label: 'Response',
124
+ position: { x: 200, y: 0 },
125
+ config: { status: 200 },
126
+ inputMap: {
127
+ body: { sourceNodeId: 'input', sourceField: 'x' },
128
+ },
129
+ },
130
+ ],
131
+ edges: [{ id: 'e1', source: 'input', target: 'response' }],
132
+ } as unknown as WorkflowDefinition;
133
+
134
+ const url = await boot(op);
135
+ const res = await fetch(`${url}/promote`, {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify({ x: 'domain-value' }),
139
+ });
140
+ expect(res.status).toBe(200);
141
+ const body = await res.json();
142
+ expect(body).toBe('domain-value');
143
+ });
144
+
145
+ it('passes the merged input (top-level body fields + body/params/query/headers/user) to runs.create', async () => {
146
+ const op = {
147
+ ...base,
148
+ id: 'spy-op',
149
+ name: 'SpyOp',
150
+ nodes: [],
151
+ edges: [],
152
+ http: { method: 'POST', path: '/spy' },
153
+ } as unknown as WorkflowDefinition;
154
+
155
+ const createSpy = vi.fn().mockReturnValue({
156
+ handle: { runToEnd: () => Promise.resolve({ nodeStates: {} } as unknown as import('../src/engine').WorkflowRun) },
157
+ });
158
+ const stubRuns = { create: createSpy } as unknown as import('./runRegistry').RunRegistry;
159
+
160
+ const app = express();
161
+ app.use(express.json());
162
+ app.post(op.http!.path, makeOperationHandler({ runs: stubRuns, op }));
163
+ server = app.listen(0, '127.0.0.1');
164
+ await new Promise<void>((resolve) => server!.once('listening', resolve));
165
+ const addr = server!.address();
166
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
167
+
168
+ await fetch(`http://127.0.0.1:${port}${op.http!.path}`, {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/json' },
171
+ body: JSON.stringify({ x: 'domain-value', body: 'shadowed-by-request-body-key' }),
172
+ });
173
+
174
+ expect(createSpy).toHaveBeenCalledTimes(1);
175
+ const opts = createSpy.mock.calls[0][1] as { input: Record<string, unknown> };
176
+ // Domain field promoted to the top...
177
+ expect(opts.input.x).toBe('domain-value');
178
+ // ...but the request's own `body` key always wins over a same-named
179
+ // field inside the posted body (params/query/body/headers/user last).
180
+ expect(opts.input.body).toEqual({ x: 'domain-value', body: 'shadowed-by-request-body-key' });
181
+ });
182
+ });
183
+
184
+ describe('auth policy enforcement', () => {
185
+ const policy: AuthPolicy = { scheme: 'bearer', secretRef: 'T' };
186
+ const env: OperationRunEnv = { secrets: { T: 'good' }, vars: {}, environment: 'default', safeMode: false };
187
+
188
+ // Input node echoes the run's whole input ({params, query, body, headers,
189
+ // user}); Response node pulls `user` from it so we can assert the resolved
190
+ // auth `user` was carried into the run input.
191
+ const op = {
192
+ ...base,
193
+ id: 'secure-echo',
194
+ name: 'SecureEcho',
195
+ http: { method: 'POST', path: '/secure-echo' },
196
+ nodes: [
197
+ {
198
+ id: 'input',
199
+ type: 'Input',
200
+ label: 'Input',
201
+ position: { x: 0, y: 0 },
202
+ config: { fields: [], defaults: {} },
203
+ },
204
+ {
205
+ id: 'response',
206
+ type: 'Response',
207
+ label: 'Response',
208
+ position: { x: 200, y: 0 },
209
+ config: { status: 200 },
210
+ inputMap: {
211
+ body: { sourceNodeId: 'input', sourceField: 'user' },
212
+ },
213
+ },
214
+ ],
215
+ edges: [{ id: 'e1', source: 'input', target: 'response' }],
216
+ } as unknown as WorkflowDefinition;
217
+
218
+ function bootSecure(): Promise<string> {
219
+ const runs = new RunRegistry(() => undefined, createDefaultRegistry());
220
+ const verifiers = createDefaultVerifierRegistry();
221
+ const app = express();
222
+ app.use(express.json());
223
+ app.post(op.http!.path, makeOperationHandler({ runs, op, env, policy, verifiers }));
224
+ return new Promise((resolve) => {
225
+ server = app.listen(0, '127.0.0.1', () => {
226
+ const addr = server!.address();
227
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
228
+ resolve(`http://127.0.0.1:${port}`);
229
+ });
230
+ });
231
+ }
232
+
233
+ it('401s a request with no Authorization header (auth precedes the run/body)', async () => {
234
+ const url = await bootSecure();
235
+ const res = await fetch(`${url}${op.http!.path}`, {
236
+ method: 'POST',
237
+ headers: { 'Content-Type': 'application/json' },
238
+ body: JSON.stringify({}),
239
+ });
240
+ expect(res.status).toBe(401);
241
+ const body = await res.json();
242
+ expect(body).toEqual({ error: 'unauthorized' });
243
+ });
244
+
245
+ it('runs and carries `user` into the run input when the bearer token matches', async () => {
246
+ const url = await bootSecure();
247
+ const res = await fetch(`${url}${op.http!.path}`, {
248
+ method: 'POST',
249
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer good' },
250
+ body: JSON.stringify({}),
251
+ });
252
+ expect(res.status).toBe(200);
253
+ const body = await res.json();
254
+ expect(body).toEqual({ scheme: 'bearer' });
255
+ });
256
+ });
257
+
258
+ describe('500 handling', () => {
259
+ it('returns a generic error body (not the raw message) when a node throws a non-HttpError, and logs the real error server-side', async () => {
260
+ const secretMessage = 'connection failed: postgres://user:hunter2@db.internal/prod';
261
+ const op = {
262
+ ...base,
263
+ id: 'boom',
264
+ name: 'Boom',
265
+ nodes: [],
266
+ edges: [],
267
+ http: { method: 'POST', path: '/boom' },
268
+ } as unknown as WorkflowDefinition;
269
+
270
+ // Stub RunRegistry: runToEnd rejects with an Error whose message would
271
+ // leak a secret if echoed straight to the client.
272
+ const stubRuns = {
273
+ create: () => ({
274
+ runId: 'r1',
275
+ handle: { runToEnd: () => Promise.reject(new Error(secretMessage)) },
276
+ }),
277
+ } as unknown as import('./runRegistry').RunRegistry;
278
+
279
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
280
+
281
+ const app = express();
282
+ app.use(express.json());
283
+ app.post(op.http!.path, makeOperationHandler({ runs: stubRuns, op }));
284
+ server = app.listen(0, '127.0.0.1');
285
+ await new Promise<void>((resolve) => server!.once('listening', resolve));
286
+ const addr = server!.address();
287
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
288
+
289
+ const res = await fetch(`http://127.0.0.1:${port}${op.http!.path}`, {
290
+ method: 'POST',
291
+ headers: { 'Content-Type': 'application/json' },
292
+ body: JSON.stringify({}),
293
+ });
294
+ expect(res.status).toBe(500);
295
+ const body = await res.json();
296
+ expect(body).toEqual({ error: 'internal error' });
297
+ expect(JSON.stringify(body)).not.toMatch(/hunter2/);
298
+
299
+ expect(consoleErrorSpy).toHaveBeenCalled();
300
+ const loggedArgs = consoleErrorSpy.mock.calls.flat();
301
+ expect(loggedArgs.some((a) => a instanceof Error && a.message === secretMessage)).toBe(true);
302
+
303
+ consoleErrorSpy.mockRestore();
304
+ });
305
+ });
306
+
307
+ describe('parity: in-studio Run vs live HTTP endpoint', () => {
308
+ // Same op as the "echo" case above: Input node passes the run's whole
309
+ // input ({params, query, body, headers}) through; Response node pulls
310
+ // `body` from it and pins status to 201.
311
+ const op = {
312
+ ...base,
313
+ id: 'echo-parity',
314
+ name: 'EchoParity',
315
+ http: { method: 'POST', path: '/echo-parity' },
316
+ nodes: [
317
+ {
318
+ id: 'input',
319
+ type: 'Input',
320
+ label: 'Input',
321
+ position: { x: 0, y: 0 },
322
+ config: { fields: [], defaults: {} },
323
+ },
324
+ {
325
+ id: 'response',
326
+ type: 'Response',
327
+ label: 'Response',
328
+ position: { x: 200, y: 0 },
329
+ config: { status: 201 },
330
+ inputMap: {
331
+ body: { sourceNodeId: 'input', sourceField: 'body' },
332
+ },
333
+ },
334
+ ],
335
+ edges: [{ id: 'e1', source: 'input', target: 'response' }],
336
+ } as unknown as WorkflowDefinition;
337
+
338
+ const requestBody = { id: 7, name: 'gizmo' };
339
+
340
+ it('the live HTTP endpoint and a direct RunRegistry.create (as the studio Run/scenario would issue) yield identical {status, body}', async () => {
341
+ // (a) Live HTTP path: makeOperationHandler over an in-process express listen+fetch.
342
+ const httpRuns = new RunRegistry(() => undefined, createDefaultRegistry());
343
+ const app = express();
344
+ app.use(express.json());
345
+ app.post(op.http!.path, makeOperationHandler({ runs: httpRuns, op }));
346
+ const url = await new Promise<string>((resolve) => {
347
+ server = app.listen(0, '127.0.0.1', () => {
348
+ const addr = server!.address();
349
+ const port = typeof addr === 'object' && addr ? addr.port : 0;
350
+ resolve(`http://127.0.0.1:${port}`);
351
+ });
352
+ });
353
+ const httpRes = await fetch(`${url}${op.http!.path}`, {
354
+ method: 'POST',
355
+ headers: { 'Content-Type': 'application/json' },
356
+ body: JSON.stringify(requestBody),
357
+ });
358
+ const httpResult = { status: httpRes.status, body: await httpRes.json() };
359
+
360
+ // (b) In-studio Run path: RunRegistry.create with a scenario-shaped input
361
+ // ({ body }) — the same request shape an operation scenario carries —
362
+ // run to completion, then extractResponse maps the Response node output.
363
+ const studioRuns = new RunRegistry(() => undefined, createDefaultRegistry());
364
+ const { handle } = studioRuns.create(op, {
365
+ secrets: {},
366
+ vars: {},
367
+ environment: 'default',
368
+ safeMode: false,
369
+ input: { body: requestBody },
370
+ });
371
+ const run = await handle.runToEnd();
372
+ const studioResult = extractResponse(run, op);
373
+
374
+ expect(studioResult).toEqual(httpResult);
375
+ expect(httpResult).toEqual({ status: 201, body: requestBody });
376
+ });
377
+ });
@@ -0,0 +1,96 @@
1
+ import type { RequestHandler } from 'express';
2
+ import { enforceAuth, isHttpError, validateAgainstSchema, type AuthPolicy, type WorkflowDefinition } from '../src/engine';
3
+ import { extractResponse } from './operationResult';
4
+ import type { RunRegistry } from './runRegistry';
5
+ import type { VerifierRegistry } from './auth/verifiers';
6
+
7
+ /**
8
+ * Build the Express handler for a single routed operation: request in
9
+ * ({params, query, body, headers}), schema-validate the body, run the
10
+ * operation through the SAME engine path the studio "Run" uses
11
+ * (`RunRegistry`), map the finished run to an HTTP response.
12
+ *
13
+ * The run input is the request body's own fields spread at the top level
14
+ * (so `from('input', 'ppv')` resolves a domain field posted in the body),
15
+ * PLUS the full request shape (`params`/`query`/`body`/`headers`/`user`)
16
+ * layered on top — the latter always wins on key collisions, and ops that
17
+ * read `input.body.x` keep working unchanged.
18
+ */
19
+ /** The environment values a routed operation runs with — resolved once at
20
+ * boot from the project's default environment (see server/index.ts). Falls
21
+ * back to empty secrets/vars when omitted (e.g. in unit tests). */
22
+ export interface OperationRunEnv {
23
+ secrets: Record<string, string>;
24
+ vars: Record<string, string>;
25
+ environment: string;
26
+ safeMode: boolean;
27
+ }
28
+
29
+ export function makeOperationHandler(deps: {
30
+ runs: RunRegistry;
31
+ op: WorkflowDefinition;
32
+ env?: OperationRunEnv;
33
+ policy?: AuthPolicy | null;
34
+ verifiers?: VerifierRegistry;
35
+ }): RequestHandler {
36
+ const { runs, op, policy, verifiers } = deps;
37
+ const env: OperationRunEnv = deps.env ?? { secrets: {}, vars: {}, environment: 'default', safeMode: false };
38
+ return async (req, res) => {
39
+ const request = {
40
+ params: req.params,
41
+ query: req.query,
42
+ body: req.body,
43
+ headers: req.headers,
44
+ };
45
+
46
+ try {
47
+ // Auth precedes validation: an unauthenticated/misconfigured request
48
+ // never reaches schema validation or the run, and 401/500 from the
49
+ // verifier is mapped by the HttpError branch below — never a generic
50
+ // 500 that could imply the request otherwise would have run.
51
+ let user: unknown;
52
+ if (policy) {
53
+ if (!verifiers) throw new Error('makeOperationHandler: policy set but no verifiers registry provided');
54
+ ({ user } = enforceAuth({ policy, request, secrets: env.secrets, verifiers }));
55
+ }
56
+
57
+ const schema = op.http?.inputSchema;
58
+ if (schema) {
59
+ const error = validateAgainstSchema(schema, request.body);
60
+ if (error !== null) {
61
+ res.status(400).json({ error });
62
+ return;
63
+ }
64
+ }
65
+
66
+ // Promote the request body's fields to the top of the run input so
67
+ // flows that read domain fields directly (`from('input', 'ppv')`)
68
+ // resolve them, while keeping `body` (and params/query/headers/user)
69
+ // available for ops that read `input.body.x`. Spread the body FIRST
70
+ // so params/query/body/headers/user always win over a colliding key.
71
+ const bodyFields =
72
+ request.body && typeof request.body === 'object' && !Array.isArray(request.body)
73
+ ? (request.body as Record<string, unknown>)
74
+ : {};
75
+ const { handle } = runs.create(op, {
76
+ ...env,
77
+ input: { ...bodyFields, ...request, user },
78
+ });
79
+ const run = await handle.runToEnd();
80
+ const { status, body } = extractResponse(run, op);
81
+ res.status(status).json(body ?? null);
82
+ } catch (err) {
83
+ if (isHttpError(err)) {
84
+ res.status(err.status).json(err.body ?? null);
85
+ return;
86
+ }
87
+ // A node's thrown Error can carry secrets in its message (e.g. a DB
88
+ // connection string or a URL with an API key) — never echo it to the
89
+ // (currently unauthenticated) client. Log the real error server-side
90
+ // and return a generic body instead. HttpError above stays the
91
+ // sanctioned way for a node to control the client-visible status+body.
92
+ console.error('[operation] run failed:', err);
93
+ res.status(500).json({ error: 'internal error' });
94
+ }
95
+ };
96
+ }