onbuzz 4.9.13 → 4.10.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/node_modules/glob/README.md +31 -5
- package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
- package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/glob.js +2 -1
- package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
- package/node_modules/glob/dist/commonjs/index.min.js +3 -3
- package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
- package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/commonjs/pattern.js +4 -0
- package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
- package/node_modules/glob/dist/esm/glob.d.ts +8 -0
- package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/glob.js +2 -1
- package/node_modules/glob/dist/esm/glob.js.map +1 -1
- package/node_modules/glob/dist/esm/index.min.js +3 -3
- package/node_modules/glob/dist/esm/index.min.js.map +4 -4
- package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
- package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
- package/node_modules/glob/dist/esm/pattern.js +4 -0
- package/node_modules/glob/dist/esm/pattern.js.map +1 -1
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
- package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
- package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
- package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
- package/node_modules/glob/node_modules/minimatch/README.md +76 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
- package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
- package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
- package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
- package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
- package/node_modules/glob/node_modules/minimatch/package.json +17 -11
- package/node_modules/glob/package.json +10 -13
- package/node_modules/minipass/LICENSE.md +55 -0
- package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
- package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/commonjs/index.js +13 -3
- package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
- package/node_modules/minipass/dist/esm/index.d.ts +12 -16
- package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
- package/node_modules/minipass/dist/esm/index.js +3 -1
- package/node_modules/minipass/dist/esm/index.js.map +1 -1
- package/node_modules/minipass/package.json +9 -14
- package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
- package/node_modules/path-scurry/package.json +8 -24
- package/package.json +1 -1
- package/scripts/debug-balance-probe.mjs +35 -35
- package/scripts/push-image.sh +43 -43
- package/scripts/setup-acr.sh +65 -65
- package/scripts/verify-optional-deps.js +96 -1
- package/src/__tests__/composioCliFlags.test.js +239 -239
- package/src/analyzers/CSSAnalyzer.js +298 -297
- package/src/analyzers/ConfigValidator.js +691 -690
- package/src/analyzers/ESLintAnalyzer.js +320 -320
- package/src/analyzers/JavaScriptAnalyzer.js +260 -261
- package/src/analyzers/PrettierFormatter.js +246 -247
- package/src/analyzers/PythonAnalyzer.js +283 -283
- package/src/analyzers/SecurityAnalyzer.js +729 -729
- package/src/analyzers/SparrowAnalyzer.js +341 -341
- package/src/analyzers/TypeScriptAnalyzer.js +247 -247
- package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
- package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
- package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
- package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
- package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
- package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
- package/src/analyzers/codeCloneDetector/detector.js +250 -250
- package/src/analyzers/codeCloneDetector/index.js +194 -192
- package/src/analyzers/codeCloneDetector/parser.js +199 -199
- package/src/core/__tests__/agentPool.test.js +866 -866
- package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
- package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
- package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
- package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
- package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
- package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
- package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
- package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
- package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
- package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
- package/src/core/__tests__/flowEndToEnd.test.js +565 -565
- package/src/core/__tests__/flowFieldMapping.test.js +188 -189
- package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
- package/src/core/__tests__/flowSavePayload.test.js +170 -169
- package/src/core/__tests__/flowTemplates.test.js +311 -311
- package/src/core/__tests__/flowVersionStore.test.js +123 -123
- package/src/core/__tests__/messageProcessor.test.js +669 -669
- package/src/core/__tests__/stateManager.test.js +0 -1
- package/src/core/agentPool.js +2474 -2475
- package/src/core/agentScheduler.js +1 -4
- package/src/core/contextManager.js +708 -708
- package/src/core/flowExecutor.js +1510 -1510
- package/src/core/flowFieldMapping.js +136 -138
- package/src/core/messageProcessor.js +953 -954
- package/src/core/orchestrator.js +593 -595
- package/src/core/stateManager.js +1765 -1752
- package/src/index.js +1221 -1221
- package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
- package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
- package/src/interfaces/__tests__/imageServing.test.js +228 -228
- package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
- package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
- package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
- package/src/interfaces/schedulerRoutes.js +50 -50
- package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
- package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
- package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
- package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
- package/src/interfaces/terminal/api/session.js +265 -266
- package/src/interfaces/terminal/api/websocket.js +496 -497
- package/src/interfaces/terminal/components/AgentCreator.js +691 -705
- package/src/interfaces/terminal/components/AgentEditor.js +676 -678
- package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
- package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
- package/src/interfaces/terminal/components/Header.js +28 -28
- package/src/interfaces/terminal/components/Layout.js +598 -603
- package/src/interfaces/terminal/components/MessageList.js +280 -281
- package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
- package/src/interfaces/terminal/components/StatusBar.js +2 -0
- package/src/interfaces/terminal/index.js +168 -168
- package/src/interfaces/terminal/state/useAgentControl.js +496 -496
- package/src/interfaces/terminal/state/useAgents.js +537 -537
- package/src/interfaces/terminal/state/useMessages.js +629 -630
- package/src/interfaces/terminal/state/useTools.js +554 -554
- package/src/interfaces/terminal/utils/debugLogger.js +44 -44
- package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
- package/src/interfaces/webServer.js +7578 -7579
- package/src/interfaces/webServer.js.bak +7046 -7046
- package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
- package/src/modules/fileExplorer/controller.js +470 -469
- package/src/modules/fileExplorer/routes.js +285 -286
- package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
- package/src/modules/widget/__tests__/routes.test.js +677 -678
- package/src/modules/widget/__tests__/runtime.test.js +401 -401
- package/src/modules/widget/__tests__/versioning.test.js +309 -309
- package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
- package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
- package/src/modules/widget/routes.js +435 -435
- package/src/modules/widget/runtime/bundle.js +640 -640
- package/src/modules/widget/runtime/webComponentBundle.js +470 -470
- package/src/modules/widget/schema.js +182 -181
- package/src/modules/widget/widgetTool.js +1389 -1389
- package/src/services/__tests__/agentActivityService.test.js +401 -402
- package/src/services/__tests__/benchmarkService.test.js +184 -184
- package/src/services/__tests__/contextInjectionService.test.js +246 -246
- package/src/services/__tests__/conversationQuery.test.js +721 -723
- package/src/services/__tests__/credentialVault.test.js +469 -469
- package/src/services/__tests__/discordService.integration.test.js +638 -639
- package/src/services/__tests__/flowContextService.test.js +590 -590
- package/src/services/__tests__/memoryService.test.js +1 -1
- package/src/services/__tests__/messageSource.test.js +380 -380
- package/src/services/__tests__/modelRouterNaming.test.js +111 -111
- package/src/services/__tests__/projectDetector.test.js +34 -34
- package/src/services/__tests__/promptService.test.js +242 -242
- package/src/services/__tests__/telegramService.test.js +941 -941
- package/src/services/__tests__/tokenCountingService.test.js +48 -48
- package/src/services/agentActivityService.js +419 -420
- package/src/services/aiService.js +2997 -3001
- package/src/services/apiKeyManager.js +359 -359
- package/src/services/benchmarkService.js +196 -196
- package/src/services/codebaseKnowledgeService.js +2 -2
- package/src/services/composioService.js +738 -738
- package/src/services/conversationCompactionService.js +1258 -1257
- package/src/services/credentialVault.js +685 -685
- package/src/services/discordService.js +792 -793
- package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
- package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
- package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
- package/src/services/embeddings/autoRecall.js +218 -219
- package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
- package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
- package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
- package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
- package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
- package/src/services/embeddings/indexers/composioIndexer.js +279 -279
- package/src/services/embeddings/providerInterface.js +206 -206
- package/src/services/embeddings/providers/localProvider.js +11 -7
- package/src/services/embeddings/providers/openaiProvider.js +101 -101
- package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
- package/src/services/errorHandler.js +809 -809
- package/src/services/flowContextService.js +586 -586
- package/src/services/grounding/MockAdapter.js +125 -125
- package/src/services/modelRouterService.js +26 -31
- package/src/services/modelsService.js +322 -322
- package/src/services/ollamaService.js +452 -452
- package/src/services/projectDetector.js +403 -404
- package/src/services/promptService.js +418 -418
- package/src/services/qualityInspector.js +795 -795
- package/src/services/scheduleService.js +726 -726
- package/src/services/serviceRegistry.js +386 -386
- package/src/services/telegrafBot.js +174 -174
- package/src/services/telegramService.js +1972 -1972
- package/src/services/visualEditorBridge.js +1033 -1033
- package/src/services/visualEditorServer.js +1769 -1774
- package/src/services/whatsappService.js +667 -668
- package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
- package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
- package/src/tools/__tests__/agentDelayTool.test.js +342 -342
- package/src/tools/__tests__/baseTool.test.js +3 -3
- package/src/tools/__tests__/codeMapTool.test.js +915 -915
- package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
- package/src/tools/__tests__/fileTreeTool.test.js +274 -274
- package/src/tools/__tests__/filesystemTool.test.js +815 -815
- package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
- package/src/tools/__tests__/imageTool.validator.test.js +194 -194
- package/src/tools/__tests__/jobDoneTool.test.js +580 -581
- package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
- package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
- package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
- package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
- package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
- package/src/tools/__tests__/memoryTool.test.js +1 -1
- package/src/tools/__tests__/seekTool.test.js +282 -282
- package/src/tools/__tests__/skillsTool.search.test.js +164 -164
- package/src/tools/__tests__/skillsTool.test.js +226 -226
- package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
- package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
- package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
- package/src/tools/__tests__/taskManagerTool.test.js +866 -866
- package/src/tools/__tests__/terminalTool.test.js +448 -448
- package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
- package/src/tools/__tests__/userPromptTool.test.js +297 -297
- package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
- package/src/tools/__tests__/webTool.e2e.test.js +609 -603
- package/src/tools/__tests__/webTool.unit.test.js +195 -195
- package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
- package/src/tools/agentCommunicationTool.js +8 -10
- package/src/tools/agentDelayTool.js +496 -497
- package/src/tools/asyncToolManager.js +602 -603
- package/src/tools/baseTool.js +12 -11
- package/src/tools/cloneDetectionTool.js +576 -581
- package/src/tools/codeMapTool.js +0 -6
- package/src/tools/composioTool.js +617 -617
- package/src/tools/dependencyResolverTool.js +1211 -1212
- package/src/tools/desktop/DesktopTool.js +629 -638
- package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
- package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
- package/src/tools/desktop/__tests__/osController.test.js +364 -364
- package/src/tools/desktop/osController.js +491 -491
- package/src/tools/docxTool.js +623 -623
- package/src/tools/excelTool.js +636 -636
- package/src/tools/fileContentReplaceTool.js +5 -7
- package/src/tools/fileSystemTool.js +12 -19
- package/src/tools/fileTreeTool.js +840 -840
- package/src/tools/foundryWebSearchTool.js +273 -273
- package/src/tools/helpTool.js +198 -198
- package/src/tools/imageTool.js +1397 -1397
- package/src/tools/importAnalyzerTool.js +1056 -1056
- package/src/tools/jobDoneTool.js +495 -495
- package/src/tools/memoryTool.js +1 -1
- package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
- package/src/tools/office/pres/archetypes/agenda.js +61 -61
- package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
- package/src/tools/office/pres/archetypes/bigStat.js +140 -142
- package/src/tools/office/pres/archetypes/closing.js +70 -70
- package/src/tools/office/pres/archetypes/hero.js +70 -70
- package/src/tools/office/pres/archetypes/productHero.js +93 -94
- package/src/tools/office/pres/archetypes/table.js +73 -74
- package/src/tools/office/pres/backgrounds/orb.js +66 -66
- package/src/tools/office/pres/components.js +422 -423
- package/src/tools/officeTool.js +441 -441
- package/src/tools/pdfTool.js +625 -627
- package/src/tools/platformControlTool.js +1081 -1081
- package/src/tools/seekTool.js +917 -918
- package/src/tools/skillsTool.js +1 -1
- package/src/tools/staticAnalysisTool.js +2143 -2146
- package/src/tools/taskManagerTool.js +3324 -3324
- package/src/tools/terminalTool.js +2615 -2618
- package/src/tools/videoTool.js +1303 -1303
- package/src/tools/visionTool.js +508 -508
- package/src/tools/visualEditorTool.js +1289 -1290
- package/src/tools/webTool.js +3368 -3368
- package/src/tools/whatsappTool.js +464 -464
- package/src/types/__tests__/agent.test.js +499 -499
- package/src/types/__tests__/contextReference.test.js +606 -606
- package/src/types/__tests__/conversation.test.js +555 -555
- package/src/types/__tests__/toolCommand.test.js +584 -584
- package/src/types/contextReference.js +974 -971
- package/src/types/conversation.js +729 -729
- package/src/types/toolCommand.js +746 -746
- package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
- package/src/utilities/__tests__/auditReport.test.js +328 -328
- package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
- package/src/utilities/__tests__/jsonRepair.test.js +103 -104
- package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
- package/src/utilities/__tests__/platformUtils.test.js +80 -87
- package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
- package/src/utilities/__tests__/toolConstants.test.js +92 -94
- package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
- package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
- package/src/utilities/attachmentValidator.js +284 -288
- package/src/utilities/authCache.js.backup-1779570472481 +121 -121
- package/src/utilities/browserStealth.js +631 -630
- package/src/utilities/configManager.js +616 -617
- package/src/utilities/directoryAccessManager.js +564 -565
- package/src/utilities/fileProcessor.js +308 -307
- package/src/utilities/humanBehavior.js +454 -453
- package/src/utilities/logger.js +479 -479
- package/src/utilities/structuredFileValidator.js +696 -699
- package/src/utilities/tagParser.js +5 -10
- package/src/utilities/userDataDir.js +308 -308
- package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
- package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
- package/node_modules/minipass/LICENSE +0 -15
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/package.json +0 -0
|
@@ -1,629 +1,629 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the marketplace proxy router on the local CLI server.
|
|
3
|
-
*
|
|
4
|
-
* These cover six behavior categories that together protect the install
|
|
5
|
-
* flow end-to-end:
|
|
6
|
-
*
|
|
7
|
-
* A. Proxy round-trip — method, body, and response are passed through
|
|
8
|
-
* byte-identically. The browser sees what upstream returned.
|
|
9
|
-
*
|
|
10
|
-
* B. Auth header forwarding — Authorization (the JWT bearer) is
|
|
11
|
-
* forwarded to upstream verbatim. The local server never
|
|
12
|
-
* synthesises auth and never strips it.
|
|
13
|
-
*
|
|
14
|
-
* C. Integrity mismatch — when upstream's X-Content-Hash header does
|
|
15
|
-
* not match the SHA-256 of the downloaded body, the proxy MUST
|
|
16
|
-
* respond 502 with `error: 'integrity_mismatch'` and populate
|
|
17
|
-
* expected/actual hash + byte counts. A corrupted body must NEVER
|
|
18
|
-
* reach the renderer as 200-OK.
|
|
19
|
-
*
|
|
20
|
-
* D. Content-Length mismatch — Content-Length: 1000 + 500-byte body
|
|
21
|
-
* → 502 `content_length_mismatch`. Client must NOT see a
|
|
22
|
-
* truncated success body.
|
|
23
|
-
*
|
|
24
|
-
* E. Stream truncation — mid-stream upstream error surfaces as 502
|
|
25
|
-
* `stream_truncated`. Client does NOT receive a partial 200.
|
|
26
|
-
*
|
|
27
|
-
* F. Error mapping — upstream 404/401/500 are passed through with
|
|
28
|
-
* their status + body so the renderer's error toasts read the
|
|
29
|
-
* same as a direct call would.
|
|
30
|
-
*
|
|
31
|
-
* Pure DI — the router is constructed with a `fetchImpl` stub, no
|
|
32
|
-
* real HTTP, no environment dependence.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import { describe, test, expect, jest
|
|
36
|
-
import express from 'express';
|
|
37
|
-
import { createServer } from 'http';
|
|
38
|
-
import crypto from 'crypto';
|
|
39
|
-
import {
|
|
40
|
-
createMarketplaceRouter,
|
|
41
|
-
parseContentHashHeader,
|
|
42
|
-
verifyDownloadBytes,
|
|
43
|
-
INTEGRITY_ERROR_CODES,
|
|
44
|
-
__test__,
|
|
45
|
-
} from '../marketplaceRoutes.js';
|
|
46
|
-
|
|
47
|
-
const UPSTREAM = 'https://marketplace.test';
|
|
48
|
-
const SAMPLE_JWT = 'eyJ.sample.jwt';
|
|
49
|
-
|
|
50
|
-
// ──────────────────────────────────────────────────────────────────
|
|
51
|
-
// helpers
|
|
52
|
-
// ──────────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Build a minimal fetch-compatible Response-like object for the stub.
|
|
56
|
-
* Real WHATWG Response would also work, but constructing it manually
|
|
57
|
-
* lets us set Content-Length to a deliberately wrong value (which the
|
|
58
|
-
* spec Response constructor would silently overwrite) — that's exactly
|
|
59
|
-
* the corner we need to test.
|
|
60
|
-
*/
|
|
61
|
-
function makeUpstreamResponse({ status = 200, headers = {}, body = '', bodyBytes = null, throwsOnRead = false } = {}) {
|
|
62
|
-
// Normalise header keys to lowercase to mimic WHATWG Headers behaviour.
|
|
63
|
-
const lower = {};
|
|
64
|
-
for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;
|
|
65
|
-
const buf = bodyBytes ?? Buffer.from(body);
|
|
66
|
-
return {
|
|
67
|
-
ok: status >= 200 && status < 300,
|
|
68
|
-
status,
|
|
69
|
-
headers: {
|
|
70
|
-
get: (k) => {
|
|
71
|
-
const v = lower[k.toLowerCase()];
|
|
72
|
-
return v === undefined ? null : v;
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
async arrayBuffer() {
|
|
76
|
-
if (throwsOnRead) throw new Error('upstream stream aborted');
|
|
77
|
-
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
78
|
-
},
|
|
79
|
-
async text() {
|
|
80
|
-
if (throwsOnRead) throw new Error('upstream stream aborted');
|
|
81
|
-
return buf.toString('utf8');
|
|
82
|
-
},
|
|
83
|
-
async json() {
|
|
84
|
-
if (throwsOnRead) throw new Error('upstream stream aborted');
|
|
85
|
-
return JSON.parse(buf.toString('utf8'));
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function sha256Hex(buf) {
|
|
91
|
-
return crypto.createHash('sha256').update(Buffer.isBuffer(buf) ? buf : Buffer.from(buf)).digest('hex');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function startServer(router) {
|
|
95
|
-
const app = express();
|
|
96
|
-
app.use(express.json());
|
|
97
|
-
app.use('/api/marketplace', router);
|
|
98
|
-
const server = createServer(app);
|
|
99
|
-
await new Promise(r => server.listen(0, r));
|
|
100
|
-
const port = server.address().port;
|
|
101
|
-
return {
|
|
102
|
-
server,
|
|
103
|
-
baseUrl: `http://127.0.0.1:${port}`,
|
|
104
|
-
stop: () => new Promise(r => server.close(r)),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function buildRouter({ fetchImpl, logger = { info: () => {}, warn: () => {}, error: () => {} } } = {}) {
|
|
109
|
-
return createMarketplaceRouter({
|
|
110
|
-
upstreamBaseUrl: UPSTREAM,
|
|
111
|
-
fetchImpl,
|
|
112
|
-
logger,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ──────────────────────────────────────────────────────────────────
|
|
117
|
-
// Pure-function unit tests for the integrity helpers
|
|
118
|
-
// ──────────────────────────────────────────────────────────────────
|
|
119
|
-
|
|
120
|
-
describe('parseContentHashHeader', () => {
|
|
121
|
-
test('accepts canonical "sha256-<hex>"', () => {
|
|
122
|
-
const hex = sha256Hex('hello');
|
|
123
|
-
expect(parseContentHashHeader(`sha256-${hex}`)).toBe(hex);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('accepts upper-case sha256 prefix', () => {
|
|
127
|
-
const hex = sha256Hex('hello');
|
|
128
|
-
expect(parseContentHashHeader(`SHA256-${hex.toUpperCase()}`)).toBe(hex);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('accepts bare hex (no prefix)', () => {
|
|
132
|
-
const hex = sha256Hex('x');
|
|
133
|
-
expect(parseContentHashHeader(hex)).toBe(hex);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('returns null for missing / non-hex / empty', () => {
|
|
137
|
-
expect(parseContentHashHeader(undefined)).toBeNull();
|
|
138
|
-
expect(parseContentHashHeader('')).toBeNull();
|
|
139
|
-
expect(parseContentHashHeader('sha256-not-hex')).toBeNull();
|
|
140
|
-
expect(parseContentHashHeader('!')).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('verifyDownloadBytes', () => {
|
|
145
|
-
const body = Buffer.from('{"hello":"world"}', 'utf8');
|
|
146
|
-
const correctHash = sha256Hex(body);
|
|
147
|
-
|
|
148
|
-
test('ok when hash + length both match', () => {
|
|
149
|
-
const r = verifyDownloadBytes(body, { expectedHash: correctHash, expectedBytes: body.length });
|
|
150
|
-
expect(r.ok).toBe(true);
|
|
151
|
-
expect(r.actualHash).toBe(correctHash);
|
|
152
|
-
expect(r.actualBytes).toBe(body.length);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('ok when only one expectation is given (other is null)', () => {
|
|
156
|
-
expect(verifyDownloadBytes(body, { expectedHash: correctHash, expectedBytes: null }).ok).toBe(true);
|
|
157
|
-
expect(verifyDownloadBytes(body, { expectedHash: null, expectedBytes: body.length }).ok).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test('flags content_length_mismatch before integrity_mismatch', () => {
|
|
161
|
-
const r = verifyDownloadBytes(body, { expectedHash: 'deadbeef', expectedBytes: 9999 });
|
|
162
|
-
expect(r.ok).toBe(false);
|
|
163
|
-
expect(r.code).toBe(INTEGRITY_ERROR_CODES.CONTENT_LENGTH_MISMATCH);
|
|
164
|
-
expect(r.expectedBytes).toBe(9999);
|
|
165
|
-
expect(r.actualBytes).toBe(body.length);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('flags integrity_mismatch when hash differs and length is fine', () => {
|
|
169
|
-
const r = verifyDownloadBytes(body, { expectedHash: 'deadbeef', expectedBytes: body.length });
|
|
170
|
-
expect(r.ok).toBe(false);
|
|
171
|
-
expect(r.code).toBe(INTEGRITY_ERROR_CODES.INTEGRITY_MISMATCH);
|
|
172
|
-
expect(r.expectedHash).toBe('deadbeef');
|
|
173
|
-
expect(r.actualHash).toBe(correctHash);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ──────────────────────────────────────────────────────────────────
|
|
178
|
-
// A. Proxy round-trip — body byte-identical, method & path correct
|
|
179
|
-
// ──────────────────────────────────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
describe('marketplace proxy — round-trip', () => {
|
|
182
|
-
test('GET /items forwards query string + returns upstream JSON byte-identical', async () => {
|
|
183
|
-
const upstreamBody = { success: true, items: [{ id: 'a', name: 'A' }, { id: 'b', name: 'B' }], page: 2 };
|
|
184
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
185
|
-
status: 200,
|
|
186
|
-
headers: { 'content-type': 'application/json' },
|
|
187
|
-
body: JSON.stringify(upstreamBody),
|
|
188
|
-
}));
|
|
189
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items?q=foo&type=skill&page=2&limit=10`);
|
|
193
|
-
expect(res.status).toBe(200);
|
|
194
|
-
expect(await res.json()).toEqual(upstreamBody);
|
|
195
|
-
|
|
196
|
-
// Upstream was hit with the full querystring + GET
|
|
197
|
-
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
198
|
-
const [calledUrl, calledInit] = fetchImpl.mock.calls[0];
|
|
199
|
-
expect(calledUrl).toBe(`${UPSTREAM}/api/items?q=foo&type=skill&page=2&limit=10`);
|
|
200
|
-
expect(calledInit.method).toBe('GET');
|
|
201
|
-
} finally { await stop(); }
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('POST /items forwards the JSON body verbatim', async () => {
|
|
205
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
206
|
-
status: 200,
|
|
207
|
-
body: JSON.stringify({ success: true, item: { id: 'new' } }),
|
|
208
|
-
}));
|
|
209
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
210
|
-
|
|
211
|
-
const payload = {
|
|
212
|
-
type: 'skill',
|
|
213
|
-
name: 'Test Skill',
|
|
214
|
-
description: 'hi',
|
|
215
|
-
tags: ['ai', 'utility'],
|
|
216
|
-
content: { sections: [{ name: 's1' }] },
|
|
217
|
-
metadata: { difficulty: 'easy' },
|
|
218
|
-
authorName: 'Tester',
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
223
|
-
method: 'POST',
|
|
224
|
-
headers: { 'Content-Type': 'application/json' },
|
|
225
|
-
body: JSON.stringify(payload),
|
|
226
|
-
});
|
|
227
|
-
expect(res.status).toBe(200);
|
|
228
|
-
|
|
229
|
-
const [calledUrl, calledInit] = fetchImpl.mock.calls[0];
|
|
230
|
-
expect(calledUrl).toBe(`${UPSTREAM}/api/items`);
|
|
231
|
-
expect(calledInit.method).toBe('POST');
|
|
232
|
-
expect(JSON.parse(calledInit.body)).toEqual(payload);
|
|
233
|
-
} finally { await stop(); }
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test('PUT /items/:id and DELETE /items/:id reach the right upstream path', async () => {
|
|
237
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
238
|
-
status: 200, body: JSON.stringify({ success: true }),
|
|
239
|
-
}));
|
|
240
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
await fetch(`${baseUrl}/api/marketplace/items/abc-123`, {
|
|
244
|
-
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
245
|
-
body: JSON.stringify({ name: 'New' }),
|
|
246
|
-
});
|
|
247
|
-
expect(fetchImpl.mock.calls[0][0]).toBe(`${UPSTREAM}/api/items/abc-123`);
|
|
248
|
-
expect(fetchImpl.mock.calls[0][1].method).toBe('PUT');
|
|
249
|
-
|
|
250
|
-
await fetch(`${baseUrl}/api/marketplace/items/abc-123`, { method: 'DELETE' });
|
|
251
|
-
expect(fetchImpl.mock.calls[1][0]).toBe(`${UPSTREAM}/api/items/abc-123`);
|
|
252
|
-
expect(fetchImpl.mock.calls[1][1].method).toBe('DELETE');
|
|
253
|
-
} finally { await stop(); }
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test('POST /items/:id/rate forwards body and reaches the rate path', async () => {
|
|
257
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
258
|
-
status: 200, body: JSON.stringify({ success: true, rating_avg: 4.5 }),
|
|
259
|
-
}));
|
|
260
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
await fetch(`${baseUrl}/api/marketplace/items/xyz/rate`, {
|
|
264
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
265
|
-
body: JSON.stringify({ rating: 5, review: 'great', userName: 'me' }),
|
|
266
|
-
});
|
|
267
|
-
expect(fetchImpl.mock.calls[0][0]).toBe(`${UPSTREAM}/api/items/xyz/rate`);
|
|
268
|
-
expect(JSON.parse(fetchImpl.mock.calls[0][1].body)).toEqual({ rating: 5, review: 'great', userName: 'me' });
|
|
269
|
-
} finally { await stop(); }
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ──────────────────────────────────────────────────────────────────
|
|
274
|
-
// B. Auth header forwarding
|
|
275
|
-
// ──────────────────────────────────────────────────────────────────
|
|
276
|
-
|
|
277
|
-
describe('marketplace proxy — auth forwarding', () => {
|
|
278
|
-
test('Authorization header is forwarded verbatim to upstream', async () => {
|
|
279
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
280
|
-
status: 200, body: JSON.stringify({ success: true, items: [] }),
|
|
281
|
-
}));
|
|
282
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
286
|
-
headers: { 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
287
|
-
});
|
|
288
|
-
const headers = fetchImpl.mock.calls[0][1].headers;
|
|
289
|
-
expect(headers['Authorization']).toBe(`Bearer ${SAMPLE_JWT}`);
|
|
290
|
-
} finally { await stop(); }
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
test('no Authorization header forwarded when caller has none', async () => {
|
|
294
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
295
|
-
status: 200, body: JSON.stringify({ success: true, items: [] }),
|
|
296
|
-
}));
|
|
297
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
await fetch(`${baseUrl}/api/marketplace/items`);
|
|
301
|
-
const headers = fetchImpl.mock.calls[0][1].headers;
|
|
302
|
-
expect(headers['Authorization']).toBeUndefined();
|
|
303
|
-
} finally { await stop(); }
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test('Authorization forwarded on POST /items (publish path)', async () => {
|
|
307
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
308
|
-
status: 200, body: JSON.stringify({ success: true, item: { id: 'x' } }),
|
|
309
|
-
}));
|
|
310
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
314
|
-
method: 'POST',
|
|
315
|
-
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
316
|
-
body: JSON.stringify({ type: 'skill', name: 'X' }),
|
|
317
|
-
});
|
|
318
|
-
expect(fetchImpl.mock.calls[0][1].headers['Authorization']).toBe(`Bearer ${SAMPLE_JWT}`);
|
|
319
|
-
} finally { await stop(); }
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// ──────────────────────────────────────────────────────────────────
|
|
324
|
-
// C + D. Integrity & Content-Length verification on /items/:id/download
|
|
325
|
-
// ──────────────────────────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
describe('marketplace proxy — download integrity', () => {
|
|
328
|
-
test('passes verified body + Content-Hash + X-Integrity-Verified=true on match', async () => {
|
|
329
|
-
const body = Buffer.from(JSON.stringify({ name: 'My Skill', sections: [] }), 'utf8');
|
|
330
|
-
const hex = sha256Hex(body);
|
|
331
|
-
|
|
332
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
333
|
-
status: 200,
|
|
334
|
-
bodyBytes: body,
|
|
335
|
-
headers: {
|
|
336
|
-
'content-type': 'application/json',
|
|
337
|
-
'content-length': String(body.length),
|
|
338
|
-
'x-content-hash': `sha256-${hex}`,
|
|
339
|
-
},
|
|
340
|
-
}));
|
|
341
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
345
|
-
expect(res.status).toBe(200);
|
|
346
|
-
expect(res.headers.get('x-integrity-verified')).toBe('true');
|
|
347
|
-
expect(res.headers.get('x-content-hash')).toBe(`sha256-${hex}`);
|
|
348
|
-
expect(res.headers.get('content-length')).toBe(String(body.length));
|
|
349
|
-
const back = Buffer.from(await res.arrayBuffer());
|
|
350
|
-
expect(back.equals(body)).toBe(true);
|
|
351
|
-
} finally { await stop(); }
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test('502 integrity_mismatch when upstream hash header is wrong', async () => {
|
|
355
|
-
const body = Buffer.from(JSON.stringify({ name: 'Tampered' }), 'utf8');
|
|
356
|
-
const realHex = sha256Hex(body);
|
|
357
|
-
const wrongHex = 'deadbeef'.repeat(8);
|
|
358
|
-
|
|
359
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
360
|
-
status: 200,
|
|
361
|
-
bodyBytes: body,
|
|
362
|
-
headers: {
|
|
363
|
-
'content-type': 'application/json',
|
|
364
|
-
'content-length': String(body.length),
|
|
365
|
-
'x-content-hash': `sha256-${wrongHex}`,
|
|
366
|
-
},
|
|
367
|
-
}));
|
|
368
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
372
|
-
expect(res.status).toBe(502);
|
|
373
|
-
const j = await res.json();
|
|
374
|
-
expect(j.error).toBe('integrity_mismatch');
|
|
375
|
-
expect(j.expectedHash).toBe(wrongHex);
|
|
376
|
-
expect(j.actualHash).toBe(realHex);
|
|
377
|
-
expect(j.expectedBytes).toBe(body.length);
|
|
378
|
-
expect(j.actualBytes).toBe(body.length);
|
|
379
|
-
// Client must NOT have received the body.
|
|
380
|
-
expect(res.headers.get('x-integrity-verified')).not.toBe('true');
|
|
381
|
-
} finally { await stop(); }
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test('502 content_length_mismatch when upstream Content-Length is wrong', async () => {
|
|
385
|
-
const body = Buffer.from('a'.repeat(500), 'utf8');
|
|
386
|
-
const hex = sha256Hex(body);
|
|
387
|
-
|
|
388
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
389
|
-
status: 200,
|
|
390
|
-
bodyBytes: body,
|
|
391
|
-
headers: {
|
|
392
|
-
'content-type': 'application/octet-stream',
|
|
393
|
-
'content-length': '1000', // wrong: claims 1000
|
|
394
|
-
'x-content-hash': `sha256-${hex}`,
|
|
395
|
-
},
|
|
396
|
-
}));
|
|
397
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
401
|
-
expect(res.status).toBe(502);
|
|
402
|
-
const j = await res.json();
|
|
403
|
-
expect(j.error).toBe('content_length_mismatch');
|
|
404
|
-
expect(j.expectedBytes).toBe(1000);
|
|
405
|
-
expect(j.actualBytes).toBe(500);
|
|
406
|
-
} finally { await stop(); }
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
test('legacy upstream (no integrity headers) passes through with X-Integrity-Verified=false', async () => {
|
|
410
|
-
const body = Buffer.from('{"legacy":true}', 'utf8');
|
|
411
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
412
|
-
status: 200,
|
|
413
|
-
bodyBytes: body,
|
|
414
|
-
headers: { 'content-type': 'application/json' },
|
|
415
|
-
}));
|
|
416
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/legacy/download`);
|
|
420
|
-
expect(res.status).toBe(200);
|
|
421
|
-
expect(res.headers.get('x-integrity-verified')).toBe('false');
|
|
422
|
-
const back = Buffer.from(await res.arrayBuffer());
|
|
423
|
-
expect(back.equals(body)).toBe(true);
|
|
424
|
-
} finally { await stop(); }
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
// ──────────────────────────────────────────────────────────────────
|
|
429
|
-
// E. Stream truncation
|
|
430
|
-
// ──────────────────────────────────────────────────────────────────
|
|
431
|
-
|
|
432
|
-
describe('marketplace proxy — stream truncation', () => {
|
|
433
|
-
test('upstream errors mid-stream → 502 stream_truncated, NOT 200 partial', async () => {
|
|
434
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
435
|
-
status: 200,
|
|
436
|
-
headers: {
|
|
437
|
-
'content-type': 'application/json',
|
|
438
|
-
'content-length': '500',
|
|
439
|
-
'x-content-hash': `sha256-${sha256Hex('does not matter')}`,
|
|
440
|
-
},
|
|
441
|
-
throwsOnRead: true,
|
|
442
|
-
}));
|
|
443
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
447
|
-
expect(res.status).toBe(502);
|
|
448
|
-
const j = await res.json();
|
|
449
|
-
expect(j.error).toBe('stream_truncated');
|
|
450
|
-
expect(typeof j.details).toBe('string');
|
|
451
|
-
// Never integrity_verified
|
|
452
|
-
expect(res.headers.get('x-integrity-verified')).not.toBe('true');
|
|
453
|
-
} finally { await stop(); }
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test('upstream network failure on download → 502 upstream_unreachable', async () => {
|
|
457
|
-
const fetchImpl = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
458
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
459
|
-
|
|
460
|
-
try {
|
|
461
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
462
|
-
expect(res.status).toBe(502);
|
|
463
|
-
const j = await res.json();
|
|
464
|
-
expect(j.error).toBe('upstream_unreachable');
|
|
465
|
-
} finally { await stop(); }
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// ──────────────────────────────────────────────────────────────────
|
|
470
|
-
// F. Error mapping
|
|
471
|
-
// ──────────────────────────────────────────────────────────────────
|
|
472
|
-
|
|
473
|
-
describe('marketplace proxy — error mapping', () => {
|
|
474
|
-
test.each([
|
|
475
|
-
[404, { error: 'not found' }],
|
|
476
|
-
[401, { error: 'unauthorized' }],
|
|
477
|
-
[500, { error: 'oops' }],
|
|
478
|
-
])('upstream %i is preserved as-is on /items/:id', async (status, body) => {
|
|
479
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
480
|
-
status,
|
|
481
|
-
body: JSON.stringify(body),
|
|
482
|
-
headers: { 'content-type': 'application/json' },
|
|
483
|
-
}));
|
|
484
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/missing`);
|
|
488
|
-
expect(res.status).toBe(status);
|
|
489
|
-
expect(await res.json()).toEqual(body);
|
|
490
|
-
} finally { await stop(); }
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
test('upstream 404 on download is preserved (no integrity check on error body)', async () => {
|
|
494
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
495
|
-
status: 404,
|
|
496
|
-
body: JSON.stringify({ error: 'not found' }),
|
|
497
|
-
headers: { 'content-type': 'application/json' },
|
|
498
|
-
}));
|
|
499
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/missing/download`);
|
|
503
|
-
expect(res.status).toBe(404);
|
|
504
|
-
expect(await res.json()).toEqual({ error: 'not found' });
|
|
505
|
-
} finally { await stop(); }
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
test('upstream network failure on a JSON endpoint → 502 upstream_unreachable', async () => {
|
|
509
|
-
const fetchImpl = jest.fn().mockRejectedValue(new Error('ENOTFOUND'));
|
|
510
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items`);
|
|
514
|
-
expect(res.status).toBe(502);
|
|
515
|
-
const j = await res.json();
|
|
516
|
-
expect(j.error).toBe('upstream_unreachable');
|
|
517
|
-
expect(j.details).toBe('ENOTFOUND');
|
|
518
|
-
} finally { await stop(); }
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
// ──────────────────────────────────────────────────────────────────
|
|
523
|
-
// G. token_expired / reauth code surface — body byte-identical
|
|
524
|
-
// forwarding so the web-UI guard can discriminate by `code`.
|
|
525
|
-
//
|
|
526
|
-
// Marketplace auth middleware returns one of:
|
|
527
|
-
// 401 { error: 'Token expired', code: 'token_expired' }
|
|
528
|
-
// 401 { error: 'Invalid token', code: 'invalid_token' }
|
|
529
|
-
// 401 { error: 'Authentication required', code: 'no_token' }
|
|
530
|
-
// The proxy MUST forward both the 401 status AND the body verbatim
|
|
531
|
-
// so the renderer's withReauthGuard sees `code` intact. Only
|
|
532
|
-
// 'token_expired' triggers the reauth modal; the other two are
|
|
533
|
-
// real auth failures the user has to resolve explicitly.
|
|
534
|
-
// ──────────────────────────────────────────────────────────────────
|
|
535
|
-
|
|
536
|
-
describe('marketplace proxy — token_expired / reauth code surface', () => {
|
|
537
|
-
test.each([
|
|
538
|
-
['token_expired', 'Token expired'],
|
|
539
|
-
['invalid_token', 'Invalid token'],
|
|
540
|
-
['no_token', 'Authentication required'],
|
|
541
|
-
])('upstream 401 { code: %s } is forwarded byte-identical on JSON endpoints',
|
|
542
|
-
async (code, errorMsg) => {
|
|
543
|
-
const upstreamBody = { error: errorMsg, code };
|
|
544
|
-
const upstreamJson = JSON.stringify(upstreamBody);
|
|
545
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
546
|
-
status: 401,
|
|
547
|
-
body: upstreamJson,
|
|
548
|
-
headers: { 'content-type': 'application/json' },
|
|
549
|
-
}));
|
|
550
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
551
|
-
|
|
552
|
-
try {
|
|
553
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
554
|
-
headers: { 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
555
|
-
});
|
|
556
|
-
// Status forwarded as 401 (NOT coalesced to 502/500).
|
|
557
|
-
expect(res.status).toBe(401);
|
|
558
|
-
const j = await res.json();
|
|
559
|
-
// Body byte-identical: both `error` and `code` survive.
|
|
560
|
-
expect(j).toEqual(upstreamBody);
|
|
561
|
-
expect(j.code).toBe(code);
|
|
562
|
-
expect(j.error).toBe(errorMsg);
|
|
563
|
-
} finally { await stop(); }
|
|
564
|
-
}
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
test('upstream 401 with NO code field (legacy upstream) is forwarded unchanged', async () => {
|
|
568
|
-
// Some non-marketplace upstreams (or older deployments) return 401
|
|
569
|
-
// without a `code` discriminator. The proxy must not invent one and
|
|
570
|
-
// must not strip the existing error field — back-compat with any
|
|
571
|
-
// client that pre-dates the code-aware reauth flow.
|
|
572
|
-
const legacyBody = { error: 'unauthorized' };
|
|
573
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
574
|
-
status: 401,
|
|
575
|
-
body: JSON.stringify(legacyBody),
|
|
576
|
-
headers: { 'content-type': 'application/json' },
|
|
577
|
-
}));
|
|
578
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
579
|
-
|
|
580
|
-
try {
|
|
581
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/abc`);
|
|
582
|
-
expect(res.status).toBe(401);
|
|
583
|
-
const j = await res.json();
|
|
584
|
-
expect(j).toEqual(legacyBody);
|
|
585
|
-
expect(j.code).toBeUndefined();
|
|
586
|
-
} finally { await stop(); }
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test('upstream 401 on /items/:id/download is forwarded with body intact (integrity check does NOT mask auth failure)', async () => {
|
|
590
|
-
// The download path is the security-critical one: when upstream
|
|
591
|
-
// returns 401, the proxy's integrity buffering must NOT swallow
|
|
592
|
-
// the auth-error envelope and replace it with a 502. The web-UI
|
|
593
|
-
// guard relies on receiving the same { error, code } body it would
|
|
594
|
-
// on any other endpoint.
|
|
595
|
-
const upstreamBody = { error: 'Token expired', code: 'token_expired' };
|
|
596
|
-
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
597
|
-
status: 401,
|
|
598
|
-
body: JSON.stringify(upstreamBody),
|
|
599
|
-
headers: { 'content-type': 'application/json' },
|
|
600
|
-
}));
|
|
601
|
-
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`, {
|
|
605
|
-
headers: { 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
606
|
-
});
|
|
607
|
-
// 401 (NOT 502 integrity_mismatch / upstream_unreachable).
|
|
608
|
-
expect(res.status).toBe(401);
|
|
609
|
-
const j = await res.json();
|
|
610
|
-
expect(j).toEqual(upstreamBody);
|
|
611
|
-
expect(j.code).toBe('token_expired');
|
|
612
|
-
// The integrity-verified header MUST NOT be set on an error
|
|
613
|
-
// response — would mislead the renderer into trusting an
|
|
614
|
-
// error envelope as a verified payload.
|
|
615
|
-
expect(res.headers.get('x-integrity-verified')).not.toBe('true');
|
|
616
|
-
} finally { await stop(); }
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
// ──────────────────────────────────────────────────────────────────
|
|
621
|
-
// __test__ export sanity (so future refactors can't quietly drop them)
|
|
622
|
-
// ──────────────────────────────────────────────────────────────────
|
|
623
|
-
|
|
624
|
-
describe('exported test helpers', () => {
|
|
625
|
-
test('__test__ exposes parseContentHashHeader + verifyDownloadBytes', () => {
|
|
626
|
-
expect(__test__.parseContentHashHeader).toBe(parseContentHashHeader);
|
|
627
|
-
expect(__test__.verifyDownloadBytes).toBe(verifyDownloadBytes);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the marketplace proxy router on the local CLI server.
|
|
3
|
+
*
|
|
4
|
+
* These cover six behavior categories that together protect the install
|
|
5
|
+
* flow end-to-end:
|
|
6
|
+
*
|
|
7
|
+
* A. Proxy round-trip — method, body, and response are passed through
|
|
8
|
+
* byte-identically. The browser sees what upstream returned.
|
|
9
|
+
*
|
|
10
|
+
* B. Auth header forwarding — Authorization (the JWT bearer) is
|
|
11
|
+
* forwarded to upstream verbatim. The local server never
|
|
12
|
+
* synthesises auth and never strips it.
|
|
13
|
+
*
|
|
14
|
+
* C. Integrity mismatch — when upstream's X-Content-Hash header does
|
|
15
|
+
* not match the SHA-256 of the downloaded body, the proxy MUST
|
|
16
|
+
* respond 502 with `error: 'integrity_mismatch'` and populate
|
|
17
|
+
* expected/actual hash + byte counts. A corrupted body must NEVER
|
|
18
|
+
* reach the renderer as 200-OK.
|
|
19
|
+
*
|
|
20
|
+
* D. Content-Length mismatch — Content-Length: 1000 + 500-byte body
|
|
21
|
+
* → 502 `content_length_mismatch`. Client must NOT see a
|
|
22
|
+
* truncated success body.
|
|
23
|
+
*
|
|
24
|
+
* E. Stream truncation — mid-stream upstream error surfaces as 502
|
|
25
|
+
* `stream_truncated`. Client does NOT receive a partial 200.
|
|
26
|
+
*
|
|
27
|
+
* F. Error mapping — upstream 404/401/500 are passed through with
|
|
28
|
+
* their status + body so the renderer's error toasts read the
|
|
29
|
+
* same as a direct call would.
|
|
30
|
+
*
|
|
31
|
+
* Pure DI — the router is constructed with a `fetchImpl` stub, no
|
|
32
|
+
* real HTTP, no environment dependence.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { describe, test, expect, jest } from '@jest/globals';
|
|
36
|
+
import express from 'express';
|
|
37
|
+
import { createServer } from 'http';
|
|
38
|
+
import crypto from 'crypto';
|
|
39
|
+
import {
|
|
40
|
+
createMarketplaceRouter,
|
|
41
|
+
parseContentHashHeader,
|
|
42
|
+
verifyDownloadBytes,
|
|
43
|
+
INTEGRITY_ERROR_CODES,
|
|
44
|
+
__test__,
|
|
45
|
+
} from '../marketplaceRoutes.js';
|
|
46
|
+
|
|
47
|
+
const UPSTREAM = 'https://marketplace.test';
|
|
48
|
+
const SAMPLE_JWT = 'eyJ.sample.jwt';
|
|
49
|
+
|
|
50
|
+
// ──────────────────────────────────────────────────────────────────
|
|
51
|
+
// helpers
|
|
52
|
+
// ──────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a minimal fetch-compatible Response-like object for the stub.
|
|
56
|
+
* Real WHATWG Response would also work, but constructing it manually
|
|
57
|
+
* lets us set Content-Length to a deliberately wrong value (which the
|
|
58
|
+
* spec Response constructor would silently overwrite) — that's exactly
|
|
59
|
+
* the corner we need to test.
|
|
60
|
+
*/
|
|
61
|
+
function makeUpstreamResponse({ status = 200, headers = {}, body = '', bodyBytes = null, throwsOnRead = false } = {}) {
|
|
62
|
+
// Normalise header keys to lowercase to mimic WHATWG Headers behaviour.
|
|
63
|
+
const lower = {};
|
|
64
|
+
for (const [k, v] of Object.entries(headers)) lower[k.toLowerCase()] = v;
|
|
65
|
+
const buf = bodyBytes ?? Buffer.from(body);
|
|
66
|
+
return {
|
|
67
|
+
ok: status >= 200 && status < 300,
|
|
68
|
+
status,
|
|
69
|
+
headers: {
|
|
70
|
+
get: (k) => {
|
|
71
|
+
const v = lower[k.toLowerCase()];
|
|
72
|
+
return v === undefined ? null : v;
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
async arrayBuffer() {
|
|
76
|
+
if (throwsOnRead) throw new Error('upstream stream aborted');
|
|
77
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
78
|
+
},
|
|
79
|
+
async text() {
|
|
80
|
+
if (throwsOnRead) throw new Error('upstream stream aborted');
|
|
81
|
+
return buf.toString('utf8');
|
|
82
|
+
},
|
|
83
|
+
async json() {
|
|
84
|
+
if (throwsOnRead) throw new Error('upstream stream aborted');
|
|
85
|
+
return JSON.parse(buf.toString('utf8'));
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sha256Hex(buf) {
|
|
91
|
+
return crypto.createHash('sha256').update(Buffer.isBuffer(buf) ? buf : Buffer.from(buf)).digest('hex');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function startServer(router) {
|
|
95
|
+
const app = express();
|
|
96
|
+
app.use(express.json());
|
|
97
|
+
app.use('/api/marketplace', router);
|
|
98
|
+
const server = createServer(app);
|
|
99
|
+
await new Promise(r => server.listen(0, r));
|
|
100
|
+
const port = server.address().port;
|
|
101
|
+
return {
|
|
102
|
+
server,
|
|
103
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
104
|
+
stop: () => new Promise(r => server.close(r)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildRouter({ fetchImpl, logger = { info: () => {}, warn: () => {}, error: () => {} } } = {}) {
|
|
109
|
+
return createMarketplaceRouter({
|
|
110
|
+
upstreamBaseUrl: UPSTREAM,
|
|
111
|
+
fetchImpl,
|
|
112
|
+
logger,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ──────────────────────────────────────────────────────────────────
|
|
117
|
+
// Pure-function unit tests for the integrity helpers
|
|
118
|
+
// ──────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
describe('parseContentHashHeader', () => {
|
|
121
|
+
test('accepts canonical "sha256-<hex>"', () => {
|
|
122
|
+
const hex = sha256Hex('hello');
|
|
123
|
+
expect(parseContentHashHeader(`sha256-${hex}`)).toBe(hex);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('accepts upper-case sha256 prefix', () => {
|
|
127
|
+
const hex = sha256Hex('hello');
|
|
128
|
+
expect(parseContentHashHeader(`SHA256-${hex.toUpperCase()}`)).toBe(hex);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('accepts bare hex (no prefix)', () => {
|
|
132
|
+
const hex = sha256Hex('x');
|
|
133
|
+
expect(parseContentHashHeader(hex)).toBe(hex);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('returns null for missing / non-hex / empty', () => {
|
|
137
|
+
expect(parseContentHashHeader(undefined)).toBeNull();
|
|
138
|
+
expect(parseContentHashHeader('')).toBeNull();
|
|
139
|
+
expect(parseContentHashHeader('sha256-not-hex')).toBeNull();
|
|
140
|
+
expect(parseContentHashHeader('!')).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('verifyDownloadBytes', () => {
|
|
145
|
+
const body = Buffer.from('{"hello":"world"}', 'utf8');
|
|
146
|
+
const correctHash = sha256Hex(body);
|
|
147
|
+
|
|
148
|
+
test('ok when hash + length both match', () => {
|
|
149
|
+
const r = verifyDownloadBytes(body, { expectedHash: correctHash, expectedBytes: body.length });
|
|
150
|
+
expect(r.ok).toBe(true);
|
|
151
|
+
expect(r.actualHash).toBe(correctHash);
|
|
152
|
+
expect(r.actualBytes).toBe(body.length);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('ok when only one expectation is given (other is null)', () => {
|
|
156
|
+
expect(verifyDownloadBytes(body, { expectedHash: correctHash, expectedBytes: null }).ok).toBe(true);
|
|
157
|
+
expect(verifyDownloadBytes(body, { expectedHash: null, expectedBytes: body.length }).ok).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('flags content_length_mismatch before integrity_mismatch', () => {
|
|
161
|
+
const r = verifyDownloadBytes(body, { expectedHash: 'deadbeef', expectedBytes: 9999 });
|
|
162
|
+
expect(r.ok).toBe(false);
|
|
163
|
+
expect(r.code).toBe(INTEGRITY_ERROR_CODES.CONTENT_LENGTH_MISMATCH);
|
|
164
|
+
expect(r.expectedBytes).toBe(9999);
|
|
165
|
+
expect(r.actualBytes).toBe(body.length);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('flags integrity_mismatch when hash differs and length is fine', () => {
|
|
169
|
+
const r = verifyDownloadBytes(body, { expectedHash: 'deadbeef', expectedBytes: body.length });
|
|
170
|
+
expect(r.ok).toBe(false);
|
|
171
|
+
expect(r.code).toBe(INTEGRITY_ERROR_CODES.INTEGRITY_MISMATCH);
|
|
172
|
+
expect(r.expectedHash).toBe('deadbeef');
|
|
173
|
+
expect(r.actualHash).toBe(correctHash);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ──────────────────────────────────────────────────────────────────
|
|
178
|
+
// A. Proxy round-trip — body byte-identical, method & path correct
|
|
179
|
+
// ──────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
describe('marketplace proxy — round-trip', () => {
|
|
182
|
+
test('GET /items forwards query string + returns upstream JSON byte-identical', async () => {
|
|
183
|
+
const upstreamBody = { success: true, items: [{ id: 'a', name: 'A' }, { id: 'b', name: 'B' }], page: 2 };
|
|
184
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
185
|
+
status: 200,
|
|
186
|
+
headers: { 'content-type': 'application/json' },
|
|
187
|
+
body: JSON.stringify(upstreamBody),
|
|
188
|
+
}));
|
|
189
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items?q=foo&type=skill&page=2&limit=10`);
|
|
193
|
+
expect(res.status).toBe(200);
|
|
194
|
+
expect(await res.json()).toEqual(upstreamBody);
|
|
195
|
+
|
|
196
|
+
// Upstream was hit with the full querystring + GET
|
|
197
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
198
|
+
const [calledUrl, calledInit] = fetchImpl.mock.calls[0];
|
|
199
|
+
expect(calledUrl).toBe(`${UPSTREAM}/api/items?q=foo&type=skill&page=2&limit=10`);
|
|
200
|
+
expect(calledInit.method).toBe('GET');
|
|
201
|
+
} finally { await stop(); }
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('POST /items forwards the JSON body verbatim', async () => {
|
|
205
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
206
|
+
status: 200,
|
|
207
|
+
body: JSON.stringify({ success: true, item: { id: 'new' } }),
|
|
208
|
+
}));
|
|
209
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
210
|
+
|
|
211
|
+
const payload = {
|
|
212
|
+
type: 'skill',
|
|
213
|
+
name: 'Test Skill',
|
|
214
|
+
description: 'hi',
|
|
215
|
+
tags: ['ai', 'utility'],
|
|
216
|
+
content: { sections: [{ name: 's1' }] },
|
|
217
|
+
metadata: { difficulty: 'easy' },
|
|
218
|
+
authorName: 'Tester',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify(payload),
|
|
226
|
+
});
|
|
227
|
+
expect(res.status).toBe(200);
|
|
228
|
+
|
|
229
|
+
const [calledUrl, calledInit] = fetchImpl.mock.calls[0];
|
|
230
|
+
expect(calledUrl).toBe(`${UPSTREAM}/api/items`);
|
|
231
|
+
expect(calledInit.method).toBe('POST');
|
|
232
|
+
expect(JSON.parse(calledInit.body)).toEqual(payload);
|
|
233
|
+
} finally { await stop(); }
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('PUT /items/:id and DELETE /items/:id reach the right upstream path', async () => {
|
|
237
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
238
|
+
status: 200, body: JSON.stringify({ success: true }),
|
|
239
|
+
}));
|
|
240
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await fetch(`${baseUrl}/api/marketplace/items/abc-123`, {
|
|
244
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({ name: 'New' }),
|
|
246
|
+
});
|
|
247
|
+
expect(fetchImpl.mock.calls[0][0]).toBe(`${UPSTREAM}/api/items/abc-123`);
|
|
248
|
+
expect(fetchImpl.mock.calls[0][1].method).toBe('PUT');
|
|
249
|
+
|
|
250
|
+
await fetch(`${baseUrl}/api/marketplace/items/abc-123`, { method: 'DELETE' });
|
|
251
|
+
expect(fetchImpl.mock.calls[1][0]).toBe(`${UPSTREAM}/api/items/abc-123`);
|
|
252
|
+
expect(fetchImpl.mock.calls[1][1].method).toBe('DELETE');
|
|
253
|
+
} finally { await stop(); }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('POST /items/:id/rate forwards body and reaches the rate path', async () => {
|
|
257
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
258
|
+
status: 200, body: JSON.stringify({ success: true, rating_avg: 4.5 }),
|
|
259
|
+
}));
|
|
260
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await fetch(`${baseUrl}/api/marketplace/items/xyz/rate`, {
|
|
264
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ rating: 5, review: 'great', userName: 'me' }),
|
|
266
|
+
});
|
|
267
|
+
expect(fetchImpl.mock.calls[0][0]).toBe(`${UPSTREAM}/api/items/xyz/rate`);
|
|
268
|
+
expect(JSON.parse(fetchImpl.mock.calls[0][1].body)).toEqual({ rating: 5, review: 'great', userName: 'me' });
|
|
269
|
+
} finally { await stop(); }
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ──────────────────────────────────────────────────────────────────
|
|
274
|
+
// B. Auth header forwarding
|
|
275
|
+
// ──────────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
describe('marketplace proxy — auth forwarding', () => {
|
|
278
|
+
test('Authorization header is forwarded verbatim to upstream', async () => {
|
|
279
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
280
|
+
status: 200, body: JSON.stringify({ success: true, items: [] }),
|
|
281
|
+
}));
|
|
282
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
286
|
+
headers: { 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
287
|
+
});
|
|
288
|
+
const headers = fetchImpl.mock.calls[0][1].headers;
|
|
289
|
+
expect(headers['Authorization']).toBe(`Bearer ${SAMPLE_JWT}`);
|
|
290
|
+
} finally { await stop(); }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('no Authorization header forwarded when caller has none', async () => {
|
|
294
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
295
|
+
status: 200, body: JSON.stringify({ success: true, items: [] }),
|
|
296
|
+
}));
|
|
297
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await fetch(`${baseUrl}/api/marketplace/items`);
|
|
301
|
+
const headers = fetchImpl.mock.calls[0][1].headers;
|
|
302
|
+
expect(headers['Authorization']).toBeUndefined();
|
|
303
|
+
} finally { await stop(); }
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('Authorization forwarded on POST /items (publish path)', async () => {
|
|
307
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
308
|
+
status: 200, body: JSON.stringify({ success: true, item: { id: 'x' } }),
|
|
309
|
+
}));
|
|
310
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
316
|
+
body: JSON.stringify({ type: 'skill', name: 'X' }),
|
|
317
|
+
});
|
|
318
|
+
expect(fetchImpl.mock.calls[0][1].headers['Authorization']).toBe(`Bearer ${SAMPLE_JWT}`);
|
|
319
|
+
} finally { await stop(); }
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ──────────────────────────────────────────────────────────────────
|
|
324
|
+
// C + D. Integrity & Content-Length verification on /items/:id/download
|
|
325
|
+
// ──────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
describe('marketplace proxy — download integrity', () => {
|
|
328
|
+
test('passes verified body + Content-Hash + X-Integrity-Verified=true on match', async () => {
|
|
329
|
+
const body = Buffer.from(JSON.stringify({ name: 'My Skill', sections: [] }), 'utf8');
|
|
330
|
+
const hex = sha256Hex(body);
|
|
331
|
+
|
|
332
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
333
|
+
status: 200,
|
|
334
|
+
bodyBytes: body,
|
|
335
|
+
headers: {
|
|
336
|
+
'content-type': 'application/json',
|
|
337
|
+
'content-length': String(body.length),
|
|
338
|
+
'x-content-hash': `sha256-${hex}`,
|
|
339
|
+
},
|
|
340
|
+
}));
|
|
341
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
345
|
+
expect(res.status).toBe(200);
|
|
346
|
+
expect(res.headers.get('x-integrity-verified')).toBe('true');
|
|
347
|
+
expect(res.headers.get('x-content-hash')).toBe(`sha256-${hex}`);
|
|
348
|
+
expect(res.headers.get('content-length')).toBe(String(body.length));
|
|
349
|
+
const back = Buffer.from(await res.arrayBuffer());
|
|
350
|
+
expect(back.equals(body)).toBe(true);
|
|
351
|
+
} finally { await stop(); }
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('502 integrity_mismatch when upstream hash header is wrong', async () => {
|
|
355
|
+
const body = Buffer.from(JSON.stringify({ name: 'Tampered' }), 'utf8');
|
|
356
|
+
const realHex = sha256Hex(body);
|
|
357
|
+
const wrongHex = 'deadbeef'.repeat(8);
|
|
358
|
+
|
|
359
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
360
|
+
status: 200,
|
|
361
|
+
bodyBytes: body,
|
|
362
|
+
headers: {
|
|
363
|
+
'content-type': 'application/json',
|
|
364
|
+
'content-length': String(body.length),
|
|
365
|
+
'x-content-hash': `sha256-${wrongHex}`,
|
|
366
|
+
},
|
|
367
|
+
}));
|
|
368
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
372
|
+
expect(res.status).toBe(502);
|
|
373
|
+
const j = await res.json();
|
|
374
|
+
expect(j.error).toBe('integrity_mismatch');
|
|
375
|
+
expect(j.expectedHash).toBe(wrongHex);
|
|
376
|
+
expect(j.actualHash).toBe(realHex);
|
|
377
|
+
expect(j.expectedBytes).toBe(body.length);
|
|
378
|
+
expect(j.actualBytes).toBe(body.length);
|
|
379
|
+
// Client must NOT have received the body.
|
|
380
|
+
expect(res.headers.get('x-integrity-verified')).not.toBe('true');
|
|
381
|
+
} finally { await stop(); }
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('502 content_length_mismatch when upstream Content-Length is wrong', async () => {
|
|
385
|
+
const body = Buffer.from('a'.repeat(500), 'utf8');
|
|
386
|
+
const hex = sha256Hex(body);
|
|
387
|
+
|
|
388
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
389
|
+
status: 200,
|
|
390
|
+
bodyBytes: body,
|
|
391
|
+
headers: {
|
|
392
|
+
'content-type': 'application/octet-stream',
|
|
393
|
+
'content-length': '1000', // wrong: claims 1000
|
|
394
|
+
'x-content-hash': `sha256-${hex}`,
|
|
395
|
+
},
|
|
396
|
+
}));
|
|
397
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
401
|
+
expect(res.status).toBe(502);
|
|
402
|
+
const j = await res.json();
|
|
403
|
+
expect(j.error).toBe('content_length_mismatch');
|
|
404
|
+
expect(j.expectedBytes).toBe(1000);
|
|
405
|
+
expect(j.actualBytes).toBe(500);
|
|
406
|
+
} finally { await stop(); }
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('legacy upstream (no integrity headers) passes through with X-Integrity-Verified=false', async () => {
|
|
410
|
+
const body = Buffer.from('{"legacy":true}', 'utf8');
|
|
411
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
412
|
+
status: 200,
|
|
413
|
+
bodyBytes: body,
|
|
414
|
+
headers: { 'content-type': 'application/json' },
|
|
415
|
+
}));
|
|
416
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/legacy/download`);
|
|
420
|
+
expect(res.status).toBe(200);
|
|
421
|
+
expect(res.headers.get('x-integrity-verified')).toBe('false');
|
|
422
|
+
const back = Buffer.from(await res.arrayBuffer());
|
|
423
|
+
expect(back.equals(body)).toBe(true);
|
|
424
|
+
} finally { await stop(); }
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ──────────────────────────────────────────────────────────────────
|
|
429
|
+
// E. Stream truncation
|
|
430
|
+
// ──────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
describe('marketplace proxy — stream truncation', () => {
|
|
433
|
+
test('upstream errors mid-stream → 502 stream_truncated, NOT 200 partial', async () => {
|
|
434
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
435
|
+
status: 200,
|
|
436
|
+
headers: {
|
|
437
|
+
'content-type': 'application/json',
|
|
438
|
+
'content-length': '500',
|
|
439
|
+
'x-content-hash': `sha256-${sha256Hex('does not matter')}`,
|
|
440
|
+
},
|
|
441
|
+
throwsOnRead: true,
|
|
442
|
+
}));
|
|
443
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
447
|
+
expect(res.status).toBe(502);
|
|
448
|
+
const j = await res.json();
|
|
449
|
+
expect(j.error).toBe('stream_truncated');
|
|
450
|
+
expect(typeof j.details).toBe('string');
|
|
451
|
+
// Never integrity_verified
|
|
452
|
+
expect(res.headers.get('x-integrity-verified')).not.toBe('true');
|
|
453
|
+
} finally { await stop(); }
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('upstream network failure on download → 502 upstream_unreachable', async () => {
|
|
457
|
+
const fetchImpl = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
458
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`);
|
|
462
|
+
expect(res.status).toBe(502);
|
|
463
|
+
const j = await res.json();
|
|
464
|
+
expect(j.error).toBe('upstream_unreachable');
|
|
465
|
+
} finally { await stop(); }
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// ──────────────────────────────────────────────────────────────────
|
|
470
|
+
// F. Error mapping
|
|
471
|
+
// ──────────────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
describe('marketplace proxy — error mapping', () => {
|
|
474
|
+
test.each([
|
|
475
|
+
[404, { error: 'not found' }],
|
|
476
|
+
[401, { error: 'unauthorized' }],
|
|
477
|
+
[500, { error: 'oops' }],
|
|
478
|
+
])('upstream %i is preserved as-is on /items/:id', async (status, body) => {
|
|
479
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
480
|
+
status,
|
|
481
|
+
body: JSON.stringify(body),
|
|
482
|
+
headers: { 'content-type': 'application/json' },
|
|
483
|
+
}));
|
|
484
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/missing`);
|
|
488
|
+
expect(res.status).toBe(status);
|
|
489
|
+
expect(await res.json()).toEqual(body);
|
|
490
|
+
} finally { await stop(); }
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('upstream 404 on download is preserved (no integrity check on error body)', async () => {
|
|
494
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
495
|
+
status: 404,
|
|
496
|
+
body: JSON.stringify({ error: 'not found' }),
|
|
497
|
+
headers: { 'content-type': 'application/json' },
|
|
498
|
+
}));
|
|
499
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/missing/download`);
|
|
503
|
+
expect(res.status).toBe(404);
|
|
504
|
+
expect(await res.json()).toEqual({ error: 'not found' });
|
|
505
|
+
} finally { await stop(); }
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('upstream network failure on a JSON endpoint → 502 upstream_unreachable', async () => {
|
|
509
|
+
const fetchImpl = jest.fn().mockRejectedValue(new Error('ENOTFOUND'));
|
|
510
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items`);
|
|
514
|
+
expect(res.status).toBe(502);
|
|
515
|
+
const j = await res.json();
|
|
516
|
+
expect(j.error).toBe('upstream_unreachable');
|
|
517
|
+
expect(j.details).toBe('ENOTFOUND');
|
|
518
|
+
} finally { await stop(); }
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// ──────────────────────────────────────────────────────────────────
|
|
523
|
+
// G. token_expired / reauth code surface — body byte-identical
|
|
524
|
+
// forwarding so the web-UI guard can discriminate by `code`.
|
|
525
|
+
//
|
|
526
|
+
// Marketplace auth middleware returns one of:
|
|
527
|
+
// 401 { error: 'Token expired', code: 'token_expired' }
|
|
528
|
+
// 401 { error: 'Invalid token', code: 'invalid_token' }
|
|
529
|
+
// 401 { error: 'Authentication required', code: 'no_token' }
|
|
530
|
+
// The proxy MUST forward both the 401 status AND the body verbatim
|
|
531
|
+
// so the renderer's withReauthGuard sees `code` intact. Only
|
|
532
|
+
// 'token_expired' triggers the reauth modal; the other two are
|
|
533
|
+
// real auth failures the user has to resolve explicitly.
|
|
534
|
+
// ──────────────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
describe('marketplace proxy — token_expired / reauth code surface', () => {
|
|
537
|
+
test.each([
|
|
538
|
+
['token_expired', 'Token expired'],
|
|
539
|
+
['invalid_token', 'Invalid token'],
|
|
540
|
+
['no_token', 'Authentication required'],
|
|
541
|
+
])('upstream 401 { code: %s } is forwarded byte-identical on JSON endpoints',
|
|
542
|
+
async (code, errorMsg) => {
|
|
543
|
+
const upstreamBody = { error: errorMsg, code };
|
|
544
|
+
const upstreamJson = JSON.stringify(upstreamBody);
|
|
545
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
546
|
+
status: 401,
|
|
547
|
+
body: upstreamJson,
|
|
548
|
+
headers: { 'content-type': 'application/json' },
|
|
549
|
+
}));
|
|
550
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items`, {
|
|
554
|
+
headers: { 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
555
|
+
});
|
|
556
|
+
// Status forwarded as 401 (NOT coalesced to 502/500).
|
|
557
|
+
expect(res.status).toBe(401);
|
|
558
|
+
const j = await res.json();
|
|
559
|
+
// Body byte-identical: both `error` and `code` survive.
|
|
560
|
+
expect(j).toEqual(upstreamBody);
|
|
561
|
+
expect(j.code).toBe(code);
|
|
562
|
+
expect(j.error).toBe(errorMsg);
|
|
563
|
+
} finally { await stop(); }
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
test('upstream 401 with NO code field (legacy upstream) is forwarded unchanged', async () => {
|
|
568
|
+
// Some non-marketplace upstreams (or older deployments) return 401
|
|
569
|
+
// without a `code` discriminator. The proxy must not invent one and
|
|
570
|
+
// must not strip the existing error field — back-compat with any
|
|
571
|
+
// client that pre-dates the code-aware reauth flow.
|
|
572
|
+
const legacyBody = { error: 'unauthorized' };
|
|
573
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
574
|
+
status: 401,
|
|
575
|
+
body: JSON.stringify(legacyBody),
|
|
576
|
+
headers: { 'content-type': 'application/json' },
|
|
577
|
+
}));
|
|
578
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/abc`);
|
|
582
|
+
expect(res.status).toBe(401);
|
|
583
|
+
const j = await res.json();
|
|
584
|
+
expect(j).toEqual(legacyBody);
|
|
585
|
+
expect(j.code).toBeUndefined();
|
|
586
|
+
} finally { await stop(); }
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('upstream 401 on /items/:id/download is forwarded with body intact (integrity check does NOT mask auth failure)', async () => {
|
|
590
|
+
// The download path is the security-critical one: when upstream
|
|
591
|
+
// returns 401, the proxy's integrity buffering must NOT swallow
|
|
592
|
+
// the auth-error envelope and replace it with a 502. The web-UI
|
|
593
|
+
// guard relies on receiving the same { error, code } body it would
|
|
594
|
+
// on any other endpoint.
|
|
595
|
+
const upstreamBody = { error: 'Token expired', code: 'token_expired' };
|
|
596
|
+
const fetchImpl = jest.fn().mockResolvedValue(makeUpstreamResponse({
|
|
597
|
+
status: 401,
|
|
598
|
+
body: JSON.stringify(upstreamBody),
|
|
599
|
+
headers: { 'content-type': 'application/json' },
|
|
600
|
+
}));
|
|
601
|
+
const { baseUrl, stop } = await startServer(buildRouter({ fetchImpl }));
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
const res = await fetch(`${baseUrl}/api/marketplace/items/abc/download`, {
|
|
605
|
+
headers: { 'Authorization': `Bearer ${SAMPLE_JWT}` },
|
|
606
|
+
});
|
|
607
|
+
// 401 (NOT 502 integrity_mismatch / upstream_unreachable).
|
|
608
|
+
expect(res.status).toBe(401);
|
|
609
|
+
const j = await res.json();
|
|
610
|
+
expect(j).toEqual(upstreamBody);
|
|
611
|
+
expect(j.code).toBe('token_expired');
|
|
612
|
+
// The integrity-verified header MUST NOT be set on an error
|
|
613
|
+
// response — would mislead the renderer into trusting an
|
|
614
|
+
// error envelope as a verified payload.
|
|
615
|
+
expect(res.headers.get('x-integrity-verified')).not.toBe('true');
|
|
616
|
+
} finally { await stop(); }
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// ──────────────────────────────────────────────────────────────────
|
|
621
|
+
// __test__ export sanity (so future refactors can't quietly drop them)
|
|
622
|
+
// ──────────────────────────────────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
describe('exported test helpers', () => {
|
|
625
|
+
test('__test__ exposes parseContentHashHeader + verifyDownloadBytes', () => {
|
|
626
|
+
expect(__test__.parseContentHashHeader).toBe(parseContentHashHeader);
|
|
627
|
+
expect(__test__.verifyDownloadBytes).toBe(verifyDownloadBytes);
|
|
628
|
+
});
|
|
629
|
+
});
|