@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,95 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ // The loopback hosts the dev server's own mail capture listens on (LOWDEFY_DEV_SMTP_PORT).
20
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
21
+
22
+ // An SMTP request whose connection points at the dev server's mail capture is the one request of
23
+ // an external type that is safe to run for real under a data session: nothing leaves the machine,
24
+ // and the journey's `email` step reads what it captures.
25
+ function isDevMailCapture({ connectionType, connection }) {
26
+ const port = process.env.LOWDEFY_DEV_SMTP_PORT;
27
+ if (connectionType !== 'SMTP' || type.isNone(port)) return false;
28
+ return LOOPBACK_HOSTS.has(connection?.host) && String(connection?.port) === String(port);
29
+ }
30
+
31
+ // What a sunk request carries: where it was aimed and what it asked for, never the connection's
32
+ // properties, which hold its credentials. Request properties are evaluated app config and
33
+ // payload; secrets are scrubbed.
34
+ function describeSunkRequest({ context, args, connectionType }) {
35
+ const scrub = context.scrubSecrets ?? ((value) => value);
36
+ return {
37
+ connectionId: args.connectionId,
38
+ connectionType,
39
+ requestId: args.requestId,
40
+ pageId: args.pageId ?? null,
41
+ endpointId: args.endpointId ?? null,
42
+ request: type.isUndefined(args.request)
43
+ ? null
44
+ : JSON.parse(scrub(JSON.stringify(args.request))),
45
+ };
46
+ }
47
+
48
+ // The plugin connection map for a data session's requests. A connection type that declares
49
+ // meta.dataSet 'external' reaches an outside service and keeps its real target, so under a data
50
+ // session every one of its request resolvers is replaced by a sink. A single request type can
51
+ // declare meta.dataSet 'external' on its resolver too, and only that resolver is replaced, which
52
+ // suits a walled connection whose other request types read and write the walled database. A sunk
53
+ // request is recorded on the session (session.sunk) and answers null, and nothing reaches the service. This is the one
54
+ // place real mail and HTTP are stopped, in the process that would send them, so a branch a click
55
+ // never takes, or an id an operator computes, cannot reach out either. It leaves the resolver's
56
+ // schema and meta in place, so the request is still validated, read-checked and write-checked
57
+ // as for real. Types with no request resolvers (an Mcp config container) have nothing to sink
58
+ // and stay refused by the click rails. Returns a new map; the plugin map is never mutated.
59
+ function sinkExternalConnections({ context, connections, session }) {
60
+ const sunk = {};
61
+ Object.entries(connections).forEach(([connectionType, definition]) => {
62
+ const requests = definition?.requests;
63
+ if (!type.isObject(requests)) {
64
+ sunk[connectionType] = definition;
65
+ return;
66
+ }
67
+ const connectionExternal = definition?.meta?.dataSet === 'external';
68
+ let anySunk = false;
69
+ const sunkRequests = {};
70
+ Object.entries(requests).forEach(([requestType, resolver]) => {
71
+ // A request type can declare meta.dataSet 'external' itself, so a walled connection that
72
+ // holds database access can still have request types that reach an outside service sunk.
73
+ if (!connectionExternal && resolver?.meta?.dataSet !== 'external') {
74
+ sunkRequests[requestType] = resolver;
75
+ return;
76
+ }
77
+ anySunk = true;
78
+ const sink = async (args) => {
79
+ if (isDevMailCapture({ connectionType, connection: args.connection })) {
80
+ return resolver(args);
81
+ }
82
+ session.sunk.push({
83
+ ...describeSunkRequest({ context, args, connectionType }),
84
+ requestType,
85
+ });
86
+ return null;
87
+ };
88
+ sunkRequests[requestType] = Object.assign(sink, resolver);
89
+ });
90
+ sunk[connectionType] = anySunk ? { ...definition, requests: sunkRequests } : definition;
91
+ });
92
+ return sunk;
93
+ }
94
+
95
+ export default sinkExternalConnections;
@@ -0,0 +1,135 @@
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 { jest } from '@jest/globals';
18
+
19
+ import sinkExternalConnections from './sinkExternalConnections.js';
20
+
21
+ const realSend = jest.fn(async () => ({ sent: true }));
22
+ realSend.schema = { type: 'object' };
23
+ realSend.meta = { checkWrite: true };
24
+ const realGather = jest.fn(async () => ({ downloaded: true }));
25
+ realGather.schema = { type: 'object' };
26
+ realGather.meta = { checkRead: true, dataSet: 'external' };
27
+ const realRead = jest.fn(async () => ['doc']);
28
+ realRead.meta = { checkRead: true };
29
+ const realMongo = jest.fn(async () => ['row']);
30
+
31
+ const connections = {
32
+ SMTP: { schema: {}, meta: { dataSet: 'external' }, requests: { SMTPMailSend: realSend } },
33
+ MongoDBCollection: { meta: { dataSet: 'redirect' }, requests: { MongoDBFind: realMongo } },
34
+ Mcp: { meta: { dataSet: 'external' } },
35
+ Evidence: { meta: { dataSet: 'walled' }, requests: { Gather: realGather, Read: realRead } },
36
+ };
37
+ const context = { scrubSecrets: (value) => value.replace('s3cret', '[REDACTED]') };
38
+
39
+ let session;
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ delete process.env.LOWDEFY_DEV_SMTP_PORT;
43
+ session = { sunk: [] };
44
+ });
45
+
46
+ function args(overrides = {}) {
47
+ return {
48
+ connection: { host: 'smtp.example.com', port: 587, pass: 's3cret' },
49
+ connectionId: 'notifications-email',
50
+ requestId: 'send',
51
+ pageId: 'review',
52
+ request: { to: 'a@example.com', note: 's3cret' },
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ test('an external request is recorded and answers null, and never reaches the real resolver', async () => {
58
+ const map = sinkExternalConnections({ context, connections, session });
59
+ const response = await map.SMTP.requests.SMTPMailSend(args());
60
+ expect(response).toBeNull();
61
+ expect(realSend).not.toHaveBeenCalled();
62
+ expect(session.sunk).toEqual([
63
+ {
64
+ connectionId: 'notifications-email',
65
+ connectionType: 'SMTP',
66
+ requestType: 'SMTPMailSend',
67
+ requestId: 'send',
68
+ pageId: 'review',
69
+ endpointId: null,
70
+ request: { to: 'a@example.com', note: '[REDACTED]' },
71
+ },
72
+ ]);
73
+ // The connection's own properties, which hold its credentials, are never recorded.
74
+ expect(JSON.stringify(session.sunk)).not.toContain('smtp.example.com');
75
+ });
76
+
77
+ test('a sunk resolver keeps the schema and meta of the real one', () => {
78
+ const map = sinkExternalConnections({ context, connections, session });
79
+ expect(map.SMTP.requests.SMTPMailSend.schema).toBe(realSend.schema);
80
+ expect(map.SMTP.requests.SMTPMailSend.meta).toBe(realSend.meta);
81
+ });
82
+
83
+ test('redirect types, types without requests and the plugin map are left alone', () => {
84
+ const map = sinkExternalConnections({ context, connections, session });
85
+ expect(map.MongoDBCollection).toBe(connections.MongoDBCollection);
86
+ expect(map.Mcp).toBe(connections.Mcp);
87
+ expect(connections.SMTP.requests.SMTPMailSend).toBe(realSend);
88
+ });
89
+
90
+ test('an SMTP request to the dev mail capture runs for real, any other host is sunk', async () => {
91
+ process.env.LOWDEFY_DEV_SMTP_PORT = '2525';
92
+ const map = sinkExternalConnections({ context, connections, session });
93
+ const capture = args({ connection: { host: '127.0.0.1', port: 2525 } });
94
+ expect(await map.SMTP.requests.SMTPMailSend(capture)).toEqual({ sent: true });
95
+ expect(realSend).toHaveBeenCalledTimes(1);
96
+ await map.SMTP.requests.SMTPMailSend(args({ connection: { host: '127.0.0.1', port: 587 } }));
97
+ await map.SMTP.requests.SMTPMailSend(
98
+ args({ connection: { host: 'smtp.example.com', port: 2525 } })
99
+ );
100
+ expect(realSend).toHaveBeenCalledTimes(1);
101
+ expect(session.sunk).toHaveLength(2);
102
+ });
103
+
104
+ test('a request type that declares external is sunk on a walled connection, its siblings run for real', async () => {
105
+ const map = sinkExternalConnections({ context, connections, session });
106
+ const response = await map.Evidence.requests.Gather(
107
+ args({ connectionId: 'evidence', requestId: 'gather' })
108
+ );
109
+ expect(response).toBeNull();
110
+ expect(realGather).not.toHaveBeenCalled();
111
+ expect(session.sunk).toEqual([
112
+ {
113
+ connectionId: 'evidence',
114
+ connectionType: 'Evidence',
115
+ requestType: 'Gather',
116
+ requestId: 'gather',
117
+ pageId: 'review',
118
+ endpointId: null,
119
+ request: { to: 'a@example.com', note: '[REDACTED]' },
120
+ },
121
+ ]);
122
+ expect(map.Evidence.requests.Gather.meta).toBe(realGather.meta);
123
+ expect(map.Evidence.meta).toBe(connections.Evidence.meta);
124
+ expect(map.Evidence.requests.Read).toBe(realRead);
125
+ expect(await map.Evidence.requests.Read(args())).toEqual(['doc']);
126
+ expect(session.sunk).toHaveLength(1);
127
+ });
128
+
129
+ test('a connection with no external request type is returned as is, and the plugin map is not mutated', () => {
130
+ const map = sinkExternalConnections({ context, connections, session });
131
+ expect(map.MongoDBCollection).toBe(connections.MongoDBCollection);
132
+ expect(connections.Evidence.requests.Gather).toBe(realGather);
133
+ const plain = { Plain: { meta: { dataSet: 'walled' }, requests: { Read: realRead } } };
134
+ expect(sinkExternalConnections({ context, connections: plain, session }).Plain).toBe(plain.Plain);
135
+ });
@@ -0,0 +1,62 @@
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 'fs';
18
+ import path from 'path';
19
+
20
+ const mapFiles = new Set(['keyMap.json', 'refMap.json']);
21
+ const jsMapFiles = new Set([
22
+ 'plugins/operators/clientJsMap.js',
23
+ 'plugins/operators/serverJsMap.js',
24
+ ]);
25
+
26
+ function isMapFile(filePath) {
27
+ return mapFiles.has(filePath) || filePath.startsWith('jitMaps/');
28
+ }
29
+
30
+ function readLiveKeyPrefix({ buildDirectory }) {
31
+ return JSON.parse(fs.readFileSync(path.join(buildDirectory, 'idCounter.json'), 'utf8')).prefix;
32
+ }
33
+
34
+ // A config rebuild publishes new maps while a page build that started against the
35
+ // previous build may still be running. That build's keyMap and refMap describe the
36
+ // previous build, so writing them would replace the new maps, and errors would resolve
37
+ // against the wrong build until the next rebuild. Its jitMaps/ files would outlive the
38
+ // publish that cleared the old build's. The build's key prefix names the build
39
+ // a context was created from.
40
+ //
41
+ // The JS map files are written whole from a context's jsMap, so a page build on
42
+ // a context that is no longer the kept one (a config publish, or the map budget,
43
+ // discarded it) would replace the entries pages built on the newer context
44
+ // added. Pages built on the discarded context are rebuilt on their next request
45
+ // or API call, which writes their entries again.
46
+ function skipStaleMapWrites({ buildDirectory, context, keyPrefix, isKeptContext }) {
47
+ const writeBuildArtifact = context.writeBuildArtifact;
48
+ context.writeBuildArtifact = async (filePath, content, options) => {
49
+ if (isMapFile(filePath) && readLiveKeyPrefix({ buildDirectory }) !== keyPrefix) {
50
+ return;
51
+ }
52
+ if (
53
+ jsMapFiles.has(filePath) &&
54
+ (!isKeptContext() || readLiveKeyPrefix({ buildDirectory }) !== keyPrefix)
55
+ ) {
56
+ return;
57
+ }
58
+ await writeBuildArtifact(filePath, content, options);
59
+ };
60
+ }
61
+
62
+ export default skipStaleMapWrites;
@@ -0,0 +1,113 @@
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 'fs';
18
+ import os from 'os';
19
+ import path from 'path';
20
+
21
+ import { jest } from '@jest/globals';
22
+
23
+ import skipStaleMapWrites from './skipStaleMapWrites.js';
24
+
25
+ let buildDirectory;
26
+
27
+ function publishBuild(prefix) {
28
+ fs.writeFileSync(
29
+ path.join(buildDirectory, 'idCounter.json'),
30
+ JSON.stringify({ prefix, counter: 10 })
31
+ );
32
+ }
33
+
34
+ beforeEach(() => {
35
+ buildDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-stale-maps-'));
36
+ });
37
+
38
+ afterEach(() => {
39
+ fs.rmSync(buildDirectory, { recursive: true });
40
+ });
41
+
42
+ test('a page build writes its maps while its config build is the live one', async () => {
43
+ publishBuild('a1b2_');
44
+ const writeBuildArtifact = jest.fn();
45
+ const context = { writeBuildArtifact };
46
+ skipStaleMapWrites({ buildDirectory, context, keyPrefix: 'a1b2_', isKeptContext: () => true });
47
+
48
+ await context.writeBuildArtifact('keyMap.json', '{}');
49
+ await context.writeBuildArtifact('refMap.json', '{}');
50
+
51
+ expect(writeBuildArtifact.mock.calls).toEqual([
52
+ ['keyMap.json', '{}'],
53
+ ['refMap.json', '{}'],
54
+ ]);
55
+ });
56
+
57
+ test('a page build started before a config rebuild does not replace the new maps', async () => {
58
+ publishBuild('a1b2_');
59
+ const writeBuildArtifact = jest.fn();
60
+ const context = { writeBuildArtifact };
61
+ skipStaleMapWrites({ buildDirectory, context, keyPrefix: 'a1b2_', isKeptContext: () => true });
62
+
63
+ publishBuild('c3d4_');
64
+ await context.writeBuildArtifact('keyMap.json', '{}');
65
+ await context.writeBuildArtifact('refMap.json', '{}');
66
+ await context.writeBuildArtifact('pages/home.json', '{}');
67
+
68
+ expect(writeBuildArtifact.mock.calls).toEqual([['pages/home.json', '{}']]);
69
+ });
70
+
71
+ test('a page build started before a config rebuild writes no jitMaps file', async () => {
72
+ publishBuild('a1b2_');
73
+ const writeBuildArtifact = jest.fn();
74
+ const context = { writeBuildArtifact };
75
+ skipStaleMapWrites({ buildDirectory, context, keyPrefix: 'a1b2_', isKeptContext: () => true });
76
+
77
+ await context.writeBuildArtifact('jitMaps/abc123-1-1.json', '{}');
78
+ publishBuild('c3d4_');
79
+ await context.writeBuildArtifact('jitMaps/abc123-1-2.json', '{}');
80
+
81
+ expect(writeBuildArtifact.mock.calls).toEqual([['jitMaps/abc123-1-1.json', '{}']]);
82
+ });
83
+
84
+ test('a page build on a context that is no longer kept does not write the JS maps', async () => {
85
+ publishBuild('a1b2_');
86
+ const writeBuildArtifact = jest.fn();
87
+ const context = { writeBuildArtifact };
88
+ let kept = true;
89
+ skipStaleMapWrites({ buildDirectory, context, keyPrefix: 'a1b2_', isKeptContext: () => kept });
90
+
91
+ await context.writeBuildArtifact('plugins/operators/serverJsMap.js', 'a');
92
+ kept = false;
93
+ await context.writeBuildArtifact('plugins/operators/serverJsMap.js', 'b');
94
+ await context.writeBuildArtifact('plugins/operators/clientJsMap.js', 'b');
95
+ await context.writeBuildArtifact('pages/home.json', '{}');
96
+
97
+ expect(writeBuildArtifact.mock.calls).toEqual([
98
+ ['plugins/operators/serverJsMap.js', 'a'],
99
+ ['pages/home.json', '{}'],
100
+ ]);
101
+ });
102
+
103
+ test('a page build started before a config rebuild does not write the JS maps', async () => {
104
+ publishBuild('a1b2_');
105
+ const writeBuildArtifact = jest.fn();
106
+ const context = { writeBuildArtifact };
107
+ skipStaleMapWrites({ buildDirectory, context, keyPrefix: 'a1b2_', isKeptContext: () => true });
108
+
109
+ publishBuild('c3d4_');
110
+ await context.writeBuildArtifact('plugins/operators/serverJsMap.js', 'a');
111
+
112
+ expect(writeBuildArtifact.mock.calls).toEqual([]);
113
+ });
@@ -0,0 +1,67 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ import readInstanceRecord from './readInstanceRecord.js';
20
+
21
+ // Vite can evaluate the app entry more than once (an SSR reload, the
22
+ // websocket upgrade path), and each evaluation calls this.
23
+ const STARTED = Symbol.for('lowdefy.server-dev.idleGc');
24
+
25
+ /*
26
+ Hands the child's V8 slack back once the server goes quiet: one full GC per
27
+ quiet period, when the activity the manager records in the instance record
28
+ (lastActivityAt, activeRequests, building) says nothing has used the server
29
+ for quietMs and nothing is in flight. It runs again only after activity
30
+ resumes (a newer lastActivityAt). A child started without --expose-gc has no
31
+ globalThis.gc and does nothing.
32
+ */
33
+ function startIdleGc({
34
+ configDirectory,
35
+ gc = globalThis.gc,
36
+ readInstance = readInstanceRecord,
37
+ now = Date.now,
38
+ pollMs = 5000,
39
+ quietMs = 10000,
40
+ }) {
41
+ if (globalThis[STARTED] === true || !type.isFunction(gc)) {
42
+ return;
43
+ }
44
+ globalThis[STARTED] = true;
45
+ let collectedFor = null;
46
+ const interval = setInterval(() => {
47
+ const record = readInstance({ configDirectory });
48
+ if (type.isNone(record) || !type.isString(record.lastActivityAt)) {
49
+ return;
50
+ }
51
+ if (record.building === true || (record.activeRequests ?? 0) > 0) {
52
+ return;
53
+ }
54
+ if (record.lastActivityAt === collectedFor) {
55
+ return;
56
+ }
57
+ if (now() - Date.parse(record.lastActivityAt) < quietMs) {
58
+ return;
59
+ }
60
+ collectedFor = record.lastActivityAt;
61
+ gc();
62
+ }, pollMs);
63
+ // Never the reason the child stays up.
64
+ interval.unref();
65
+ }
66
+
67
+ export default startIdleGc;
@@ -0,0 +1,138 @@
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 { jest } from '@jest/globals';
18
+
19
+ import startIdleGc from './startIdleGc.js';
20
+
21
+ const STARTED = Symbol.for('lowdefy.server-dev.idleGc');
22
+ const START = Date.parse('2026-10-03T10:00:00.000Z');
23
+
24
+ let clock;
25
+ let record;
26
+ let gc;
27
+
28
+ function start(options = {}) {
29
+ startIdleGc({
30
+ configDirectory: '/apps/tenant',
31
+ gc,
32
+ readInstance: () => record,
33
+ now: () => clock,
34
+ ...options,
35
+ });
36
+ }
37
+
38
+ function tick(ms) {
39
+ clock += ms;
40
+ jest.advanceTimersByTime(ms);
41
+ }
42
+
43
+ beforeEach(() => {
44
+ // performance is not redefinable on this Node, and nothing here reads it.
45
+ jest.useFakeTimers({ doNotFake: ['performance'] });
46
+ delete globalThis[STARTED];
47
+ clock = START;
48
+ gc = jest.fn();
49
+ record = {
50
+ lastActivityAt: new Date(START).toISOString(),
51
+ activeRequests: 0,
52
+ building: false,
53
+ };
54
+ });
55
+
56
+ afterEach(() => {
57
+ jest.clearAllTimers();
58
+ jest.useRealTimers();
59
+ delete globalThis[STARTED];
60
+ });
61
+
62
+ test('startIdleGc collects once the server has been quiet for 10 s', () => {
63
+ start();
64
+
65
+ tick(5000);
66
+ expect(gc).not.toHaveBeenCalled();
67
+ tick(5000);
68
+ expect(gc).toHaveBeenCalledTimes(1);
69
+ tick(60_000);
70
+ expect(gc).toHaveBeenCalledTimes(1);
71
+ });
72
+
73
+ test('startIdleGc does not collect while a request is in flight', () => {
74
+ record.activeRequests = 1;
75
+ start();
76
+
77
+ tick(60_000);
78
+
79
+ expect(gc).not.toHaveBeenCalled();
80
+ });
81
+
82
+ test('startIdleGc does not collect while a build runs', () => {
83
+ record.building = true;
84
+ start();
85
+
86
+ tick(60_000);
87
+
88
+ expect(gc).not.toHaveBeenCalled();
89
+ });
90
+
91
+ test('startIdleGc collects again only after activity resumes', () => {
92
+ start();
93
+ tick(10_000);
94
+ expect(gc).toHaveBeenCalledTimes(1);
95
+
96
+ record = { ...record, lastActivityAt: new Date(clock).toISOString() };
97
+ tick(5000);
98
+ expect(gc).toHaveBeenCalledTimes(1);
99
+ tick(5000);
100
+
101
+ expect(gc).toHaveBeenCalledTimes(2);
102
+ });
103
+
104
+ test('startIdleGc does nothing when the instance record has no activity', () => {
105
+ record = null;
106
+ start();
107
+
108
+ tick(60_000);
109
+
110
+ expect(gc).not.toHaveBeenCalled();
111
+ });
112
+
113
+ test('startIdleGc does nothing when the child was started without gc exposed', () => {
114
+ // The gc default is globalThis.gc, which every later test file in a jest worker has once
115
+ // createIdleGc.test.mjs has set --expose-gc in it.
116
+ const exposedGc = globalThis.gc;
117
+ delete globalThis.gc;
118
+ gc = undefined;
119
+ try {
120
+ start();
121
+
122
+ expect(globalThis[STARTED]).toBeUndefined();
123
+ expect(jest.getTimerCount()).toBe(0);
124
+ } finally {
125
+ if (exposedGc !== undefined) {
126
+ globalThis.gc = exposedGc;
127
+ }
128
+ }
129
+ });
130
+
131
+ test('startIdleGc starts one interval however often the app entry is evaluated', () => {
132
+ start();
133
+ start();
134
+
135
+ expect(jest.getTimerCount()).toBe(1);
136
+ tick(10_000);
137
+ expect(gc).toHaveBeenCalledTimes(1);
138
+ });
@@ -0,0 +1,70 @@
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
+ // Run by importAppCode.test.mjs in its own process: Jest can load neither Vite
18
+ // nor a module URL with a query. Loads the build's importAppCode the way the dev
19
+ // server's JIT page builds do, then imports an app code file, imports it again
20
+ // unchanged, edits it and imports it again. With `vite`, through Vite's SSR
21
+ // module runner (@lowdefy/build linked, as in the monorepo); with `node`,
22
+ // natively (@lowdefy/build installed from npm, which Vite externalises).
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { createRequire } from 'node:module';
26
+ import { fileURLToPath, pathToFileURL } from 'node:url';
27
+
28
+ import { createServer } from 'vite';
29
+
30
+ const [loader, configDir] = process.argv.slice(2);
31
+ const require = createRequire(import.meta.url);
32
+ const serverDevDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
33
+
34
+ const buildDevEntry = require.resolve('@lowdefy/build/dev');
35
+ const modulePath = path.join(path.dirname(buildDevEntry), 'utils/createImportAppCode.js');
36
+
37
+ let server = null;
38
+ if (loader === 'vite') {
39
+ server = await createServer({
40
+ configFile: false,
41
+ root: serverDevDir,
42
+ logLevel: 'error',
43
+ appType: 'custom',
44
+ server: { middlewareMode: true, hmr: false, watch: null },
45
+ });
46
+ }
47
+
48
+ try {
49
+ const { default: createImportAppCode } = server
50
+ ? await server.ssrLoadModule(modulePath)
51
+ : await import(pathToFileURL(modulePath).href);
52
+ const importAppCode = createImportAppCode({ directories: { config: configDir } });
53
+ const filePath = path.join(configDir, 'transformer.js');
54
+
55
+ fs.writeFileSync(filePath, "export default () => 'before';\n");
56
+ const first = await importAppCode('transformer.js');
57
+ const unchanged = await importAppCode('transformer.js');
58
+ fs.writeFileSync(filePath, "export default () => 'after';\n");
59
+ const edited = await importAppCode('transformer.js');
60
+
61
+ process.stdout.write(
62
+ JSON.stringify({
63
+ first: first(),
64
+ edited: edited(),
65
+ unchangedReused: unchanged === first,
66
+ })
67
+ );
68
+ } finally {
69
+ await server?.close();
70
+ }