@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,620 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
deleteEnvironmentSecret,
|
|
7
|
+
loadEnvironments,
|
|
8
|
+
resolveRunSafety,
|
|
9
|
+
setEnvironmentAuth,
|
|
10
|
+
setEnvironmentSecret,
|
|
11
|
+
type EnvironmentDefinition,
|
|
12
|
+
} from './environments';
|
|
13
|
+
|
|
14
|
+
let dir: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
dir = mkdtempSync(join(tmpdir(), 'emberflow-env-test-'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function write(name: string, contents: unknown): void {
|
|
25
|
+
writeFileSync(join(dir, name), typeof contents === 'string' ? contents : JSON.stringify(contents));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('loadEnvironments', () => {
|
|
29
|
+
it('loads a well-formed environments file', () => {
|
|
30
|
+
write('emberflow.environments.json', {
|
|
31
|
+
defaultEnvironment: 'local',
|
|
32
|
+
environments: {
|
|
33
|
+
local: { vars: { A: '1' }, secrets: { B: '2' } },
|
|
34
|
+
prod: { protected: true, vars: {}, secrets: {} },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const result = loadEnvironments(dir);
|
|
38
|
+
expect(result.defaultEnvironment).toBe('local');
|
|
39
|
+
expect(result.environments.local).toEqual({ vars: { A: '1' }, secrets: { B: '2' } });
|
|
40
|
+
expect(result.environments.prod).toEqual({ protected: true, vars: {}, secrets: {} });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('fills in missing vars/secrets objects', () => {
|
|
44
|
+
write('emberflow.environments.json', {
|
|
45
|
+
defaultEnvironment: 'local',
|
|
46
|
+
environments: { local: {} },
|
|
47
|
+
});
|
|
48
|
+
const result = loadEnvironments(dir);
|
|
49
|
+
expect(result.environments.local).toEqual({ vars: {}, secrets: {} });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('falls back to a synthesized local environment when only secrets.json exists', () => {
|
|
53
|
+
write('emberflow.secrets.json', { API_KEY: 'shh' });
|
|
54
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
55
|
+
const result = loadEnvironments(dir);
|
|
56
|
+
logSpy.mockRestore();
|
|
57
|
+
expect(result).toEqual({
|
|
58
|
+
defaultEnvironment: 'local',
|
|
59
|
+
environments: { local: { vars: {}, secrets: { API_KEY: 'shh' } } },
|
|
60
|
+
// Legacy secrets are a deliberate setup — this project boots real.
|
|
61
|
+
configured: true,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('logs a hint when falling back to the legacy secrets file', () => {
|
|
66
|
+
write('emberflow.secrets.json', {});
|
|
67
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
68
|
+
loadEnvironments(dir);
|
|
69
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(logSpy.mock.calls[0][0]).toContain('emberflow.environments.json');
|
|
71
|
+
logSpy.mockRestore();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('defaults to an empty local environment when neither file exists', () => {
|
|
75
|
+
const result = loadEnvironments(dir);
|
|
76
|
+
expect(result).toEqual({
|
|
77
|
+
defaultEnvironment: 'local',
|
|
78
|
+
environments: { local: { vars: {}, secrets: {} } },
|
|
79
|
+
// Nothing was ever configured — drives the default-mock boot.
|
|
80
|
+
configured: false,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('prefers the environments file over the legacy secrets file when both exist', () => {
|
|
85
|
+
write('emberflow.secrets.json', { LEGACY: 'x' });
|
|
86
|
+
write('emberflow.environments.json', {
|
|
87
|
+
defaultEnvironment: 'dev',
|
|
88
|
+
environments: { dev: { vars: {}, secrets: {} } },
|
|
89
|
+
});
|
|
90
|
+
const result = loadEnvironments(dir);
|
|
91
|
+
expect(result.defaultEnvironment).toBe('dev');
|
|
92
|
+
expect(result.environments.LEGACY).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('throws a clear error for invalid JSON', () => {
|
|
96
|
+
write('emberflow.environments.json', '{ not json');
|
|
97
|
+
expect(() => loadEnvironments(dir)).toThrow(/not valid JSON/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('throws when the top level is not an object', () => {
|
|
101
|
+
write('emberflow.environments.json', ['nope']);
|
|
102
|
+
expect(() => loadEnvironments(dir)).toThrow(/expected a JSON object/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('throws when defaultEnvironment is missing', () => {
|
|
106
|
+
write('emberflow.environments.json', { environments: { local: {} } });
|
|
107
|
+
expect(() => loadEnvironments(dir)).toThrow(/"defaultEnvironment" must be a non-empty string/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('throws when environments is missing', () => {
|
|
111
|
+
write('emberflow.environments.json', { defaultEnvironment: 'local' });
|
|
112
|
+
expect(() => loadEnvironments(dir)).toThrow(/"environments" must be an object/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('throws when environments is empty', () => {
|
|
116
|
+
write('emberflow.environments.json', { defaultEnvironment: 'local', environments: {} });
|
|
117
|
+
expect(() => loadEnvironments(dir)).toThrow(/at least one environment/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('throws when defaultEnvironment does not name a defined environment', () => {
|
|
121
|
+
write('emberflow.environments.json', {
|
|
122
|
+
defaultEnvironment: 'missing',
|
|
123
|
+
environments: { local: {} },
|
|
124
|
+
});
|
|
125
|
+
expect(() => loadEnvironments(dir)).toThrow(/is not one of the defined environments/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('throws when an environment vars value is not a string map', () => {
|
|
129
|
+
write('emberflow.environments.json', {
|
|
130
|
+
defaultEnvironment: 'local',
|
|
131
|
+
environments: { local: { vars: { A: 1 }, secrets: {} } },
|
|
132
|
+
});
|
|
133
|
+
expect(() => loadEnvironments(dir)).toThrow(/environments\.local\.vars must be an object of string -> string/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('throws when an environment secrets value is neither a name list nor a string map', () => {
|
|
137
|
+
write('emberflow.environments.json', {
|
|
138
|
+
defaultEnvironment: 'local',
|
|
139
|
+
environments: { local: { vars: {}, secrets: { A: false } } },
|
|
140
|
+
});
|
|
141
|
+
expect(() => loadEnvironments(dir)).toThrow(
|
|
142
|
+
/environments\.local\.secrets must be an array of key names or an object of string -> string/,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('rejects a secrets list containing a non-string element', () => {
|
|
147
|
+
write('emberflow.environments.json', {
|
|
148
|
+
defaultEnvironment: 'local',
|
|
149
|
+
environments: { local: { vars: {}, secrets: ['OK', 3] } },
|
|
150
|
+
});
|
|
151
|
+
expect(() => loadEnvironments(dir)).toThrow(
|
|
152
|
+
/environments\.local\.secrets must be an array of key names/,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('throws when protected is not a boolean', () => {
|
|
157
|
+
write('emberflow.environments.json', {
|
|
158
|
+
defaultEnvironment: 'local',
|
|
159
|
+
environments: { local: { vars: {}, secrets: {}, protected: 'yes' } },
|
|
160
|
+
});
|
|
161
|
+
expect(() => loadEnvironments(dir)).toThrow(/environments\.local\.protected must be a boolean/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('accepts an auth.attach.prefix string', () => {
|
|
165
|
+
write('emberflow.environments.json', {
|
|
166
|
+
defaultEnvironment: 'local',
|
|
167
|
+
environments: {
|
|
168
|
+
local: {
|
|
169
|
+
vars: {},
|
|
170
|
+
secrets: {},
|
|
171
|
+
auth: { attach: { as: 'header', name: 'Authorization', secretRef: 'S', prefix: 'Bearer ' } },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const result = loadEnvironments(dir);
|
|
176
|
+
expect(result.environments.local.auth?.attach.prefix).toBe('Bearer ');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('throws when auth.attach.prefix is not a string', () => {
|
|
180
|
+
write('emberflow.environments.json', {
|
|
181
|
+
defaultEnvironment: 'local',
|
|
182
|
+
environments: {
|
|
183
|
+
local: {
|
|
184
|
+
vars: {},
|
|
185
|
+
secrets: {},
|
|
186
|
+
auth: { attach: { as: 'header', name: 'Authorization', secretRef: 'S', prefix: 123 } },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
expect(() => loadEnvironments(dir)).toThrow(/auth\.attach\.prefix must be a string/);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('value/structure split: secrets file resolution', () => {
|
|
195
|
+
it('loads the new shape: name-list in environments.json + values in secrets.json', () => {
|
|
196
|
+
write('emberflow.environments.json', {
|
|
197
|
+
defaultEnvironment: 'dev',
|
|
198
|
+
environments: { dev: { vars: { A: '1' }, secrets: ['API_KEY', 'DB_URL'] } },
|
|
199
|
+
});
|
|
200
|
+
write('emberflow.secrets.json', { dev: { API_KEY: 'k', DB_URL: 'postgres://x' } });
|
|
201
|
+
const result = loadEnvironments(dir);
|
|
202
|
+
expect(result.environments.dev.secrets).toEqual({ API_KEY: 'k', DB_URL: 'postgres://x' });
|
|
203
|
+
expect(result.environments.dev.vars).toEqual({ A: '1' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("resolves a declared-but-unvalued key to '' so requireSecret still throws", () => {
|
|
207
|
+
write('emberflow.environments.json', {
|
|
208
|
+
defaultEnvironment: 'dev',
|
|
209
|
+
environments: { dev: { vars: {}, secrets: ['API_KEY', 'MISSING'] } },
|
|
210
|
+
});
|
|
211
|
+
write('emberflow.secrets.json', { dev: { API_KEY: 'k' } });
|
|
212
|
+
const result = loadEnvironments(dir);
|
|
213
|
+
expect(result.environments.dev.secrets).toEqual({ API_KEY: 'k', MISSING: '' });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('merges names from environments.json with keys present only in the secrets file', () => {
|
|
217
|
+
write('emberflow.environments.json', {
|
|
218
|
+
defaultEnvironment: 'dev',
|
|
219
|
+
environments: { dev: { vars: {}, secrets: ['DECLARED'] } },
|
|
220
|
+
});
|
|
221
|
+
write('emberflow.secrets.json', { dev: { DECLARED: 'a', EXTRA: 'b' } });
|
|
222
|
+
const result = loadEnvironments(dir);
|
|
223
|
+
expect(result.environments.dev.secrets).toEqual({ DECLARED: 'a', EXTRA: 'b' });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('treats a legacy flat secrets.json as env "local" (no environments.json)', () => {
|
|
227
|
+
write('emberflow.secrets.json', { API_KEY: 'shh' });
|
|
228
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
229
|
+
const result = loadEnvironments(dir);
|
|
230
|
+
logSpy.mockRestore();
|
|
231
|
+
expect(result.environments.local.secrets).toEqual({ API_KEY: 'shh' });
|
|
232
|
+
expect(result.configured).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('reads new-shape secrets keyed by env name even without environments.json', () => {
|
|
236
|
+
write('emberflow.secrets.json', { local: { A: '1' }, prod: { B: '2' } });
|
|
237
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
238
|
+
const result = loadEnvironments(dir);
|
|
239
|
+
logSpy.mockRestore();
|
|
240
|
+
expect(result.environments.local.secrets).toEqual({ A: '1' });
|
|
241
|
+
expect(result.environments.prod.secrets).toEqual({ B: '2' });
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('$ENV indirection', () => {
|
|
246
|
+
it('resolves $ENV:VAR to process.env at load', () => {
|
|
247
|
+
process.env.EMBERFLOW_TEST_SECRET = 'from-env';
|
|
248
|
+
write('emberflow.environments.json', {
|
|
249
|
+
defaultEnvironment: 'dev',
|
|
250
|
+
environments: { dev: { vars: {}, secrets: ['TOK'] } },
|
|
251
|
+
});
|
|
252
|
+
write('emberflow.secrets.json', { dev: { TOK: '$ENV:EMBERFLOW_TEST_SECRET' } });
|
|
253
|
+
try {
|
|
254
|
+
const result = loadEnvironments(dir);
|
|
255
|
+
expect(result.environments.dev.secrets.TOK).toBe('from-env');
|
|
256
|
+
} finally {
|
|
257
|
+
delete process.env.EMBERFLOW_TEST_SECRET;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("resolves a missing $ENV var to '' and warns naming the variable", () => {
|
|
262
|
+
delete process.env.EMBERFLOW_TEST_ABSENT;
|
|
263
|
+
write('emberflow.environments.json', {
|
|
264
|
+
defaultEnvironment: 'dev',
|
|
265
|
+
environments: { dev: { vars: {}, secrets: ['TOK'] } },
|
|
266
|
+
});
|
|
267
|
+
write('emberflow.secrets.json', { dev: { TOK: '$ENV:EMBERFLOW_TEST_ABSENT' } });
|
|
268
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
269
|
+
const result = loadEnvironments(dir);
|
|
270
|
+
expect(result.environments.dev.secrets.TOK).toBe('');
|
|
271
|
+
expect(warnSpy.mock.calls.some((c) => String(c[0]).includes('EMBERFLOW_TEST_ABSENT'))).toBe(true);
|
|
272
|
+
warnSpy.mockRestore();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('auto-migration of old inline-value format', () => {
|
|
277
|
+
it('moves values into secrets.json, rewrites environments.json name-only, keeps a .bak', () => {
|
|
278
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
279
|
+
write('emberflow.environments.json', {
|
|
280
|
+
defaultEnvironment: 'dev',
|
|
281
|
+
environments: {
|
|
282
|
+
dev: { vars: { A: '1' }, secrets: { API_KEY: 'realkey', DB_URL: 'realdb' } },
|
|
283
|
+
prod: { protected: true, vars: {}, secrets: { API_KEY: 'prodkey' } },
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
const result = loadEnvironments(dir);
|
|
287
|
+
|
|
288
|
+
// Values are resolved correctly post-migration.
|
|
289
|
+
expect(result.environments.dev.secrets).toEqual({ API_KEY: 'realkey', DB_URL: 'realdb' });
|
|
290
|
+
expect(result.environments.prod.secrets).toEqual({ API_KEY: 'prodkey' });
|
|
291
|
+
|
|
292
|
+
// environments.json is now structure-only (name lists, no values).
|
|
293
|
+
const envFile = JSON.parse(readFileSync(join(dir, 'emberflow.environments.json'), 'utf8'));
|
|
294
|
+
expect(envFile.environments.dev.secrets.sort()).toEqual(['API_KEY', 'DB_URL']);
|
|
295
|
+
expect(envFile.environments.prod.secrets).toEqual(['API_KEY']);
|
|
296
|
+
|
|
297
|
+
// Values live in the secrets file (new nested shape).
|
|
298
|
+
const secretsFile = JSON.parse(readFileSync(join(dir, 'emberflow.secrets.json'), 'utf8'));
|
|
299
|
+
expect(secretsFile).toEqual({
|
|
300
|
+
dev: { API_KEY: 'realkey', DB_URL: 'realdb' },
|
|
301
|
+
prod: { API_KEY: 'prodkey' },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// A .bak of the original environments.json was kept.
|
|
305
|
+
expect(existsSync(join(dir, 'emberflow.environments.json.bak'))).toBe(true);
|
|
306
|
+
const bak = JSON.parse(readFileSync(join(dir, 'emberflow.environments.json.bak'), 'utf8'));
|
|
307
|
+
expect(bak.environments.dev.secrets).toEqual({ API_KEY: 'realkey', DB_URL: 'realdb' });
|
|
308
|
+
|
|
309
|
+
// Exactly one migration info line.
|
|
310
|
+
expect(logSpy.mock.calls.filter((c) => String(c[0]).includes('Migrated')).length).toBe(1);
|
|
311
|
+
logSpy.mockRestore();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('is idempotent: a second load does not migrate again or rewrite the .bak', () => {
|
|
315
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
316
|
+
write('emberflow.environments.json', {
|
|
317
|
+
defaultEnvironment: 'dev',
|
|
318
|
+
environments: { dev: { vars: {}, secrets: { API_KEY: 'realkey' } } },
|
|
319
|
+
});
|
|
320
|
+
loadEnvironments(dir);
|
|
321
|
+
const bakAfterFirst = readFileSync(join(dir, 'emberflow.environments.json.bak'), 'utf8');
|
|
322
|
+
logSpy.mockClear();
|
|
323
|
+
const result = loadEnvironments(dir);
|
|
324
|
+
// No second migration log.
|
|
325
|
+
expect(logSpy.mock.calls.filter((c) => String(c[0]).includes('Migrated')).length).toBe(0);
|
|
326
|
+
// .bak unchanged (still the original).
|
|
327
|
+
expect(readFileSync(join(dir, 'emberflow.environments.json.bak'), 'utf8')).toBe(bakAfterFirst);
|
|
328
|
+
expect(result.environments.dev.secrets).toEqual({ API_KEY: 'realkey' });
|
|
329
|
+
logSpy.mockRestore();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('writes the migrated secrets file with 0600 permissions', () => {
|
|
333
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
334
|
+
write('emberflow.environments.json', {
|
|
335
|
+
defaultEnvironment: 'dev',
|
|
336
|
+
environments: { dev: { vars: {}, secrets: { API_KEY: 'realkey' } } },
|
|
337
|
+
});
|
|
338
|
+
loadEnvironments(dir);
|
|
339
|
+
const mode = statSync(join(dir, 'emberflow.secrets.json')).mode & 0o777;
|
|
340
|
+
expect(mode).toBe(0o600);
|
|
341
|
+
vi.restoreAllMocks();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('secrets-file writers persist values out-of-band and chmod 0600', () => {
|
|
346
|
+
function readJson(name: string): any {
|
|
347
|
+
return JSON.parse(readFileSync(join(dir, name), 'utf8'));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
it('setEnvironmentSecret writes the value to secrets.json (0600) and the name to environments.json', async () => {
|
|
351
|
+
write('emberflow.environments.json', {
|
|
352
|
+
defaultEnvironment: 'dev',
|
|
353
|
+
environments: { dev: { vars: {}, secrets: [] } },
|
|
354
|
+
});
|
|
355
|
+
await setEnvironmentSecret(dir, 'dev', 'API_KEY', 'topsecret');
|
|
356
|
+
|
|
357
|
+
// Value is NOT in environments.json — only the key name is.
|
|
358
|
+
const envFile = readJson('emberflow.environments.json');
|
|
359
|
+
expect(envFile.environments.dev.secrets).toEqual(['API_KEY']);
|
|
360
|
+
expect(JSON.stringify(envFile)).not.toContain('topsecret');
|
|
361
|
+
|
|
362
|
+
// Value IS in the secrets file, under the env name, at 0600.
|
|
363
|
+
expect(readJson('emberflow.secrets.json')).toEqual({ dev: { API_KEY: 'topsecret' } });
|
|
364
|
+
expect(statSync(join(dir, 'emberflow.secrets.json')).mode & 0o777).toBe(0o600);
|
|
365
|
+
|
|
366
|
+
// And it resolves back through the loader.
|
|
367
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
368
|
+
expect(loadEnvironments(dir).environments.dev.secrets).toEqual({ API_KEY: 'topsecret' });
|
|
369
|
+
vi.restoreAllMocks();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('deleteEnvironmentSecret removes both the name and the value', async () => {
|
|
373
|
+
write('emberflow.environments.json', {
|
|
374
|
+
defaultEnvironment: 'dev',
|
|
375
|
+
environments: { dev: { vars: {}, secrets: ['API_KEY', 'OTHER'] } },
|
|
376
|
+
});
|
|
377
|
+
write('emberflow.secrets.json', { dev: { API_KEY: 'k', OTHER: 'o' } });
|
|
378
|
+
await deleteEnvironmentSecret(dir, 'dev', 'API_KEY');
|
|
379
|
+
expect(readJson('emberflow.environments.json').environments.dev.secrets).toEqual(['OTHER']);
|
|
380
|
+
expect(readJson('emberflow.secrets.json')).toEqual({ dev: { OTHER: 'o' } });
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('secrets-file hardening warnings', () => {
|
|
385
|
+
it('auto-tightens a group/other-readable secrets file to 0600 and logs an info line', () => {
|
|
386
|
+
write('emberflow.environments.json', {
|
|
387
|
+
defaultEnvironment: 'dev',
|
|
388
|
+
environments: { dev: { vars: {}, secrets: ['API_KEY'] } },
|
|
389
|
+
});
|
|
390
|
+
// write() uses default 0644 — group/other readable.
|
|
391
|
+
const secretsPath = join(dir, 'emberflow.secrets.json');
|
|
392
|
+
writeFileSync(secretsPath, JSON.stringify({ dev: { API_KEY: 'k' } }), {
|
|
393
|
+
mode: 0o644,
|
|
394
|
+
});
|
|
395
|
+
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
396
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
397
|
+
loadEnvironments(dir);
|
|
398
|
+
expect(infoSpy.mock.calls.some((c) => /tightened emberflow\.secrets\.json to 0600/.test(String(c[0])))).toBe(
|
|
399
|
+
true,
|
|
400
|
+
);
|
|
401
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
402
|
+
expect(statSync(secretsPath).mode & 0o777).toBe(0o600);
|
|
403
|
+
infoSpy.mockRestore();
|
|
404
|
+
warnSpy.mockRestore();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('resolveRunSafety', () => {
|
|
409
|
+
const open: EnvironmentDefinition = { vars: {}, secrets: {} };
|
|
410
|
+
const guarded: EnvironmentDefinition = { vars: {}, secrets: {}, protected: true };
|
|
411
|
+
|
|
412
|
+
it('defaults to unsafe (false) on a non-protected environment', () => {
|
|
413
|
+
expect(resolveRunSafety('local', open, {})).toEqual({ ok: true, safeMode: false });
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('defaults to safe (true) on a protected environment', () => {
|
|
417
|
+
expect(resolveRunSafety('prod', guarded, {})).toEqual({ ok: true, safeMode: true });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('honors an explicit safeMode:true on any environment', () => {
|
|
421
|
+
expect(resolveRunSafety('local', open, { safeMode: true })).toEqual({ ok: true, safeMode: true });
|
|
422
|
+
expect(resolveRunSafety('prod', guarded, { safeMode: true })).toEqual({ ok: true, safeMode: true });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('honors an explicit safeMode:false on a non-protected environment', () => {
|
|
426
|
+
expect(resolveRunSafety('local', open, { safeMode: false })).toEqual({ ok: true, safeMode: false });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('refuses safeMode:false on a protected environment without a matching confirm', () => {
|
|
430
|
+
expect(resolveRunSafety('prod', guarded, { safeMode: false })).toEqual({
|
|
431
|
+
ok: false,
|
|
432
|
+
error: "unsafe run on protected environment 'prod' requires confirm",
|
|
433
|
+
});
|
|
434
|
+
expect(resolveRunSafety('prod', guarded, { safeMode: false, confirm: 'staging' })).toEqual({
|
|
435
|
+
ok: false,
|
|
436
|
+
error: "unsafe run on protected environment 'prod' requires confirm",
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('allows safeMode:false on a protected environment with a matching confirm', () => {
|
|
441
|
+
expect(resolveRunSafety('prod', guarded, { safeMode: false, confirm: 'prod' })).toEqual({
|
|
442
|
+
ok: true,
|
|
443
|
+
safeMode: false,
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe('setEnvironmentSecret', () => {
|
|
449
|
+
it('sets a secret on an existing environment and persists it', async () => {
|
|
450
|
+
write('emberflow.environments.json', {
|
|
451
|
+
defaultEnvironment: 'dev',
|
|
452
|
+
environments: {
|
|
453
|
+
dev: { vars: {}, secrets: {} },
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
await setEnvironmentSecret(dir, 'dev', 'sessionCookie', 'abc');
|
|
457
|
+
const result = loadEnvironments(dir);
|
|
458
|
+
expect(result.environments.dev.secrets.sessionCookie).toBe('abc');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('throws when the environment is unknown', async () => {
|
|
462
|
+
write('emberflow.environments.json', {
|
|
463
|
+
defaultEnvironment: 'dev',
|
|
464
|
+
environments: {
|
|
465
|
+
dev: { vars: {}, secrets: {} },
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
await expect(setEnvironmentSecret(dir, 'staging', 'sessionCookie', 'abc')).rejects.toThrow();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('serializes concurrent writes so two different keys both survive (no last-write-wins clobber)', async () => {
|
|
472
|
+
write('emberflow.environments.json', {
|
|
473
|
+
defaultEnvironment: 'dev',
|
|
474
|
+
environments: {
|
|
475
|
+
dev: { vars: {}, secrets: {} },
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
// Fire both without awaiting between them — a naive sync read-modify-write
|
|
479
|
+
// would have the second call clobber the first's in-memory read.
|
|
480
|
+
const p1 = setEnvironmentSecret(dir, 'dev', 'keyA', 'valueA');
|
|
481
|
+
const p2 = setEnvironmentSecret(dir, 'dev', 'keyB', 'valueB');
|
|
482
|
+
await Promise.all([p1, p2]);
|
|
483
|
+
const result = loadEnvironments(dir);
|
|
484
|
+
expect(result.environments.dev.secrets.keyA).toBe('valueA');
|
|
485
|
+
expect(result.environments.dev.secrets.keyB).toBe('valueB');
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe('setEnvironmentAuth', () => {
|
|
490
|
+
it('sets auth on an existing environment and persists it', async () => {
|
|
491
|
+
write('emberflow.environments.json', {
|
|
492
|
+
defaultEnvironment: 'dev',
|
|
493
|
+
environments: {
|
|
494
|
+
dev: { vars: {}, secrets: {} },
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
await setEnvironmentAuth(dir, 'dev', {
|
|
498
|
+
attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
499
|
+
});
|
|
500
|
+
const result = loadEnvironments(dir);
|
|
501
|
+
expect(result.environments.dev.auth).toEqual({
|
|
502
|
+
attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('rejects an invalid attach.as with a message naming the problem', async () => {
|
|
507
|
+
write('emberflow.environments.json', {
|
|
508
|
+
defaultEnvironment: 'dev',
|
|
509
|
+
environments: {
|
|
510
|
+
dev: { vars: {}, secrets: {} },
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
await expect(
|
|
514
|
+
setEnvironmentAuth(dir, 'dev', {
|
|
515
|
+
attach: { as: 'bogus' as unknown as 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
516
|
+
}),
|
|
517
|
+
).rejects.toThrow(/auth\.attach\.as/);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('clears auth when passed null', async () => {
|
|
521
|
+
write('emberflow.environments.json', {
|
|
522
|
+
defaultEnvironment: 'dev',
|
|
523
|
+
environments: {
|
|
524
|
+
dev: {
|
|
525
|
+
vars: {},
|
|
526
|
+
secrets: {},
|
|
527
|
+
auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
await setEnvironmentAuth(dir, 'dev', null);
|
|
532
|
+
const result = loadEnvironments(dir);
|
|
533
|
+
expect(result.environments.dev.auth).toBeUndefined();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('throws when the environment is unknown', async () => {
|
|
537
|
+
write('emberflow.environments.json', {
|
|
538
|
+
defaultEnvironment: 'dev',
|
|
539
|
+
environments: {
|
|
540
|
+
dev: { vars: {}, secrets: {} },
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
await expect(
|
|
544
|
+
setEnvironmentAuth(dir, 'staging', {
|
|
545
|
+
attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
546
|
+
}),
|
|
547
|
+
).rejects.toThrow();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('preserves the environment other fields (secrets, vars, protected)', async () => {
|
|
551
|
+
write('emberflow.environments.json', {
|
|
552
|
+
defaultEnvironment: 'dev',
|
|
553
|
+
environments: {
|
|
554
|
+
dev: { vars: { region: 'us' }, secrets: { apiKey: 'shh' }, protected: true },
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
await setEnvironmentAuth(dir, 'dev', {
|
|
558
|
+
attach: { as: 'header', name: 'X-Auth', secretRef: 'apiKey' },
|
|
559
|
+
});
|
|
560
|
+
const result = loadEnvironments(dir);
|
|
561
|
+
expect(result.environments.dev.vars).toEqual({ region: 'us' });
|
|
562
|
+
expect(result.environments.dev.secrets).toEqual({ apiKey: 'shh' });
|
|
563
|
+
expect(result.environments.dev.protected).toBe(true);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('serializes concurrent setEnvironmentAuth + setEnvironmentSecret so both survive (shared queue)', async () => {
|
|
567
|
+
write('emberflow.environments.json', {
|
|
568
|
+
defaultEnvironment: 'dev',
|
|
569
|
+
environments: {
|
|
570
|
+
dev: { vars: {}, secrets: {} },
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
const p1 = setEnvironmentAuth(dir, 'dev', {
|
|
574
|
+
attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
575
|
+
});
|
|
576
|
+
const p2 = setEnvironmentSecret(dir, 'dev', 'sessionCookie', 'abc');
|
|
577
|
+
await Promise.all([p1, p2]);
|
|
578
|
+
const result = loadEnvironments(dir);
|
|
579
|
+
expect(result.environments.dev.auth).toEqual({
|
|
580
|
+
attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
581
|
+
});
|
|
582
|
+
expect(result.environments.dev.secrets.sessionCookie).toBe('abc');
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe('deleteEnvironmentSecret', () => {
|
|
587
|
+
it('removes an existing secret', async () => {
|
|
588
|
+
write('emberflow.environments.json', {
|
|
589
|
+
defaultEnvironment: 'dev',
|
|
590
|
+
environments: {
|
|
591
|
+
dev: { vars: {}, secrets: { sessionCookie: 'abc' } },
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
await deleteEnvironmentSecret(dir, 'dev', 'sessionCookie');
|
|
595
|
+
const result = loadEnvironments(dir);
|
|
596
|
+
expect(result.environments.dev.secrets.sessionCookie).toBeUndefined();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('is a no-op when the key is missing', async () => {
|
|
600
|
+
write('emberflow.environments.json', {
|
|
601
|
+
defaultEnvironment: 'dev',
|
|
602
|
+
environments: {
|
|
603
|
+
dev: { vars: {}, secrets: { other: 'x' } },
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
await expect(deleteEnvironmentSecret(dir, 'dev', 'sessionCookie')).resolves.toBeUndefined();
|
|
607
|
+
const result = loadEnvironments(dir);
|
|
608
|
+
expect(result.environments.dev.secrets).toEqual({ other: 'x' });
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('throws when the environment is unknown', async () => {
|
|
612
|
+
write('emberflow.environments.json', {
|
|
613
|
+
defaultEnvironment: 'dev',
|
|
614
|
+
environments: {
|
|
615
|
+
dev: { vars: {}, secrets: {} },
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
await expect(deleteEnvironmentSecret(dir, 'staging', 'sessionCookie')).rejects.toThrow();
|
|
619
|
+
});
|
|
620
|
+
});
|