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.
Files changed (451) hide show
  1. package/node_modules/glob/README.md +31 -5
  2. package/node_modules/glob/dist/commonjs/glob.d.ts +8 -0
  3. package/node_modules/glob/dist/commonjs/glob.d.ts.map +1 -1
  4. package/node_modules/glob/dist/commonjs/glob.js +2 -1
  5. package/node_modules/glob/dist/commonjs/glob.js.map +1 -1
  6. package/node_modules/glob/dist/commonjs/index.min.js +3 -3
  7. package/node_modules/glob/dist/commonjs/index.min.js.map +4 -4
  8. package/node_modules/glob/dist/commonjs/pattern.d.ts +3 -0
  9. package/node_modules/glob/dist/commonjs/pattern.d.ts.map +1 -1
  10. package/node_modules/glob/dist/commonjs/pattern.js +4 -0
  11. package/node_modules/glob/dist/commonjs/pattern.js.map +1 -1
  12. package/node_modules/glob/dist/esm/glob.d.ts +8 -0
  13. package/node_modules/glob/dist/esm/glob.d.ts.map +1 -1
  14. package/node_modules/glob/dist/esm/glob.js +2 -1
  15. package/node_modules/glob/dist/esm/glob.js.map +1 -1
  16. package/node_modules/glob/dist/esm/index.min.js +3 -3
  17. package/node_modules/glob/dist/esm/index.min.js.map +4 -4
  18. package/node_modules/glob/dist/esm/pattern.d.ts +3 -0
  19. package/node_modules/glob/dist/esm/pattern.d.ts.map +1 -1
  20. package/node_modules/glob/dist/esm/pattern.js +4 -0
  21. package/node_modules/glob/dist/esm/pattern.js.map +1 -1
  22. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/README.md +7 -10
  23. package/node_modules/{@isaacs → glob/node_modules}/balanced-match/package.json +7 -18
  24. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/README.md +3 -6
  25. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.js +6 -4
  26. package/node_modules/glob/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -0
  27. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.js +6 -4
  28. package/node_modules/glob/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  29. package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/package.json +11 -7
  30. package/node_modules/glob/node_modules/minimatch/README.md +76 -1
  31. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts +1 -1
  32. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.d.ts.map +1 -1
  33. package/node_modules/glob/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js.map +1 -1
  34. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts +4 -2
  35. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.d.ts.map +1 -1
  36. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js +309 -55
  37. package/node_modules/glob/node_modules/minimatch/dist/commonjs/ast.js.map +1 -1
  38. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.d.ts.map +1 -1
  39. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js +2 -4
  40. package/node_modules/glob/node_modules/minimatch/dist/commonjs/brace-expressions.js.map +1 -1
  41. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts +1 -1
  42. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.d.ts.map +1 -1
  43. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js +4 -4
  44. package/node_modules/glob/node_modules/minimatch/dist/commonjs/escape.js.map +1 -1
  45. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts +81 -1
  46. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.d.ts.map +1 -1
  47. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js +232 -134
  48. package/node_modules/glob/node_modules/minimatch/dist/commonjs/index.js.map +1 -1
  49. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts +1 -1
  50. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.d.ts.map +1 -1
  51. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js +8 -8
  52. package/node_modules/glob/node_modules/minimatch/dist/commonjs/unescape.js.map +1 -1
  53. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts +1 -1
  54. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.d.ts.map +1 -1
  55. package/node_modules/glob/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -1
  56. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts +4 -2
  57. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.d.ts.map +1 -1
  58. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js +309 -55
  59. package/node_modules/glob/node_modules/minimatch/dist/esm/ast.js.map +1 -1
  60. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.d.ts.map +1 -1
  61. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js +2 -4
  62. package/node_modules/glob/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -1
  63. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts +1 -1
  64. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.d.ts.map +1 -1
  65. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js +4 -4
  66. package/node_modules/glob/node_modules/minimatch/dist/esm/escape.js.map +1 -1
  67. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts +81 -1
  68. package/node_modules/glob/node_modules/minimatch/dist/esm/index.d.ts.map +1 -1
  69. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js +232 -134
  70. package/node_modules/glob/node_modules/minimatch/dist/esm/index.js.map +1 -1
  71. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts +1 -1
  72. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.d.ts.map +1 -1
  73. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js +8 -8
  74. package/node_modules/glob/node_modules/minimatch/dist/esm/unescape.js.map +1 -1
  75. package/node_modules/glob/node_modules/minimatch/package.json +17 -11
  76. package/node_modules/glob/package.json +10 -13
  77. package/node_modules/minipass/LICENSE.md +55 -0
  78. package/node_modules/minipass/dist/commonjs/index.d.ts +12 -16
  79. package/node_modules/minipass/dist/commonjs/index.d.ts.map +1 -1
  80. package/node_modules/minipass/dist/commonjs/index.js +13 -3
  81. package/node_modules/minipass/dist/commonjs/index.js.map +1 -1
  82. package/node_modules/minipass/dist/esm/index.d.ts +12 -16
  83. package/node_modules/minipass/dist/esm/index.d.ts.map +1 -1
  84. package/node_modules/minipass/dist/esm/index.js +3 -1
  85. package/node_modules/minipass/dist/esm/index.js.map +1 -1
  86. package/node_modules/minipass/package.json +9 -14
  87. package/node_modules/path-scurry/node_modules/lru-cache/README.md +96 -10
  88. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.d.ts.map +1 -0
  89. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel-browser.js.map +1 -0
  90. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.d.ts +5 -0
  91. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
  92. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts +1400 -0
  93. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.d.ts.map +1 -0
  94. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
  95. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js.map +1 -0
  96. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
  97. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js.map +7 -0
  98. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts +12 -0
  99. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.d.ts.map +1 -0
  100. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
  101. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js.map +1 -0
  102. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.cjs.map +1 -0
  103. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
  104. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.d.ts +5 -0
  105. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
  106. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts +109 -32
  107. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.d.ts.map +1 -1
  108. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +334 -197
  109. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js.map +1 -1
  110. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +1 -1
  111. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js.map +4 -4
  112. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.d.ts.map +1 -0
  113. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel-node.js.map +1 -0
  114. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.d.ts +5 -0
  115. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
  116. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts +1400 -0
  117. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.d.ts.map +1 -0
  118. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
  119. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js.map +1 -0
  120. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
  121. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js.map +7 -0
  122. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts +12 -0
  123. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.d.ts.map +1 -0
  124. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
  125. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js.map +1 -0
  126. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts +12 -0
  127. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.d.ts.map +1 -0
  128. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
  129. package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js.map +1 -0
  130. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.d.ts.map +1 -0
  131. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel-browser.js.map +1 -0
  132. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.d.ts +5 -0
  133. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
  134. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts +1400 -0
  135. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.d.ts.map +1 -0
  136. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
  137. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js.map +1 -0
  138. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
  139. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js.map +7 -0
  140. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts +12 -0
  141. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.d.ts.map +1 -0
  142. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
  143. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js.map +1 -0
  144. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
  145. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.mjs.map +1 -0
  146. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.d.ts +5 -0
  147. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
  148. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts +109 -32
  149. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.d.ts.map +1 -1
  150. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +333 -196
  151. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js.map +1 -1
  152. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +1 -1
  153. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js.map +4 -4
  154. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.d.ts.map +1 -0
  155. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel-node.js.map +1 -0
  156. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.d.ts +5 -0
  157. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
  158. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts +1400 -0
  159. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.d.ts.map +1 -0
  160. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
  161. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js.map +1 -0
  162. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
  163. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js.map +7 -0
  164. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts +12 -0
  165. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.d.ts.map +1 -0
  166. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
  167. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js.map +1 -0
  168. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts +12 -0
  169. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.d.ts.map +1 -0
  170. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
  171. package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js.map +1 -0
  172. package/node_modules/path-scurry/node_modules/lru-cache/package.json +71 -18
  173. package/node_modules/path-scurry/package.json +8 -24
  174. package/package.json +1 -1
  175. package/scripts/debug-balance-probe.mjs +35 -35
  176. package/scripts/push-image.sh +43 -43
  177. package/scripts/setup-acr.sh +65 -65
  178. package/scripts/verify-optional-deps.js +96 -1
  179. package/src/__tests__/composioCliFlags.test.js +239 -239
  180. package/src/analyzers/CSSAnalyzer.js +298 -297
  181. package/src/analyzers/ConfigValidator.js +691 -690
  182. package/src/analyzers/ESLintAnalyzer.js +320 -320
  183. package/src/analyzers/JavaScriptAnalyzer.js +260 -261
  184. package/src/analyzers/PrettierFormatter.js +246 -247
  185. package/src/analyzers/PythonAnalyzer.js +283 -283
  186. package/src/analyzers/SecurityAnalyzer.js +729 -729
  187. package/src/analyzers/SparrowAnalyzer.js +341 -341
  188. package/src/analyzers/TypeScriptAnalyzer.js +247 -247
  189. package/src/analyzers/__tests__/CSSAnalyzer.test.js +41 -41
  190. package/src/analyzers/__tests__/ConfigValidator.test.js +362 -362
  191. package/src/analyzers/__tests__/JavaScriptAnalyzer.test.js +40 -40
  192. package/src/analyzers/__tests__/PythonAnalyzer.test.js +205 -208
  193. package/src/analyzers/__tests__/SecurityAnalyzer.test.js +303 -303
  194. package/src/analyzers/__tests__/TypeScriptAnalyzer.test.js +187 -187
  195. package/src/analyzers/codeCloneDetector/analyzer.js +344 -344
  196. package/src/analyzers/codeCloneDetector/detector.js +250 -250
  197. package/src/analyzers/codeCloneDetector/index.js +194 -192
  198. package/src/analyzers/codeCloneDetector/parser.js +199 -199
  199. package/src/core/__tests__/agentPool.test.js +866 -866
  200. package/src/core/__tests__/agentPoolAutoResume.test.js +209 -209
  201. package/src/core/__tests__/agentPoolWakeOnMessage.test.js +315 -315
  202. package/src/core/__tests__/agentScheduler.emptyResponseChatStall.test.js +213 -213
  203. package/src/core/__tests__/agentScheduler.errorCategorisation.test.js +246 -246
  204. package/src/core/__tests__/agentScheduler.firstChunkTimeout.test.js +138 -138
  205. package/src/core/__tests__/agentScheduler.modeTransitions.test.js +233 -233
  206. package/src/core/__tests__/agentScheduler.nativePromptPick.test.js +319 -319
  207. package/src/core/__tests__/agentScheduler.taskLifecycleInstruction.test.js +78 -78
  208. package/src/core/__tests__/agentScheduler.visualizer.test.js +258 -258
  209. package/src/core/__tests__/flowCheckpointStore.test.js +140 -140
  210. package/src/core/__tests__/flowEndToEnd.test.js +565 -565
  211. package/src/core/__tests__/flowFieldMapping.test.js +188 -189
  212. package/src/core/__tests__/flowLintClientMirror.test.js +96 -98
  213. package/src/core/__tests__/flowSavePayload.test.js +170 -169
  214. package/src/core/__tests__/flowTemplates.test.js +311 -311
  215. package/src/core/__tests__/flowVersionStore.test.js +123 -123
  216. package/src/core/__tests__/messageProcessor.test.js +669 -669
  217. package/src/core/__tests__/stateManager.test.js +0 -1
  218. package/src/core/agentPool.js +2474 -2475
  219. package/src/core/agentScheduler.js +1 -4
  220. package/src/core/contextManager.js +708 -708
  221. package/src/core/flowExecutor.js +1510 -1510
  222. package/src/core/flowFieldMapping.js +136 -138
  223. package/src/core/messageProcessor.js +953 -954
  224. package/src/core/orchestrator.js +593 -595
  225. package/src/core/stateManager.js +1765 -1752
  226. package/src/index.js +1221 -1221
  227. package/src/interfaces/__tests__/archivedAgentDelete.test.js +207 -207
  228. package/src/interfaces/__tests__/bulkAgentRoute.test.js +361 -361
  229. package/src/interfaces/__tests__/imageServing.test.js +228 -228
  230. package/src/interfaces/__tests__/remoteSessionAuth.test.js +308 -308
  231. package/src/interfaces/__tests__/videoJobsRoutes.test.js +178 -179
  232. package/src/interfaces/__tests__/webServer.marketplace.test.js +629 -629
  233. package/src/interfaces/schedulerRoutes.js +50 -50
  234. package/src/interfaces/terminal/__tests__/smoke/connection.test.js +341 -350
  235. package/src/interfaces/terminal/__tests__/smoke/enhancements.test.js +156 -156
  236. package/src/interfaces/terminal/__tests__/smoke/imports.test.js +325 -330
  237. package/src/interfaces/terminal/__tests__/smoke/tools.test.js +385 -388
  238. package/src/interfaces/terminal/api/session.js +265 -266
  239. package/src/interfaces/terminal/api/websocket.js +496 -497
  240. package/src/interfaces/terminal/components/AgentCreator.js +691 -705
  241. package/src/interfaces/terminal/components/AgentEditor.js +676 -678
  242. package/src/interfaces/terminal/components/AgentSwitcher.js +331 -330
  243. package/src/interfaces/terminal/components/ErrorPanel.js +263 -264
  244. package/src/interfaces/terminal/components/Header.js +28 -28
  245. package/src/interfaces/terminal/components/Layout.js +598 -603
  246. package/src/interfaces/terminal/components/MessageList.js +280 -281
  247. package/src/interfaces/terminal/components/SettingsPanel.js +410 -415
  248. package/src/interfaces/terminal/components/StatusBar.js +2 -0
  249. package/src/interfaces/terminal/index.js +168 -168
  250. package/src/interfaces/terminal/state/useAgentControl.js +496 -496
  251. package/src/interfaces/terminal/state/useAgents.js +537 -537
  252. package/src/interfaces/terminal/state/useMessages.js +629 -630
  253. package/src/interfaces/terminal/state/useTools.js +554 -554
  254. package/src/interfaces/terminal/utils/debugLogger.js +44 -44
  255. package/src/interfaces/terminal/utils/settingsStorage.js +232 -232
  256. package/src/interfaces/webServer.js +7578 -7579
  257. package/src/interfaces/webServer.js.bak +7046 -7046
  258. package/src/modules/fileExplorer/__tests__/zipDownload.test.js +237 -237
  259. package/src/modules/fileExplorer/controller.js +470 -469
  260. package/src/modules/fileExplorer/routes.js +285 -286
  261. package/src/modules/widget/__tests__/isDisabled.test.js +41 -41
  262. package/src/modules/widget/__tests__/routes.test.js +677 -678
  263. package/src/modules/widget/__tests__/runtime.test.js +401 -401
  264. package/src/modules/widget/__tests__/versioning.test.js +309 -309
  265. package/src/modules/widget/__tests__/webComponentRuntime.test.js +565 -565
  266. package/src/modules/widget/__tests__/widgetTool.test.js +316 -316
  267. package/src/modules/widget/routes.js +435 -435
  268. package/src/modules/widget/runtime/bundle.js +640 -640
  269. package/src/modules/widget/runtime/webComponentBundle.js +470 -470
  270. package/src/modules/widget/schema.js +182 -181
  271. package/src/modules/widget/widgetTool.js +1389 -1389
  272. package/src/services/__tests__/agentActivityService.test.js +401 -402
  273. package/src/services/__tests__/benchmarkService.test.js +184 -184
  274. package/src/services/__tests__/contextInjectionService.test.js +246 -246
  275. package/src/services/__tests__/conversationQuery.test.js +721 -723
  276. package/src/services/__tests__/credentialVault.test.js +469 -469
  277. package/src/services/__tests__/discordService.integration.test.js +638 -639
  278. package/src/services/__tests__/flowContextService.test.js +590 -590
  279. package/src/services/__tests__/memoryService.test.js +1 -1
  280. package/src/services/__tests__/messageSource.test.js +380 -380
  281. package/src/services/__tests__/modelRouterNaming.test.js +111 -111
  282. package/src/services/__tests__/projectDetector.test.js +34 -34
  283. package/src/services/__tests__/promptService.test.js +242 -242
  284. package/src/services/__tests__/telegramService.test.js +941 -941
  285. package/src/services/__tests__/tokenCountingService.test.js +48 -48
  286. package/src/services/agentActivityService.js +419 -420
  287. package/src/services/aiService.js +2997 -3001
  288. package/src/services/apiKeyManager.js +359 -359
  289. package/src/services/benchmarkService.js +196 -196
  290. package/src/services/codebaseKnowledgeService.js +2 -2
  291. package/src/services/composioService.js +738 -738
  292. package/src/services/conversationCompactionService.js +1258 -1257
  293. package/src/services/credentialVault.js +685 -685
  294. package/src/services/discordService.js +792 -793
  295. package/src/services/embeddings/__tests__/azureCustomProvider.test.js +232 -232
  296. package/src/services/embeddings/__tests__/embeddingService.test.js +417 -417
  297. package/src/services/embeddings/__tests__/localProvider.test.js +263 -263
  298. package/src/services/embeddings/autoRecall.js +218 -219
  299. package/src/services/embeddings/indexers/__tests__/agentIndexer.test.js +232 -232
  300. package/src/services/embeddings/indexers/__tests__/memoryIndexer.test.js +418 -418
  301. package/src/services/embeddings/indexers/__tests__/reminisceIndexer.test.js +356 -357
  302. package/src/services/embeddings/indexers/__tests__/skillsIndexer.test.js +145 -145
  303. package/src/services/embeddings/indexers/__tests__/taskIndexer.test.js +146 -146
  304. package/src/services/embeddings/indexers/composioIndexer.js +279 -279
  305. package/src/services/embeddings/providerInterface.js +206 -206
  306. package/src/services/embeddings/providers/localProvider.js +11 -7
  307. package/src/services/embeddings/providers/openaiProvider.js +101 -101
  308. package/src/services/embeddings/vectorStore/inMemoryJsonStore.js +356 -356
  309. package/src/services/errorHandler.js +809 -809
  310. package/src/services/flowContextService.js +586 -586
  311. package/src/services/grounding/MockAdapter.js +125 -125
  312. package/src/services/modelRouterService.js +26 -31
  313. package/src/services/modelsService.js +322 -322
  314. package/src/services/ollamaService.js +452 -452
  315. package/src/services/projectDetector.js +403 -404
  316. package/src/services/promptService.js +418 -418
  317. package/src/services/qualityInspector.js +795 -795
  318. package/src/services/scheduleService.js +726 -726
  319. package/src/services/serviceRegistry.js +386 -386
  320. package/src/services/telegrafBot.js +174 -174
  321. package/src/services/telegramService.js +1972 -1972
  322. package/src/services/visualEditorBridge.js +1033 -1033
  323. package/src/services/visualEditorServer.js +1769 -1774
  324. package/src/services/whatsappService.js +667 -668
  325. package/src/tools/__tests__/agentCommunicationTool.findAgent.test.js +226 -226
  326. package/src/tools/__tests__/agentCommunicationTool.test.js +3 -3
  327. package/src/tools/__tests__/agentDelayTool.test.js +342 -342
  328. package/src/tools/__tests__/baseTool.test.js +3 -3
  329. package/src/tools/__tests__/codeMapTool.test.js +915 -915
  330. package/src/tools/__tests__/fileContentReplaceTool.test.js +309 -309
  331. package/src/tools/__tests__/fileTreeTool.test.js +274 -274
  332. package/src/tools/__tests__/filesystemTool.test.js +815 -815
  333. package/src/tools/__tests__/foundryWebSearchTool.test.js +252 -252
  334. package/src/tools/__tests__/imageTool.validator.test.js +194 -194
  335. package/src/tools/__tests__/jobDoneTool.test.js +580 -581
  336. package/src/tools/__tests__/memoryTool.forgetStale.test.js +272 -272
  337. package/src/tools/__tests__/memoryTool.reminisce.test.js +2 -2
  338. package/src/tools/__tests__/memoryTool.reminisceSemanticSearch.test.js +301 -301
  339. package/src/tools/__tests__/memoryTool.semanticSearch.test.js +405 -405
  340. package/src/tools/__tests__/memoryTool.teamPool.test.js +293 -293
  341. package/src/tools/__tests__/memoryTool.test.js +1 -1
  342. package/src/tools/__tests__/seekTool.test.js +282 -282
  343. package/src/tools/__tests__/skillsTool.search.test.js +164 -164
  344. package/src/tools/__tests__/skillsTool.test.js +226 -226
  345. package/src/tools/__tests__/staticAnalysisTool.test.js +509 -509
  346. package/src/tools/__tests__/taskManagerTool.discipline.test.js +137 -137
  347. package/src/tools/__tests__/taskManagerTool.search.test.js +143 -143
  348. package/src/tools/__tests__/taskManagerTool.test.js +866 -866
  349. package/src/tools/__tests__/terminalTool.test.js +448 -448
  350. package/src/tools/__tests__/toolShapeForgiveness.test.js +259 -260
  351. package/src/tools/__tests__/userPromptTool.test.js +297 -297
  352. package/src/tools/__tests__/videoTool.jobs.test.js +147 -147
  353. package/src/tools/__tests__/webTool.e2e.test.js +609 -603
  354. package/src/tools/__tests__/webTool.unit.test.js +195 -195
  355. package/src/tools/__tests__/webTool.visionModel.test.js +75 -75
  356. package/src/tools/agentCommunicationTool.js +8 -10
  357. package/src/tools/agentDelayTool.js +496 -497
  358. package/src/tools/asyncToolManager.js +602 -603
  359. package/src/tools/baseTool.js +12 -11
  360. package/src/tools/cloneDetectionTool.js +576 -581
  361. package/src/tools/codeMapTool.js +0 -6
  362. package/src/tools/composioTool.js +617 -617
  363. package/src/tools/dependencyResolverTool.js +1211 -1212
  364. package/src/tools/desktop/DesktopTool.js +629 -638
  365. package/src/tools/desktop/__tests__/DesktopTool.e2e.test.js +306 -306
  366. package/src/tools/desktop/__tests__/DesktopTool.test.js +507 -507
  367. package/src/tools/desktop/__tests__/osController.test.js +364 -364
  368. package/src/tools/desktop/osController.js +491 -491
  369. package/src/tools/docxTool.js +623 -623
  370. package/src/tools/excelTool.js +636 -636
  371. package/src/tools/fileContentReplaceTool.js +5 -7
  372. package/src/tools/fileSystemTool.js +12 -19
  373. package/src/tools/fileTreeTool.js +840 -840
  374. package/src/tools/foundryWebSearchTool.js +273 -273
  375. package/src/tools/helpTool.js +198 -198
  376. package/src/tools/imageTool.js +1397 -1397
  377. package/src/tools/importAnalyzerTool.js +1056 -1056
  378. package/src/tools/jobDoneTool.js +495 -495
  379. package/src/tools/memoryTool.js +1 -1
  380. package/src/tools/office/pres/__tests__/presSystem.test.js +365 -365
  381. package/src/tools/office/pres/archetypes/agenda.js +61 -61
  382. package/src/tools/office/pres/archetypes/bentoGrid.js +218 -219
  383. package/src/tools/office/pres/archetypes/bigStat.js +140 -142
  384. package/src/tools/office/pres/archetypes/closing.js +70 -70
  385. package/src/tools/office/pres/archetypes/hero.js +70 -70
  386. package/src/tools/office/pres/archetypes/productHero.js +93 -94
  387. package/src/tools/office/pres/archetypes/table.js +73 -74
  388. package/src/tools/office/pres/backgrounds/orb.js +66 -66
  389. package/src/tools/office/pres/components.js +422 -423
  390. package/src/tools/officeTool.js +441 -441
  391. package/src/tools/pdfTool.js +625 -627
  392. package/src/tools/platformControlTool.js +1081 -1081
  393. package/src/tools/seekTool.js +917 -918
  394. package/src/tools/skillsTool.js +1 -1
  395. package/src/tools/staticAnalysisTool.js +2143 -2146
  396. package/src/tools/taskManagerTool.js +3324 -3324
  397. package/src/tools/terminalTool.js +2615 -2618
  398. package/src/tools/videoTool.js +1303 -1303
  399. package/src/tools/visionTool.js +508 -508
  400. package/src/tools/visualEditorTool.js +1289 -1290
  401. package/src/tools/webTool.js +3368 -3368
  402. package/src/tools/whatsappTool.js +464 -464
  403. package/src/types/__tests__/agent.test.js +499 -499
  404. package/src/types/__tests__/contextReference.test.js +606 -606
  405. package/src/types/__tests__/conversation.test.js +555 -555
  406. package/src/types/__tests__/toolCommand.test.js +584 -584
  407. package/src/types/contextReference.js +974 -971
  408. package/src/types/conversation.js +729 -729
  409. package/src/types/toolCommand.js +746 -746
  410. package/src/utilities/__tests__/attachmentValidator.test.js +80 -80
  411. package/src/utilities/__tests__/auditReport.test.js +328 -328
  412. package/src/utilities/__tests__/directoryAccessManager.test.js +388 -388
  413. package/src/utilities/__tests__/jsonRepair.test.js +103 -104
  414. package/src/utilities/__tests__/modeTransitionReasons.test.js +105 -105
  415. package/src/utilities/__tests__/platformUtils.test.js +80 -87
  416. package/src/utilities/__tests__/structuredFileValidator.test.js +261 -263
  417. package/src/utilities/__tests__/toolConstants.test.js +92 -94
  418. package/src/utilities/__tests__/useIsTouchDevice.detect.test.js +114 -114
  419. package/src/utilities/__tests__/webUiUtilSync.test.js +117 -117
  420. package/src/utilities/attachmentValidator.js +284 -288
  421. package/src/utilities/authCache.js.backup-1779570472481 +121 -121
  422. package/src/utilities/browserStealth.js +631 -630
  423. package/src/utilities/configManager.js +616 -617
  424. package/src/utilities/directoryAccessManager.js +564 -565
  425. package/src/utilities/fileProcessor.js +308 -307
  426. package/src/utilities/humanBehavior.js +454 -453
  427. package/src/utilities/logger.js +479 -479
  428. package/src/utilities/structuredFileValidator.js +696 -699
  429. package/src/utilities/tagParser.js +5 -10
  430. package/src/utilities/userDataDir.js +308 -308
  431. package/node_modules/@isaacs/brace-expansion/dist/commonjs/index.js.map +0 -1
  432. package/node_modules/@isaacs/brace-expansion/dist/esm/index.js.map +0 -1
  433. package/node_modules/minipass/LICENSE +0 -15
  434. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/LICENSE.md +0 -0
  435. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts +0 -0
  436. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.d.ts.map +0 -0
  437. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js +0 -0
  438. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/index.js.map +0 -0
  439. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/commonjs/package.json +0 -0
  440. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts +0 -0
  441. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.d.ts.map +0 -0
  442. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js +0 -0
  443. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/index.js.map +0 -0
  444. /package/node_modules/{@isaacs → glob/node_modules}/balanced-match/dist/esm/package.json +0 -0
  445. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/LICENSE +0 -0
  446. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts +0 -0
  447. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/index.d.ts.map +0 -0
  448. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/commonjs/package.json +0 -0
  449. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts +0 -0
  450. /package/node_modules/{@isaacs → glob/node_modules}/brace-expansion/dist/esm/index.d.ts.map +0 -0
  451. /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, beforeEach } 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
- });
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
+ });