@lowdefy/server-dev 6.1.0 → 7.0.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 (623) hide show
  1. package/client/App.jsx +52 -24
  2. package/client/Inspector.jsx +73 -30
  3. package/client/JourneyObserver.jsx +47 -0
  4. package/client/Page.jsx +54 -22
  5. package/client/Recorder.jsx +73 -0
  6. package/client/Routing.jsx +37 -13
  7. package/client/clientBuildImports.test.mjs +2 -0
  8. package/client/devStreamConnections.test.mjs +40 -0
  9. package/client/feedback/FeedbackMount.jsx +2 -2
  10. package/client/feedback/FeedbackOverlay.jsx +7 -4
  11. package/client/feedback/feedbackStyles.js +3 -0
  12. package/client/journeyObserver/startJourneyObserver.js +101 -0
  13. package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
  14. package/client/openInEditor/OpenInEditorListener.jsx +2 -0
  15. package/client/recorder/buildEngineRecord.js +37 -0
  16. package/client/recorder/buildInteractionRecord.js +64 -0
  17. package/client/recorder/buildTraceEvent.js +57 -0
  18. package/client/recorder/buildTraceEvent.test.mjs +81 -0
  19. package/client/recorder/chunkRecords.js +69 -0
  20. package/client/recorder/chunkRecords.test.mjs +59 -0
  21. package/client/recorder/collectEventActions.js +30 -0
  22. package/client/recorder/collectEventEndpoints.js +40 -0
  23. package/client/recorder/collectEventRequests.js +39 -0
  24. package/client/recorder/createPairingBuffer.js +187 -0
  25. package/client/recorder/createPairingBuffer.test.mjs +405 -0
  26. package/client/recorder/createPasswordRedactor.js +84 -0
  27. package/client/recorder/createRecordBatcher.js +54 -0
  28. package/client/recorder/createRecordBatcher.test.mjs +76 -0
  29. package/client/recorder/createRecorder.js +216 -0
  30. package/client/recorder/createRecorder.test.mjs +318 -0
  31. package/client/recorder/createRecordingSession.js +61 -0
  32. package/client/recorder/createRecordingSession.test.mjs +72 -0
  33. package/client/recorder/diffStateWrites.js +77 -0
  34. package/client/recorder/diffStateWrites.test.mjs +66 -0
  35. package/client/recorder/isEventCall.js +30 -0
  36. package/client/recorder/jsonType.js +39 -0
  37. package/client/recorder/keyChord.js +47 -0
  38. package/client/recorder/keyChord.test.mjs +50 -0
  39. package/client/recorder/sendRecords.js +40 -0
  40. package/client/recorder/toJsonSafe.js +29 -0
  41. package/lib/client/BuildingPage.jsx +3 -1
  42. package/lib/client/ErrorBar.jsx +17 -13
  43. package/lib/client/RedirectingPage.jsx +56 -0
  44. package/lib/client/auth/Auth.jsx +11 -6
  45. package/lib/client/auth/AuthConfigured.jsx +222 -36
  46. package/lib/client/auth/AuthNotConfigured.js +45 -4
  47. package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
  48. package/lib/client/auth/authTemplateSync.test.mjs +40 -0
  49. package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
  50. package/lib/client/auth/createFetchCheckingBuild.js +45 -0
  51. package/lib/client/bundleBuildId.js +25 -0
  52. package/lib/client/collectVisibleBlockIds.js +51 -0
  53. package/lib/client/getCspNonce.js +25 -0
  54. package/lib/client/getPagePath.js +36 -0
  55. package/lib/client/getPagePath.test.mjs +51 -0
  56. package/lib/client/utils/formatErrorsForCopy.js +30 -0
  57. package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
  58. package/lib/client/utils/getErrorBarColor.js +25 -0
  59. package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
  60. package/lib/client/utils/request.js +8 -1
  61. package/lib/client/utils/usePageConfig.js +89 -25
  62. package/lib/client/utils/usePageConfig.test.mjs +198 -13
  63. package/lib/configTree/buildConfigTree.mjs +58 -0
  64. package/lib/configTree/buildConfigTree.test.mjs +77 -0
  65. package/lib/configTree/collectConfigText.mjs +101 -0
  66. package/lib/configTree/collectConfigText.test.mjs +116 -0
  67. package/lib/configTree/prepareScratchServer.mjs +46 -0
  68. package/lib/configTree/runConfigTreeBuild.mjs +88 -0
  69. package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
  70. package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
  71. package/lib/docs/JourneyStepError.js +30 -0
  72. package/lib/docs/acquireBrowserSlot.js +30 -0
  73. package/lib/docs/addNestedEndpoints.js +54 -0
  74. package/lib/docs/addNestedEndpoints.test.mjs +185 -0
  75. package/lib/docs/addWriteFlags.js +77 -0
  76. package/lib/docs/buildEditedPages.js +50 -0
  77. package/lib/docs/buildWaitHeader.js +21 -0
  78. package/lib/docs/captureAnnotatedScreenshot.js +23 -12
  79. package/lib/docs/checkAgentFiles.js +148 -0
  80. package/lib/docs/checkAgentFiles.test.mjs +225 -0
  81. package/lib/docs/checkStepReferences.js +239 -0
  82. package/lib/docs/checkStepReferences.test.mjs +173 -0
  83. package/lib/docs/checkpointStore.js +3 -0
  84. package/lib/docs/clientErrorStore.js +18 -1
  85. package/lib/docs/clientErrorStore.test.mjs +46 -0
  86. package/lib/docs/collectExercised.js +52 -0
  87. package/lib/docs/createBrowserLifecycle.js +102 -0
  88. package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
  89. package/lib/docs/createDocsMcpServer.js +345 -388
  90. package/lib/docs/createDocsMcpServer.test.mjs +644 -4
  91. package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
  92. package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
  93. package/lib/docs/createJourneyActors.js +213 -0
  94. package/lib/docs/createJourneyActors.test.mjs +79 -0
  95. package/lib/docs/createJourneyObservations.js +80 -0
  96. package/lib/docs/createLeftOriginError.js +26 -0
  97. package/lib/docs/createNetworkCounter.js +101 -0
  98. package/lib/docs/dataSets/checkSharedCollectionIds.js +67 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  104. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  105. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  106. package/lib/docs/dataSets/duplicateIndexName.js +24 -0
  107. package/lib/docs/dataSets/findCollectionCollisions.js +52 -0
  108. package/lib/docs/dataSets/getDataStore.js +106 -0
  109. package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
  110. package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
  111. package/lib/docs/dataSets/groupDataSetByCollection.js +44 -0
  112. package/lib/docs/dataSets/insertFixture.js +57 -0
  113. package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
  114. package/lib/docs/dataSets/isDataStoreGone.js +29 -0
  115. package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
  116. package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
  117. package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
  118. package/lib/docs/dataSets/openDataSession.js +119 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -0
  120. package/lib/docs/dataSets/pickDataStorePort.js +58 -0
  121. package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
  122. package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
  123. package/lib/docs/dataSets/readDataSession.js +41 -0
  124. package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
  125. package/lib/docs/dataSets/readDataSet.js +68 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +220 -0
  127. package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
  128. package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
  129. package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
  130. package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
  131. package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
  132. package/lib/docs/dataSets/sweepOrphanDatabases.js +33 -0
  133. package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
  134. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  135. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
  136. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  137. package/lib/docs/devEventBus.js +136 -0
  138. package/lib/docs/devEventBus.test.mjs +269 -0
  139. package/lib/docs/devToolDefinitions.js +529 -0
  140. package/lib/docs/docSources.js +21 -0
  141. package/lib/docs/docs.test.mjs +171 -15
  142. package/lib/docs/docsIndex.test.mjs +377 -0
  143. package/lib/docs/evalOperator.js +5 -3
  144. package/lib/docs/evalOperatorHeadless.js +84 -12
  145. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  146. package/lib/docs/evalOperatorInTab.js +2 -1
  147. package/lib/docs/findConfig.js +95 -49
  148. package/lib/docs/fitResponse.js +81 -0
  149. package/lib/docs/fitResponse.test.mjs +128 -0
  150. package/lib/docs/formatFeedback.js +26 -5
  151. package/lib/docs/formatFeedback.test.mjs +48 -0
  152. package/lib/docs/getAppMap.js +7 -1
  153. package/lib/docs/getBrowser.js +150 -80
  154. package/lib/docs/getBrowser.test.mjs +407 -16
  155. package/lib/docs/getBuildId.js +61 -0
  156. package/lib/docs/getBuildId.test.mjs +84 -0
  157. package/lib/docs/getBuildStatus.js +54 -6
  158. package/lib/docs/getBuildStatus.test.mjs +133 -0
  159. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  160. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  161. package/lib/docs/getCoreDoc.js +9 -1
  162. package/lib/docs/getDoc.js +100 -0
  163. package/lib/docs/getDocsIndex.js +152 -0
  164. package/lib/docs/getHazards.js +115 -0
  165. package/lib/docs/getHazards.test.mjs +175 -0
  166. package/lib/docs/getMailOutboxDirectory.js +26 -0
  167. package/lib/docs/getOverview.js +79 -13
  168. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  169. package/lib/docs/getPageBuildStatus.js +46 -0
  170. package/lib/docs/getPluginDoc.js +21 -23
  171. package/lib/docs/getSchema.js +33 -1
  172. package/lib/docs/getStaleStatus.js +36 -0
  173. package/lib/docs/getStaleStatus.test.mjs +74 -0
  174. package/lib/docs/guardJourneyOrigin.js +39 -0
  175. package/lib/docs/inspectState.js +5 -3
  176. package/lib/docs/inspectStateFromTab.js +2 -2
  177. package/lib/docs/inspectStateHeadless.js +89 -27
  178. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  179. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  180. package/lib/docs/installHeadlessShell.js +123 -0
  181. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  182. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  183. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  184. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  185. package/lib/docs/isPageReady.js +77 -0
  186. package/lib/docs/isPageReady.test.mjs +235 -0
  187. package/lib/docs/isRunErrorEntry.js +25 -0
  188. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  189. package/lib/docs/journeyRunRecording.js +35 -0
  190. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  191. package/lib/docs/keepEventStreamAlive.js +68 -0
  192. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  193. package/lib/docs/launchBrowser.js +83 -0
  194. package/lib/docs/launchBrowser.test.mjs +175 -0
  195. package/lib/docs/listModuleDocEntries.js +69 -0
  196. package/lib/docs/listPackageDocFiles.js +67 -0
  197. package/lib/docs/listPluginDocEntries.js +111 -0
  198. package/lib/docs/listPlugins.js +9 -58
  199. package/lib/docs/listTypes.js +14 -6
  200. package/lib/docs/loadFrameworkHazards.js +40 -0
  201. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  202. package/lib/docs/loadState.js +103 -35
  203. package/lib/docs/makeModuleManifestDoc.js +174 -0
  204. package/lib/docs/matchExercisedPages.js +35 -0
  205. package/lib/docs/matchJourneyRoute.js +69 -0
  206. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  207. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  208. package/lib/docs/nextJourneySequence.js +30 -0
  209. package/lib/docs/noBrowserError.js +22 -0
  210. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  211. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  212. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  213. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  214. package/lib/docs/observe/evaluateInvariants.js +183 -0
  215. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  216. package/lib/docs/observe/findingKey.js +29 -0
  217. package/lib/docs/observe/hasNoEffect.js +31 -0
  218. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  219. package/lib/docs/observe/installStepObserver.js +72 -0
  220. package/lib/docs/observe/isAppApiUrl.js +33 -0
  221. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  222. package/lib/docs/observe/readStepObserver.js +42 -0
  223. package/lib/docs/observe/relativeSource.js +29 -0
  224. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  225. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  226. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  227. package/lib/docs/observe/usesSearchStage.js +45 -0
  228. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  229. package/lib/docs/observe/watchJourneyContext.js +75 -0
  230. package/lib/docs/observe/withAppErrors.js +31 -0
  231. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  232. package/lib/docs/openJourney.js +94 -0
  233. package/lib/docs/openJourneyEmail.js +41 -0
  234. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  235. package/lib/docs/pageBlocks.js +47 -0
  236. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  237. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  238. package/lib/docs/rails/authEngineActions.js +53 -0
  239. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  240. package/lib/docs/rails/authEngineMethods.js +52 -0
  241. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  242. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  243. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  244. package/lib/docs/rails/checkClickRails.js +56 -0
  245. package/lib/docs/rails/createDataSetRails.js +82 -0
  246. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  247. package/lib/docs/rails/findExternalBlocks.js +147 -0
  248. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  249. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  250. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  251. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  252. package/lib/docs/readConnectionPlugins.js +26 -0
  253. package/lib/docs/readDevAuthMode.js +32 -0
  254. package/lib/docs/readDocEntry.js +40 -0
  255. package/lib/docs/readDocTitle.js +26 -0
  256. package/lib/docs/readJourneyEmailMatch.js +57 -0
  257. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  258. package/lib/docs/readJourneySession.js +56 -0
  259. package/lib/docs/readMailOutbox.js +37 -0
  260. package/lib/docs/readPagePath.js +27 -0
  261. package/lib/docs/readPluginPackages.js +99 -0
  262. package/lib/docs/readProxyBuildWait.js +38 -0
  263. package/lib/docs/readRoutineSteps.js +45 -0
  264. package/lib/docs/registerShownTextEngine.js +118 -0
  265. package/lib/docs/requestRestart.js +30 -0
  266. package/lib/docs/requestRestart.test.mjs +49 -0
  267. package/lib/docs/resolvePageInstance.js +46 -0
  268. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  269. package/lib/docs/resolveSource.js +11 -7
  270. package/lib/docs/resolveSource.test.mjs +1 -1
  271. package/lib/docs/resolveToolCaller.js +62 -0
  272. package/lib/docs/reviewPage.js +51 -0
  273. package/lib/docs/reviewPageBuilds.js +56 -0
  274. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  275. package/lib/docs/runCheck.js +75 -0
  276. package/lib/docs/runCheck.test.mjs +91 -0
  277. package/lib/docs/runCheckChild.js +49 -0
  278. package/lib/docs/runEndpoint.js +185 -0
  279. package/lib/docs/runEndpoint.test.mjs +509 -0
  280. package/lib/docs/runErrorBuffers.js +99 -0
  281. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  282. package/lib/docs/runJourney.js +347 -0
  283. package/lib/docs/runJourney.test.mjs +2383 -0
  284. package/lib/docs/runJourneyAsUsers.js +44 -0
  285. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  286. package/lib/docs/runJourneySteps.js +1320 -0
  287. package/lib/docs/runRequest.js +100 -59
  288. package/lib/docs/runRequest.test.mjs +190 -0
  289. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  290. package/lib/docs/scaffoldPage.js +3 -1
  291. package/lib/docs/screenshotPage.js +163 -11
  292. package/lib/docs/screenshotPage.test.mjs +263 -1
  293. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  294. package/lib/docs/searchDocs.js +82 -30
  295. package/lib/docs/searchDocs.test.mjs +100 -0
  296. package/lib/docs/searchIcons.js +90 -0
  297. package/lib/docs/searchIcons.test.mjs +127 -0
  298. package/lib/docs/selectExercised.js +39 -0
  299. package/lib/docs/selectFinalState.js +66 -0
  300. package/lib/docs/selectFinalState.test.mjs +51 -0
  301. package/lib/docs/serverErrorStore.js +54 -0
  302. package/lib/docs/serverErrorStore.test.mjs +50 -0
  303. package/lib/docs/setupTestFixtures.mjs +133 -39
  304. package/lib/docs/shownTextSelector.js +28 -0
  305. package/lib/docs/snapshotState.js +13 -5
  306. package/lib/docs/splitSearchTerms.js +62 -0
  307. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  308. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  309. package/lib/docs/tabAvailable.js +6 -5
  310. package/lib/docs/tabChannel.js +127 -21
  311. package/lib/docs/tabChannel.test.mjs +187 -29
  312. package/lib/docs/unsettledPageNote.js +24 -0
  313. package/lib/docs/validateJourneyMail.js +36 -0
  314. package/lib/docs/validateJourneyTimeout.js +38 -0
  315. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  316. package/lib/docs/validateRecordingParam.js +42 -0
  317. package/lib/docs/validateStateSelection.js +33 -0
  318. package/lib/docs/validateStateSelection.test.mjs +33 -0
  319. package/lib/docs/validateViewport.js +51 -0
  320. package/lib/docs/validateViewport.test.mjs +54 -0
  321. package/lib/docs/waitForBuild.js +50 -0
  322. package/lib/docs/waitForBuild.test.mjs +75 -0
  323. package/lib/docs/waitForJourneyEmail.js +77 -0
  324. package/lib/docs/withBrowserSlot.js +45 -0
  325. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  326. package/lib/docs/withDataSession.js +53 -0
  327. package/lib/server/applyDataSetRedirect.js +83 -0
  328. package/lib/server/applyDataSetRedirect.test.mjs +340 -0
  329. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
  330. package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
  331. package/lib/server/auth/createSystemContext.js +73 -0
  332. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  333. package/lib/server/auth/getHeadlessUser.js +9 -8
  334. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  335. package/lib/server/auth/getMockUser.js +18 -14
  336. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  337. package/lib/server/auth/headlessUser.js +3 -2
  338. package/lib/server/auth/journeyActor.js +35 -0
  339. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  340. package/lib/server/checkPageRecord.js +44 -0
  341. package/lib/server/contextMapBudget.js +24 -0
  342. package/lib/server/contextMapBudget.test.mjs +98 -0
  343. package/lib/server/continueJitKeys.js +28 -0
  344. package/lib/server/continueJitKeys.test.mjs +43 -0
  345. package/lib/server/createDataSessionEndedError.js +33 -0
  346. package/lib/server/createDataSetRefusal.js +26 -0
  347. package/lib/server/createLowdefyContext.js +168 -41
  348. package/lib/server/createLowdefyContext.test.mjs +328 -8
  349. package/lib/server/getClientAddress.js +51 -0
  350. package/lib/server/getClientAddress.test.mjs +56 -0
  351. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  352. package/lib/server/hashConfigContent.js +26 -0
  353. package/lib/server/importAppCode.test.mjs +57 -0
  354. package/lib/server/isRoutineStep.js +27 -0
  355. package/lib/server/jitPageBuilder.js +309 -126
  356. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  357. package/lib/server/jitPageBuilder.test.mjs +218 -0
  358. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  359. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  360. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  361. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  362. package/lib/server/journeyCookies.js +22 -0
  363. package/lib/server/journeyCookies.test.mjs +145 -0
  364. package/lib/server/loadDynamicJsMap.js +47 -0
  365. package/lib/server/log/createHandleError.js +38 -2
  366. package/lib/server/log/createHandleError.test.mjs +125 -2
  367. package/lib/server/log/createLogger.js +3 -0
  368. package/lib/server/log/createLogger.test.mjs +15 -0
  369. package/lib/server/log/logRequest.js +2 -3
  370. package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
  371. package/lib/server/mutants/applyMutant.js +39 -0
  372. package/lib/server/mutants/artifactPatterns.js +25 -0
  373. package/lib/server/mutants/buildAnchor.js +45 -0
  374. package/lib/server/mutants/collectSharedIds.js +41 -0
  375. package/lib/server/mutants/configPathSegments.js +38 -0
  376. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  377. package/lib/server/mutants/describeEvent.js +22 -0
  378. package/lib/server/mutants/enumerateArtifact.js +56 -0
  379. package/lib/server/mutants/findNodeByKey.js +45 -0
  380. package/lib/server/mutants/findNodeByPointer.js +49 -0
  381. package/lib/server/mutants/getArtifactKind.js +32 -0
  382. package/lib/server/mutants/groupMutantCopies.js +56 -0
  383. package/lib/server/mutants/hashArtifact.js +25 -0
  384. package/lib/server/mutants/hashMutantTarget.js +34 -0
  385. package/lib/server/mutants/listMutants.js +122 -0
  386. package/lib/server/mutants/listMutants.test.mjs +502 -0
  387. package/lib/server/mutants/mutantId.js +30 -0
  388. package/lib/server/mutants/mutantRuns.js +57 -0
  389. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  390. package/lib/server/mutants/operators/dropAction.js +47 -0
  391. package/lib/server/mutants/operators/dropBlock.js +44 -0
  392. package/lib/server/mutants/operators/dropPayload.js +52 -0
  393. package/lib/server/mutants/operators/dropStep.js +44 -0
  394. package/lib/server/mutants/operators/flipVisible.js +53 -0
  395. package/lib/server/mutants/operators/index.js +37 -0
  396. package/lib/server/mutants/operators/retargetLink.js +72 -0
  397. package/lib/server/mutants/operators/skipValidate.js +77 -0
  398. package/lib/server/mutants/operators/swapIf.js +112 -0
  399. package/lib/server/mutants/operators.test.mjs +396 -0
  400. package/lib/server/mutants/positionFreePath.js +38 -0
  401. package/lib/server/mutants/removeFromParent.js +28 -0
  402. package/lib/server/mutants/test/fixtures.mjs +173 -0
  403. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  404. package/lib/server/mutants/validateMutantParam.js +69 -0
  405. package/lib/server/mutants/validateMutantsBody.js +88 -0
  406. package/lib/server/mutants/walkArtifact.js +179 -0
  407. package/lib/server/pageBuildRecords.js +131 -0
  408. package/lib/server/pageBuildRecords.test.mjs +258 -0
  409. package/lib/server/pageCache.mjs +22 -7
  410. package/lib/server/pageCache.test.mjs +22 -7
  411. package/lib/server/pageRebuilds.test.mjs +543 -0
  412. package/lib/server/pruneJitMaps.js +36 -0
  413. package/lib/server/pruneJitMaps.test.mjs +55 -0
  414. package/lib/server/readInstanceRecord.js +33 -0
  415. package/lib/server/readInstanceRecord.test.mjs +99 -0
  416. package/lib/server/readMergedMaps.js +86 -0
  417. package/lib/server/readMergedMaps.test.mjs +79 -0
  418. package/lib/server/recording/appendRecords.js +33 -0
  419. package/lib/server/recording/errorRecordingStamp.js +34 -0
  420. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  421. package/lib/server/recording/isRecordingEnabled.js +31 -0
  422. package/lib/server/recording/readRecordingCookie.js +71 -0
  423. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  424. package/lib/server/recording/recordingCookiePayload.js +33 -0
  425. package/lib/server/recording/servedBuilds.js +46 -0
  426. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  427. package/lib/server/sinkExternalConnections.js +95 -0
  428. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  429. package/lib/server/skipStaleMapWrites.js +62 -0
  430. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  431. package/lib/server/startIdleGc.js +67 -0
  432. package/lib/server/startIdleGc.test.mjs +138 -0
  433. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  434. package/lib/server/trackSessionWork.js +30 -0
  435. package/lib/vite/createDevServerExclude.js +40 -0
  436. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  437. package/manager/getContext.mjs +24 -2
  438. package/manager/processes/initialBuild.mjs +14 -0
  439. package/manager/processes/initialBuild.test.mjs +101 -0
  440. package/manager/processes/lowdefyBuild.mjs +30 -7
  441. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
  442. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  443. package/manager/processes/optimizeDependencies.mjs +59 -0
  444. package/manager/processes/restartServer.mjs +34 -6
  445. package/manager/processes/restartServer.test.mjs +162 -0
  446. package/manager/processes/shutdownServer.mjs +11 -0
  447. package/manager/processes/shutdownServer.test.mjs +45 -0
  448. package/manager/processes/startFirstServer.mjs +46 -0
  449. package/manager/processes/startFirstServer.test.mjs +326 -0
  450. package/manager/processes/startMailSink.mjs +135 -0
  451. package/manager/processes/startMailSink.test.mjs +146 -0
  452. package/manager/processes/startProxy.mjs +129 -12
  453. package/manager/processes/startProxy.test.mjs +366 -0
  454. package/manager/processes/startServer.mjs +47 -6
  455. package/manager/processes/startServer.test.mjs +155 -0
  456. package/manager/processes/startWatchers.mjs +6 -4
  457. package/manager/processes/syncServer.mjs +69 -0
  458. package/manager/processes/syncServer.test.mjs +141 -0
  459. package/manager/processes/waitForDevServer.mjs +48 -0
  460. package/manager/processes/waitForDevServer.test.mjs +112 -0
  461. package/manager/run.mjs +128 -43
  462. package/manager/utils/BatchChanges.mjs +21 -5
  463. package/manager/utils/BatchChanges.test.mjs +77 -0
  464. package/manager/utils/acquireDevInstance.mjs +99 -0
  465. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  466. package/manager/utils/createBuildActivity.mjs +67 -0
  467. package/manager/utils/createBuildActivity.test.mjs +58 -0
  468. package/manager/utils/createChangeSignal.mjs +39 -0
  469. package/manager/utils/createChangeSignal.test.mjs +62 -0
  470. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  471. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  472. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  473. package/manager/utils/createDotPathIgnore.mjs +44 -0
  474. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  475. package/manager/utils/createIdleGc.mjs +51 -0
  476. package/manager/utils/createIdleGc.test.mjs +77 -0
  477. package/manager/utils/createRequestActivity.mjs +100 -0
  478. package/manager/utils/createRequestActivity.test.mjs +121 -0
  479. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  480. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  481. package/manager/utils/createServerEnv.mjs +39 -0
  482. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  483. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  485. package/manager/utils/importFresh.mjs +46 -0
  486. package/manager/utils/importFresh.test.mjs +72 -0
  487. package/manager/utils/importFreshWorker.mjs +20 -0
  488. package/manager/utils/killTaggedBrowser.mjs +35 -0
  489. package/manager/utils/pagePathChanged.mjs +56 -0
  490. package/manager/utils/pruneRecordings.mjs +85 -0
  491. package/manager/utils/pruneRecordings.test.mjs +117 -0
  492. package/manager/utils/publishBuildDirectory.mjs +121 -0
  493. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  494. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  495. package/manager/utils/readBasePath.mjs +27 -0
  496. package/manager/utils/readBasePath.test.mjs +43 -0
  497. package/manager/utils/readBuildStatusWait.mjs +67 -0
  498. package/manager/utils/readManagerVersion.mjs +30 -0
  499. package/manager/utils/readManagerVersion.test.mjs +35 -0
  500. package/manager/utils/readPluginDefinitions.mjs +36 -0
  501. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  502. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  503. package/manager/utils/resolvePorts.mjs +49 -0
  504. package/manager/utils/setupWatcher.mjs +19 -11
  505. package/manager/utils/startRecordingPruner.mjs +45 -0
  506. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  507. package/manager/utils/waitForChildExit.mjs +53 -0
  508. package/manager/utils/waitForChildExit.test.mjs +70 -0
  509. package/manager/utils/waitForServer.mjs +44 -0
  510. package/manager/utils/writeBuildStatus.mjs +1 -1
  511. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  512. package/manager/watchers/envWatcher.mjs +9 -2
  513. package/manager/watchers/envWatcher.test.mjs +87 -0
  514. package/manager/watchers/listServerSidePackages.mjs +47 -0
  515. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  516. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  517. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  518. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  519. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  520. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  521. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  522. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  523. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  524. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  525. package/package.json +60 -47
  526. package/package.original.json +60 -47
  527. package/src/app.js +131 -20
  528. package/src/html/renderDevPage.js +63 -11
  529. package/src/html/renderDevPage.test.mjs +138 -0
  530. package/src/lib/getRequestPath.js +29 -0
  531. package/src/middleware/createSameOriginGuard.js +37 -0
  532. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  533. package/src/middleware/errorHandler.js +51 -1
  534. package/src/middleware/errorHandler.test.mjs +173 -3
  535. package/src/middleware/isRebindingSafeHost.js +44 -0
  536. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  537. package/src/middleware/localDevToolsOnly.js +38 -0
  538. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  539. package/src/middleware/noStore.js +27 -0
  540. package/src/middleware/noStore.test.mjs +48 -0
  541. package/src/middleware/requestTimeout.js +38 -0
  542. package/src/middleware/staleFlag.js +76 -0
  543. package/src/middleware/staleFlag.test.mjs +127 -0
  544. package/src/middleware/stampCaller.js +38 -0
  545. package/src/middleware/stampCaller.test.mjs +64 -0
  546. package/src/routes/agent.js +15 -3
  547. package/src/routes/auth.js +29 -20
  548. package/src/routes/auth.test.mjs +30 -56
  549. package/src/routes/clientError.js +13 -13
  550. package/src/routes/cron.js +3 -1
  551. package/src/routes/cronForward.js +3 -1
  552. package/src/routes/detached.js +22 -9
  553. package/src/routes/devInspect.js +17 -8
  554. package/src/routes/devInspect.test.mjs +44 -7
  555. package/src/routes/devRecording.js +113 -0
  556. package/src/routes/devRecording.test.mjs +253 -0
  557. package/src/routes/docs/buildStatus.js +10 -2
  558. package/src/routes/docs/buildStatus.test.mjs +46 -0
  559. package/src/routes/docs/content.js +5 -3
  560. package/src/routes/docs/evalOperator.js +15 -1
  561. package/src/routes/docs/events.js +50 -0
  562. package/src/routes/docs/events.test.mjs +117 -0
  563. package/src/routes/docs/find.js +9 -1
  564. package/src/routes/docs/icons.js +30 -0
  565. package/src/routes/docs/inspectState.js +16 -1
  566. package/src/routes/docs/journey.js +194 -0
  567. package/src/routes/docs/journey.test.mjs +401 -0
  568. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  569. package/src/routes/docs/loadState.js +4 -2
  570. package/src/routes/docs/mcp.js +34 -3
  571. package/src/routes/docs/mcp.test.mjs +130 -0
  572. package/src/routes/docs/mutants.js +78 -0
  573. package/src/routes/docs/mutants.test.mjs +180 -0
  574. package/src/routes/docs/parsePathParamsParam.js +41 -0
  575. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  576. package/src/routes/docs/parseUserParam.js +27 -21
  577. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  578. package/src/routes/docs/restart.js +35 -0
  579. package/src/routes/docs/restart.test.mjs +64 -0
  580. package/src/routes/docs/runEndpoint.js +54 -0
  581. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  582. package/src/routes/docs/runRequest.js +25 -8
  583. package/src/routes/docs/runRequest.test.mjs +92 -0
  584. package/src/routes/docs/screenshot.js +50 -2
  585. package/src/routes/docs/screenshot.test.mjs +117 -0
  586. package/src/routes/docs/search.js +15 -2
  587. package/src/routes/docs/snapshotState.js +17 -3
  588. package/src/routes/endpoints.js +27 -26
  589. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  590. package/src/routes/feedback.js +10 -13
  591. package/src/routes/jitPage.js +99 -35
  592. package/src/routes/jitPage.test.mjs +248 -0
  593. package/src/routes/mcp.js +82 -5
  594. package/src/routes/mcp.test.mjs +215 -0
  595. package/src/routes/mcpMemberToken.test.mjs +125 -0
  596. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  597. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  598. package/src/routes/mountDevPageRoutes.js +25 -0
  599. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  600. package/src/routes/mountOauthDiscovery.js +44 -0
  601. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  602. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  604. package/src/routes/reload.js +16 -0
  605. package/src/routes/request.js +6 -4
  606. package/src/routes/request.test.mjs +9 -5
  607. package/src/routes/requestBody.test.mjs +159 -0
  608. package/src/routes/usage.js +6 -2
  609. package/src/routes/user.js +27 -0
  610. package/src/routes/websocket.js +29 -8
  611. package/src/routes/websocket.test.mjs +65 -0
  612. package/src/routes/wellKnownFallback.js +30 -0
  613. package/src/routes/wellKnownFallback.test.mjs +32 -0
  614. package/src/websocket/devWebSocket.js +41 -26
  615. package/src/websocket/devWebSocket.test.mjs +157 -0
  616. package/vite.config.js +53 -23
  617. package/lib/server/auth/getDevSession.js +0 -70
  618. package/lib/server/auth/getDevSession.test.mjs +0 -138
  619. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  620. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  621. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  622. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  623. package/src/routes/apiPage.js +0 -37
