@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,486 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
const ENVIRONMENTS_FILENAME = 'emberflow.environments.json';
|
|
5
|
+
const SECRETS_FILENAME = 'emberflow.secrets.json';
|
|
6
|
+
function isPlainObject(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function isStringRecord(value) {
|
|
10
|
+
if (!isPlainObject(value))
|
|
11
|
+
return false;
|
|
12
|
+
return Object.values(value).every((v) => typeof v === 'string');
|
|
13
|
+
}
|
|
14
|
+
function isStringArray(value) {
|
|
15
|
+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
16
|
+
}
|
|
17
|
+
/** A nested `{ envName: { KEY: value } }` secrets-file map (new shape). */
|
|
18
|
+
function isNestedSecretsMap(value) {
|
|
19
|
+
if (!isPlainObject(value))
|
|
20
|
+
return false;
|
|
21
|
+
return Object.values(value).every((v) => isStringRecord(v));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validates an `EnvAuth` value's shape. Returns an error message (without any
|
|
25
|
+
* `environments.<name>.` prefix) naming the problem, or `null` if valid.
|
|
26
|
+
* Shared by `validateShape` (file load) and `setEnvironmentAuth` (writer) so
|
|
27
|
+
* the rules can't drift between the two call sites.
|
|
28
|
+
*/
|
|
29
|
+
function validateAuthShape(auth) {
|
|
30
|
+
if (!isPlainObject(auth)) {
|
|
31
|
+
return 'auth must be an object';
|
|
32
|
+
}
|
|
33
|
+
const attach = auth.attach;
|
|
34
|
+
if (!isPlainObject(attach)) {
|
|
35
|
+
return 'auth.attach must be an object';
|
|
36
|
+
}
|
|
37
|
+
if (attach.as !== 'cookie' && attach.as !== 'header') {
|
|
38
|
+
return 'auth.attach.as must be "cookie" or "header"';
|
|
39
|
+
}
|
|
40
|
+
if (typeof attach.name !== 'string') {
|
|
41
|
+
return 'auth.attach.name must be a string';
|
|
42
|
+
}
|
|
43
|
+
if (typeof attach.secretRef !== 'string') {
|
|
44
|
+
return 'auth.attach.secretRef must be a string';
|
|
45
|
+
}
|
|
46
|
+
if (attach.prefix !== undefined && typeof attach.prefix !== 'string') {
|
|
47
|
+
return 'auth.attach.prefix must be a string';
|
|
48
|
+
}
|
|
49
|
+
if (auth.login !== undefined && !isPlainObject(auth.login)) {
|
|
50
|
+
return 'auth.login must be an object';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** Throws with a message pinpointing what's wrong in the file, for a clear boot-time failure. */
|
|
55
|
+
function validateShape(parsed) {
|
|
56
|
+
const prefix = `${ENVIRONMENTS_FILENAME} is malformed`;
|
|
57
|
+
if (!isPlainObject(parsed)) {
|
|
58
|
+
throw new Error(`${prefix}: expected a JSON object at the top level`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof parsed.defaultEnvironment !== 'string' || parsed.defaultEnvironment.length === 0) {
|
|
61
|
+
throw new Error(`${prefix}: "defaultEnvironment" must be a non-empty string`);
|
|
62
|
+
}
|
|
63
|
+
if (!isPlainObject(parsed.environments)) {
|
|
64
|
+
throw new Error(`${prefix}: "environments" must be an object of name -> environment`);
|
|
65
|
+
}
|
|
66
|
+
const names = Object.keys(parsed.environments);
|
|
67
|
+
if (names.length === 0) {
|
|
68
|
+
throw new Error(`${prefix}: "environments" must define at least one environment`);
|
|
69
|
+
}
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const env = parsed.environments[name];
|
|
72
|
+
if (!isPlainObject(env)) {
|
|
73
|
+
throw new Error(`${prefix}: environments.${name} must be an object`);
|
|
74
|
+
}
|
|
75
|
+
if (env.vars !== undefined && !isStringRecord(env.vars)) {
|
|
76
|
+
throw new Error(`${prefix}: environments.${name}.vars must be an object of string -> string`);
|
|
77
|
+
}
|
|
78
|
+
// `secrets` is a LIST of key names (new, structure-only shape). The old
|
|
79
|
+
// map-of-values shape is still accepted here so a pre-migration file loads;
|
|
80
|
+
// loadEnvironments migrates its values out on the next load.
|
|
81
|
+
if (env.secrets !== undefined && !isStringArray(env.secrets) && !isStringRecord(env.secrets)) {
|
|
82
|
+
throw new Error(`${prefix}: environments.${name}.secrets must be an array of key names or an object of string -> string`);
|
|
83
|
+
}
|
|
84
|
+
if (env.protected !== undefined && typeof env.protected !== 'boolean') {
|
|
85
|
+
throw new Error(`${prefix}: environments.${name}.protected must be a boolean`);
|
|
86
|
+
}
|
|
87
|
+
if (env.auth !== undefined) {
|
|
88
|
+
const authError = validateAuthShape(env.auth);
|
|
89
|
+
if (authError !== null) {
|
|
90
|
+
throw new Error(`${prefix}: environments.${name}.${authError}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!(parsed.defaultEnvironment in parsed.environments)) {
|
|
95
|
+
throw new Error(`${prefix}: defaultEnvironment "${parsed.defaultEnvironment}" is not one of the defined environments (${names.join(', ')})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Secret VALUES live in emberflow.secrets.json, structure-only in
|
|
100
|
+
// environments.json. Everything below implements that split: reading the
|
|
101
|
+
// secrets file (new nested shape + legacy flat), $ENV indirection, file
|
|
102
|
+
// hardening warnings, and the one-time auto-migration of old inline values.
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
const secretsFileMode = 0o600;
|
|
105
|
+
/** One-shot per-process warning dedupe so repeated loads don't spam the log. */
|
|
106
|
+
const warnedOnce = new Set();
|
|
107
|
+
function warnOnce(key, message) {
|
|
108
|
+
if (warnedOnce.has(key))
|
|
109
|
+
return;
|
|
110
|
+
warnedOnce.add(key);
|
|
111
|
+
console.warn(message);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Reads `emberflow.secrets.json`. Accepts the new nested
|
|
115
|
+
* `{ envName: { KEY: value } }` shape and the legacy flat `{ KEY: value }`
|
|
116
|
+
* shape (mapped to env "local", preserving the historical synthesized-env
|
|
117
|
+
* behavior). Missing or invalid file → empty. Values may be `$ENV:VAR`
|
|
118
|
+
* indirections; they are resolved at merge time, not here.
|
|
119
|
+
*/
|
|
120
|
+
function readSecretsFile(secretsPath) {
|
|
121
|
+
if (!existsSync(secretsPath))
|
|
122
|
+
return { byEnv: {}, legacyFlat: false, existed: false };
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(readFileSync(secretsPath, 'utf8'));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return { byEnv: {}, legacyFlat: false, existed: true };
|
|
129
|
+
}
|
|
130
|
+
if (isNestedSecretsMap(parsed)) {
|
|
131
|
+
// Empty {} is ambiguous but harmless as an empty nested map.
|
|
132
|
+
return { byEnv: parsed, legacyFlat: false, existed: true };
|
|
133
|
+
}
|
|
134
|
+
if (isStringRecord(parsed)) {
|
|
135
|
+
return { byEnv: { local: parsed }, legacyFlat: true, existed: true };
|
|
136
|
+
}
|
|
137
|
+
return { byEnv: {}, legacyFlat: false, existed: true };
|
|
138
|
+
}
|
|
139
|
+
/** Writes the nested secrets map and locks the file down to 0600. */
|
|
140
|
+
function writeSecretsFile(secretsPath, byEnv) {
|
|
141
|
+
writeFileSync(secretsPath, JSON.stringify(byEnv, null, 2) + '\n');
|
|
142
|
+
try {
|
|
143
|
+
chmodSync(secretsPath, secretsFileMode);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Best-effort on platforms/filesystems without POSIX modes.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Resolves a stored secret value. A value of the form `$ENV:VAR_NAME`
|
|
151
|
+
* indirects to `process.env.VAR_NAME`; a missing/empty env var resolves to ''
|
|
152
|
+
* and warns (once) naming the variable. All other values pass through.
|
|
153
|
+
*/
|
|
154
|
+
function resolveSecretValue(raw) {
|
|
155
|
+
const match = /^\$ENV:(.+)$/.exec(raw);
|
|
156
|
+
if (!match)
|
|
157
|
+
return raw;
|
|
158
|
+
const varName = match[1];
|
|
159
|
+
const fromEnv = process.env[varName];
|
|
160
|
+
if (fromEnv === undefined || fromEnv === '') {
|
|
161
|
+
warnOnce(`envvar:${varName}`, `[emberflow] secret indirection $ENV:${varName} is unset — resolving to empty. Set ${varName} in the environment.`);
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
return fromEnv;
|
|
165
|
+
}
|
|
166
|
+
/** True when the file is tracked by git. Safe/cheap; false when not a repo. */
|
|
167
|
+
function isTrackedByGit(cwd, relPath) {
|
|
168
|
+
try {
|
|
169
|
+
execFileSync('git', ['ls-files', '--error-unmatch', relPath], {
|
|
170
|
+
cwd,
|
|
171
|
+
stdio: 'ignore',
|
|
172
|
+
});
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Warns (once per process) if the secrets file is group/other-readable or is
|
|
181
|
+
* tracked by git — both leak plaintext credentials.
|
|
182
|
+
*/
|
|
183
|
+
const hardeningChecked = new Set();
|
|
184
|
+
function checkSecretsFileHardening(cwd, secretsPath) {
|
|
185
|
+
if (!existsSync(secretsPath))
|
|
186
|
+
return;
|
|
187
|
+
// Runs once per path per process — loadEnvironments is called on every
|
|
188
|
+
// GET /environments and we don't want to spawn `git` on that hot path.
|
|
189
|
+
if (hardeningChecked.has(secretsPath))
|
|
190
|
+
return;
|
|
191
|
+
hardeningChecked.add(secretsPath);
|
|
192
|
+
try {
|
|
193
|
+
const mode = statSync(secretsPath).mode & 0o777;
|
|
194
|
+
if ((mode & 0o077) !== 0) {
|
|
195
|
+
// Loose perms on a hand-written secrets file are fixable in place — do
|
|
196
|
+
// it and say so, rather than nagging on every load with no remediation.
|
|
197
|
+
try {
|
|
198
|
+
chmodSync(secretsPath, secretsFileMode);
|
|
199
|
+
console.info(`[emberflow] tightened ${SECRETS_FILENAME} to 0600`);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Couldn't chmod (e.g. read-only filesystem) — fall back to warning
|
|
203
|
+
// since we weren't able to fix it ourselves.
|
|
204
|
+
warnOnce(`mode:${secretsPath}`, `[emberflow] SECURITY: ${SECRETS_FILENAME} is readable by group/other (mode 0${mode.toString(8)}); ` +
|
|
205
|
+
`it holds plaintext secrets. Run: chmod 600 ${SECRETS_FILENAME}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// stat failure is non-fatal
|
|
211
|
+
}
|
|
212
|
+
if (isTrackedByGit(cwd, SECRETS_FILENAME)) {
|
|
213
|
+
warnOnce(`git:${secretsPath}`, `[emberflow] SECURITY: ${SECRETS_FILENAME} is tracked by git and holds plaintext secrets. ` +
|
|
214
|
+
`Remove it from the repo: git rm --cached ${SECRETS_FILENAME} (it should stay gitignored).`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** True when any environment still carries inline non-empty secret VALUES. */
|
|
218
|
+
function needsMigration(parsed) {
|
|
219
|
+
for (const env of Object.values(parsed.environments)) {
|
|
220
|
+
const s = env.secrets;
|
|
221
|
+
if (s !== undefined && !Array.isArray(s) && Object.values(s).some((v) => v !== '')) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* One-time migration: moves inline secret VALUES out of environments.json into
|
|
229
|
+
* emberflow.secrets.json (0600), rewrites environments.json so each env's
|
|
230
|
+
* `secrets` is a list of key NAMES, and keeps a `.bak` of the original
|
|
231
|
+
* environments.json on first migration. Idempotent — after it runs there are
|
|
232
|
+
* no inline values left, so `needsMigration` returns false on the next load.
|
|
233
|
+
*/
|
|
234
|
+
function migrateEnvironmentsFile(environmentsPath, secretsPath, parsed) {
|
|
235
|
+
const backupPath = `${environmentsPath}.bak`;
|
|
236
|
+
if (!existsSync(backupPath)) {
|
|
237
|
+
copyFileSync(environmentsPath, backupPath);
|
|
238
|
+
}
|
|
239
|
+
const { byEnv } = readSecretsFile(secretsPath);
|
|
240
|
+
for (const [name, env] of Object.entries(parsed.environments)) {
|
|
241
|
+
const s = env.secrets;
|
|
242
|
+
if (s === undefined || Array.isArray(s))
|
|
243
|
+
continue;
|
|
244
|
+
const bucket = { ...(byEnv[name] ?? {}) };
|
|
245
|
+
for (const [key, value] of Object.entries(s)) {
|
|
246
|
+
// environments.json is the authoritative current source — its values win.
|
|
247
|
+
if (value !== '')
|
|
248
|
+
bucket[key] = value;
|
|
249
|
+
}
|
|
250
|
+
byEnv[name] = bucket;
|
|
251
|
+
env.secrets = Object.keys(s);
|
|
252
|
+
}
|
|
253
|
+
writeSecretsFile(secretsPath, byEnv);
|
|
254
|
+
writeFileSync(environmentsPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
255
|
+
console.log(`[emberflow] Migrated inline secret values from ${ENVIRONMENTS_FILENAME} into ${SECRETS_FILENAME} (0600). ` +
|
|
256
|
+
`${ENVIRONMENTS_FILENAME} now holds secret key NAMES only; a .bak of the original was kept.`);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Merges an env's declared key names with the values present in the secrets
|
|
260
|
+
* file: resolved map = names ∪ secrets-file keys, value from the secrets file
|
|
261
|
+
* (with $ENV resolution) or '' when absent so requireSecret still throws.
|
|
262
|
+
*/
|
|
263
|
+
function resolveEnvironment(env, fileSecrets) {
|
|
264
|
+
const names = Array.isArray(env.secrets) ? env.secrets : Object.keys(env.secrets ?? {});
|
|
265
|
+
const keys = new Set([...names, ...Object.keys(fileSecrets ?? {})]);
|
|
266
|
+
const secrets = {};
|
|
267
|
+
for (const key of keys) {
|
|
268
|
+
const raw = fileSecrets?.[key];
|
|
269
|
+
secrets[key] = raw === undefined ? '' : resolveSecretValue(raw);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
vars: env.vars ?? {},
|
|
273
|
+
secrets,
|
|
274
|
+
...(env.protected !== undefined ? { protected: env.protected } : {}),
|
|
275
|
+
...(env.auth !== undefined ? { auth: env.auth } : {}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolves the effective `safeMode` for a run against a given environment.
|
|
280
|
+
*
|
|
281
|
+
* - Omitted `safeMode` defaults to the environment's protection: `true` when
|
|
282
|
+
* `protected`, `false` otherwise.
|
|
283
|
+
* - Explicitly requesting `safeMode: false` on a protected environment is
|
|
284
|
+
* refused unless `confirm` matches the environment name exactly.
|
|
285
|
+
* - Any other explicit request (including `safeMode: true` on any env, or
|
|
286
|
+
* `safeMode: false` on a non-protected env) is honored as-is.
|
|
287
|
+
*/
|
|
288
|
+
export function resolveRunSafety(envName, env, request) {
|
|
289
|
+
const isProtected = !!env.protected;
|
|
290
|
+
if (request.safeMode === undefined) {
|
|
291
|
+
return { ok: true, safeMode: isProtected };
|
|
292
|
+
}
|
|
293
|
+
if (request.safeMode === false && isProtected && request.confirm !== envName) {
|
|
294
|
+
return { ok: false, error: `unsafe run on protected environment '${envName}' requires confirm` };
|
|
295
|
+
}
|
|
296
|
+
return { ok: true, safeMode: request.safeMode };
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Loads `emberflow.environments.json` from `cwd`.
|
|
300
|
+
*
|
|
301
|
+
* - File present: parsed and validated; malformed files throw with a clear
|
|
302
|
+
* message (meant to fail the boot loudly rather than run with bad config).
|
|
303
|
+
* - File absent but `emberflow.secrets.json` present: synthesizes a single
|
|
304
|
+
* `local` environment from it (logs a hint to migrate) so existing setups
|
|
305
|
+
* keep working.
|
|
306
|
+
* - Neither file present: an empty `local` environment (matches the previous
|
|
307
|
+
* loadSecrets() behavior of defaulting to {} when nothing is configured).
|
|
308
|
+
*
|
|
309
|
+
* Secret VALUES are read from `emberflow.secrets.json` and merged onto the key
|
|
310
|
+
* NAMES declared in environments.json. A first load of an old-format file
|
|
311
|
+
* (inline values) auto-migrates the values out. See the helpers above.
|
|
312
|
+
*/
|
|
313
|
+
function parseEnvironmentsFile(environmentsPath) {
|
|
314
|
+
const raw = readFileSync(environmentsPath, 'utf8');
|
|
315
|
+
let parsed;
|
|
316
|
+
try {
|
|
317
|
+
parsed = JSON.parse(raw);
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
throw new Error(`${ENVIRONMENTS_FILENAME} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
321
|
+
}
|
|
322
|
+
validateShape(parsed);
|
|
323
|
+
return parsed;
|
|
324
|
+
}
|
|
325
|
+
export function loadEnvironments(cwd) {
|
|
326
|
+
const environmentsPath = resolve(cwd, ENVIRONMENTS_FILENAME);
|
|
327
|
+
const secretsPath = resolve(cwd, SECRETS_FILENAME);
|
|
328
|
+
if (existsSync(environmentsPath)) {
|
|
329
|
+
let parsed = parseEnvironmentsFile(environmentsPath);
|
|
330
|
+
if (needsMigration(parsed)) {
|
|
331
|
+
migrateEnvironmentsFile(environmentsPath, secretsPath, parsed);
|
|
332
|
+
// Re-read so the resolved view reflects the rewritten name-list file.
|
|
333
|
+
parsed = parseEnvironmentsFile(environmentsPath);
|
|
334
|
+
}
|
|
335
|
+
checkSecretsFileHardening(cwd, secretsPath);
|
|
336
|
+
const { byEnv } = readSecretsFile(secretsPath);
|
|
337
|
+
const environments = {};
|
|
338
|
+
for (const [name, env] of Object.entries(parsed.environments)) {
|
|
339
|
+
environments[name] = resolveEnvironment(env, byEnv[name]);
|
|
340
|
+
}
|
|
341
|
+
return { defaultEnvironment: parsed.defaultEnvironment, environments, configured: true };
|
|
342
|
+
}
|
|
343
|
+
if (existsSync(secretsPath)) {
|
|
344
|
+
console.log(`[emberflow] No ${ENVIRONMENTS_FILENAME} found; synthesizing a "local" environment from ${SECRETS_FILENAME}. ` +
|
|
345
|
+
`Create ${ENVIRONMENTS_FILENAME} to configure multiple environments (see emberflow.environments.example.json).`);
|
|
346
|
+
checkSecretsFileHardening(cwd, secretsPath);
|
|
347
|
+
const { byEnv } = readSecretsFile(secretsPath);
|
|
348
|
+
const names = Object.keys(byEnv);
|
|
349
|
+
const environments = {};
|
|
350
|
+
for (const name of names) {
|
|
351
|
+
environments[name] = resolveEnvironment({ secrets: Object.keys(byEnv[name]) }, byEnv[name]);
|
|
352
|
+
}
|
|
353
|
+
// Preserve the historical single synthesized-env behavior: always expose a
|
|
354
|
+
// "local" env even when the file was empty.
|
|
355
|
+
if (!environments.local) {
|
|
356
|
+
environments.local = { vars: {}, secrets: {} };
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
defaultEnvironment: environments.local ? 'local' : names[0],
|
|
360
|
+
environments,
|
|
361
|
+
configured: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
defaultEnvironment: 'local',
|
|
366
|
+
environments: { local: { vars: {}, secrets: {} } },
|
|
367
|
+
configured: false,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// Serializes all environment-file read-modify-write cycles through a single
|
|
371
|
+
// promise chain so concurrent setEnvironmentSecret calls can't clobber each
|
|
372
|
+
// other (last-write-wins on a sync read-modify-write would drop a secret).
|
|
373
|
+
let writeQueue = Promise.resolve();
|
|
374
|
+
function enqueue(fn) {
|
|
375
|
+
const p = writeQueue.then(fn);
|
|
376
|
+
writeQueue = p.catch(() => { });
|
|
377
|
+
return p;
|
|
378
|
+
}
|
|
379
|
+
/** Current secret key NAMES for an env, from either on-disk shape. */
|
|
380
|
+
function secretNames(env) {
|
|
381
|
+
return Array.isArray(env.secrets) ? [...env.secrets] : Object.keys(env.secrets ?? {});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Folds any pre-migration inline map VALUES into `bucket` so converting an
|
|
385
|
+
* env's `secrets` to a name list never drops a sibling key's value. File
|
|
386
|
+
* values already present win (they are the current source of truth).
|
|
387
|
+
*/
|
|
388
|
+
function foldInlineValues(env, bucket) {
|
|
389
|
+
if (Array.isArray(env.secrets) || !env.secrets)
|
|
390
|
+
return;
|
|
391
|
+
for (const [k, v] of Object.entries(env.secrets)) {
|
|
392
|
+
if (v !== '' && !(k in bucket))
|
|
393
|
+
bucket[k] = v;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Sets a single secret's VALUE in `emberflow.secrets.json` (0600) and ensures
|
|
398
|
+
* its key NAME is listed on the environment in `emberflow.environments.json`,
|
|
399
|
+
* both at `cwd`. The value never touches environments.json. Throws if the
|
|
400
|
+
* environments file is missing/malformed or the environment name is unknown.
|
|
401
|
+
* Concurrent calls are serialized so two in-flight writes never clobber each other.
|
|
402
|
+
*/
|
|
403
|
+
export function setEnvironmentSecret(cwd, envName, key, value) {
|
|
404
|
+
return enqueue(() => {
|
|
405
|
+
const environmentsPath = resolve(cwd, ENVIRONMENTS_FILENAME);
|
|
406
|
+
const secretsPath = resolve(cwd, SECRETS_FILENAME);
|
|
407
|
+
const parsed = parseEnvironmentsFile(environmentsPath);
|
|
408
|
+
const env = parsed.environments[envName];
|
|
409
|
+
if (!env) {
|
|
410
|
+
throw new Error(`${ENVIRONMENTS_FILENAME}: unknown environment "${envName}"`);
|
|
411
|
+
}
|
|
412
|
+
// Value → secrets file (0600).
|
|
413
|
+
const { byEnv } = readSecretsFile(secretsPath);
|
|
414
|
+
const bucket = { ...(byEnv[envName] ?? {}) };
|
|
415
|
+
foldInlineValues(env, bucket);
|
|
416
|
+
bucket[key] = value;
|
|
417
|
+
byEnv[envName] = bucket;
|
|
418
|
+
writeSecretsFile(secretsPath, byEnv);
|
|
419
|
+
// Name → environments.json (list form).
|
|
420
|
+
const names = secretNames(env);
|
|
421
|
+
if (!names.includes(key))
|
|
422
|
+
names.push(key);
|
|
423
|
+
env.secrets = names;
|
|
424
|
+
writeFileSync(environmentsPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Sets (or, when `auth` is `null`, clears) an environment's `auth` config in
|
|
429
|
+
* `emberflow.environments.json` at `cwd` and persists the change. Validates
|
|
430
|
+
* the auth shape before writing. Throws if the environments file is
|
|
431
|
+
* missing/malformed, the environment name is unknown, or `auth` is invalid.
|
|
432
|
+
* Preserves the environment's other fields (secrets, vars, protected,
|
|
433
|
+
* unknown fields). Shares the write queue with `setEnvironmentSecret` so
|
|
434
|
+
* concurrent writers can't clobber each other.
|
|
435
|
+
*/
|
|
436
|
+
export function setEnvironmentAuth(cwd, envName, auth) {
|
|
437
|
+
return enqueue(() => {
|
|
438
|
+
if (auth !== null) {
|
|
439
|
+
const authError = validateAuthShape(auth);
|
|
440
|
+
if (authError !== null) {
|
|
441
|
+
throw new Error(`${ENVIRONMENTS_FILENAME}: environments.${envName}.${authError}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const environmentsPath = resolve(cwd, ENVIRONMENTS_FILENAME);
|
|
445
|
+
const parsed = parseEnvironmentsFile(environmentsPath);
|
|
446
|
+
const env = parsed.environments[envName];
|
|
447
|
+
if (!env) {
|
|
448
|
+
throw new Error(`${ENVIRONMENTS_FILENAME}: unknown environment "${envName}"`);
|
|
449
|
+
}
|
|
450
|
+
if (auth === null) {
|
|
451
|
+
delete env.auth;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
env.auth = auth;
|
|
455
|
+
}
|
|
456
|
+
writeFileSync(environmentsPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Removes a single secret from an environment: drops its key NAME from
|
|
461
|
+
* `emberflow.environments.json` and its VALUE from `emberflow.secrets.json`,
|
|
462
|
+
* both at `cwd`. Missing key resolves without error. Throws if the environments
|
|
463
|
+
* file is missing/malformed or the environment name is unknown. Shares the
|
|
464
|
+
* write queue with the other writers.
|
|
465
|
+
*/
|
|
466
|
+
export function deleteEnvironmentSecret(cwd, envName, key) {
|
|
467
|
+
return enqueue(() => {
|
|
468
|
+
const environmentsPath = resolve(cwd, ENVIRONMENTS_FILENAME);
|
|
469
|
+
const secretsPath = resolve(cwd, SECRETS_FILENAME);
|
|
470
|
+
const parsed = parseEnvironmentsFile(environmentsPath);
|
|
471
|
+
const env = parsed.environments[envName];
|
|
472
|
+
if (!env) {
|
|
473
|
+
throw new Error(`${ENVIRONMENTS_FILENAME}: unknown environment "${envName}"`);
|
|
474
|
+
}
|
|
475
|
+
// Value out of the secrets file (preserving sibling inline values).
|
|
476
|
+
const { byEnv } = readSecretsFile(secretsPath);
|
|
477
|
+
const bucket = { ...(byEnv[envName] ?? {}) };
|
|
478
|
+
foldInlineValues(env, bucket);
|
|
479
|
+
delete bucket[key];
|
|
480
|
+
byEnv[envName] = bucket;
|
|
481
|
+
writeSecretsFile(secretsPath, byEnv);
|
|
482
|
+
// Name out of environments.json (normalizing to list form).
|
|
483
|
+
env.secrets = secretNames(env).filter((n) => n !== key);
|
|
484
|
+
writeFileSync(environmentsPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
485
|
+
});
|
|
486
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { WorkflowDefinition } from '../src/engine/index.js';
|
|
2
|
+
export interface FlowStoreOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Project mode: scenarios live in <id>.scenarios.json next to the flow
|
|
5
|
+
* file (reviewable, hand-editable "stories"), not embedded in the flow.
|
|
6
|
+
*/
|
|
7
|
+
scenarioSidecars?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runner-owned store of workflow files. Each flow lives at `<dir>/<id>.json`,
|
|
11
|
+
* pretty-printed. The directory is the source of truth when the runner is
|
|
12
|
+
* online; the browser adopts it on first successful health check.
|
|
13
|
+
*/
|
|
14
|
+
export declare class FlowStore {
|
|
15
|
+
readonly dir: string;
|
|
16
|
+
private readonly sidecars;
|
|
17
|
+
constructor(dir?: string, options?: FlowStoreOptions);
|
|
18
|
+
private sidecarPath;
|
|
19
|
+
/** Merge <id>.scenarios.json into the flow when sidecar mode is on. */
|
|
20
|
+
private withSidecar;
|
|
21
|
+
/** All parseable *.json flows. Unparseable files are skipped with a warning. */
|
|
22
|
+
list(): WorkflowDefinition[];
|
|
23
|
+
/** Load a single flow by id, or undefined when its file is missing/unparseable. */
|
|
24
|
+
load(id: string): WorkflowDefinition | undefined;
|
|
25
|
+
/** Persist a flow. Its id must be a non-empty string and names the file. */
|
|
26
|
+
save(flow: WorkflowDefinition): void;
|
|
27
|
+
/** Delete a flow file (and any scenario sidecar). Returns false when it did not exist. */
|
|
28
|
+
remove(id: string): boolean;
|
|
29
|
+
/** Seed the given flows only when the directory holds no json files yet. */
|
|
30
|
+
seedIfEmpty(flows: WorkflowDefinition[]): void;
|
|
31
|
+
/**
|
|
32
|
+
* Seed only flows the store doesn't know yet (by id). Existing copies —
|
|
33
|
+
* possibly user-edited — are never overwritten, so new built-in example
|
|
34
|
+
* flows appear after an upgrade without clobbering anything.
|
|
35
|
+
*/
|
|
36
|
+
seedMissing(flows: WorkflowDefinition[]): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Runner-owned store of workflow files. Each flow lives at `<dir>/<id>.json`,
|
|
5
|
+
* pretty-printed. The directory is the source of truth when the runner is
|
|
6
|
+
* online; the browser adopts it on first successful health check.
|
|
7
|
+
*/
|
|
8
|
+
export class FlowStore {
|
|
9
|
+
dir;
|
|
10
|
+
sidecars;
|
|
11
|
+
constructor(dir = resolve(process.cwd(), 'workflows'), options = {}) {
|
|
12
|
+
this.dir = dir;
|
|
13
|
+
this.sidecars = options.scenarioSidecars ?? false;
|
|
14
|
+
mkdirSync(this.dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
sidecarPath(id) {
|
|
17
|
+
return join(this.dir, `${id}.scenarios.json`);
|
|
18
|
+
}
|
|
19
|
+
/** Merge <id>.scenarios.json into the flow when sidecar mode is on. */
|
|
20
|
+
withSidecar(flow) {
|
|
21
|
+
if (!this.sidecars)
|
|
22
|
+
return flow;
|
|
23
|
+
const path = this.sidecarPath(flow.id);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return flow;
|
|
26
|
+
try {
|
|
27
|
+
const scenarios = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
return Array.isArray(scenarios) && scenarios.length > 0 ? { ...flow, scenarios } : flow;
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.warn(`[flowStore] skipping unparseable ${flow.id}.scenarios.json: ${String(err)}`);
|
|
32
|
+
return flow;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** All parseable *.json flows. Unparseable files are skipped with a warning. */
|
|
36
|
+
list() {
|
|
37
|
+
const flows = [];
|
|
38
|
+
for (const file of readdirSync(this.dir)) {
|
|
39
|
+
if (!file.endsWith('.json') || file.endsWith('.scenarios.json'))
|
|
40
|
+
continue;
|
|
41
|
+
const path = join(this.dir, file);
|
|
42
|
+
try {
|
|
43
|
+
const flow = JSON.parse(readFileSync(path, 'utf8'));
|
|
44
|
+
flows.push(this.withSidecar(flow));
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.warn(`[flowStore] skipping unparseable ${file}: ${String(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return flows;
|
|
51
|
+
}
|
|
52
|
+
/** Load a single flow by id, or undefined when its file is missing/unparseable. */
|
|
53
|
+
load(id) {
|
|
54
|
+
const path = join(this.dir, `${id}.json`);
|
|
55
|
+
if (!existsSync(path))
|
|
56
|
+
return undefined;
|
|
57
|
+
try {
|
|
58
|
+
const flow = JSON.parse(readFileSync(path, 'utf8'));
|
|
59
|
+
return this.withSidecar(flow);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.warn(`[flowStore] skipping unparseable ${id}.json: ${String(err)}`);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Persist a flow. Its id must be a non-empty string and names the file. */
|
|
67
|
+
save(flow) {
|
|
68
|
+
if (!flow || typeof flow.id !== 'string' || flow.id.length === 0) {
|
|
69
|
+
throw new Error('Flow must have a non-empty string id');
|
|
70
|
+
}
|
|
71
|
+
const path = join(this.dir, `${flow.id}.json`);
|
|
72
|
+
if (this.sidecars) {
|
|
73
|
+
const { scenarios, ...rest } = flow;
|
|
74
|
+
writeFileSync(path, `${JSON.stringify(rest, null, 2)}\n`, 'utf8');
|
|
75
|
+
const sidecar = this.sidecarPath(flow.id);
|
|
76
|
+
if (scenarios && scenarios.length > 0) {
|
|
77
|
+
writeFileSync(sidecar, `${JSON.stringify(scenarios, null, 2)}\n`, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
else if (existsSync(sidecar)) {
|
|
80
|
+
unlinkSync(sidecar);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
writeFileSync(path, `${JSON.stringify(flow, null, 2)}\n`, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
/** Delete a flow file (and any scenario sidecar). Returns false when it did not exist. */
|
|
87
|
+
remove(id) {
|
|
88
|
+
const sidecar = this.sidecarPath(id);
|
|
89
|
+
if (existsSync(sidecar))
|
|
90
|
+
unlinkSync(sidecar);
|
|
91
|
+
const path = join(this.dir, `${id}.json`);
|
|
92
|
+
if (!existsSync(path))
|
|
93
|
+
return false;
|
|
94
|
+
unlinkSync(path);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
/** Seed the given flows only when the directory holds no json files yet. */
|
|
98
|
+
seedIfEmpty(flows) {
|
|
99
|
+
const hasJson = readdirSync(this.dir).some((f) => f.endsWith('.json') && !f.endsWith('.scenarios.json'));
|
|
100
|
+
if (hasJson)
|
|
101
|
+
return;
|
|
102
|
+
for (const flow of flows)
|
|
103
|
+
this.save(flow);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Seed only flows the store doesn't know yet (by id). Existing copies —
|
|
107
|
+
* possibly user-edited — are never overwritten, so new built-in example
|
|
108
|
+
* flows appear after an upgrade without clobbering anything.
|
|
109
|
+
*/
|
|
110
|
+
seedMissing(flows) {
|
|
111
|
+
const known = new Set(this.list().map((f) => f.id));
|
|
112
|
+
for (const flow of flows) {
|
|
113
|
+
if (!known.has(flow.id))
|
|
114
|
+
this.save(flow);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RequestHandler } from 'express';
|
|
2
|
+
import { type AuthPolicy, type WorkflowDefinition } from '../src/engine/index.js';
|
|
3
|
+
import type { RunRegistry } from './runRegistry.js';
|
|
4
|
+
import type { VerifierRegistry } from './auth/verifiers.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build the Express handler for a single routed operation: request in
|
|
7
|
+
* ({params, query, body, headers}), schema-validate the body, run the
|
|
8
|
+
* operation through the SAME engine path the studio "Run" uses
|
|
9
|
+
* (`RunRegistry`), map the finished run to an HTTP response.
|
|
10
|
+
*
|
|
11
|
+
* The run input is the request body's own fields spread at the top level
|
|
12
|
+
* (so `from('input', 'ppv')` resolves a domain field posted in the body),
|
|
13
|
+
* PLUS the full request shape (`params`/`query`/`body`/`headers`/`user`)
|
|
14
|
+
* layered on top — the latter always wins on key collisions, and ops that
|
|
15
|
+
* read `input.body.x` keep working unchanged.
|
|
16
|
+
*/
|
|
17
|
+
/** The environment values a routed operation runs with — resolved once at
|
|
18
|
+
* boot from the project's default environment (see server/index.ts). Falls
|
|
19
|
+
* back to empty secrets/vars when omitted (e.g. in unit tests). */
|
|
20
|
+
export interface OperationRunEnv {
|
|
21
|
+
secrets: Record<string, string>;
|
|
22
|
+
vars: Record<string, string>;
|
|
23
|
+
environment: string;
|
|
24
|
+
safeMode: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function makeOperationHandler(deps: {
|
|
27
|
+
runs: RunRegistry;
|
|
28
|
+
op: WorkflowDefinition;
|
|
29
|
+
env?: OperationRunEnv;
|
|
30
|
+
policy?: AuthPolicy | null;
|
|
31
|
+
verifiers?: VerifierRegistry;
|
|
32
|
+
}): RequestHandler;
|