@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,154 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { loadInfrastructure } from './infrastructure';
6
+
7
+ function project(): string {
8
+ const root = mkdtempSync(join(tmpdir(), 'infra-'));
9
+ mkdirSync(join(root, 'emberflow'), { recursive: true });
10
+ return root;
11
+ }
12
+
13
+ function writeManifest(root: string, contents: string): void {
14
+ writeFileSync(join(root, 'emberflow', 'infrastructure.json'), contents);
15
+ }
16
+
17
+ describe('loadInfrastructure', () => {
18
+ let root: string;
19
+
20
+ beforeEach(() => {
21
+ root = project();
22
+ });
23
+ afterEach(() => {
24
+ rmSync(root, { recursive: true, force: true });
25
+ });
26
+
27
+ it('returns null (no warning) when the file is absent', () => {
28
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
29
+ expect(loadInfrastructure(root)).toBeNull();
30
+ expect(warn).not.toHaveBeenCalled();
31
+ warn.mockRestore();
32
+ });
33
+
34
+ it('loads a well-formed manifest, preserving fields', () => {
35
+ writeManifest(
36
+ root,
37
+ JSON.stringify({
38
+ version: 1,
39
+ scannedAt: '2026-07-12T00:00:00Z',
40
+ greenfield: false,
41
+ summary: 'Express app with Postgres (Prisma) and Stripe.',
42
+ items: [
43
+ {
44
+ id: 'postgres-main',
45
+ kind: 'database',
46
+ name: 'Postgres (Prisma)',
47
+ evidence: [{ file: 'prisma/schema.prisma', note: 'datasource db provider=postgresql' }],
48
+ suggestedSecretRefs: ['DATABASE_URL'],
49
+ suggestedVars: [],
50
+ notes: 'Schema defines User, Order, Invoice models.',
51
+ },
52
+ ],
53
+ }),
54
+ );
55
+ const manifest = loadInfrastructure(root);
56
+ expect(manifest).not.toBeNull();
57
+ expect(manifest!.summary).toContain('Postgres');
58
+ expect(manifest!.items).toHaveLength(1);
59
+ expect(manifest!.items[0]).toMatchObject({
60
+ id: 'postgres-main',
61
+ kind: 'database',
62
+ name: 'Postgres (Prisma)',
63
+ suggestedSecretRefs: ['DATABASE_URL'],
64
+ });
65
+ expect(manifest!.items[0].evidence[0]).toEqual({ file: 'prisma/schema.prisma', note: 'datasource db provider=postgresql' });
66
+ });
67
+
68
+ it('preserves unknown top-level and item fields (forward compatibility)', () => {
69
+ writeManifest(
70
+ root,
71
+ JSON.stringify({
72
+ version: 2,
73
+ greenfield: false,
74
+ futureTopField: 'keep me',
75
+ items: [{ id: 'x', kind: 'cache', name: 'Redis', futureItemField: 42 }],
76
+ }),
77
+ );
78
+ const manifest = loadInfrastructure(root);
79
+ expect(manifest!.futureTopField).toBe('keep me');
80
+ expect(manifest!.items[0].futureItemField).toBe(42);
81
+ expect(manifest!.version).toBe(2);
82
+ });
83
+
84
+ it('clamps an unknown item kind to "other" and defaults missing required fields', () => {
85
+ writeManifest(
86
+ root,
87
+ JSON.stringify({
88
+ greenfield: false,
89
+ items: [{ kind: 'blockchain' }],
90
+ }),
91
+ );
92
+ const manifest = loadInfrastructure(root);
93
+ expect(manifest!.items[0].kind).toBe('other');
94
+ expect(manifest!.items[0].name).toBe('Unnamed');
95
+ expect(manifest!.items[0].id).toBe('item-0');
96
+ expect(manifest!.items[0].evidence).toEqual([]);
97
+ expect(manifest!.items[0].suggestedSecretRefs).toEqual([]);
98
+ // version defaults to 1 when absent.
99
+ expect(manifest!.version).toBe(1);
100
+ });
101
+
102
+ it('supports a greenfield manifest with empty items', () => {
103
+ writeManifest(root, JSON.stringify({ version: 1, greenfield: true, summary: 'Empty project.', items: [] }));
104
+ const manifest = loadInfrastructure(root);
105
+ expect(manifest!.greenfield).toBe(true);
106
+ expect(manifest!.items).toEqual([]);
107
+ });
108
+
109
+ it('returns null + one warning on invalid JSON', () => {
110
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
111
+ writeManifest(root, '{ not json');
112
+ expect(loadInfrastructure(root)).toBeNull();
113
+ expect(warn).toHaveBeenCalledTimes(1);
114
+ // Repeat load: deduped, no second warning.
115
+ expect(loadInfrastructure(root)).toBeNull();
116
+ expect(warn).toHaveBeenCalledTimes(1);
117
+ warn.mockRestore();
118
+ });
119
+
120
+ it('the invalid-JSON warning never echoes the parse error message (no file-content leakage)', () => {
121
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
122
+ // A "secret" fragment near the break point — JSON.parse's own error
123
+ // message would normally quote a few characters around here.
124
+ writeManifest(root, '{ "token": "sk_live_super_secret_value丹');
125
+ expect(loadInfrastructure(root)).toBeNull();
126
+ expect(warn).toHaveBeenCalledTimes(1);
127
+ const message = warn.mock.calls[0][0] as string;
128
+ expect(message).toContain('is not valid JSON');
129
+ expect(message).not.toContain('sk_live_super_secret_value');
130
+ warn.mockRestore();
131
+ });
132
+
133
+ it('returns null + warning when items is not an array', () => {
134
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
135
+ writeManifest(root, JSON.stringify({ version: 1, greenfield: false, items: 'nope' }));
136
+ expect(loadInfrastructure(root)).toBeNull();
137
+ expect(warn).toHaveBeenCalledTimes(1);
138
+ warn.mockRestore();
139
+ });
140
+
141
+ it('drops non-object items but keeps the valid siblings', () => {
142
+ writeManifest(
143
+ root,
144
+ JSON.stringify({
145
+ version: 1,
146
+ greenfield: false,
147
+ items: [null, 'bad', { id: 'ok', kind: 'llm', name: 'OpenAI' }],
148
+ }),
149
+ );
150
+ const manifest = loadInfrastructure(root);
151
+ expect(manifest!.items).toHaveLength(1);
152
+ expect(manifest!.items[0].name).toBe('OpenAI');
153
+ });
154
+ });
@@ -0,0 +1,161 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * The committed, secrets-free infrastructure manifest the scout writes to
6
+ * `emberflow/infrastructure.json` and agents/studio read back. It describes
7
+ * what a (usually brownfield) project already uses — databases, external APIs,
8
+ * providers — with evidence pointing at the files that prove it. Env-var
9
+ * NAMES only ever appear here; never values.
10
+ */
11
+
12
+ /** The closed set of infrastructure categories the scout classifies items into. */
13
+ export const INFRASTRUCTURE_KINDS = [
14
+ 'database',
15
+ 'http-api',
16
+ 'queue',
17
+ 'cache',
18
+ 'email',
19
+ 'llm',
20
+ 'auth',
21
+ 'framework',
22
+ 'storage',
23
+ 'other',
24
+ ] as const;
25
+
26
+ export type InfrastructureKind = (typeof INFRASTRUCTURE_KINDS)[number];
27
+
28
+ /** A single file:note pointer proving an item exists. `note` is optional. */
29
+ export interface InfrastructureEvidence {
30
+ file: string;
31
+ note?: string;
32
+ /** Unknown fields are preserved on read (forward-compatibility). */
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ /** One discovered piece of infrastructure. */
37
+ export interface InfrastructureItem {
38
+ id: string;
39
+ kind: InfrastructureKind;
40
+ name: string;
41
+ evidence: InfrastructureEvidence[];
42
+ /** Env-var NAMES this item likely needs (e.g. "DATABASE_URL") — never values. */
43
+ suggestedSecretRefs: string[];
44
+ /** Non-secret config var NAMES this item likely needs. */
45
+ suggestedVars: string[];
46
+ notes?: string;
47
+ /** Unknown fields are preserved on read (forward-compatibility). */
48
+ [key: string]: unknown;
49
+ }
50
+
51
+ export interface InfrastructureManifest {
52
+ version: number;
53
+ scannedAt?: string;
54
+ greenfield: boolean;
55
+ summary?: string;
56
+ items: InfrastructureItem[];
57
+ /** Unknown top-level fields are preserved on read (forward-compatibility). */
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ export const INFRASTRUCTURE_FILENAME = join('emberflow', 'infrastructure.json');
62
+
63
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
64
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
65
+ }
66
+
67
+ function isInfrastructureKind(value: unknown): value is InfrastructureKind {
68
+ return typeof value === 'string' && (INFRASTRUCTURE_KINDS as readonly string[]).includes(value);
69
+ }
70
+
71
+ /**
72
+ * Leniently coerces one raw item into an `InfrastructureItem`. Missing/invalid
73
+ * required fields are defaulted rather than rejected (name/id fall back,
74
+ * unknown `kind` normalizes to "other") and unknown fields pass through, so a
75
+ * slightly-off item from the agent still renders instead of nuking the whole
76
+ * manifest. Returns null only when the item isn't an object at all.
77
+ */
78
+ function coerceItem(raw: unknown, index: number): InfrastructureItem | null {
79
+ if (!isPlainObject(raw)) return null;
80
+ const evidenceRaw = Array.isArray(raw.evidence) ? raw.evidence : [];
81
+ const evidence: InfrastructureEvidence[] = evidenceRaw
82
+ .filter(isPlainObject)
83
+ .map((e) => ({ ...e, file: typeof e.file === 'string' ? e.file : '', note: typeof e.note === 'string' ? e.note : undefined }));
84
+ const secretRefs = Array.isArray(raw.suggestedSecretRefs)
85
+ ? raw.suggestedSecretRefs.filter((s): s is string => typeof s === 'string')
86
+ : [];
87
+ const vars = Array.isArray(raw.suggestedVars)
88
+ ? raw.suggestedVars.filter((s): s is string => typeof s === 'string')
89
+ : [];
90
+ return {
91
+ ...raw,
92
+ id: typeof raw.id === 'string' && raw.id.length > 0 ? raw.id : `item-${index}`,
93
+ kind: isInfrastructureKind(raw.kind) ? raw.kind : 'other',
94
+ name: typeof raw.name === 'string' && raw.name.length > 0 ? raw.name : 'Unnamed',
95
+ evidence,
96
+ suggestedSecretRefs: secretRefs,
97
+ suggestedVars: vars,
98
+ ...(typeof raw.notes === 'string' ? { notes: raw.notes } : {}),
99
+ };
100
+ }
101
+
102
+ /** One-shot per-path warn dedupe so repeated GETs don't spam the log. */
103
+ const warnedOnce = new Set<string>();
104
+
105
+ /**
106
+ * Reads `emberflow/infrastructure.json` from `projectRoot`.
107
+ *
108
+ * - Absent file → `null` (no warning: not-scouted-yet is a normal state).
109
+ * - Malformed (unreadable, invalid JSON, or not the expected shape) → `null`
110
+ * plus a single warning per path per process. The CALLER decides keep-last-good.
111
+ * - Valid → a leniently-normalized `InfrastructureManifest`: required fields
112
+ * defaulted, item `kind`s clamped to the enum, unknown fields preserved.
113
+ */
114
+ export function loadInfrastructure(projectRoot: string): InfrastructureManifest | null {
115
+ const path = join(projectRoot, INFRASTRUCTURE_FILENAME);
116
+ if (!existsSync(path)) return null;
117
+
118
+ const warn = (message: string): null => {
119
+ if (!warnedOnce.has(path)) {
120
+ warnedOnce.add(path);
121
+ console.warn(`[emberflow] ${INFRASTRUCTURE_FILENAME} ${message} — ignoring it (keeping last good state). Re-run the infrastructure scout to regenerate it.`);
122
+ }
123
+ return null;
124
+ };
125
+
126
+ let parsed: unknown;
127
+ try {
128
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
129
+ } catch {
130
+ // Deliberately NOT interpolating the parse error's message: on malformed
131
+ // JSON, JSON.parse's error text often echoes a fragment of the file
132
+ // content around the failure point (e.g. "Unexpected token o in JSON at
133
+ // position 5 ... <10 chars of the file>"), which could leak a fragment of
134
+ // a secret value if one was pasted into this file. Keep the warning generic.
135
+ return warn('is not valid JSON');
136
+ }
137
+ if (!isPlainObject(parsed)) {
138
+ return warn('is not a JSON object at the top level');
139
+ }
140
+ if (parsed.items !== undefined && !Array.isArray(parsed.items)) {
141
+ return warn('has an "items" field that is not an array');
142
+ }
143
+
144
+ // A successful read clears any prior warning for this path, so a fixed file
145
+ // warns again cleanly if it later regresses.
146
+ warnedOnce.delete(path);
147
+
148
+ const rawItems = Array.isArray(parsed.items) ? parsed.items : [];
149
+ const items = rawItems
150
+ .map((item, i) => coerceItem(item, i))
151
+ .filter((item): item is InfrastructureItem => item !== null);
152
+
153
+ return {
154
+ ...parsed,
155
+ version: typeof parsed.version === 'number' ? parsed.version : 1,
156
+ ...(typeof parsed.scannedAt === 'string' ? { scannedAt: parsed.scannedAt } : {}),
157
+ greenfield: parsed.greenfield === true,
158
+ ...(typeof parsed.summary === 'string' ? { summary: parsed.summary } : {}),
159
+ items,
160
+ };
161
+ }
@@ -0,0 +1,105 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { spawn, type ChildProcess } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ // Boots a real runner subprocess over a temp project (the setupStatus.test.ts
8
+ // pattern) and drives GET /infrastructure + the /setup-status infrastructure
9
+ // field. The route re-reads per request, so one runner covers absent → present
10
+ // → malformed (keep-last-good) by rewriting the file between fetches.
11
+
12
+ const PORT = 8154;
13
+ const base = `http://127.0.0.1:${PORT}`;
14
+
15
+ async function waitHealthy(url: string, tries = 40): Promise<void> {
16
+ for (let i = 0; i < tries; i++) {
17
+ try {
18
+ if ((await fetch(url)).ok) return;
19
+ } catch {
20
+ /* not up yet */
21
+ }
22
+ await new Promise((r) => setTimeout(r, 150));
23
+ }
24
+ throw new Error('runner did not become healthy');
25
+ }
26
+
27
+ let proc: ChildProcess;
28
+ let projectDir: string;
29
+
30
+ const manifestPath = (): string => join(projectDir, 'emberflow', 'infrastructure.json');
31
+
32
+ const GOOD_MANIFEST = {
33
+ version: 1,
34
+ scannedAt: '2026-07-12T00:00:00Z',
35
+ greenfield: false,
36
+ summary: 'Express app with Postgres (Prisma).',
37
+ items: [
38
+ {
39
+ id: 'postgres-main',
40
+ kind: 'database',
41
+ name: 'Postgres (Prisma)',
42
+ evidence: [{ file: 'prisma/schema.prisma', note: 'datasource db provider=postgresql' }],
43
+ suggestedSecretRefs: ['DATABASE_URL'],
44
+ suggestedVars: [],
45
+ },
46
+ ],
47
+ };
48
+
49
+ beforeAll(async () => {
50
+ projectDir = mkdtempSync(join(tmpdir(), 'infraroute-'));
51
+ mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
52
+ writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
53
+
54
+ proc = spawn('npx', ['tsx', 'server/index.ts'], {
55
+ env: {
56
+ ...process.env,
57
+ EMBERFLOW_RUNNER_PORT: String(PORT),
58
+ EMBERFLOW_PROJECT: projectDir,
59
+ },
60
+ stdio: 'ignore',
61
+ });
62
+
63
+ await waitHealthy(`${base}/healthz`);
64
+ }, 20_000);
65
+
66
+ afterAll(() => {
67
+ proc?.kill();
68
+ if (projectDir) rmSync(projectDir, { recursive: true, force: true });
69
+ });
70
+
71
+ describe('GET /infrastructure', () => {
72
+ it('reports not present when no manifest exists', async () => {
73
+ const res = await fetch(`${base}/infrastructure`);
74
+ expect(res.status).toBe(200);
75
+ expect(await res.json()).toEqual({ present: false });
76
+
77
+ // /setup-status agrees.
78
+ const status = await (await fetch(`${base}/setup-status`)).json();
79
+ expect(status.infrastructure).toEqual({ present: false });
80
+ });
81
+
82
+ it('serves the manifest once written (re-read per request)', async () => {
83
+ writeFileSync(manifestPath(), JSON.stringify(GOOD_MANIFEST));
84
+ const body = await (await fetch(`${base}/infrastructure`)).json();
85
+ expect(body.present).toBe(true);
86
+ expect(body.manifest.items).toHaveLength(1);
87
+ expect(body.manifest.items[0].name).toBe('Postgres (Prisma)');
88
+
89
+ // /setup-status carries the shallow summary from the same loader.
90
+ const status = await (await fetch(`${base}/setup-status`)).json();
91
+ expect(status.infrastructure).toEqual({
92
+ present: true,
93
+ scannedAt: '2026-07-12T00:00:00Z',
94
+ itemCount: 1,
95
+ });
96
+ });
97
+
98
+ it('keeps the last good manifest when the file becomes malformed', async () => {
99
+ writeFileSync(manifestPath(), '{ broken json');
100
+ const body = await (await fetch(`${base}/infrastructure`)).json();
101
+ // Still present, still the last good manifest.
102
+ expect(body.present).toBe(true);
103
+ expect(body.manifest.items[0].name).toBe('Postgres (Prisma)');
104
+ });
105
+ });
@@ -0,0 +1,153 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { performLogin } from './login';
6
+ import type { EnvironmentDefinition } from './environments';
7
+
8
+ let dir: string;
9
+
10
+ beforeEach(() => {
11
+ dir = mkdtempSync(join(tmpdir(), 'emberflow-login-test-'));
12
+ });
13
+
14
+ afterEach(() => {
15
+ rmSync(dir, { recursive: true, force: true });
16
+ });
17
+
18
+ function write(name: string, contents: unknown): void {
19
+ writeFileSync(join(dir, name), typeof contents === 'string' ? contents : JSON.stringify(contents));
20
+ }
21
+
22
+ function baseEnvDef(): EnvironmentDefinition {
23
+ return {
24
+ vars: {},
25
+ secrets: {},
26
+ auth: {
27
+ attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
28
+ login: {
29
+ request: { method: 'POST', url: 'https://example.test/login' },
30
+ capture: { from: 'set-cookie', cookieName: 'session' },
31
+ },
32
+ },
33
+ };
34
+ }
35
+
36
+ describe('performLogin', () => {
37
+ it('fires the login request, captures the credential, and stores it', async () => {
38
+ write('emberflow.environments.json', {
39
+ defaultEnvironment: 'local',
40
+ environments: { local: { vars: {}, secrets: {} } },
41
+ });
42
+ const envDef = baseEnvDef();
43
+
44
+ const fetchMock = async () => ({
45
+ status: 200,
46
+ headers: new Headers({ 'set-cookie': 'session=zzz; Path=/' }),
47
+ json: async () => ({}),
48
+ });
49
+
50
+ const result = await performLogin(dir, 'local', envDef, { fetch: fetchMock as unknown as typeof fetch });
51
+
52
+ expect(result).toEqual({ secretRef: 'sessionCookie' });
53
+
54
+ // The captured credential VALUE lands in the secrets file (keyed by env),
55
+ // never in environments.json — which now records only the key name.
56
+ const secrets = JSON.parse(readFileSync(join(dir, 'emberflow.secrets.json'), 'utf8'));
57
+ expect(secrets.local.sessionCookie).toBe('zzz');
58
+ const stored = JSON.parse(readFileSync(join(dir, 'emberflow.environments.json'), 'utf8'));
59
+ expect(stored.environments.local.secrets).toContain('sessionCookie');
60
+ expect(JSON.stringify(stored)).not.toContain('zzz');
61
+ });
62
+
63
+ it('sends the parsed bodyRef secret as the request body (default content-type)', async () => {
64
+ write('emberflow.environments.json', {
65
+ defaultEnvironment: 'local',
66
+ environments: { local: { vars: {}, secrets: {} } },
67
+ });
68
+ const envDef = baseEnvDef();
69
+ envDef.secrets.loginBody = '{"email":"a@b.test","password":"pw"}';
70
+ envDef.auth!.login!.request.bodyRef = 'loginBody';
71
+
72
+ let capturedInit: RequestInit | undefined;
73
+ const fetchMock = async (_url: string, init?: RequestInit) => {
74
+ capturedInit = init;
75
+ return {
76
+ status: 200,
77
+ headers: new Headers({ 'set-cookie': 'session=zzz; Path=/' }),
78
+ json: async () => ({}),
79
+ };
80
+ };
81
+
82
+ await performLogin(dir, 'local', envDef, { fetch: fetchMock as unknown as typeof fetch });
83
+
84
+ expect(capturedInit?.body).toBe(JSON.stringify({ email: 'a@b.test', password: 'pw' }));
85
+ expect((capturedInit?.headers as Record<string, string>)['content-type']).toBe('application/json');
86
+ });
87
+
88
+ it('lets an explicitly cased request content-type header override the default', async () => {
89
+ write('emberflow.environments.json', {
90
+ defaultEnvironment: 'local',
91
+ environments: { local: { vars: {}, secrets: {} } },
92
+ });
93
+ const envDef = baseEnvDef();
94
+ envDef.secrets.loginBody = '{"email":"a@b.test"}';
95
+ envDef.auth!.login!.request.bodyRef = 'loginBody';
96
+ envDef.auth!.login!.request.headers = { 'Content-Type': 'application/vnd.api+json' };
97
+
98
+ let capturedInit: RequestInit | undefined;
99
+ const fetchMock = async (_url: string, init?: RequestInit) => {
100
+ capturedInit = init;
101
+ return {
102
+ status: 200,
103
+ headers: new Headers({ 'set-cookie': 'session=zzz; Path=/' }),
104
+ json: async () => ({}),
105
+ };
106
+ };
107
+
108
+ await performLogin(dir, 'local', envDef, { fetch: fetchMock as unknown as typeof fetch });
109
+
110
+ const headers = capturedInit?.headers as Record<string, string>;
111
+ // Caller's content-type wins, and there is no duplicate differently-cased key.
112
+ expect(headers['content-type']).toBe('application/vnd.api+json');
113
+ expect(headers['Content-Type']).toBeUndefined();
114
+ });
115
+
116
+ it('throws when the bodyRef secret is not valid JSON', async () => {
117
+ write('emberflow.environments.json', {
118
+ defaultEnvironment: 'local',
119
+ environments: { local: { vars: {}, secrets: {} } },
120
+ });
121
+ const envDef = baseEnvDef();
122
+ envDef.secrets.loginBody = 'not-json{';
123
+ envDef.auth!.login!.request.bodyRef = 'loginBody';
124
+
125
+ const fetchMock = async () => ({
126
+ status: 200,
127
+ headers: new Headers({ 'set-cookie': 'session=zzz' }),
128
+ json: async () => ({}),
129
+ });
130
+
131
+ await expect(
132
+ performLogin(dir, 'local', envDef, { fetch: fetchMock as unknown as typeof fetch }),
133
+ ).rejects.toThrow('login body secret "loginBody" for environment "local" is not valid JSON');
134
+ });
135
+
136
+ it('throws when the login response is non-2xx', async () => {
137
+ write('emberflow.environments.json', {
138
+ defaultEnvironment: 'local',
139
+ environments: { local: { vars: {}, secrets: {} } },
140
+ });
141
+ const envDef = baseEnvDef();
142
+
143
+ const fetchMock = async () => ({
144
+ status: 401,
145
+ headers: new Headers(),
146
+ json: async () => ({}),
147
+ });
148
+
149
+ await expect(
150
+ performLogin(dir, 'local', envDef, { fetch: fetchMock as unknown as typeof fetch }),
151
+ ).rejects.toThrow('login failed: 401');
152
+ });
153
+ });
@@ -0,0 +1,67 @@
1
+ import { captureCredential } from './authAttach';
2
+ import { setEnvironmentSecret, type EnvironmentDefinition } from './environments';
3
+
4
+ /** Minimal response shape performLogin needs from the injected fetch. */
5
+ interface LoginResponse {
6
+ status: number;
7
+ headers: Headers;
8
+ json: () => Promise<any>;
9
+ }
10
+
11
+ export interface PerformLoginDeps {
12
+ fetch: (url: string, init?: RequestInit) => Promise<LoginResponse>;
13
+ }
14
+
15
+ /**
16
+ * Fires the configured login request for `envName`, captures the resulting
17
+ * credential per `envDef.auth.login.capture`, and persists it into the
18
+ * environment's secrets under `envDef.auth.attach.secretRef`.
19
+ *
20
+ * Never returns or logs the captured value — only the secretRef name.
21
+ */
22
+ export async function performLogin(
23
+ cwd: string,
24
+ envName: string,
25
+ envDef: EnvironmentDefinition,
26
+ deps: PerformLoginDeps = { fetch: globalThis.fetch },
27
+ ): Promise<{ secretRef: string }> {
28
+ const login = envDef.auth?.login;
29
+ if (!login) {
30
+ throw new Error(`environment "${envName}" has no auth.login configured`);
31
+ }
32
+
33
+ const { method, url, headers, bodyRef } = login.request;
34
+ const init: RequestInit = { method, ...(headers ? { headers } : {}) };
35
+ if (bodyRef !== undefined) {
36
+ const rawBody = envDef.secrets[bodyRef];
37
+ let parsed: unknown;
38
+ try {
39
+ parsed = JSON.parse(rawBody);
40
+ } catch {
41
+ // Never include the secret content in the message — only names.
42
+ throw new Error(`login body secret "${bodyRef}" for environment "${envName}" is not valid JSON`);
43
+ }
44
+ init.body = JSON.stringify(parsed);
45
+ // Lowercase all caller header keys so an explicit content-type (any casing)
46
+ // wins over — and never duplicates — the auto-added default.
47
+ const callerHeaders = Object.fromEntries(
48
+ Object.entries(headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]),
49
+ );
50
+ init.headers = { 'content-type': 'application/json', ...callerHeaders };
51
+ }
52
+
53
+ const res = await deps.fetch(url, init);
54
+ if (res.status < 200 || res.status >= 300) {
55
+ throw new Error(`login failed: ${res.status}`);
56
+ }
57
+
58
+ const value = await captureCredential(res, login.capture);
59
+ if (value === null) {
60
+ throw new Error('login succeeded but no credential captured');
61
+ }
62
+
63
+ const secretRef = envDef.auth!.attach.secretRef;
64
+ await setEnvironmentSecret(cwd, envName, secretRef, value);
65
+
66
+ return { secretRef };
67
+ }