@@ -0,0 +1,185 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { callEndpoint, getEndpointConfig, runDetachedEndpoint } from '@lowdefy/api';
18
+ import { ConfigError } from '@lowdefy/errors';
19
+ import { type } from '@lowdefy/helpers';
20
+
21
+ import isWriteRequestsAllowed from './isWriteRequestsAllowed.js';
22
+ import fitResponse from './fitResponse.js';
23
+ import resolveToolCaller from './resolveToolCaller.js';
24
+ import withDataSession from './withDataSession.js';
25
+
26
+ // Executes an Api endpoint routine the same way POST /api/endpoints/<endpointId>
27
+ // does (src/routes/endpoints.js), but gated for agent use. Endpoints are not
28
+ // classified read/write - a routine has no checkWrite meta and a single routine
29
+ // can read, write and call other endpoints - so running one always needs the
30
+ // app's explicit opt-in via lowdefy.yaml's cli.agentTools.allowWriteRequests.
31
+ // Never throws for outcomes an agent should reason about: refusals, a :reject
32
+ // or :throw in the routine, and faults that escape callEndpoint (auth
33
+ // refusals, missing connections, InternalApi endpoints) all come back as data.
34
+ // Only malformed input throws, as a ConfigError.
35
+ //
36
+ // system: true runs the routine through runDetachedEndpoint instead - the same
37
+ // entry point POST /api/detached uses - so it executes as a system context
38
+ // exactly like a cron or detached run: no session, no user, `_user` undefined,
39
+ // endpoint auth not checked and InternalApi endpoints callable. That is the
40
+ // only local way to exercise a schedules-only InternalApi routine without
41
+ // CRON_SECRET. Nested CallApi steps with detached: true are not faked: they
42
+ // still dispatch over HTTP against the request origin and need CRON_SECRET.
43
+ //
44
+ // `user` and `data` name the caller as a journey's do (see resolveToolCaller):
45
+ // a user object, "none" (signed out) or a data set user's name. A data set
46
+ // runs the routine on that data set's own database.
47
+ async function runEndpoint({
48
+ endpointId,
49
+ payload = {},
50
+ user,
51
+ data,
52
+ system = false,
53
+ saveResponse = false,
54
+ honoContext,
55
+ }) {
56
+ if (type.isUndefined(endpointId) || !type.isString(endpointId)) {
57
+ throw new ConfigError(
58
+ `run_endpoint requires an "endpointId" string. Received ${JSON.stringify(endpointId)}.`
59
+ );
60
+ }
61
+
62
+ if (!type.isBoolean(saveResponse)) {
63
+ throw new ConfigError(
64
+ `run_endpoint "saveResponse" must be a boolean. Received ${JSON.stringify(saveResponse)}.`
65
+ );
66
+ }
67
+
68
+ if (!type.isNone(system) && !type.isBoolean(system)) {
69
+ throw new ConfigError(
70
+ `run_endpoint "system" must be a boolean. Received ${JSON.stringify(system)}.`
71
+ );
72
+ }
73
+
74
+ if (system === true && !type.isNone(user)) {
75
+ throw new ConfigError(
76
+ 'run_endpoint "system" cannot be combined with "user": a system run has no user (_user is undefined).'
77
+ );
78
+ }
79
+
80
+ const caller = await resolveToolCaller({ user, data });
81
+ if (!type.isUndefined(caller.error)) {
82
+ throw new ConfigError(`run_endpoint: ${caller.error}`);
83
+ }
84
+
85
+ const allowed = await isWriteRequestsAllowed();
86
+ if (!allowed) {
87
+ return {
88
+ refused: true,
89
+ reason:
90
+ 'Api endpoint routines are not classified read-only — a routine can write, call other endpoints and send notifications — so running one needs agent write access.',
91
+ howToEnable: 'Set cli.agentTools.allowWriteRequests: true in lowdefy.yaml (dev only).',
92
+ };
93
+ }
94
+
95
+ const ran = await withDataSession({
96
+ dataSet: caller.dataSet,
97
+ task: ({ dataSession }) =>
98
+ runEndpointInContext({
99
+ endpointId,
100
+ payload,
101
+ user: caller.user,
102
+ system,
103
+ saveResponse,
104
+ honoContext,
105
+ dataSession,
106
+ }),
107
+ });
108
+ // A data set that failed to load never ran the routine.
109
+ if (type.isString(ran.error)) {
110
+ return { refused: true, reason: ran.error };
111
+ }
112
+ return ran;
113
+ }
114
+
115
+ // The part of runEndpoint that runs in a Lowdefy context, on the data set's
116
+ // database when the call named one.
117
+ async function runEndpointInContext({
118
+ endpointId,
119
+ payload,
120
+ user,
121
+ system,
122
+ saveResponse,
123
+ honoContext,
124
+ dataSession,
125
+ }) {
126
+ // Deferred import: createLowdefyContext statically imports build/plugins/*
127
+ // artifacts, which only exist in a running server directory - importing it
128
+ // at module load would break every consumer of this module (e.g. the MCP
129
+ // server) in environments without a full build.
130
+ const { default: createLowdefyContext } = await import('../server/createLowdefyContext.js');
131
+ // "none" injects no caller: the app's own auth resolves this call, which
132
+ // carries no session, so it runs signed out.
133
+ const context = await createLowdefyContext({
134
+ c: honoContext,
135
+ user: user === 'none' ? undefined : user,
136
+ dataSession,
137
+ });
138
+
139
+ // getEndpointConfig needs the context's readConfigFile, so the endpoint is
140
+ // resolved after the context is built. Its not-found ConfigError is answered
141
+ // as a refusal rather than allowed to escape.
142
+ try {
143
+ await getEndpointConfig(context, { endpointId });
144
+ } catch {
145
+ return {
146
+ refused: true,
147
+ reason:
148
+ `Endpoint "${endpointId}" was not found. ` +
149
+ `See GET /lowdefy-docs/app-map for the endpoints that exist.`,
150
+ };
151
+ }
152
+
153
+ context.logger.info({ event: 'agent_run_endpoint', endpointId, user, system });
154
+
155
+ try {
156
+ // callEndpoint refuses InternalApi endpoints and enforces the endpoint's
157
+ // auth and payloadSchema exactly as the HTTP route does. A :reject or
158
+ // :throw resolves normally with success: false and the routine's own
159
+ // error, so neither reaches the catch below.
160
+ // The agent wrote the payload, so a payload the endpoint's payloadSchema
161
+ // refuses is the agent's to correct (outsideCaller), not a config fault.
162
+ const result = system
163
+ ? await runDetachedEndpoint(context, { endpointId, outsideCaller: true, payload })
164
+ : await callEndpoint(context, {
165
+ blockId: undefined,
166
+ endpointId,
167
+ outsideCaller: true,
168
+ pageId: undefined,
169
+ payload,
170
+ });
171
+ return { refused: false, ...fitResponse({ result, name: endpointId, saveResponse }) };
172
+ } catch (error) {
173
+ return {
174
+ refused: false,
175
+ error: {
176
+ name: error.name,
177
+ message: error.message,
178
+ source: error.source,
179
+ configKey: error.configKey,
180
+ },
181
+ };
182
+ }
183
+ }
184
+
185
+ export default runEndpoint;
@@ -0,0 +1,509 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+
21
+ import { jest } from '@jest/globals';
22
+
23
+ // runEndpoint reads lowdefy.yaml, builds a Lowdefy context and calls into
24
+ // @lowdefy/api - all of which only exist in a running server directory, so mock
25
+ // them and leave runEndpoint's own validation, gating and pass-through under
26
+ // test.
27
+ const mockCallEndpoint = jest.fn();
28
+ const mockRunDetachedEndpoint = jest.fn();
29
+ const mockGetEndpointConfig = jest.fn();
30
+ const mockIsWriteRequestsAllowed = jest.fn();
31
+ const mockCreateLowdefyContext = jest.fn();
32
+ const mockLoggerInfo = jest.fn();
33
+
34
+ jest.unstable_mockModule('@lowdefy/api', () => ({
35
+ callEndpoint: mockCallEndpoint,
36
+ getEndpointConfig: mockGetEndpointConfig,
37
+ runDetachedEndpoint: mockRunDetachedEndpoint,
38
+ }));
39
+ jest.unstable_mockModule('./isWriteRequestsAllowed.js', () => ({
40
+ default: mockIsWriteRequestsAllowed,
41
+ }));
42
+ jest.unstable_mockModule('../server/createLowdefyContext.js', () => ({
43
+ default: mockCreateLowdefyContext,
44
+ }));
45
+ // A data set caller loads a data set into the dev server's memory store; both
46
+ // are mocked so the test checks who the routine runs as and on which database.
47
+ const mockReadDataSet = jest.fn();
48
+ const mockOpenDataSession = jest.fn();
49
+ jest.unstable_mockModule('./dataSets/readDataSet.js', () => ({ default: mockReadDataSet }));
50
+ jest.unstable_mockModule('./dataSets/getDataStore.js', () => ({ default: async () => ({}) }));
51
+ jest.unstable_mockModule('./dataSets/openDataSession.js', () => ({
52
+ default: mockOpenDataSession,
53
+ }));
54
+ jest.unstable_mockModule('./readDevAuthMode.js', () => ({
55
+ default: () => ({ authConfigured: true, mockUserActive: false }),
56
+ }));
57
+
58
+ const { ConfigError } = await import('@lowdefy/errors');
59
+ const { MAX_INLINE_RESPONSE_CHARS } = await import('./fitResponse.js');
60
+ const { default: runEndpoint } = await import('./runEndpoint.js');
61
+
62
+ const honoContext = { req: { path: '/lowdefy-docs/run-endpoint' } };
63
+ const context = { logger: { info: mockLoggerInfo } };
64
+
65
+ beforeEach(() => {
66
+ jest.clearAllMocks();
67
+ mockIsWriteRequestsAllowed.mockResolvedValue(true);
68
+ mockCreateLowdefyContext.mockResolvedValue(context);
69
+ mockGetEndpointConfig.mockResolvedValue({ id: 'endpoint:create_order', type: 'Api' });
70
+ mockCallEndpoint.mockResolvedValue({
71
+ error: null,
72
+ response: { orderId: 'o_1' },
73
+ status: 'success',
74
+ success: true,
75
+ });
76
+ mockRunDetachedEndpoint.mockResolvedValue({
77
+ error: null,
78
+ response: { purged: 3 },
79
+ status: 'success',
80
+ success: true,
81
+ });
82
+ });
83
+
84
+ test('runEndpoint throws a ConfigError when endpointId is missing', async () => {
85
+ await expect(runEndpoint({ honoContext })).rejects.toThrow(ConfigError);
86
+ await expect(runEndpoint({ honoContext })).rejects.toThrow(
87
+ 'run_endpoint requires an "endpointId" string. Received undefined.'
88
+ );
89
+ expect(mockIsWriteRequestsAllowed).not.toHaveBeenCalled();
90
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
91
+ });
92
+
93
+ test('runEndpoint throws a ConfigError when endpointId is not a string', async () => {
94
+ await expect(runEndpoint({ endpointId: 42, honoContext })).rejects.toThrow(
95
+ 'run_endpoint requires an "endpointId" string. Received 42.'
96
+ );
97
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
98
+ });
99
+
100
+ test('runEndpoint throws a ConfigError when user is neither an object nor a name', async () => {
101
+ await expect(
102
+ runEndpoint({ endpointId: 'create_order', user: ['admin'], honoContext })
103
+ ).rejects.toThrow(ConfigError);
104
+ await expect(
105
+ runEndpoint({ endpointId: 'create_order', user: ['admin'], honoContext })
106
+ ).rejects.toThrow(/run_endpoint: "user" should be a user object/);
107
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
108
+ });
109
+
110
+ test('runEndpoint throws a ConfigError for a data set user name without data', async () => {
111
+ await expect(
112
+ runEndpoint({ endpointId: 'create_order', user: 'member', honoContext })
113
+ ).rejects.toThrow(
114
+ 'run_endpoint: The call\'s user "member" names a data set user, but the call has no "data".'
115
+ );
116
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
117
+ });
118
+
119
+ test("runEndpoint runs as a data set user, on the data set's own database", async () => {
120
+ const member = { id: 'u_member', roles: ['member'] };
121
+ const dataSet = { name: 'crm', users: { member } };
122
+ const session = { id: 'session1', state: 'open' };
123
+ const close = jest.fn(async () => {});
124
+ mockReadDataSet.mockResolvedValue(dataSet);
125
+ mockOpenDataSession.mockResolvedValue({ cookie: 'session1', session, close });
126
+
127
+ const result = await runEndpoint({
128
+ endpointId: 'create_order',
129
+ user: 'member',
130
+ data: 'crm',
131
+ honoContext,
132
+ });
133
+
134
+ expect(result.refused).toBe(false);
135
+ expect(result.success).toBe(true);
136
+ expect(mockOpenDataSession).toHaveBeenCalledWith({ dataSet });
137
+ expect(mockCreateLowdefyContext).toHaveBeenCalledWith({
138
+ c: honoContext,
139
+ user: member,
140
+ dataSession: session,
141
+ });
142
+ expect(close).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
+ test('runEndpoint closes the data session when the routine throws', async () => {
146
+ const close = jest.fn(async () => {});
147
+ mockReadDataSet.mockResolvedValue({ name: 'crm', users: { member: { id: 'u_member' } } });
148
+ mockOpenDataSession.mockResolvedValue({ cookie: 'session1', session: {}, close });
149
+ mockCallEndpoint.mockRejectedValue(new Error('boom'));
150
+
151
+ const result = await runEndpoint({
152
+ endpointId: 'create_order',
153
+ user: 'member',
154
+ data: 'crm',
155
+ honoContext,
156
+ });
157
+
158
+ expect(result.error.message).toEqual('boom');
159
+ expect(close).toHaveBeenCalledTimes(1);
160
+ });
161
+
162
+ test('runEndpoint refuses a data set that fails to load without running the routine', async () => {
163
+ mockReadDataSet.mockResolvedValue({ name: 'crm', users: { member: { id: 'u_member' } } });
164
+ mockOpenDataSession.mockRejectedValue(new Error('Data set "crm": fixture orders is not valid.'));
165
+
166
+ const result = await runEndpoint({
167
+ endpointId: 'create_order',
168
+ user: 'member',
169
+ data: 'crm',
170
+ honoContext,
171
+ });
172
+
173
+ expect(result).toEqual({
174
+ refused: true,
175
+ reason: 'Data set "crm": fixture orders is not valid.',
176
+ });
177
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
178
+ });
179
+
180
+ test('runEndpoint runs signed out for user "none", with no injected caller', async () => {
181
+ await runEndpoint({ endpointId: 'create_order', user: 'none', honoContext });
182
+
183
+ expect(mockCreateLowdefyContext).toHaveBeenCalledWith({
184
+ c: honoContext,
185
+ user: undefined,
186
+ dataSession: undefined,
187
+ });
188
+ expect(mockOpenDataSession).not.toHaveBeenCalled();
189
+ });
190
+
191
+ test('runEndpoint refuses every endpoint when agent write access is disabled', async () => {
192
+ mockIsWriteRequestsAllowed.mockResolvedValue(false);
193
+
194
+ const result = await runEndpoint({ endpointId: 'create_order', honoContext });
195
+
196
+ expect(result).toEqual({
197
+ refused: true,
198
+ reason:
199
+ 'Api endpoint routines are not classified read-only — a routine can write, call other endpoints and send notifications — so running one needs agent write access.',
200
+ howToEnable: 'Set cli.agentTools.allowWriteRequests: true in lowdefy.yaml (dev only).',
201
+ });
202
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
203
+ expect(mockGetEndpointConfig).not.toHaveBeenCalled();
204
+ expect(mockCallEndpoint).not.toHaveBeenCalled();
205
+ });
206
+
207
+ test('runEndpoint refuses a write-gated endpoint for an impersonated caller too', async () => {
208
+ mockIsWriteRequestsAllowed.mockResolvedValue(false);
209
+
210
+ const result = await runEndpoint({
211
+ endpointId: 'create_order',
212
+ user: { roles: ['admin'] },
213
+ honoContext,
214
+ });
215
+
216
+ expect(result.refused).toBe(true);
217
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
218
+ });
219
+
220
+ test('runEndpoint returns a not-found refusal when the endpoint does not exist', async () => {
221
+ mockGetEndpointConfig.mockRejectedValue(
222
+ new ConfigError('API Endpoint "missing_endpoint" does not exist.')
223
+ );
224
+
225
+ const result = await runEndpoint({ endpointId: 'missing_endpoint', honoContext });
226
+
227
+ expect(result).toEqual({
228
+ refused: true,
229
+ reason:
230
+ 'Endpoint "missing_endpoint" was not found. See GET /lowdefy-docs/app-map for the endpoints that exist.',
231
+ });
232
+ expect(mockGetEndpointConfig).toHaveBeenCalledWith(context, { endpointId: 'missing_endpoint' });
233
+ expect(mockCallEndpoint).not.toHaveBeenCalled();
234
+ expect(mockLoggerInfo).not.toHaveBeenCalled();
235
+ });
236
+
237
+ test('runEndpoint runs a successful routine and returns its result', async () => {
238
+ const result = await runEndpoint({
239
+ endpointId: 'create_order',
240
+ payload: { sku: 'A1' },
241
+ honoContext,
242
+ });
243
+
244
+ expect(mockCreateLowdefyContext).toHaveBeenCalledWith({ c: honoContext, user: undefined });
245
+ expect(mockCallEndpoint).toHaveBeenCalledWith(context, {
246
+ blockId: undefined,
247
+ endpointId: 'create_order',
248
+ outsideCaller: true,
249
+ pageId: undefined,
250
+ payload: { sku: 'A1' },
251
+ });
252
+ expect(result).toEqual({
253
+ refused: false,
254
+ error: null,
255
+ response: { orderId: 'o_1' },
256
+ status: 'success',
257
+ success: true,
258
+ });
259
+ });
260
+
261
+ test('runEndpoint defaults the payload to an empty object', async () => {
262
+ await runEndpoint({ endpointId: 'create_order', honoContext });
263
+
264
+ expect(mockCallEndpoint).toHaveBeenCalledWith(context, expect.objectContaining({ payload: {} }));
265
+ });
266
+
267
+ test('runEndpoint passes the user to createLowdefyContext and logs the run', async () => {
268
+ const user = { roles: ['admin'], organization_id: 'org_1' };
269
+
270
+ await runEndpoint({ endpointId: 'create_order', user, honoContext });
271
+
272
+ expect(mockCreateLowdefyContext).toHaveBeenCalledWith({ c: honoContext, user });
273
+ expect(mockLoggerInfo).toHaveBeenCalledWith({
274
+ event: 'agent_run_endpoint',
275
+ endpointId: 'create_order',
276
+ user,
277
+ system: false,
278
+ });
279
+ });
280
+
281
+ test('runEndpoint with system: true runs through runDetachedEndpoint instead of callEndpoint', async () => {
282
+ const result = await runEndpoint({
283
+ endpointId: 'purge_stale',
284
+ payload: { mode: 'full' },
285
+ system: true,
286
+ honoContext,
287
+ });
288
+
289
+ expect(mockCreateLowdefyContext).toHaveBeenCalledWith({ c: honoContext, user: undefined });
290
+ expect(mockRunDetachedEndpoint).toHaveBeenCalledWith(context, {
291
+ endpointId: 'purge_stale',
292
+ outsideCaller: true,
293
+ payload: { mode: 'full' },
294
+ });
295
+ expect(mockCallEndpoint).not.toHaveBeenCalled();
296
+ expect(mockLoggerInfo).toHaveBeenCalledWith({
297
+ event: 'agent_run_endpoint',
298
+ endpointId: 'purge_stale',
299
+ user: undefined,
300
+ system: true,
301
+ });
302
+ expect(result).toEqual({
303
+ refused: false,
304
+ error: null,
305
+ response: { purged: 3 },
306
+ status: 'success',
307
+ success: true,
308
+ });
309
+ });
310
+
311
+ test('runEndpoint with system: true still needs agent write access', async () => {
312
+ mockIsWriteRequestsAllowed.mockResolvedValue(false);
313
+
314
+ const result = await runEndpoint({ endpointId: 'purge_stale', system: true, honoContext });
315
+
316
+ expect(result.refused).toBe(true);
317
+ expect(mockRunDetachedEndpoint).not.toHaveBeenCalled();
318
+ });
319
+
320
+ test('runEndpoint with system: true returns a :reject as data', async () => {
321
+ mockRunDetachedEndpoint.mockResolvedValue({
322
+ error: { name: 'Error', message: 'Nothing to purge.' },
323
+ response: null,
324
+ status: 'reject',
325
+ success: false,
326
+ });
327
+
328
+ const result = await runEndpoint({ endpointId: 'purge_stale', system: true, honoContext });
329
+
330
+ expect(result).toEqual({
331
+ refused: false,
332
+ error: { name: 'Error', message: 'Nothing to purge.' },
333
+ response: null,
334
+ status: 'reject',
335
+ success: false,
336
+ });
337
+ });
338
+
339
+ test('runEndpoint with system: true returns faults that escape runDetachedEndpoint as an error object', async () => {
340
+ const fault = new ConfigError('CallApi detached requires CRON_SECRET.', { configKey: 'k2' });
341
+ mockRunDetachedEndpoint.mockRejectedValue(fault);
342
+
343
+ const result = await runEndpoint({ endpointId: 'purge_stale', system: true, honoContext });
344
+
345
+ expect(result).toEqual({
346
+ refused: false,
347
+ error: {
348
+ name: 'ConfigError',
349
+ message: 'CallApi detached requires CRON_SECRET.',
350
+ source: null,
351
+ configKey: 'k2',
352
+ },
353
+ });
354
+ });
355
+
356
+ test('runEndpoint throws a ConfigError when system is combined with user', async () => {
357
+ await expect(
358
+ runEndpoint({
359
+ endpointId: 'purge_stale',
360
+ system: true,
361
+ user: { roles: ['admin'] },
362
+ honoContext,
363
+ })
364
+ ).rejects.toThrow(ConfigError);
365
+ await expect(
366
+ runEndpoint({
367
+ endpointId: 'purge_stale',
368
+ system: true,
369
+ user: { roles: ['admin'] },
370
+ honoContext,
371
+ })
372
+ ).rejects.toThrow('run_endpoint "system" cannot be combined with "user"');
373
+ expect(mockIsWriteRequestsAllowed).not.toHaveBeenCalled();
374
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
375
+ });
376
+
377
+ test('runEndpoint throws a ConfigError when system is not a boolean', async () => {
378
+ await expect(
379
+ runEndpoint({ endpointId: 'purge_stale', system: 'yes', honoContext })
380
+ ).rejects.toThrow('run_endpoint "system" must be a boolean. Received "yes".');
381
+ expect(mockCreateLowdefyContext).not.toHaveBeenCalled();
382
+ });
383
+
384
+ test('runEndpoint with system: false runs through callEndpoint as before', async () => {
385
+ await runEndpoint({ endpointId: 'create_order', system: false, honoContext });
386
+
387
+ expect(mockCallEndpoint).toHaveBeenCalled();
388
+ expect(mockRunDetachedEndpoint).not.toHaveBeenCalled();
389
+ });
390
+
391
+ test('runEndpoint returns a :reject as data rather than throwing', async () => {
392
+ mockCallEndpoint.mockResolvedValue({
393
+ error: { name: 'Error', message: 'Out of stock.' },
394
+ response: null,
395
+ status: 'reject',
396
+ success: false,
397
+ });
398
+
399
+ const result = await runEndpoint({ endpointId: 'create_order', honoContext });
400
+
401
+ expect(result).toEqual({
402
+ refused: false,
403
+ error: { name: 'Error', message: 'Out of stock.' },
404
+ response: null,
405
+ status: 'reject',
406
+ success: false,
407
+ });
408
+ });
409
+
410
+ test('runEndpoint returns a :throw as data with status error', async () => {
411
+ mockCallEndpoint.mockResolvedValue({
412
+ error: { name: 'Error', message: 'Payment provider down.' },
413
+ response: null,
414
+ status: 'error',
415
+ success: false,
416
+ });
417
+
418
+ const result = await runEndpoint({ endpointId: 'create_order', honoContext });
419
+
420
+ expect(result.refused).toBe(false);
421
+ expect(result.success).toBe(false);
422
+ expect(result.status).toBe('error');
423
+ expect(result.error.message).toBe('Payment provider down.');
424
+ });
425
+
426
+ test('runEndpoint returns the routine error of a failed endpoint, which has no response', async () => {
427
+ // callEndpoint's result for a failed routine: the wire error with the full error
428
+ // beside it as devError, and response undefined.
429
+ const endpointError = {
430
+ '~e': { name: 'ServiceError', message: 'Something went wrong.', configKey: 'k9' },
431
+ devError: {
432
+ '~e': {
433
+ name: 'ServiceError',
434
+ message: 'MongoDB: MongoDB rejected the MongoDBFind on collection "docs" (code 27).',
435
+ source: 'api/search.yaml:40',
436
+ configKey: 'k9',
437
+ },
438
+ },
439
+ };
440
+ mockCallEndpoint.mockResolvedValue({
441
+ error: endpointError,
442
+ response: undefined,
443
+ status: 'error',
444
+ success: false,
445
+ });
446
+
447
+ const result = await runEndpoint({ endpointId: 'search', honoContext });
448
+
449
+ expect(result).toEqual({
450
+ refused: false,
451
+ error: endpointError,
452
+ response: undefined,
453
+ status: 'error',
454
+ success: false,
455
+ });
456
+ });
457
+
458
+ test('runEndpoint returns faults that escape callEndpoint as an error object', async () => {
459
+ const fault = new ConfigError('API Endpoint "internal_only" does not exist.', {
460
+ configKey: 'k1',
461
+ });
462
+ fault.source = 'api/internal.yaml:3';
463
+ mockCallEndpoint.mockRejectedValue(fault);
464
+
465
+ const result = await runEndpoint({ endpointId: 'internal_only', honoContext });
466
+
467
+ expect(result).toEqual({
468
+ refused: false,
469
+ error: {
470
+ name: 'ConfigError',
471
+ message: 'API Endpoint "internal_only" does not exist.',
472
+ source: 'api/internal.yaml:3',
473
+ configKey: 'k1',
474
+ },
475
+ });
476
+ });
477
+
478
+ test('runEndpoint writes an oversized response to a file', async () => {
479
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-run-endpoint-'));
480
+ process.env.LOWDEFY_DIRECTORY_CONFIG = configDirectory;
481
+ const response = 'x'.repeat(MAX_INLINE_RESPONSE_CHARS + 10);
482
+ mockCallEndpoint.mockResolvedValue({
483
+ error: null,
484
+ response,
485
+ status: 'success',
486
+ success: true,
487
+ });
488
+
489
+ try {
490
+ const result = await runEndpoint({ endpointId: 'create_order', honoContext });
491
+
492
+ expect(result.refused).toBe(false);
493
+ expect(result.response).toBeUndefined();
494
+ expect(result.responseChars).toBe(MAX_INLINE_RESPONSE_CHARS + 12);
495
+ expect(path.dirname(result.responseFile)).toBe(
496
+ path.join(configDirectory, '.lowdefy', 'responses')
497
+ );
498
+ expect(JSON.parse(fs.readFileSync(result.responseFile, 'utf8'))).toBe(response);
499
+ expect(result.status).toBe('success');
500
+ } finally {
501
+ delete process.env.LOWDEFY_DIRECTORY_CONFIG;
502
+ }
503
+ });
504
+
505
+ test('runEndpoint throws a ConfigError when saveResponse is not a boolean', async () => {
506
+ await expect(
507
+ runEndpoint({ endpointId: 'create_order', saveResponse: 'yes', honoContext })
508
+ ).rejects.toThrow(ConfigError);
509
+ });