@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,324 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, rmSync, renameSync, readFileSync as rf, existsSync as ex } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { ApiStore } from './apiStore';
|
|
6
|
+
|
|
7
|
+
let root: string;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
root = join(tmpdir(), `apistore-${Math.random().toString(36).slice(2)}`);
|
|
10
|
+
mkdirSync(join(root, 'claims', 'claims'), { recursive: true });
|
|
11
|
+
mkdirSync(join(root, 'billing'), { recursive: true });
|
|
12
|
+
writeFileSync(
|
|
13
|
+
join(root, 'claims', 'claims', 'create.json'),
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
id: 'create-claim',
|
|
16
|
+
name: 'Create claim',
|
|
17
|
+
version: 1,
|
|
18
|
+
nodes: [],
|
|
19
|
+
edges: [],
|
|
20
|
+
http: { method: 'POST', path: '/claims' },
|
|
21
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
22
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(root, 'claims', 'claims', 'create.scenarios.json'),
|
|
27
|
+
JSON.stringify([{ name: 'happy', input: { body: {} } }]),
|
|
28
|
+
);
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(root, 'billing', 'charge.json'),
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
id: 'charge',
|
|
33
|
+
name: 'Charge',
|
|
34
|
+
version: 1,
|
|
35
|
+
nodes: [],
|
|
36
|
+
edges: [],
|
|
37
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
38
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
43
|
+
|
|
44
|
+
describe('ApiStore read', () => {
|
|
45
|
+
it('lists all operations across the tree with ids and http', () => {
|
|
46
|
+
const store = new ApiStore(root);
|
|
47
|
+
const ids = store.list().map((o) => o.id).sort();
|
|
48
|
+
expect(ids).toEqual(['charge', 'create-claim']);
|
|
49
|
+
const create = store.list().find((o) => o.id === 'create-claim')!;
|
|
50
|
+
expect(create.http).toEqual({ method: 'POST', path: '/claims' });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('loads a single operation by id and merges its scenarios sidecar', () => {
|
|
54
|
+
const store = new ApiStore(root);
|
|
55
|
+
const op = store.load('create-claim')!;
|
|
56
|
+
expect(op.name).toBe('Create claim');
|
|
57
|
+
expect(op.scenarios).toEqual([{ name: 'happy', input: { body: {} } }]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('maps an id to its on-disk relative path', () => {
|
|
61
|
+
const store = new ApiStore(root);
|
|
62
|
+
expect(store.pathOf('create-claim')).toBe('claims/claims/create');
|
|
63
|
+
expect(store.pathOf('charge')).toBe('billing/charge');
|
|
64
|
+
expect(store.pathOf('nope')).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('listSummaries returns each op with its path and http', () => {
|
|
68
|
+
const summaries = new ApiStore(root).listSummaries();
|
|
69
|
+
const create = summaries.find((s) => s.id === 'create-claim')!;
|
|
70
|
+
expect(create.path).toBe('claims/claims/create');
|
|
71
|
+
expect(create.http).toEqual({ method: 'POST', path: '/claims' });
|
|
72
|
+
expect(create.name).toBe('Create claim');
|
|
73
|
+
const charge = summaries.find((s) => s.id === 'charge')!;
|
|
74
|
+
expect(charge.path).toBe('billing/charge');
|
|
75
|
+
expect(charge.http).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('groups operations into an api/folder tree', () => {
|
|
79
|
+
const tree = new ApiStore(root).tree();
|
|
80
|
+
const claims = tree.apis.find((a) => a.name === 'claims')!;
|
|
81
|
+
expect(claims.folders.map((f) => f.name)).toEqual(['claims']);
|
|
82
|
+
expect(claims.folders[0].operations.map((o) => o.id)).toEqual(['create-claim']);
|
|
83
|
+
const billing = tree.apis.find((a) => a.name === 'billing')!;
|
|
84
|
+
expect(billing.operations.map((o) => o.id)).toEqual(['charge']);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('ApiStore write', () => {
|
|
89
|
+
it('saves a new operation into an api/folder with its scenario sidecar', () => {
|
|
90
|
+
const store = new ApiStore(root);
|
|
91
|
+
store.save(
|
|
92
|
+
{
|
|
93
|
+
id: 'del-claim',
|
|
94
|
+
name: 'Delete claim',
|
|
95
|
+
version: 1,
|
|
96
|
+
nodes: [],
|
|
97
|
+
edges: [],
|
|
98
|
+
http: { method: 'DELETE', path: '/claims/:id' },
|
|
99
|
+
scenarios: [{ id: 's', name: 's', input: {} }],
|
|
100
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
101
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
102
|
+
},
|
|
103
|
+
'claims/claims/delete',
|
|
104
|
+
);
|
|
105
|
+
const main = JSON.parse(rf(join(root, 'claims', 'claims', 'delete.json'), 'utf8'));
|
|
106
|
+
expect(main.id).toBe('del-claim');
|
|
107
|
+
expect(main.scenarios).toBeUndefined(); // scenarios live in the sidecar
|
|
108
|
+
expect(ex(join(root, 'claims', 'claims', 'delete.scenarios.json'))).toBe(true);
|
|
109
|
+
expect(store.load('del-claim')!.scenarios).toEqual([{ id: 's', name: 's', input: {} }]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('removes an operation and its sidecar by id', () => {
|
|
113
|
+
const store = new ApiStore(root);
|
|
114
|
+
expect(store.remove('create-claim')).toBe(true);
|
|
115
|
+
expect(store.load('create-claim')).toBeUndefined();
|
|
116
|
+
expect(ex(join(root, 'claims', 'claims', 'create.json'))).toBe(false);
|
|
117
|
+
expect(ex(join(root, 'claims', 'claims', 'create.scenarios.json'))).toBe(false);
|
|
118
|
+
expect(store.remove('nope')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('saves op-level mocks alongside scenarios in an object-shaped sidecar', () => {
|
|
122
|
+
const store = new ApiStore(root);
|
|
123
|
+
store.save(
|
|
124
|
+
{
|
|
125
|
+
id: 'del-claim',
|
|
126
|
+
name: 'Delete claim',
|
|
127
|
+
version: 1,
|
|
128
|
+
nodes: [],
|
|
129
|
+
edges: [],
|
|
130
|
+
scenarios: [{ id: 's', name: 's', input: {} }],
|
|
131
|
+
mocks: { dbNode: { rows: [] } },
|
|
132
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
133
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
134
|
+
},
|
|
135
|
+
'claims/claims/delete',
|
|
136
|
+
);
|
|
137
|
+
const sidecar = JSON.parse(rf(join(root, 'claims', 'claims', 'delete.scenarios.json'), 'utf8'));
|
|
138
|
+
expect(sidecar).toEqual({ scenarios: [{ id: 's', name: 's', input: {} }], mocks: { dbNode: { rows: [] } } });
|
|
139
|
+
const loaded = store.load('del-claim')!;
|
|
140
|
+
expect(loaded.scenarios).toEqual([{ id: 's', name: 's', input: {} }]);
|
|
141
|
+
expect(loaded.mocks).toEqual({ dbNode: { rows: [] } });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('re-saving with scenarios only preserves the sidecar\'s existing top-level mocks untouched', () => {
|
|
145
|
+
const store = new ApiStore(root);
|
|
146
|
+
store.save(
|
|
147
|
+
{
|
|
148
|
+
id: 'del-claim',
|
|
149
|
+
name: 'Delete claim',
|
|
150
|
+
version: 1,
|
|
151
|
+
nodes: [],
|
|
152
|
+
edges: [],
|
|
153
|
+
scenarios: [{ id: 's', name: 's', input: {} }],
|
|
154
|
+
mocks: { dbNode: { rows: [] } },
|
|
155
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
156
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
157
|
+
},
|
|
158
|
+
'claims/claims/delete',
|
|
159
|
+
);
|
|
160
|
+
// Re-save as if a caller edited only scenarios and passed a flow object
|
|
161
|
+
// that never carried `mocks` at all (e.g. a client that doesn't round-trip
|
|
162
|
+
// unknown fields) — the sidecar's mocks must survive untouched.
|
|
163
|
+
store.save(
|
|
164
|
+
{
|
|
165
|
+
id: 'del-claim',
|
|
166
|
+
name: 'Delete claim',
|
|
167
|
+
version: 1,
|
|
168
|
+
nodes: [],
|
|
169
|
+
edges: [],
|
|
170
|
+
scenarios: [{ id: 's', name: 's', input: {} }, { id: 's2', name: 's2', input: {} }],
|
|
171
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
172
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
173
|
+
},
|
|
174
|
+
'claims/claims/delete',
|
|
175
|
+
);
|
|
176
|
+
const loaded = store.load('del-claim')!;
|
|
177
|
+
expect(loaded.scenarios).toEqual([{ id: 's', name: 's', input: {} }, { id: 's2', name: 's2', input: {} }]);
|
|
178
|
+
expect(loaded.mocks).toEqual({ dbNode: { rows: [] } });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('loads a legacy bare-array sidecar (no mocks) exactly as before', () => {
|
|
182
|
+
const store = new ApiStore(root);
|
|
183
|
+
const op = store.load('create-claim')!;
|
|
184
|
+
expect(op.scenarios).toEqual([{ name: 'happy', input: { body: {} } }]);
|
|
185
|
+
expect(op.mocks).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('ApiStore.existsAt', () => {
|
|
190
|
+
it('reports true for an existing op path and false for a free one', () => {
|
|
191
|
+
const store = new ApiStore(root);
|
|
192
|
+
expect(store.existsAt('billing/charge')).toBe(true);
|
|
193
|
+
expect(store.existsAt('billing/refund')).toBe(false);
|
|
194
|
+
expect(store.existsAt('claims/claims/create')).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('ApiStore stale-cache resistance (out-of-band disk edits)', () => {
|
|
199
|
+
it('still finds a renamed op after a warm scan, and finds new ops without reconstruction', () => {
|
|
200
|
+
const store = new ApiStore(root);
|
|
201
|
+
// Warm the index.
|
|
202
|
+
expect(store.load('charge')).toBeDefined();
|
|
203
|
+
|
|
204
|
+
// Rename on disk out-of-band (same in-file id, new path).
|
|
205
|
+
renameSync(join(root, 'billing', 'charge.json'), join(root, 'billing', 'renamed.json'));
|
|
206
|
+
expect(store.load('charge')).toBeDefined();
|
|
207
|
+
expect(store.pathOf('charge')).toBe('billing/renamed');
|
|
208
|
+
|
|
209
|
+
// Add a brand-new op file on disk after construction.
|
|
210
|
+
writeFileSync(
|
|
211
|
+
join(root, 'billing', 'refund.json'),
|
|
212
|
+
JSON.stringify({
|
|
213
|
+
id: 'refund',
|
|
214
|
+
name: 'Refund',
|
|
215
|
+
version: 1,
|
|
216
|
+
nodes: [],
|
|
217
|
+
edges: [],
|
|
218
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
219
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
expect(store.load('refund')).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('warns when two files share the same in-file id', () => {
|
|
226
|
+
writeFileSync(
|
|
227
|
+
join(root, 'billing', 'dup.json'),
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
id: 'charge', // duplicate of billing/charge.json's id
|
|
230
|
+
name: 'Duplicate charge',
|
|
231
|
+
version: 1,
|
|
232
|
+
nodes: [],
|
|
233
|
+
edges: [],
|
|
234
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
235
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
239
|
+
new ApiStore(root).list();
|
|
240
|
+
expect(warn.mock.calls.some(([msg]) => String(msg).includes('duplicate operation id "charge"'))).toBe(true);
|
|
241
|
+
warn.mockRestore();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('warns when saving over a file that holds a different op id', () => {
|
|
245
|
+
const store = new ApiStore(root);
|
|
246
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
247
|
+
store.save(
|
|
248
|
+
{
|
|
249
|
+
id: 'new-id',
|
|
250
|
+
name: 'New name',
|
|
251
|
+
version: 1,
|
|
252
|
+
nodes: [],
|
|
253
|
+
edges: [],
|
|
254
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
255
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
256
|
+
},
|
|
257
|
+
'billing/charge', // currently holds id 'charge'
|
|
258
|
+
);
|
|
259
|
+
expect(warn.mock.calls.some(([msg]) => String(msg).includes('overwriting') && String(msg).includes('"charge"'))).toBe(true);
|
|
260
|
+
warn.mockRestore();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('ApiStore.resolveAuth', () => {
|
|
265
|
+
it('inherits an api-level _meta policy, op overrides win', () => {
|
|
266
|
+
mkdirSync(join(root, 'svc'), { recursive: true });
|
|
267
|
+
writeFileSync(join(root, 'svc', '_meta.json'), JSON.stringify({ auth: { scheme: 'bearer', secretRef: 'T' } }));
|
|
268
|
+
writeFileSync(
|
|
269
|
+
join(root, 'svc', 'pub.json'),
|
|
270
|
+
JSON.stringify({
|
|
271
|
+
id: 'pub-op-id',
|
|
272
|
+
name: 'Public op',
|
|
273
|
+
version: 1,
|
|
274
|
+
nodes: [],
|
|
275
|
+
edges: [],
|
|
276
|
+
http: { method: 'GET', path: '/pub', auth: 'none' },
|
|
277
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
278
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
writeFileSync(
|
|
282
|
+
join(root, 'svc', 'sec.json'),
|
|
283
|
+
JSON.stringify({
|
|
284
|
+
id: 'sec-op-id',
|
|
285
|
+
name: 'Secure op',
|
|
286
|
+
version: 1,
|
|
287
|
+
nodes: [],
|
|
288
|
+
edges: [],
|
|
289
|
+
http: { method: 'GET', path: '/sec' },
|
|
290
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
291
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
const store = new ApiStore(root);
|
|
295
|
+
expect(store.resolveAuth('sec-op-id')).toEqual({ scheme: 'bearer', secretRef: 'T' });
|
|
296
|
+
expect(store.resolveAuth('pub-op-id')).toBeNull();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('_meta.json is not listed as an operation', () => {
|
|
300
|
+
writeFileSync(join(root, '_meta.json'), JSON.stringify({ auth: { scheme: 'apiKey', secretRef: 'K' } }));
|
|
301
|
+
expect(new ApiStore(root).list().some((o) => (o.id as string) === '_meta')).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('fails closed (throws) when a present _meta.json in the chain is unparseable, instead of silently resolving to public', () => {
|
|
305
|
+
mkdirSync(join(root, 'svc'), { recursive: true });
|
|
306
|
+
// Corrupt JSON — e.g. a partial write or a typo.
|
|
307
|
+
writeFileSync(join(root, 'svc', '_meta.json'), '{ "auth": { "scheme": "bearer", ');
|
|
308
|
+
writeFileSync(
|
|
309
|
+
join(root, 'svc', 'op.json'),
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
id: 'broken-meta-op',
|
|
312
|
+
name: 'Broken meta op',
|
|
313
|
+
version: 1,
|
|
314
|
+
nodes: [],
|
|
315
|
+
edges: [],
|
|
316
|
+
http: { method: 'GET', path: '/svc-op' },
|
|
317
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
318
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
const store = new ApiStore(root);
|
|
322
|
+
expect(() => store.resolveAuth('broken-meta-op')).toThrow(/unparseable/i);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import type { AuthPolicy, HttpTrigger, WorkflowDefinition } from '../src/engine';
|
|
4
|
+
|
|
5
|
+
export interface OpSummary { id: string; name: string; path: string; http?: HttpTrigger }
|
|
6
|
+
export interface FolderNode { name: string; folders: FolderNode[]; operations: OpSummary[] }
|
|
7
|
+
export interface ApiNode { name: string; folders: FolderNode[]; operations: OpSummary[] }
|
|
8
|
+
export interface ApiTree { apis: ApiNode[] }
|
|
9
|
+
|
|
10
|
+
/** File-backed store over an `apis/<api>/<folder…>/<op>.json` tree. Keys ops by
|
|
11
|
+
* their in-file `id`; tracks each op's relative path (POSIX, no `.json`). */
|
|
12
|
+
export class ApiStore {
|
|
13
|
+
readonly dir: string;
|
|
14
|
+
/** id → relative path (POSIX, no extension). Rebuilt on every scan. */
|
|
15
|
+
private index = new Map<string, string>();
|
|
16
|
+
|
|
17
|
+
constructor(dir: string) {
|
|
18
|
+
this.dir = dir;
|
|
19
|
+
mkdirSync(this.dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private sidecarFor(relPath: string): string {
|
|
23
|
+
return join(this.dir, ...relPath.split('/')) + '.scenarios.json';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Sidecar files come in two shapes: the legacy bare array (scenarios only —
|
|
27
|
+
* still written when an op has no mocks, for human-editability/back-compat)
|
|
28
|
+
* and `{ scenarios?, mocks? }`, used once an op carries op-level mocks. */
|
|
29
|
+
private withSidecar(flow: WorkflowDefinition, relPath: string): WorkflowDefinition {
|
|
30
|
+
const path = this.sidecarFor(relPath);
|
|
31
|
+
if (!existsSync(path)) return flow;
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
|
34
|
+
if (Array.isArray(raw)) {
|
|
35
|
+
return raw.length > 0 ? { ...flow, scenarios: raw as WorkflowDefinition['scenarios'] } : flow;
|
|
36
|
+
}
|
|
37
|
+
if (raw && typeof raw === 'object') {
|
|
38
|
+
const { scenarios, mocks } = raw as { scenarios?: WorkflowDefinition['scenarios']; mocks?: Record<string, unknown> };
|
|
39
|
+
let out = flow;
|
|
40
|
+
if (Array.isArray(scenarios) && scenarios.length > 0) out = { ...out, scenarios };
|
|
41
|
+
if (mocks && typeof mocks === 'object') out = { ...out, mocks };
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
return flow;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn(`[apiStore] skipping unparseable ${relPath}.scenarios.json: ${String(err)}`);
|
|
47
|
+
return flow;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Reads just the sidecar's top-level `mocks`, ignoring `scenarios` — used
|
|
52
|
+
* by `save` to preserve mocks when the incoming flow doesn't carry them. */
|
|
53
|
+
private readSidecarMocks(relPath: string): Record<string, unknown> | undefined {
|
|
54
|
+
const path = this.sidecarFor(relPath);
|
|
55
|
+
if (!existsSync(path)) return undefined;
|
|
56
|
+
try {
|
|
57
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
|
58
|
+
if (Array.isArray(raw) || !raw || typeof raw !== 'object') return undefined;
|
|
59
|
+
const { mocks } = raw as { mocks?: Record<string, unknown> };
|
|
60
|
+
return mocks && typeof mocks === 'object' ? mocks : undefined;
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Recursively collect operation files as { flow, relPath }. */
|
|
67
|
+
private scan(): Array<{ flow: WorkflowDefinition; relPath: string }> {
|
|
68
|
+
const out: Array<{ flow: WorkflowDefinition; relPath: string }> = [];
|
|
69
|
+
this.index = new Map();
|
|
70
|
+
const walk = (absDir: string, relParts: string[]): void => {
|
|
71
|
+
for (const entry of readdirSync(absDir)) {
|
|
72
|
+
const abs = join(absDir, entry);
|
|
73
|
+
if (statSync(abs).isDirectory()) {
|
|
74
|
+
walk(abs, [...relParts, entry]);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (entry === '_meta.json') continue;
|
|
78
|
+
if (!entry.endsWith('.json') || entry.endsWith('.scenarios.json')) continue;
|
|
79
|
+
const relPath = [...relParts, entry.slice(0, -'.json'.length)].join('/');
|
|
80
|
+
try {
|
|
81
|
+
const flow = JSON.parse(readFileSync(abs, 'utf8')) as WorkflowDefinition;
|
|
82
|
+
const existing = this.index.get(flow.id);
|
|
83
|
+
if (existing !== undefined && existing !== relPath) {
|
|
84
|
+
console.warn(`[apiStore] duplicate operation id "${flow.id}" at ${existing} and ${relPath} — only one is reachable`);
|
|
85
|
+
} else {
|
|
86
|
+
this.index.set(flow.id, relPath);
|
|
87
|
+
}
|
|
88
|
+
out.push({ flow, relPath });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(`[apiStore] skipping unparseable ${relPath}.json: ${String(err)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
walk(this.dir, []);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
list(): WorkflowDefinition[] {
|
|
99
|
+
return this.scan().map(({ flow, relPath }) => this.withSidecar(flow, relPath));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One entry per operation with its on-disk relative path and `http` metadata
|
|
103
|
+
* (id/name/http from the file itself), for the studio's tree view. */
|
|
104
|
+
listSummaries(): OpSummary[] {
|
|
105
|
+
return this.scan().map(({ flow, relPath }) => ({ id: flow.id, name: flow.name, path: relPath, http: flow.http }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** True when an operation file already sits at `relPath` — used by
|
|
109
|
+
* POST /operations to refuse to silently overwrite an existing op with a
|
|
110
|
+
* brand-new one saved at the same path. */
|
|
111
|
+
existsAt(relPath: string): boolean {
|
|
112
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
113
|
+
return existsSync(abs);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
load(id: string): WorkflowDefinition | undefined {
|
|
117
|
+
const relPath = this.pathOf(id);
|
|
118
|
+
if (!relPath) return undefined;
|
|
119
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
120
|
+
if (!existsSync(abs)) return undefined;
|
|
121
|
+
try {
|
|
122
|
+
const flow = JSON.parse(readFileSync(abs, 'utf8')) as WorkflowDefinition;
|
|
123
|
+
return this.withSidecar(flow, relPath);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.warn(`[apiStore] skipping unparseable ${relPath}.json: ${String(err)}`);
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** id → relative path. Always rescans so it reflects the current on-disk state
|
|
131
|
+
* (files may be added/renamed/deleted out-of-band by a coding agent). */
|
|
132
|
+
pathOf(id: string): string | undefined {
|
|
133
|
+
this.scan();
|
|
134
|
+
return this.index.get(id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
tree(): ApiTree {
|
|
138
|
+
const ops = this.scan();
|
|
139
|
+
const apis = new Map<string, ApiNode>();
|
|
140
|
+
const folderAt = (parent: { folders: FolderNode[] }, name: string): FolderNode => {
|
|
141
|
+
let f = parent.folders.find((x) => x.name === name);
|
|
142
|
+
if (!f) { f = { name, folders: [], operations: [] }; parent.folders.push(f); }
|
|
143
|
+
return f;
|
|
144
|
+
};
|
|
145
|
+
for (const { flow, relPath } of ops) {
|
|
146
|
+
const parts = relPath.split('/'); // [api, ...folders, opName]
|
|
147
|
+
const apiName = parts[0];
|
|
148
|
+
let api = apis.get(apiName);
|
|
149
|
+
if (!api) { api = { name: apiName, folders: [], operations: [] }; apis.set(apiName, api); }
|
|
150
|
+
const folderParts = parts.slice(1, -1);
|
|
151
|
+
let container: { folders: FolderNode[]; operations: OpSummary[] } = api;
|
|
152
|
+
for (const fp of folderParts) container = folderAt(container, fp);
|
|
153
|
+
container.operations.push({ id: flow.id, name: flow.name, path: relPath, http: flow.http });
|
|
154
|
+
}
|
|
155
|
+
return { apis: [...apis.values()] };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
save(flow: WorkflowDefinition, relPath: string): void {
|
|
159
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
160
|
+
if (existsSync(abs)) {
|
|
161
|
+
try {
|
|
162
|
+
const existing = JSON.parse(readFileSync(abs, 'utf8')) as WorkflowDefinition;
|
|
163
|
+
if (existing.id !== undefined && existing.id !== flow.id) {
|
|
164
|
+
console.warn(`[apiStore] overwriting ${relPath} which held a different op id "${existing.id}"`);
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// unparseable existing file — nothing sensible to compare, proceed with the write.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
171
|
+
const { scenarios, mocks, ...rest } = flow;
|
|
172
|
+
writeFileSync(abs, `${JSON.stringify(rest, null, 2)}\n`, 'utf8');
|
|
173
|
+
const sidecar = this.sidecarFor(relPath);
|
|
174
|
+
// A flow argument that doesn't carry `mocks` at all (undefined — not an
|
|
175
|
+
// empty object) means the caller never touched op-level mocks (e.g. a
|
|
176
|
+
// scenario-only edit round-tripped through a client that doesn't forward
|
|
177
|
+
// unknown fields) — preserve whatever is already on disk rather than
|
|
178
|
+
// silently dropping it.
|
|
179
|
+
const effectiveMocks = mocks !== undefined ? mocks : this.readSidecarMocks(relPath);
|
|
180
|
+
const hasScenarios = scenarios !== undefined && scenarios.length > 0;
|
|
181
|
+
const hasMocks = effectiveMocks !== undefined && Object.keys(effectiveMocks).length > 0;
|
|
182
|
+
if (hasMocks) {
|
|
183
|
+
writeFileSync(
|
|
184
|
+
sidecar,
|
|
185
|
+
`${JSON.stringify({ ...(hasScenarios ? { scenarios } : {}), mocks: effectiveMocks }, null, 2)}\n`,
|
|
186
|
+
'utf8',
|
|
187
|
+
);
|
|
188
|
+
} else if (hasScenarios) {
|
|
189
|
+
writeFileSync(sidecar, `${JSON.stringify(scenarios, null, 2)}\n`, 'utf8');
|
|
190
|
+
} else if (existsSync(sidecar)) {
|
|
191
|
+
rmSync(sidecar);
|
|
192
|
+
}
|
|
193
|
+
this.index.set(flow.id, relPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
remove(id: string): boolean {
|
|
197
|
+
const relPath = this.pathOf(id);
|
|
198
|
+
if (!relPath) return false;
|
|
199
|
+
const abs = join(this.dir, ...relPath.split('/')) + '.json';
|
|
200
|
+
if (!existsSync(abs)) return false;
|
|
201
|
+
rmSync(abs);
|
|
202
|
+
const sidecar = this.sidecarFor(relPath);
|
|
203
|
+
if (existsSync(sidecar)) rmSync(sidecar);
|
|
204
|
+
this.index.delete(id);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Resolves the effective auth policy for an operation: walks the op's path
|
|
209
|
+
* from the API root down through each folder, reading `_meta.json`'s `auth`
|
|
210
|
+
* (nearest ancestor default), then applies the op's own `http.auth`
|
|
211
|
+
* override — `'none'` -> explicitly public (null), a policy object -> that
|
|
212
|
+
* policy, `'inherit'`/absent -> the inherited default. Returns the
|
|
213
|
+
* effective policy or null (no auth — public by default). */
|
|
214
|
+
resolveAuth(id: string): AuthPolicy | null {
|
|
215
|
+
const relPath = this.pathOf(id);
|
|
216
|
+
if (!relPath) return null;
|
|
217
|
+
const parts = relPath.split('/'); // [api, ...folders, opName]
|
|
218
|
+
let inherited: AuthPolicy | null = null;
|
|
219
|
+
for (let depth = 1; depth < parts.length; depth++) {
|
|
220
|
+
const metaPath = join(this.dir, ...parts.slice(0, depth), '_meta.json');
|
|
221
|
+
if (!existsSync(metaPath)) continue;
|
|
222
|
+
try {
|
|
223
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8')) as { auth?: AuthPolicy };
|
|
224
|
+
if (meta.auth) inherited = meta.auth;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// A present-but-corrupt _meta.json (partial write, typo, etc.) must
|
|
227
|
+
// never silently resolve to `null` (= public) — that's a fail-open
|
|
228
|
+
// security hole. Fail closed: throw and let the caller deny the op
|
|
229
|
+
// rather than mount it open. A merely ABSENT _meta.json is fine (see
|
|
230
|
+
// the `existsSync` check above) — only a broken one throws.
|
|
231
|
+
const metaRelPath = `${parts.slice(0, depth).join('/')}/_meta.json`;
|
|
232
|
+
throw new Error(`unparseable _meta.json at ${metaRelPath} — refusing to resolve auth (fail closed): ${String(err)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const op = this.load(id);
|
|
236
|
+
const override = op?.http?.auth;
|
|
237
|
+
if (override === 'none') return null;
|
|
238
|
+
if (override && override !== 'inherit') return override;
|
|
239
|
+
return inherited;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { enforceAuth } from './enforce';
|
|
3
|
+
import { createDefaultVerifierRegistry } from './verifiers';
|
|
4
|
+
import { isHttpError } from '../../src/engine';
|
|
5
|
+
|
|
6
|
+
const verifiers = createDefaultVerifierRegistry();
|
|
7
|
+
|
|
8
|
+
describe('enforceAuth', () => {
|
|
9
|
+
it('passes with a valid bearer token and returns user', () => {
|
|
10
|
+
const r = enforceAuth({
|
|
11
|
+
policy: { scheme: 'bearer', secretRef: 'T' },
|
|
12
|
+
request: { headers: { authorization: 'Bearer t' } },
|
|
13
|
+
secrets: { T: 't' },
|
|
14
|
+
verifiers,
|
|
15
|
+
});
|
|
16
|
+
expect(r.user).toEqual({ scheme: 'bearer' });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('401s on a bad token', () => {
|
|
20
|
+
try {
|
|
21
|
+
enforceAuth({
|
|
22
|
+
policy: { scheme: 'bearer', secretRef: 'T' },
|
|
23
|
+
request: { headers: {} },
|
|
24
|
+
secrets: { T: 't' },
|
|
25
|
+
verifiers,
|
|
26
|
+
});
|
|
27
|
+
expect.unreachable();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
expect(isHttpError(e) && e.status).toBe(401);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('500s when the named verifier is missing (fail closed)', () => {
|
|
34
|
+
try {
|
|
35
|
+
enforceAuth({
|
|
36
|
+
policy: { scheme: 'bearer', secretRef: 'T', verify: 'ghost' },
|
|
37
|
+
request: { headers: {} },
|
|
38
|
+
secrets: { T: 't' },
|
|
39
|
+
verifiers,
|
|
40
|
+
});
|
|
41
|
+
expect.unreachable();
|
|
42
|
+
} catch (e) {
|
|
43
|
+
expect(isHttpError(e) && e.status).toBe(500);
|
|
44
|
+
expect(isHttpError(e) && (e.body as { error?: string })?.error).toBe('auth misconfigured: no verifier ghost');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('500s when the secret is unresolved (fail closed)', () => {
|
|
49
|
+
try {
|
|
50
|
+
enforceAuth({
|
|
51
|
+
policy: { scheme: 'bearer', secretRef: 'MISSING' },
|
|
52
|
+
request: { headers: { authorization: 'Bearer t' } },
|
|
53
|
+
secrets: {},
|
|
54
|
+
verifiers,
|
|
55
|
+
});
|
|
56
|
+
expect.unreachable();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
expect(isHttpError(e) && e.status).toBe(500);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Re-exports the shared enforceAuth from src/engine/authVerify.ts. Kept as its
|
|
2
|
+
// own module (rather than importing src/engine directly at call sites) so the
|
|
3
|
+
// server-side auth call path has a stable, server-scoped entry point mirroring
|
|
4
|
+
// the file map in the plan; the underlying logic is shared with the
|
|
5
|
+
// requireAuth node (src/nodes/requireAuth.ts) to avoid divergence.
|
|
6
|
+
export { enforceAuth } from '../../src/engine';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// AuthPolicy/VerifyResult/Verifier are plain data/logic types shared with
|
|
2
|
+
// src/engine/authVerify.ts (also used by src/nodes/requireAuth.ts, which is
|
|
3
|
+
// bundled into the browser build — so src/ must not import from server/).
|
|
4
|
+
// Re-exported here rather than redefined, to avoid divergence.
|
|
5
|
+
export type { AuthPolicy, VerifyResult, Verifier } from '../../src/engine';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { bearerVerifier, apiKeyVerifier, createDefaultVerifierRegistry } from './verifiers';
|
|
3
|
+
import { isHttpError } from '../../src/engine';
|
|
4
|
+
|
|
5
|
+
const call = (v: () => unknown) => {
|
|
6
|
+
try {
|
|
7
|
+
v();
|
|
8
|
+
return null;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return e;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('default verifiers', () => {
|
|
15
|
+
it('bearer accepts a matching token', () => {
|
|
16
|
+
const r = bearerVerifier({ request: { headers: { authorization: 'Bearer s3cret' } }, policy: { scheme: 'bearer', secretRef: 'T' }, secret: 's3cret' });
|
|
17
|
+
expect(r.user).toEqual({ scheme: 'bearer' });
|
|
18
|
+
});
|
|
19
|
+
it('bearer 401s a wrong/missing token', () => {
|
|
20
|
+
const e = call(() => bearerVerifier({ request: { headers: {} }, policy: { scheme: 'bearer', secretRef: 'T' }, secret: 's3cret' }));
|
|
21
|
+
expect(isHttpError(e) && e.status).toBe(401);
|
|
22
|
+
});
|
|
23
|
+
it('bearer 500s when the secret is unset (fail closed)', () => {
|
|
24
|
+
const e = call(() => bearerVerifier({ request: { headers: { authorization: 'Bearer x' } }, policy: { scheme: 'bearer', secretRef: 'T' }, secret: undefined }));
|
|
25
|
+
expect(isHttpError(e) && e.status).toBe(500);
|
|
26
|
+
});
|
|
27
|
+
it('apiKey reads the configured header', () => {
|
|
28
|
+
const r = apiKeyVerifier({ request: { headers: { 'x-api-key': 'k' } }, policy: { scheme: 'apiKey', secretRef: 'K' }, secret: 'k' });
|
|
29
|
+
expect(r.user).toEqual({ scheme: 'apiKey' });
|
|
30
|
+
});
|
|
31
|
+
it('registry seeds bearer + apiKey', () => {
|
|
32
|
+
const reg = createDefaultVerifierRegistry();
|
|
33
|
+
expect(reg.get('bearer')).toBe(bearerVerifier);
|
|
34
|
+
expect(reg.get('apiKey')).toBe(apiKeyVerifier);
|
|
35
|
+
expect(reg.get('nope')).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
});
|