@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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
let proc: ChildProcess;
|
|
8
|
+
const PORT = 8123;
|
|
9
|
+
const base = `http://127.0.0.1:${PORT}`;
|
|
10
|
+
|
|
11
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
12
|
+
for (let i = 0; i < tries; i++) {
|
|
13
|
+
try { if ((await fetch(url)).ok) return; } catch { /* not up yet */ }
|
|
14
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
15
|
+
}
|
|
16
|
+
throw new Error('runner did not become healthy');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Spawn `server/index.ts` with the given env and wait for /healthz. */
|
|
20
|
+
function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
|
|
21
|
+
return spawn('npx', ['tsx', 'server/index.ts'], {
|
|
22
|
+
env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
|
|
23
|
+
stdio: 'ignore',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Like `bootRunner`, but pipes stderr so the caller can assert on warn logs. */
|
|
28
|
+
function bootRunnerCapturingStderr(port: number, env: Record<string, string | undefined>): {
|
|
29
|
+
proc: ChildProcess;
|
|
30
|
+
stderr: () => string;
|
|
31
|
+
} {
|
|
32
|
+
const child = spawn('npx', ['tsx', 'server/index.ts'], {
|
|
33
|
+
env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
|
|
34
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
35
|
+
});
|
|
36
|
+
let stderr = '';
|
|
37
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
38
|
+
stderr += chunk.toString();
|
|
39
|
+
});
|
|
40
|
+
return { proc: child, stderr: () => stderr };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let projectDir: string;
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
// A minimal project dir seeded with an apis/ tree — the runner reads its
|
|
47
|
+
// flows from apis/, not the legacy flat flows/.
|
|
48
|
+
projectDir = mkdtempSync(join(tmpdir(), 'apimount-'));
|
|
49
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(projectDir, 'emberflow', 'apis', 'default', 'hello.json'),
|
|
52
|
+
JSON.stringify({ id: 'hello', name: 'Hello', nodes: [], edges: [] }),
|
|
53
|
+
);
|
|
54
|
+
// A routed operation: GET /ping -> a Response node returning { status: 200, body: { pong: true } }.
|
|
55
|
+
writeFileSync(
|
|
56
|
+
join(projectDir, 'emberflow', 'apis', 'default', 'ping.json'),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
id: 'ping',
|
|
59
|
+
name: 'Ping',
|
|
60
|
+
version: 1,
|
|
61
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
62
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
63
|
+
http: { method: 'GET', path: '/ping' },
|
|
64
|
+
nodes: [
|
|
65
|
+
{
|
|
66
|
+
id: 'response',
|
|
67
|
+
type: 'Response',
|
|
68
|
+
label: 'Response',
|
|
69
|
+
position: { x: 0, y: 0 },
|
|
70
|
+
config: { status: 200, body: { pong: true } },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
edges: [],
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
77
|
+
// A configured environment keeps the runner booting in REAL serving mode —
|
|
78
|
+
// with none, it now defaults to mock and these real-path assertions 501.
|
|
79
|
+
writeFileSync(
|
|
80
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
81
|
+
JSON.stringify({ defaultEnvironment: 'dev', environments: { dev: {} } }),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
|
|
85
|
+
await waitHealthy(`${base}/healthz`);
|
|
86
|
+
}, 20_000);
|
|
87
|
+
|
|
88
|
+
afterAll(() => {
|
|
89
|
+
proc?.kill();
|
|
90
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('API mount', () => {
|
|
94
|
+
it('serves healthz at root and under /api', async () => {
|
|
95
|
+
expect((await fetch(`${base}/healthz`)).ok).toBe(true);
|
|
96
|
+
expect((await fetch(`${base}/api/healthz`)).ok).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it('healthz reports mock:false when EMBERFLOW_MOCK is unset', async () => {
|
|
99
|
+
const body = await (await fetch(`${base}/healthz`)).json();
|
|
100
|
+
expect(body.mock).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
it('serves /nodes under /api', async () => {
|
|
103
|
+
const res = await fetch(`${base}/api/nodes`);
|
|
104
|
+
expect(res.ok).toBe(true);
|
|
105
|
+
const body = await res.json();
|
|
106
|
+
expect(Array.isArray(body.nodes)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('rejects POST /agent with an unsupported intent.action', async () => {
|
|
110
|
+
const res = await fetch(`${base}/api/agent`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
intent: { action: 'delete-everything', flowId: 'hello', instruction: 'x' },
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
expect(res.status).toBe(400);
|
|
118
|
+
const body = await res.json();
|
|
119
|
+
expect(body.error).toMatch(/action/i);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('GET /api/workflows lists operations from the apis/ tree', async () => {
|
|
123
|
+
const res = await fetch(`${base}/api/workflows`);
|
|
124
|
+
expect(res.status).toBe(200);
|
|
125
|
+
const body = await res.json();
|
|
126
|
+
expect(body.flows.map((f: { id: string }) => f.id)).toContain('hello');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('GET /api/workflows also returns an operations array with path and http', async () => {
|
|
130
|
+
const res = await fetch(`${base}/api/workflows`);
|
|
131
|
+
expect(res.status).toBe(200);
|
|
132
|
+
const body = await res.json();
|
|
133
|
+
expect(Array.isArray(body.operations)).toBe(true);
|
|
134
|
+
const hello = body.operations.find((o: { id: string }) => o.id === 'hello');
|
|
135
|
+
expect(hello.path).toBe('default/hello');
|
|
136
|
+
expect(hello.http).toBeUndefined();
|
|
137
|
+
const ping = body.operations.find((o: { id: string }) => o.id === 'ping');
|
|
138
|
+
expect(ping.path).toBe('default/ping');
|
|
139
|
+
expect(ping.http).toEqual({ method: 'GET', path: '/ping' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('serves a routed operation as a live HTTP endpoint at the root path', async () => {
|
|
143
|
+
const res = await fetch(`${base}/ping`);
|
|
144
|
+
expect(res.status).toBe(200);
|
|
145
|
+
const body = await res.json();
|
|
146
|
+
expect(body).toEqual({ pong: true });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('POST /api/operations saves a new flow at the given path, visible in GET /workflows', async () => {
|
|
150
|
+
const flow = {
|
|
151
|
+
id: 'svc/thing',
|
|
152
|
+
name: 'Thing',
|
|
153
|
+
version: 1,
|
|
154
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
155
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
156
|
+
nodes: [],
|
|
157
|
+
edges: [],
|
|
158
|
+
};
|
|
159
|
+
const res = await fetch(`${base}/api/operations`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify({ flow, path: 'svc/thing' }),
|
|
163
|
+
});
|
|
164
|
+
expect(res.status).toBe(201);
|
|
165
|
+
expect(existsSync(join(projectDir, 'emberflow', 'apis', 'svc', 'thing.json'))).toBe(true);
|
|
166
|
+
|
|
167
|
+
const listed = await (await fetch(`${base}/api/workflows`)).json();
|
|
168
|
+
const op = listed.operations.find((o: { id: string }) => o.id === 'svc/thing');
|
|
169
|
+
expect(op).toBeDefined();
|
|
170
|
+
expect(op.path).toBe('svc/thing');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('POST /api/operations 409s (does not overwrite) when an operation already exists at the path', async () => {
|
|
174
|
+
const original = {
|
|
175
|
+
id: 'svc/thing',
|
|
176
|
+
name: 'Thing',
|
|
177
|
+
version: 1,
|
|
178
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
179
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
180
|
+
nodes: [],
|
|
181
|
+
edges: [],
|
|
182
|
+
};
|
|
183
|
+
// 'svc/thing' was already created by the previous test — assert the
|
|
184
|
+
// collision case directly here too so this test is self-sufficient.
|
|
185
|
+
await fetch(`${base}/api/operations`, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: { 'Content-Type': 'application/json' },
|
|
188
|
+
body: JSON.stringify({ flow: original, path: 'svc/thing' }),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const clobber = {
|
|
192
|
+
id: 'svc/thing',
|
|
193
|
+
name: 'Clobbered',
|
|
194
|
+
version: 1,
|
|
195
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
196
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
197
|
+
nodes: [],
|
|
198
|
+
edges: [],
|
|
199
|
+
};
|
|
200
|
+
const res = await fetch(`${base}/api/operations`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({ flow: clobber, path: 'svc/thing' }),
|
|
204
|
+
});
|
|
205
|
+
expect(res.status).toBe(409);
|
|
206
|
+
const body = await res.json();
|
|
207
|
+
expect(body.error).toMatch(/already exists/i);
|
|
208
|
+
|
|
209
|
+
// The original file must be untouched — no silent overwrite.
|
|
210
|
+
const onDisk = JSON.parse(
|
|
211
|
+
readFileSync(join(projectDir, 'emberflow', 'apis', 'svc', 'thing.json'), 'utf8'),
|
|
212
|
+
);
|
|
213
|
+
expect(onDisk.name).toBe('Thing');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('POST /api/operations 400s when flow.id does not match path', async () => {
|
|
217
|
+
const flow = {
|
|
218
|
+
id: 'svc/mismatched',
|
|
219
|
+
name: 'Mismatched',
|
|
220
|
+
version: 1,
|
|
221
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
222
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
223
|
+
nodes: [],
|
|
224
|
+
edges: [],
|
|
225
|
+
};
|
|
226
|
+
const res = await fetch(`${base}/api/operations`, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify({ flow, path: 'svc/other-path' }),
|
|
230
|
+
});
|
|
231
|
+
expect(res.status).toBe(400);
|
|
232
|
+
const body = await res.json();
|
|
233
|
+
expect(body.error).toMatch(/flow\.id must equal path/i);
|
|
234
|
+
expect(existsSync(join(projectDir, 'emberflow', 'apis', 'svc', 'other-path.json'))).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('POST /api/operations rejects a path-traversal path', async () => {
|
|
238
|
+
const flow = {
|
|
239
|
+
id: 'escape',
|
|
240
|
+
name: 'Escape',
|
|
241
|
+
version: 1,
|
|
242
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
243
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
244
|
+
nodes: [],
|
|
245
|
+
edges: [],
|
|
246
|
+
};
|
|
247
|
+
const res = await fetch(`${base}/api/operations`, {
|
|
248
|
+
method: 'POST',
|
|
249
|
+
headers: { 'Content-Type': 'application/json' },
|
|
250
|
+
body: JSON.stringify({ flow, path: '../../etc/escape' }),
|
|
251
|
+
});
|
|
252
|
+
expect(res.status).toBe(400);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('migration on boot', () => {
|
|
257
|
+
let legacyProjectDir: string;
|
|
258
|
+
let legacyProc: ChildProcess;
|
|
259
|
+
const LEGACY_PORT = 8124;
|
|
260
|
+
const legacyBase = `http://127.0.0.1:${LEGACY_PORT}`;
|
|
261
|
+
|
|
262
|
+
afterEach(() => {
|
|
263
|
+
legacyProc?.kill();
|
|
264
|
+
if (legacyProjectDir) rmSync(legacyProjectDir, { recursive: true, force: true });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('migrates a legacy flows/ layout into apis/default/ and serves it', async () => {
|
|
268
|
+
legacyProjectDir = mkdtempSync(join(tmpdir(), 'apimount-legacy-'));
|
|
269
|
+
mkdirSync(join(legacyProjectDir, 'emberflow', 'flows'), { recursive: true });
|
|
270
|
+
writeFileSync(
|
|
271
|
+
join(legacyProjectDir, 'emberflow', 'flows', 'legacy.json'),
|
|
272
|
+
JSON.stringify({ id: 'legacy', name: 'Legacy', nodes: [], edges: [] }),
|
|
273
|
+
);
|
|
274
|
+
writeFileSync(join(legacyProjectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
275
|
+
|
|
276
|
+
legacyProc = bootRunner(LEGACY_PORT, { EMBERFLOW_PROJECT: legacyProjectDir });
|
|
277
|
+
await waitHealthy(`${legacyBase}/healthz`);
|
|
278
|
+
|
|
279
|
+
const res = await fetch(`${legacyBase}/api/workflows`);
|
|
280
|
+
expect(res.status).toBe(200);
|
|
281
|
+
const body = await res.json();
|
|
282
|
+
expect(body.flows.map((f: { id: string }) => f.id)).toContain('legacy');
|
|
283
|
+
|
|
284
|
+
expect(existsSync(join(legacyProjectDir, 'emberflow', 'apis', 'default', 'legacy.json'))).toBe(true);
|
|
285
|
+
expect(existsSync(join(legacyProjectDir, 'emberflow', 'flows'))).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('auth enforcement', () => {
|
|
290
|
+
let secureProjectDir: string;
|
|
291
|
+
let secureProc: ChildProcess;
|
|
292
|
+
const SECURE_PORT = 8126;
|
|
293
|
+
const secureBase = `http://127.0.0.1:${SECURE_PORT}`;
|
|
294
|
+
|
|
295
|
+
afterAll(() => {
|
|
296
|
+
secureProc?.kill();
|
|
297
|
+
if (secureProjectDir) rmSync(secureProjectDir, { recursive: true, force: true });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('401s a protected operation without a token, 200s with the right bearer token', async () => {
|
|
301
|
+
secureProjectDir = mkdtempSync(join(tmpdir(), 'apimount-secure-'));
|
|
302
|
+
mkdirSync(join(secureProjectDir, 'emberflow', 'apis', 'secure'), { recursive: true });
|
|
303
|
+
// _meta.json at the api-folder level: every op under apis/secure/ inherits this bearer policy.
|
|
304
|
+
writeFileSync(
|
|
305
|
+
join(secureProjectDir, 'emberflow', 'apis', 'secure', '_meta.json'),
|
|
306
|
+
JSON.stringify({ auth: { scheme: 'bearer', secretRef: 'API_TOKEN' } }),
|
|
307
|
+
);
|
|
308
|
+
writeFileSync(
|
|
309
|
+
join(secureProjectDir, 'emberflow', 'apis', 'secure', 'protected.json'),
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
id: 'protected',
|
|
312
|
+
name: 'Protected',
|
|
313
|
+
version: 1,
|
|
314
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
315
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
316
|
+
http: { method: 'GET', path: '/protected' },
|
|
317
|
+
nodes: [
|
|
318
|
+
{
|
|
319
|
+
id: 'response',
|
|
320
|
+
type: 'Response',
|
|
321
|
+
label: 'Response',
|
|
322
|
+
position: { x: 0, y: 0 },
|
|
323
|
+
config: { status: 200, body: { ok: true } },
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
edges: [],
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
writeFileSync(join(secureProjectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
330
|
+
// Seed the default environment's secret the bearer policy's secretRef points at.
|
|
331
|
+
writeFileSync(
|
|
332
|
+
join(secureProjectDir, 'emberflow.environments.json'),
|
|
333
|
+
JSON.stringify({
|
|
334
|
+
defaultEnvironment: 'local',
|
|
335
|
+
environments: { local: { vars: {}, secrets: { API_TOKEN: 'topsecret' } } },
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
secureProc = bootRunner(SECURE_PORT, { EMBERFLOW_PROJECT: secureProjectDir });
|
|
340
|
+
await waitHealthy(`${secureBase}/healthz`);
|
|
341
|
+
|
|
342
|
+
const unauthed = await fetch(`${secureBase}/protected`);
|
|
343
|
+
expect(unauthed.status).toBe(401);
|
|
344
|
+
expect(await unauthed.json()).toEqual({ error: 'unauthorized' });
|
|
345
|
+
|
|
346
|
+
const authed = await fetch(`${secureBase}/protected`, {
|
|
347
|
+
headers: { Authorization: 'Bearer topsecret' },
|
|
348
|
+
});
|
|
349
|
+
expect(authed.status).toBe(200);
|
|
350
|
+
expect(await authed.json()).toEqual({ ok: true });
|
|
351
|
+
}, 20_000);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('broken auth config fails closed', () => {
|
|
355
|
+
let brokenProjectDir: string;
|
|
356
|
+
let brokenProc: ChildProcess;
|
|
357
|
+
let getStderr: () => string;
|
|
358
|
+
const BROKEN_PORT = 8127;
|
|
359
|
+
const brokenBase = `http://127.0.0.1:${BROKEN_PORT}`;
|
|
360
|
+
|
|
361
|
+
afterAll(() => {
|
|
362
|
+
brokenProc?.kill();
|
|
363
|
+
if (brokenProjectDir) rmSync(brokenProjectDir, { recursive: true, force: true });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('denies (500) an operation under a corrupt _meta.json instead of resolving it public, while other operations and boot still succeed', async () => {
|
|
367
|
+
brokenProjectDir = mkdtempSync(join(tmpdir(), 'apimount-broken-'));
|
|
368
|
+
mkdirSync(join(brokenProjectDir, 'emberflow', 'apis', 'broken'), { recursive: true });
|
|
369
|
+
mkdirSync(join(brokenProjectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
370
|
+
|
|
371
|
+
// Corrupt _meta.json — e.g. a partial write or a typo — at the api-folder
|
|
372
|
+
// level: every op under apis/broken/ would have inherited this policy had
|
|
373
|
+
// it parsed.
|
|
374
|
+
writeFileSync(join(brokenProjectDir, 'emberflow', 'apis', 'broken', '_meta.json'), '{ "auth": { "scheme": ');
|
|
375
|
+
writeFileSync(
|
|
376
|
+
join(brokenProjectDir, 'emberflow', 'apis', 'broken', 'affected.json'),
|
|
377
|
+
JSON.stringify({
|
|
378
|
+
id: 'affected',
|
|
379
|
+
name: 'Affected',
|
|
380
|
+
version: 1,
|
|
381
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
382
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
383
|
+
http: { method: 'GET', path: '/affected' },
|
|
384
|
+
nodes: [
|
|
385
|
+
{
|
|
386
|
+
id: 'response',
|
|
387
|
+
type: 'Response',
|
|
388
|
+
label: 'Response',
|
|
389
|
+
position: { x: 0, y: 0 },
|
|
390
|
+
config: { status: 200, body: { shouldNeverBeSeen: true } },
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
edges: [],
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
// A sibling op under an unrelated, healthy directory — must still boot and work.
|
|
397
|
+
writeFileSync(
|
|
398
|
+
join(brokenProjectDir, 'emberflow', 'apis', 'default', 'healthy.json'),
|
|
399
|
+
JSON.stringify({
|
|
400
|
+
id: 'healthy',
|
|
401
|
+
name: 'Healthy',
|
|
402
|
+
version: 1,
|
|
403
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
404
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
405
|
+
http: { method: 'GET', path: '/healthy' },
|
|
406
|
+
nodes: [
|
|
407
|
+
{
|
|
408
|
+
id: 'response',
|
|
409
|
+
type: 'Response',
|
|
410
|
+
label: 'Response',
|
|
411
|
+
position: { x: 0, y: 0 },
|
|
412
|
+
config: { status: 200, body: { ok: true } },
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
edges: [],
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
writeFileSync(join(brokenProjectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
419
|
+
// Configured environment → boots REAL (default-mock would 501 these ops).
|
|
420
|
+
writeFileSync(
|
|
421
|
+
join(brokenProjectDir, 'emberflow.environments.json'),
|
|
422
|
+
JSON.stringify({ defaultEnvironment: 'dev', environments: { dev: {} } }),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const booted = bootRunnerCapturingStderr(BROKEN_PORT, { EMBERFLOW_PROJECT: brokenProjectDir });
|
|
426
|
+
brokenProc = booted.proc;
|
|
427
|
+
getStderr = booted.stderr;
|
|
428
|
+
await waitHealthy(`${brokenBase}/healthz`);
|
|
429
|
+
|
|
430
|
+
// The op under the broken _meta.json is denied — never silently public.
|
|
431
|
+
const affected = await fetch(`${brokenBase}/affected`);
|
|
432
|
+
expect(affected.status).toBe(500);
|
|
433
|
+
expect(await affected.json()).toEqual({ error: 'auth misconfigured' });
|
|
434
|
+
|
|
435
|
+
// The rest of the runner still boots and serves other operations fine.
|
|
436
|
+
const healthy = await fetch(`${brokenBase}/healthy`);
|
|
437
|
+
expect(healthy.status).toBe(200);
|
|
438
|
+
expect(await healthy.json()).toEqual({ ok: true });
|
|
439
|
+
|
|
440
|
+
expect(getStderr()).toMatch(/operation affected has broken auth config — mounted as fail-closed \(500\)/);
|
|
441
|
+
}, 20_000);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
describe('reserved route guard', () => {
|
|
445
|
+
let reservedProjectDir: string;
|
|
446
|
+
let reservedProc: ChildProcess;
|
|
447
|
+
let getStderr: () => string;
|
|
448
|
+
const RESERVED_PORT = 8125;
|
|
449
|
+
const reservedBase = `http://127.0.0.1:${RESERVED_PORT}`;
|
|
450
|
+
|
|
451
|
+
afterAll(() => {
|
|
452
|
+
reservedProc?.kill();
|
|
453
|
+
if (reservedProjectDir) rmSync(reservedProjectDir, { recursive: true, force: true });
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('boots without crashing, skips an operation whose http.path collides with a reserved internal route, warns, and leaves the internal route intact', async () => {
|
|
457
|
+
reservedProjectDir = mkdtempSync(join(tmpdir(), 'apimount-reserved-'));
|
|
458
|
+
mkdirSync(join(reservedProjectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
459
|
+
// Operation whose http.path collides with the internal GET /workflows route.
|
|
460
|
+
writeFileSync(
|
|
461
|
+
join(reservedProjectDir, 'emberflow', 'apis', 'default', 'colliding.json'),
|
|
462
|
+
JSON.stringify({
|
|
463
|
+
id: 'colliding',
|
|
464
|
+
name: 'Colliding',
|
|
465
|
+
version: 1,
|
|
466
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
467
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
468
|
+
http: { method: 'GET', path: '/workflows' },
|
|
469
|
+
nodes: [
|
|
470
|
+
{
|
|
471
|
+
id: 'response',
|
|
472
|
+
type: 'Response',
|
|
473
|
+
label: 'Response',
|
|
474
|
+
position: { x: 0, y: 0 },
|
|
475
|
+
config: { status: 200, body: { hijacked: true } },
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
edges: [],
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
writeFileSync(join(reservedProjectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
482
|
+
// Configured environment → boots REAL (default-mock would 501 these ops).
|
|
483
|
+
writeFileSync(
|
|
484
|
+
join(reservedProjectDir, 'emberflow.environments.json'),
|
|
485
|
+
JSON.stringify({ defaultEnvironment: 'dev', environments: { dev: {} } }),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const booted = bootRunnerCapturingStderr(RESERVED_PORT, { EMBERFLOW_PROJECT: reservedProjectDir });
|
|
489
|
+
reservedProc = booted.proc;
|
|
490
|
+
getStderr = booted.stderr;
|
|
491
|
+
await waitHealthy(`${reservedBase}/healthz`);
|
|
492
|
+
|
|
493
|
+
// Internal /workflows route still works — not shadowed by the colliding operation.
|
|
494
|
+
const res = await fetch(`${reservedBase}/api/workflows`);
|
|
495
|
+
expect(res.status).toBe(200);
|
|
496
|
+
const body = await res.json();
|
|
497
|
+
expect(body.flows.map((f: { id: string }) => f.id)).toContain('colliding');
|
|
498
|
+
expect(body).not.toEqual({ hijacked: true });
|
|
499
|
+
|
|
500
|
+
expect(getStderr()).toMatch(/collides with a reserved internal route/);
|
|
501
|
+
}, 20_000);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe('root-path guard (no studio shadowing)', () => {
|
|
505
|
+
let rootProjectDir: string;
|
|
506
|
+
let rootProc: ChildProcess;
|
|
507
|
+
let getStderr: () => string;
|
|
508
|
+
const ROOT_PORT = 8126;
|
|
509
|
+
const rootBase = `http://127.0.0.1:${ROOT_PORT}`;
|
|
510
|
+
|
|
511
|
+
afterAll(() => {
|
|
512
|
+
rootProc?.kill();
|
|
513
|
+
if (rootProjectDir) rmSync(rootProjectDir, { recursive: true, force: true });
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('skips an operation whose http.path is "/" (would shadow the studio root), warns, and still mounts a sibling valid operation', async () => {
|
|
517
|
+
rootProjectDir = mkdtempSync(join(tmpdir(), 'apimount-root-'));
|
|
518
|
+
mkdirSync(join(rootProjectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
519
|
+
// Operation whose http.path is bare root — would shadow the studio SPA
|
|
520
|
+
// catch-all (mounted after operations) if not skipped.
|
|
521
|
+
writeFileSync(
|
|
522
|
+
join(rootProjectDir, 'emberflow', 'apis', 'default', 'root.json'),
|
|
523
|
+
JSON.stringify({
|
|
524
|
+
id: 'root',
|
|
525
|
+
name: 'Root',
|
|
526
|
+
version: 1,
|
|
527
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
528
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
529
|
+
http: { method: 'GET', path: '/' },
|
|
530
|
+
nodes: [
|
|
531
|
+
{
|
|
532
|
+
id: 'response',
|
|
533
|
+
type: 'Response',
|
|
534
|
+
label: 'Response',
|
|
535
|
+
position: { x: 0, y: 0 },
|
|
536
|
+
config: { status: 200, body: { hijacked: true } },
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
edges: [],
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
// A sibling valid operation — should still mount normally.
|
|
543
|
+
writeFileSync(
|
|
544
|
+
join(rootProjectDir, 'emberflow', 'apis', 'default', 'things.json'),
|
|
545
|
+
JSON.stringify({
|
|
546
|
+
id: 'things',
|
|
547
|
+
name: 'Things',
|
|
548
|
+
version: 1,
|
|
549
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
550
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
551
|
+
http: { method: 'GET', path: '/things' },
|
|
552
|
+
nodes: [
|
|
553
|
+
{
|
|
554
|
+
id: 'response',
|
|
555
|
+
type: 'Response',
|
|
556
|
+
label: 'Response',
|
|
557
|
+
position: { x: 0, y: 0 },
|
|
558
|
+
config: { status: 200, body: { ok: true } },
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
edges: [],
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
writeFileSync(join(rootProjectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
565
|
+
// Configured environment → boots REAL (default-mock would 501 these ops).
|
|
566
|
+
writeFileSync(
|
|
567
|
+
join(rootProjectDir, 'emberflow.environments.json'),
|
|
568
|
+
JSON.stringify({ defaultEnvironment: 'dev', environments: { dev: {} } }),
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
const booted = bootRunnerCapturingStderr(ROOT_PORT, { EMBERFLOW_PROJECT: rootProjectDir });
|
|
572
|
+
rootProc = booted.proc;
|
|
573
|
+
getStderr = booted.stderr;
|
|
574
|
+
await waitHealthy(`${rootBase}/healthz`);
|
|
575
|
+
|
|
576
|
+
// GET / is not hijacked by the op — without EMBERFLOW_SERVE_STUDIO the
|
|
577
|
+
// root path 404s (no static/SPA fallback registered) rather than
|
|
578
|
+
// returning the op's body.
|
|
579
|
+
const res = await fetch(`${rootBase}/`);
|
|
580
|
+
expect(res.status).not.toBe(200);
|
|
581
|
+
const bodyText = await res.text();
|
|
582
|
+
expect(bodyText).not.toMatch(/hijacked/);
|
|
583
|
+
|
|
584
|
+
// The sibling valid operation still mounts and works.
|
|
585
|
+
const thingsRes = await fetch(`${rootBase}/things`);
|
|
586
|
+
expect(thingsRes.status).toBe(200);
|
|
587
|
+
expect(await thingsRes.json()).toEqual({ ok: true });
|
|
588
|
+
|
|
589
|
+
expect(getStderr()).toMatch(/has an invalid\/root path "\/" — skipping \(would shadow the studio\)/);
|
|
590
|
+
}, 20_000);
|
|
591
|
+
});
|