@lowdefy/server-dev 6.1.0 → 7.1.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 (624) 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 +78 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +727 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/describeTargetKey.js +29 -0
  104. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  105. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  106. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  107. package/lib/docs/dataSets/duplicateIndexName.js +24 -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 +52 -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 +133 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +493 -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 +83 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +251 -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/sessionDatabaseName.js +32 -0
  133. package/lib/docs/dataSets/sweepOrphanDatabases.js +36 -0
  134. package/lib/docs/dataSets/test/app/lowdefy.yaml +225 -0
  135. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  136. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +12 -0
  137. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  138. package/lib/docs/devEventBus.js +136 -0
  139. package/lib/docs/devEventBus.test.mjs +269 -0
  140. package/lib/docs/devToolDefinitions.js +529 -0
  141. package/lib/docs/docSources.js +21 -0
  142. package/lib/docs/docs.test.mjs +171 -15
  143. package/lib/docs/docsIndex.test.mjs +377 -0
  144. package/lib/docs/evalOperator.js +5 -3
  145. package/lib/docs/evalOperatorHeadless.js +84 -12
  146. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  147. package/lib/docs/evalOperatorInTab.js +2 -1
  148. package/lib/docs/findConfig.js +95 -49
  149. package/lib/docs/fitResponse.js +81 -0
  150. package/lib/docs/fitResponse.test.mjs +128 -0
  151. package/lib/docs/formatFeedback.js +26 -5
  152. package/lib/docs/formatFeedback.test.mjs +48 -0
  153. package/lib/docs/getAppMap.js +7 -1
  154. package/lib/docs/getBrowser.js +150 -80
  155. package/lib/docs/getBrowser.test.mjs +407 -16
  156. package/lib/docs/getBuildId.js +61 -0
  157. package/lib/docs/getBuildId.test.mjs +84 -0
  158. package/lib/docs/getBuildStatus.js +81 -7
  159. package/lib/docs/getBuildStatus.test.mjs +229 -0
  160. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  161. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  162. package/lib/docs/getCoreDoc.js +9 -1
  163. package/lib/docs/getDoc.js +100 -0
  164. package/lib/docs/getDocsIndex.js +152 -0
  165. package/lib/docs/getHazards.js +115 -0
  166. package/lib/docs/getHazards.test.mjs +175 -0
  167. package/lib/docs/getMailOutboxDirectory.js +26 -0
  168. package/lib/docs/getOverview.js +79 -13
  169. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  170. package/lib/docs/getPageBuildStatus.js +46 -0
  171. package/lib/docs/getPluginDoc.js +21 -23
  172. package/lib/docs/getSchema.js +33 -1
  173. package/lib/docs/getStaleStatus.js +36 -0
  174. package/lib/docs/getStaleStatus.test.mjs +74 -0
  175. package/lib/docs/guardJourneyOrigin.js +39 -0
  176. package/lib/docs/inspectState.js +5 -3
  177. package/lib/docs/inspectStateFromTab.js +2 -2
  178. package/lib/docs/inspectStateHeadless.js +89 -27
  179. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  180. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  181. package/lib/docs/installHeadlessShell.js +123 -0
  182. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  183. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  184. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  185. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  186. package/lib/docs/isPageReady.js +77 -0
  187. package/lib/docs/isPageReady.test.mjs +235 -0
  188. package/lib/docs/isRunErrorEntry.js +25 -0
  189. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  190. package/lib/docs/journeyRunRecording.js +35 -0
  191. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  192. package/lib/docs/keepEventStreamAlive.js +68 -0
  193. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  194. package/lib/docs/launchBrowser.js +83 -0
  195. package/lib/docs/launchBrowser.test.mjs +175 -0
  196. package/lib/docs/listModuleDocEntries.js +69 -0
  197. package/lib/docs/listPackageDocFiles.js +67 -0
  198. package/lib/docs/listPluginDocEntries.js +111 -0
  199. package/lib/docs/listPlugins.js +9 -58
  200. package/lib/docs/listTypes.js +14 -6
  201. package/lib/docs/loadFrameworkHazards.js +40 -0
  202. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  203. package/lib/docs/loadState.js +103 -35
  204. package/lib/docs/makeModuleManifestDoc.js +174 -0
  205. package/lib/docs/matchExercisedPages.js +35 -0
  206. package/lib/docs/matchJourneyRoute.js +69 -0
  207. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  208. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  209. package/lib/docs/nextJourneySequence.js +30 -0
  210. package/lib/docs/noBrowserError.js +22 -0
  211. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  212. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  213. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  214. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  215. package/lib/docs/observe/evaluateInvariants.js +183 -0
  216. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  217. package/lib/docs/observe/findingKey.js +29 -0
  218. package/lib/docs/observe/hasNoEffect.js +31 -0
  219. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  220. package/lib/docs/observe/installStepObserver.js +72 -0
  221. package/lib/docs/observe/isAppApiUrl.js +33 -0
  222. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  223. package/lib/docs/observe/readStepObserver.js +42 -0
  224. package/lib/docs/observe/relativeSource.js +29 -0
  225. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  226. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  227. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  228. package/lib/docs/observe/usesSearchStage.js +45 -0
  229. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  230. package/lib/docs/observe/watchJourneyContext.js +75 -0
  231. package/lib/docs/observe/withAppErrors.js +31 -0
  232. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  233. package/lib/docs/openJourney.js +94 -0
  234. package/lib/docs/openJourneyEmail.js +41 -0
  235. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  236. package/lib/docs/pageBlocks.js +47 -0
  237. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  238. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  239. package/lib/docs/rails/authEngineActions.js +53 -0
  240. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  241. package/lib/docs/rails/authEngineMethods.js +52 -0
  242. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  243. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  244. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  245. package/lib/docs/rails/checkClickRails.js +56 -0
  246. package/lib/docs/rails/createDataSetRails.js +82 -0
  247. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  248. package/lib/docs/rails/findExternalBlocks.js +147 -0
  249. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  250. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  251. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  252. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  253. package/lib/docs/readConnectionPlugins.js +26 -0
  254. package/lib/docs/readDevAuthMode.js +32 -0
  255. package/lib/docs/readDocEntry.js +40 -0
  256. package/lib/docs/readDocTitle.js +26 -0
  257. package/lib/docs/readJourneyEmailMatch.js +57 -0
  258. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  259. package/lib/docs/readJourneySession.js +56 -0
  260. package/lib/docs/readMailOutbox.js +37 -0
  261. package/lib/docs/readPagePath.js +27 -0
  262. package/lib/docs/readPluginPackages.js +99 -0
  263. package/lib/docs/readProxyBuildWait.js +38 -0
  264. package/lib/docs/readRoutineSteps.js +45 -0
  265. package/lib/docs/registerShownTextEngine.js +118 -0
  266. package/lib/docs/requestRestart.js +30 -0
  267. package/lib/docs/requestRestart.test.mjs +49 -0
  268. package/lib/docs/resolvePageInstance.js +46 -0
  269. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  270. package/lib/docs/resolveSource.js +11 -7
  271. package/lib/docs/resolveSource.test.mjs +1 -1
  272. package/lib/docs/resolveToolCaller.js +62 -0
  273. package/lib/docs/reviewPage.js +51 -0
  274. package/lib/docs/reviewPageBuilds.js +56 -0
  275. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  276. package/lib/docs/runCheck.js +75 -0
  277. package/lib/docs/runCheck.test.mjs +91 -0
  278. package/lib/docs/runCheckChild.js +49 -0
  279. package/lib/docs/runEndpoint.js +185 -0
  280. package/lib/docs/runEndpoint.test.mjs +509 -0
  281. package/lib/docs/runErrorBuffers.js +99 -0
  282. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  283. package/lib/docs/runJourney.js +347 -0
  284. package/lib/docs/runJourney.test.mjs +2383 -0
  285. package/lib/docs/runJourneyAsUsers.js +44 -0
  286. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  287. package/lib/docs/runJourneySteps.js +1320 -0
  288. package/lib/docs/runRequest.js +100 -59
  289. package/lib/docs/runRequest.test.mjs +190 -0
  290. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  291. package/lib/docs/scaffoldPage.js +3 -1
  292. package/lib/docs/screenshotPage.js +163 -11
  293. package/lib/docs/screenshotPage.test.mjs +263 -1
  294. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  295. package/lib/docs/searchDocs.js +82 -30
  296. package/lib/docs/searchDocs.test.mjs +100 -0
  297. package/lib/docs/searchIcons.js +90 -0
  298. package/lib/docs/searchIcons.test.mjs +127 -0
  299. package/lib/docs/selectExercised.js +39 -0
  300. package/lib/docs/selectFinalState.js +66 -0
  301. package/lib/docs/selectFinalState.test.mjs +51 -0
  302. package/lib/docs/serverErrorStore.js +54 -0
  303. package/lib/docs/serverErrorStore.test.mjs +50 -0
  304. package/lib/docs/setupTestFixtures.mjs +133 -39
  305. package/lib/docs/shownTextSelector.js +28 -0
  306. package/lib/docs/snapshotState.js +13 -5
  307. package/lib/docs/splitSearchTerms.js +62 -0
  308. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  309. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  310. package/lib/docs/tabAvailable.js +6 -5
  311. package/lib/docs/tabChannel.js +127 -21
  312. package/lib/docs/tabChannel.test.mjs +187 -29
  313. package/lib/docs/unsettledPageNote.js +24 -0
  314. package/lib/docs/validateJourneyMail.js +36 -0
  315. package/lib/docs/validateJourneyTimeout.js +38 -0
  316. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  317. package/lib/docs/validateRecordingParam.js +42 -0
  318. package/lib/docs/validateStateSelection.js +33 -0
  319. package/lib/docs/validateStateSelection.test.mjs +33 -0
  320. package/lib/docs/validateViewport.js +51 -0
  321. package/lib/docs/validateViewport.test.mjs +54 -0
  322. package/lib/docs/waitForBuild.js +50 -0
  323. package/lib/docs/waitForBuild.test.mjs +75 -0
  324. package/lib/docs/waitForJourneyEmail.js +77 -0
  325. package/lib/docs/withBrowserSlot.js +45 -0
  326. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  327. package/lib/docs/withDataSession.js +53 -0
  328. package/lib/server/applyDataSetRedirect.js +92 -0
  329. package/lib/server/applyDataSetRedirect.test.mjs +446 -0
  330. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +207 -0
  331. package/lib/server/applyDataSetRedirectWalled.test.mjs +134 -0
  332. package/lib/server/auth/createSystemContext.js +73 -0
  333. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  334. package/lib/server/auth/getHeadlessUser.js +9 -8
  335. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  336. package/lib/server/auth/getMockUser.js +18 -14
  337. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  338. package/lib/server/auth/headlessUser.js +3 -2
  339. package/lib/server/auth/journeyActor.js +35 -0
  340. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  341. package/lib/server/checkPageRecord.js +44 -0
  342. package/lib/server/contextMapBudget.js +24 -0
  343. package/lib/server/contextMapBudget.test.mjs +98 -0
  344. package/lib/server/continueJitKeys.js +28 -0
  345. package/lib/server/continueJitKeys.test.mjs +43 -0
  346. package/lib/server/createDataSessionEndedError.js +33 -0
  347. package/lib/server/createDataSetRefusal.js +26 -0
  348. package/lib/server/createLowdefyContext.js +168 -41
  349. package/lib/server/createLowdefyContext.test.mjs +331 -8
  350. package/lib/server/getClientAddress.js +51 -0
  351. package/lib/server/getClientAddress.test.mjs +56 -0
  352. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  353. package/lib/server/hashConfigContent.js +26 -0
  354. package/lib/server/importAppCode.test.mjs +57 -0
  355. package/lib/server/isRoutineStep.js +27 -0
  356. package/lib/server/jitPageBuilder.js +309 -126
  357. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  358. package/lib/server/jitPageBuilder.test.mjs +218 -0
  359. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  360. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  361. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  362. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  363. package/lib/server/journeyCookies.js +22 -0
  364. package/lib/server/journeyCookies.test.mjs +145 -0
  365. package/lib/server/loadDynamicJsMap.js +47 -0
  366. package/lib/server/log/createHandleError.js +38 -2
  367. package/lib/server/log/createHandleError.test.mjs +125 -2
  368. package/lib/server/log/createLogger.js +3 -0
  369. package/lib/server/log/createLogger.test.mjs +15 -0
  370. package/lib/server/log/logRequest.js +2 -3
  371. package/lib/server/mutantDataSetWrappers.test.mjs +115 -0
  372. package/lib/server/mutants/applyMutant.js +39 -0
  373. package/lib/server/mutants/artifactPatterns.js +25 -0
  374. package/lib/server/mutants/buildAnchor.js +45 -0
  375. package/lib/server/mutants/collectSharedIds.js +41 -0
  376. package/lib/server/mutants/configPathSegments.js +38 -0
  377. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  378. package/lib/server/mutants/describeEvent.js +22 -0
  379. package/lib/server/mutants/enumerateArtifact.js +56 -0
  380. package/lib/server/mutants/findNodeByKey.js +45 -0
  381. package/lib/server/mutants/findNodeByPointer.js +49 -0
  382. package/lib/server/mutants/getArtifactKind.js +32 -0
  383. package/lib/server/mutants/groupMutantCopies.js +56 -0
  384. package/lib/server/mutants/hashArtifact.js +25 -0
  385. package/lib/server/mutants/hashMutantTarget.js +34 -0
  386. package/lib/server/mutants/listMutants.js +122 -0
  387. package/lib/server/mutants/listMutants.test.mjs +502 -0
  388. package/lib/server/mutants/mutantId.js +30 -0
  389. package/lib/server/mutants/mutantRuns.js +57 -0
  390. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  391. package/lib/server/mutants/operators/dropAction.js +47 -0
  392. package/lib/server/mutants/operators/dropBlock.js +44 -0
  393. package/lib/server/mutants/operators/dropPayload.js +52 -0
  394. package/lib/server/mutants/operators/dropStep.js +44 -0
  395. package/lib/server/mutants/operators/flipVisible.js +53 -0
  396. package/lib/server/mutants/operators/index.js +37 -0
  397. package/lib/server/mutants/operators/retargetLink.js +72 -0
  398. package/lib/server/mutants/operators/skipValidate.js +77 -0
  399. package/lib/server/mutants/operators/swapIf.js +112 -0
  400. package/lib/server/mutants/operators.test.mjs +396 -0
  401. package/lib/server/mutants/positionFreePath.js +38 -0
  402. package/lib/server/mutants/removeFromParent.js +28 -0
  403. package/lib/server/mutants/test/fixtures.mjs +173 -0
  404. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  405. package/lib/server/mutants/validateMutantParam.js +69 -0
  406. package/lib/server/mutants/validateMutantsBody.js +88 -0
  407. package/lib/server/mutants/walkArtifact.js +179 -0
  408. package/lib/server/pageBuildRecords.js +131 -0
  409. package/lib/server/pageBuildRecords.test.mjs +258 -0
  410. package/lib/server/pageCache.mjs +22 -7
  411. package/lib/server/pageCache.test.mjs +22 -7
  412. package/lib/server/pageRebuilds.test.mjs +543 -0
  413. package/lib/server/pruneJitMaps.js +36 -0
  414. package/lib/server/pruneJitMaps.test.mjs +55 -0
  415. package/lib/server/readInstanceRecord.js +33 -0
  416. package/lib/server/readInstanceRecord.test.mjs +99 -0
  417. package/lib/server/readMergedMaps.js +86 -0
  418. package/lib/server/readMergedMaps.test.mjs +79 -0
  419. package/lib/server/recording/appendRecords.js +33 -0
  420. package/lib/server/recording/errorRecordingStamp.js +34 -0
  421. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  422. package/lib/server/recording/isRecordingEnabled.js +31 -0
  423. package/lib/server/recording/readRecordingCookie.js +71 -0
  424. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  425. package/lib/server/recording/recordingCookiePayload.js +33 -0
  426. package/lib/server/recording/servedBuilds.js +46 -0
  427. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  428. package/lib/server/sinkExternalConnections.js +95 -0
  429. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  430. package/lib/server/skipStaleMapWrites.js +62 -0
  431. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  432. package/lib/server/startIdleGc.js +67 -0
  433. package/lib/server/startIdleGc.test.mjs +138 -0
  434. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  435. package/lib/server/trackSessionWork.js +30 -0
  436. package/lib/vite/createDevServerExclude.js +40 -0
  437. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  438. package/manager/getContext.mjs +25 -3
  439. package/manager/processes/initialBuild.mjs +17 -1
  440. package/manager/processes/initialBuild.test.mjs +104 -0
  441. package/manager/processes/lowdefyBuild.mjs +31 -7
  442. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +72 -0
  443. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  444. package/manager/processes/optimizeDependencies.mjs +59 -0
  445. package/manager/processes/restartServer.mjs +34 -6
  446. package/manager/processes/restartServer.test.mjs +162 -0
  447. package/manager/processes/shutdownServer.mjs +11 -0
  448. package/manager/processes/shutdownServer.test.mjs +45 -0
  449. package/manager/processes/startFirstServer.mjs +46 -0
  450. package/manager/processes/startFirstServer.test.mjs +326 -0
  451. package/manager/processes/startMailSink.mjs +135 -0
  452. package/manager/processes/startMailSink.test.mjs +146 -0
  453. package/manager/processes/startProxy.mjs +129 -12
  454. package/manager/processes/startProxy.test.mjs +366 -0
  455. package/manager/processes/startServer.mjs +47 -6
  456. package/manager/processes/startServer.test.mjs +155 -0
  457. package/manager/processes/startWatchers.mjs +6 -4
  458. package/manager/processes/syncServer.mjs +69 -0
  459. package/manager/processes/syncServer.test.mjs +141 -0
  460. package/manager/processes/waitForDevServer.mjs +48 -0
  461. package/manager/processes/waitForDevServer.test.mjs +112 -0
  462. package/manager/run.mjs +128 -43
  463. package/manager/utils/BatchChanges.mjs +21 -5
  464. package/manager/utils/BatchChanges.test.mjs +77 -0
  465. package/manager/utils/acquireDevInstance.mjs +99 -0
  466. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  467. package/manager/utils/createBuildActivity.mjs +67 -0
  468. package/manager/utils/createBuildActivity.test.mjs +58 -0
  469. package/manager/utils/createChangeSignal.mjs +39 -0
  470. package/manager/utils/createChangeSignal.test.mjs +62 -0
  471. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  472. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  473. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  474. package/manager/utils/createDotPathIgnore.mjs +44 -0
  475. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  476. package/manager/utils/createIdleGc.mjs +51 -0
  477. package/manager/utils/createIdleGc.test.mjs +77 -0
  478. package/manager/utils/createRequestActivity.mjs +100 -0
  479. package/manager/utils/createRequestActivity.test.mjs +121 -0
  480. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  481. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  482. package/manager/utils/createServerEnv.mjs +39 -0
  483. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  485. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  486. package/manager/utils/importFresh.mjs +46 -0
  487. package/manager/utils/importFresh.test.mjs +72 -0
  488. package/manager/utils/importFreshWorker.mjs +20 -0
  489. package/manager/utils/killTaggedBrowser.mjs +35 -0
  490. package/manager/utils/pagePathChanged.mjs +56 -0
  491. package/manager/utils/pruneRecordings.mjs +85 -0
  492. package/manager/utils/pruneRecordings.test.mjs +117 -0
  493. package/manager/utils/publishBuildDirectory.mjs +121 -0
  494. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  495. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  496. package/manager/utils/readBasePath.mjs +27 -0
  497. package/manager/utils/readBasePath.test.mjs +43 -0
  498. package/manager/utils/readBuildStatusWait.mjs +67 -0
  499. package/manager/utils/readManagerVersion.mjs +30 -0
  500. package/manager/utils/readManagerVersion.test.mjs +35 -0
  501. package/manager/utils/readPluginDefinitions.mjs +36 -0
  502. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  503. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  504. package/manager/utils/resolvePorts.mjs +49 -0
  505. package/manager/utils/setupWatcher.mjs +19 -11
  506. package/manager/utils/startRecordingPruner.mjs +45 -0
  507. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  508. package/manager/utils/waitForChildExit.mjs +53 -0
  509. package/manager/utils/waitForChildExit.test.mjs +70 -0
  510. package/manager/utils/waitForServer.mjs +44 -0
  511. package/manager/utils/writeBuildStatus.mjs +1 -1
  512. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  513. package/manager/watchers/envWatcher.mjs +9 -2
  514. package/manager/watchers/envWatcher.test.mjs +87 -0
  515. package/manager/watchers/listServerSidePackages.mjs +47 -0
  516. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  517. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  518. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  519. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  520. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  521. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  522. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  523. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  524. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  525. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  526. package/package.json +60 -47
  527. package/package.original.json +60 -47
  528. package/src/app.js +131 -20
  529. package/src/html/renderDevPage.js +63 -11
  530. package/src/html/renderDevPage.test.mjs +138 -0
  531. package/src/lib/getRequestPath.js +29 -0
  532. package/src/middleware/createSameOriginGuard.js +37 -0
  533. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  534. package/src/middleware/errorHandler.js +51 -1
  535. package/src/middleware/errorHandler.test.mjs +173 -3
  536. package/src/middleware/isRebindingSafeHost.js +44 -0
  537. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  538. package/src/middleware/localDevToolsOnly.js +38 -0
  539. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  540. package/src/middleware/noStore.js +27 -0
  541. package/src/middleware/noStore.test.mjs +48 -0
  542. package/src/middleware/requestTimeout.js +38 -0
  543. package/src/middleware/staleFlag.js +76 -0
  544. package/src/middleware/staleFlag.test.mjs +127 -0
  545. package/src/middleware/stampCaller.js +38 -0
  546. package/src/middleware/stampCaller.test.mjs +64 -0
  547. package/src/routes/agent.js +15 -3
  548. package/src/routes/auth.js +29 -20
  549. package/src/routes/auth.test.mjs +30 -56
  550. package/src/routes/clientError.js +13 -13
  551. package/src/routes/cron.js +3 -1
  552. package/src/routes/cronForward.js +3 -1
  553. package/src/routes/detached.js +22 -9
  554. package/src/routes/devInspect.js +17 -8
  555. package/src/routes/devInspect.test.mjs +44 -7
  556. package/src/routes/devRecording.js +113 -0
  557. package/src/routes/devRecording.test.mjs +253 -0
  558. package/src/routes/docs/buildStatus.js +10 -2
  559. package/src/routes/docs/buildStatus.test.mjs +46 -0
  560. package/src/routes/docs/content.js +5 -3
  561. package/src/routes/docs/evalOperator.js +15 -1
  562. package/src/routes/docs/events.js +50 -0
  563. package/src/routes/docs/events.test.mjs +117 -0
  564. package/src/routes/docs/find.js +9 -1
  565. package/src/routes/docs/icons.js +30 -0
  566. package/src/routes/docs/inspectState.js +16 -1
  567. package/src/routes/docs/journey.js +194 -0
  568. package/src/routes/docs/journey.test.mjs +401 -0
  569. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  570. package/src/routes/docs/loadState.js +4 -2
  571. package/src/routes/docs/mcp.js +34 -3
  572. package/src/routes/docs/mcp.test.mjs +130 -0
  573. package/src/routes/docs/mutants.js +78 -0
  574. package/src/routes/docs/mutants.test.mjs +180 -0
  575. package/src/routes/docs/parsePathParamsParam.js +41 -0
  576. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  577. package/src/routes/docs/parseUserParam.js +27 -21
  578. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  579. package/src/routes/docs/restart.js +35 -0
  580. package/src/routes/docs/restart.test.mjs +64 -0
  581. package/src/routes/docs/runEndpoint.js +54 -0
  582. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  583. package/src/routes/docs/runRequest.js +25 -8
  584. package/src/routes/docs/runRequest.test.mjs +92 -0
  585. package/src/routes/docs/screenshot.js +50 -2
  586. package/src/routes/docs/screenshot.test.mjs +117 -0
  587. package/src/routes/docs/search.js +15 -2
  588. package/src/routes/docs/snapshotState.js +17 -3
  589. package/src/routes/endpoints.js +27 -26
  590. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  591. package/src/routes/feedback.js +10 -13
  592. package/src/routes/jitPage.js +99 -35
  593. package/src/routes/jitPage.test.mjs +248 -0
  594. package/src/routes/mcp.js +82 -5
  595. package/src/routes/mcp.test.mjs +215 -0
  596. package/src/routes/mcpMemberToken.test.mjs +125 -0
  597. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  598. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  599. package/src/routes/mountDevPageRoutes.js +25 -0
  600. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  601. package/src/routes/mountOauthDiscovery.js +44 -0
  602. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  604. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  605. package/src/routes/reload.js +16 -0
  606. package/src/routes/request.js +6 -4
  607. package/src/routes/request.test.mjs +9 -5
  608. package/src/routes/requestBody.test.mjs +159 -0
  609. package/src/routes/usage.js +6 -2
  610. package/src/routes/user.js +27 -0
  611. package/src/routes/websocket.js +29 -8
  612. package/src/routes/websocket.test.mjs +65 -0
  613. package/src/routes/wellKnownFallback.js +30 -0
  614. package/src/routes/wellKnownFallback.test.mjs +32 -0
  615. package/src/websocket/devWebSocket.js +41 -26
  616. package/src/websocket/devWebSocket.test.mjs +157 -0
  617. package/vite.config.js +53 -23
  618. package/lib/server/auth/getDevSession.js +0 -70
  619. package/lib/server/auth/getDevSession.test.mjs +0 -138
  620. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  621. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  622. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  623. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  624. package/src/routes/apiPage.js +0 -37
@@ -0,0 +1,446 @@
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
+ import { callRequest } from '@lowdefy/api';
19
+ import { ConfigError } from '@lowdefy/errors';
20
+
21
+ import applyDataSetRedirect from './applyDataSetRedirect.js';
22
+
23
+ const connections = {
24
+ MongoDBCollection: { meta: { tenant: true, dataSet: 'redirect' } },
25
+ FolderRuntime: { meta: { dataSet: 'redirect' } },
26
+ AxiosHttp: { meta: { dataSet: 'external' } },
27
+ Knex: {},
28
+ Redis: { meta: {} },
29
+ PluginStore: { meta: { dataSet: 'mongo' } },
30
+ };
31
+
32
+ const artifacts = {
33
+ 'connections/tickets.json': {
34
+ id: 'connection:tickets',
35
+ connectionId: 'tickets',
36
+ '~k': 'k_tickets',
37
+ type: 'MongoDBCollection',
38
+ properties: { databaseUri: { _secret: 'MONGODB_URI' }, collection: 'tickets', write: true },
39
+ },
40
+ 'connections/no_uri.json': {
41
+ id: 'connection:no_uri',
42
+ connectionId: 'no_uri',
43
+ type: 'MongoDBCollection',
44
+ properties: { collection: 'tickets' },
45
+ },
46
+ 'connections/nested.json': {
47
+ id: 'connection:nested',
48
+ connectionId: 'nested',
49
+ type: 'MongoDBCollection',
50
+ properties: {
51
+ databaseUri: { _secret: 'MONGODB_URI' },
52
+ collection: { _if: { test: true, then: 'a', else: 'b' } },
53
+ options: { appName: { '_string.concat': ['x', 'y'] } },
54
+ },
55
+ },
56
+ 'connections/whole.json': {
57
+ id: 'connection:whole',
58
+ connectionId: 'whole',
59
+ '~k': 'k_whole',
60
+ type: 'MongoDBCollection',
61
+ properties: {
62
+ _if: { test: true, then: { databaseUri: 'mongodb://real/' }, else: {} },
63
+ },
64
+ },
65
+ 'connections/whole_ref.json': {
66
+ id: 'connection:whole_ref',
67
+ connectionId: 'whole_ref',
68
+ type: 'MongoDBCollection',
69
+ properties: { _ref: 'connection.yaml' },
70
+ },
71
+ 'connections/array.json': {
72
+ id: 'connection:array',
73
+ connectionId: 'array',
74
+ type: 'MongoDBCollection',
75
+ properties: [],
76
+ },
77
+ 'connections/folders.json': {
78
+ id: 'connection:folders',
79
+ connectionId: 'folders',
80
+ type: 'FolderRuntime',
81
+ properties: { databaseUri: { _secret: 'MONGODB_URI' }, databaseName: 'app' },
82
+ },
83
+ 'connections/contacts/contacts.json': {
84
+ id: 'connection:contacts/contacts',
85
+ connectionId: 'contacts/contacts',
86
+ type: 'MongoDBCollection',
87
+ properties: { databaseUri: { _secret: 'MONGODB_URI' }, collection: 'contacts' },
88
+ },
89
+ 'connections/api.json': {
90
+ id: 'connection:api',
91
+ connectionId: 'api',
92
+ type: 'AxiosHttp',
93
+ properties: { baseURL: 'https://example.com' },
94
+ },
95
+ 'connections/sql.json': {
96
+ id: 'connection:sql',
97
+ connectionId: 'sql',
98
+ '~k': 'k_sql',
99
+ type: 'Knex',
100
+ properties: { client: 'pg', connection: { _secret: 'PG_URI' } },
101
+ },
102
+ 'connections/cache.json': {
103
+ id: 'connection:cache',
104
+ connectionId: 'cache',
105
+ type: 'Redis',
106
+ properties: { connection: { _secret: 'REDIS_URI' } },
107
+ },
108
+ 'connections/store.json': {
109
+ id: 'connection:store',
110
+ connectionId: 'store',
111
+ type: 'PluginStore',
112
+ properties: { databaseUri: 'mongodb://real/' },
113
+ },
114
+ 'connections/unknown.json': {
115
+ id: 'connection:unknown',
116
+ connectionId: 'unknown',
117
+ type: 'NotInstalled',
118
+ properties: { databaseUri: 'mongodb://real/' },
119
+ },
120
+ 'pages/home.json': { id: 'page:home', properties: { databaseUri: 'not a connection' } },
121
+ };
122
+
123
+ function createContext() {
124
+ return { readConfigFile: async (filePath) => artifacts[filePath] ?? null };
125
+ }
126
+
127
+ // A session as openDataSession opens it for a build whose one target is the MONGODB_URI secret. The
128
+ // gate numbers a target it meets for the first time, so each test takes a fresh one.
129
+ function createSession(overrides = {}) {
130
+ return {
131
+ id: 'abc',
132
+ name: 'staging-sample',
133
+ state: 'open',
134
+ databaseUri: 'mongodb://127.0.0.1:5555/?replicaSet=testset',
135
+ prefix: 'ld_000000000001',
136
+ databaseName: 'ld_000000000001_0',
137
+ databases: new Map([[JSON.stringify(['secret:MONGODB_URI', null]), 'ld_000000000001_0']]),
138
+ databaseCount: 1,
139
+ ...overrides,
140
+ };
141
+ }
142
+
143
+ const session = createSession();
144
+
145
+ function redirectedContext(options = {}) {
146
+ const context = createContext();
147
+ applyDataSetRedirect({ context, session: options.session ?? createSession(), connections });
148
+ return context;
149
+ }
150
+
151
+ test('applyDataSetRedirect points a MongoDBCollection connection at the session database', async () => {
152
+ const context = redirectedContext();
153
+ expect(await context.readConfigFile('connections/tickets.json')).toEqual({
154
+ id: 'connection:tickets',
155
+ connectionId: 'tickets',
156
+ '~k': 'k_tickets',
157
+ type: 'MongoDBCollection',
158
+ properties: {
159
+ databaseUri: session.databaseUri,
160
+ databaseName: session.databaseName,
161
+ collection: 'tickets',
162
+ write: true,
163
+ },
164
+ });
165
+ });
166
+
167
+ test('applyDataSetRedirect points a module connection, written under its module folder, at the session database', async () => {
168
+ const context = redirectedContext();
169
+ const artifact = await context.readConfigFile('connections/contacts/contacts.json');
170
+ expect(artifact.properties).toEqual({
171
+ databaseUri: session.databaseUri,
172
+ databaseName: session.databaseName,
173
+ collection: 'contacts',
174
+ });
175
+ });
176
+
177
+ test('applyDataSetRedirect points a plugin type that declares redirect at the session database of its target', async () => {
178
+ const context = redirectedContext();
179
+ const artifact = await context.readConfigFile('connections/folders.json');
180
+ expect(artifact.properties).toEqual({
181
+ databaseUri: session.databaseUri,
182
+ databaseName: 'ld_000000000001_1',
183
+ });
184
+ });
185
+
186
+ test('applyDataSetRedirect merges the session database into a redirect connection that names no databaseUri', async () => {
187
+ const context = redirectedContext();
188
+ const artifact = await context.readConfigFile('connections/no_uri.json');
189
+ expect(artifact.properties).toEqual({
190
+ databaseUri: session.databaseUri,
191
+ databaseName: session.databaseName,
192
+ collection: 'tickets',
193
+ });
194
+ });
195
+
196
+ test('applyDataSetRedirect gives a connection with a _secret databaseUri and nested operators the session literal URI', async () => {
197
+ const context = redirectedContext();
198
+ const artifact = await context.readConfigFile('connections/nested.json');
199
+ expect(artifact.properties.databaseUri).toEqual(session.databaseUri);
200
+ expect(artifact.properties.databaseName).toEqual(session.databaseName);
201
+ expect(artifact.properties.collection).toEqual({ _if: { test: true, then: 'a', else: 'b' } });
202
+ });
203
+
204
+ test('applyDataSetRedirect refuses a redirect connection whose properties are an operator, naming the connection', async () => {
205
+ const context = redirectedContext();
206
+ const error = await context.readConfigFile('connections/whole.json').catch((caught) => caught);
207
+ expect(error).toBeInstanceOf(ConfigError);
208
+ expect(error.message).toEqual(
209
+ 'Connection "whole" (type MongoDBCollection) cannot run under data set "staging-sample": its properties are an operator, so the redirect cannot be checked.'
210
+ );
211
+ expect(error.configKey).toEqual('k_whole');
212
+ for (const filePath of ['connections/whole_ref.json', 'connections/array.json']) {
213
+ await expect(context.readConfigFile(filePath)).rejects.toThrow(
214
+ 'its properties are an operator, so the redirect cannot be checked.'
215
+ );
216
+ }
217
+ });
218
+
219
+ test('applyDataSetRedirect refuses a connection whose type declares no dataSet, even with a literal databaseUri', async () => {
220
+ const context = redirectedContext();
221
+ const error = await context.readConfigFile('connections/sql.json').catch((caught) => caught);
222
+ expect(error).toBeInstanceOf(ConfigError);
223
+ expect(error.message).toEqual(
224
+ 'Connection "sql" (type Knex) cannot run under data set "staging-sample": its type does not say how to redirect it.'
225
+ );
226
+ expect(error.configKey).toEqual('k_sql');
227
+ await expect(context.readConfigFile('connections/cache.json')).rejects.toThrow(
228
+ 'Connection "cache" (type Redis) cannot run under data set "staging-sample": its type does not say how to redirect it.'
229
+ );
230
+ await expect(context.readConfigFile('connections/store.json')).rejects.toThrow(
231
+ 'Connection "store" (type PluginStore) cannot run under data set "staging-sample": its type does not say how to redirect it.'
232
+ );
233
+ await expect(context.readConfigFile('connections/unknown.json')).rejects.toThrow(
234
+ 'Connection "unknown" (type NotInstalled) cannot run under data set "staging-sample": its type does not say how to redirect it.'
235
+ );
236
+ });
237
+
238
+ test('applyDataSetRedirect leaves an external connection and every other path untouched', async () => {
239
+ const context = redirectedContext();
240
+ expect(await context.readConfigFile('connections/api.json')).toBe(
241
+ artifacts['connections/api.json']
242
+ );
243
+ expect(await context.readConfigFile('pages/home.json')).toBe(artifacts['pages/home.json']);
244
+ expect(await context.readConfigFile('connections/missing.json')).toBeNull();
245
+ });
246
+
247
+ test('applyDataSetRedirect never mutates the artifact it read', async () => {
248
+ const context = redirectedContext();
249
+ await context.readConfigFile('connections/tickets.json');
250
+ await context.readConfigFile('connections/nested.json');
251
+ const plain = createContext();
252
+ expect((await plain.readConfigFile('connections/tickets.json')).properties.databaseUri).toEqual({
253
+ _secret: 'MONGODB_URI',
254
+ });
255
+ expect(artifacts['connections/tickets.json'].properties.databaseName).toBeUndefined();
256
+ expect(artifacts['connections/nested.json'].properties.databaseUri).toEqual({
257
+ _secret: 'MONGODB_URI',
258
+ });
259
+ });
260
+
261
+ test('applyDataSetRedirect throws on a connection read after the session has closed', async () => {
262
+ const context = redirectedContext({ session: createSession({ state: 'closed' }) });
263
+ await expect(context.readConfigFile('connections/tickets.json')).rejects.toThrow(
264
+ 'Data session abc has ended.'
265
+ );
266
+ await expect(context.readConfigFile('connections/api.json')).rejects.toThrow(
267
+ 'Data session abc has ended.'
268
+ );
269
+ expect(await context.readConfigFile('pages/home.json')).toBe(artifacts['pages/home.json']);
270
+ });
271
+
272
+ describe('database targets', () => {
273
+ const targetArtifacts = {
274
+ 'connections/app_users.json': {
275
+ connectionId: 'app_users',
276
+ type: 'MongoDBCollection',
277
+ properties: { databaseUri: { _secret: 'MONGODB_URI' }, collection: 'users' },
278
+ },
279
+ 'connections/app_users_again.json': {
280
+ connectionId: 'app_users_again',
281
+ type: 'MongoDBCollection',
282
+ properties: { databaseUri: { _secret: 'MONGODB_URI', '~r': 'r_1' }, collection: 'users' },
283
+ },
284
+ 'connections/auth_users.json': {
285
+ connectionId: 'auth_users',
286
+ type: 'MongoDBCollection',
287
+ properties: { databaseUri: { _secret: 'AUTH_MONGODB_URI' }, collection: 'users' },
288
+ },
289
+ 'connections/reports_users.json': {
290
+ connectionId: 'reports_users',
291
+ type: 'MongoDBCollection',
292
+ properties: {
293
+ databaseUri: { _secret: 'MONGODB_URI' },
294
+ databaseName: 'reports',
295
+ collection: 'users',
296
+ },
297
+ },
298
+ 'connections/computed_users.json': {
299
+ connectionId: 'computed_users',
300
+ type: 'MongoDBCollection',
301
+ properties: {
302
+ databaseUri: { _if: { test: true, then: { _secret: 'A' }, else: { _secret: 'B' } } },
303
+ collection: 'users',
304
+ },
305
+ },
306
+ };
307
+
308
+ async function readDatabaseName({ session: targetSession, connectionId }) {
309
+ const context = { readConfigFile: async (filePath) => targetArtifacts[filePath] ?? null };
310
+ applyDataSetRedirect({ context, session: targetSession, connections });
311
+ const artifact = await context.readConfigFile(`connections/${connectionId}.json`);
312
+ return artifact.properties.databaseName;
313
+ }
314
+
315
+ test('connections on one target share its session database', async () => {
316
+ const targetSession = createSession();
317
+ expect(await readDatabaseName({ session: targetSession, connectionId: 'app_users' })).toEqual(
318
+ 'ld_000000000001_0'
319
+ );
320
+ expect(
321
+ await readDatabaseName({ session: targetSession, connectionId: 'app_users_again' })
322
+ ).toEqual('ld_000000000001_0');
323
+ });
324
+
325
+ test('a different databaseUri secret or databaseName reads a session database of its own', async () => {
326
+ const targetSession = createSession();
327
+ const names = [];
328
+ for (const connectionId of ['app_users', 'auth_users', 'reports_users']) {
329
+ names.push(await readDatabaseName({ session: targetSession, connectionId }));
330
+ }
331
+ expect(names).toEqual(['ld_000000000001_0', 'ld_000000000001_1', 'ld_000000000001_2']);
332
+ // The number holds for the rest of the session.
333
+ expect(await readDatabaseName({ session: targetSession, connectionId: 'auth_users' })).toEqual(
334
+ 'ld_000000000001_1'
335
+ );
336
+ });
337
+
338
+ test('a target the session numbered when it opened keeps that database', async () => {
339
+ const targetSession = createSession({
340
+ databases: new Map([
341
+ [JSON.stringify(['secret:AUTH_MONGODB_URI', null]), 'ld_000000000001_0'],
342
+ [JSON.stringify(['secret:MONGODB_URI', null]), 'ld_000000000001_1'],
343
+ ]),
344
+ databaseCount: 2,
345
+ });
346
+ expect(await readDatabaseName({ session: targetSession, connectionId: 'auth_users' })).toEqual(
347
+ 'ld_000000000001_0'
348
+ );
349
+ expect(await readDatabaseName({ session: targetSession, connectionId: 'app_users' })).toEqual(
350
+ 'ld_000000000001_1'
351
+ );
352
+ expect(
353
+ await readDatabaseName({ session: targetSession, connectionId: 'reports_users' })
354
+ ).toEqual('ld_000000000001_2');
355
+ });
356
+
357
+ test('a connection whose target is computed per request reads the session first database', async () => {
358
+ const targetSession = createSession({ databases: new Map(), databaseCount: 1 });
359
+ expect(
360
+ await readDatabaseName({ session: targetSession, connectionId: 'computed_users' })
361
+ ).toEqual('ld_000000000001_0');
362
+ expect(await readDatabaseName({ session: targetSession, connectionId: 'auth_users' })).toEqual(
363
+ 'ld_000000000001_1'
364
+ );
365
+ });
366
+ });
367
+
368
+ describe('a request through callRequest under a data session', () => {
369
+ const resolver = jest.fn(async () => ({ ok: true }));
370
+ resolver.schema = {};
371
+ resolver.meta = { checkRead: false, checkWrite: false };
372
+
373
+ function createRequestContext({ connectionType }) {
374
+ const requestConnections = {
375
+ [connectionType]: {
376
+ ...connections[connectionType],
377
+ schema: {},
378
+ requests: { RunQuery: resolver },
379
+ },
380
+ };
381
+ const configFiles = {
382
+ 'connections/store.json': {
383
+ id: 'connection:store',
384
+ connectionId: 'store',
385
+ type: connectionType,
386
+ properties: { databaseUri: 'mongodb://real/', collection: 'rows' },
387
+ },
388
+ 'pages/home/requests/get_rows.json': {
389
+ id: 'request:home:get_rows',
390
+ type: 'RunQuery',
391
+ requestId: 'get_rows',
392
+ pageId: 'home',
393
+ connectionId: 'store',
394
+ auth: { public: true },
395
+ properties: {},
396
+ },
397
+ };
398
+ const logger = { debug: () => {}, error: () => {}, info: () => {}, warn: () => {} };
399
+ const context = {
400
+ appMeta: {},
401
+ authorizeOutcome: () => 'allow',
402
+ config: {},
403
+ connections: requestConnections,
404
+ handleError: async (error) => {
405
+ error.handled = true;
406
+ },
407
+ headers: {},
408
+ logger,
409
+ mode: 'dev',
410
+ operators: {},
411
+ organization: null,
412
+ readConfigFile: async (filePath) => configFiles[filePath] ?? null,
413
+ scrubSecrets: (value) => value,
414
+ secrets: {},
415
+ steps: {},
416
+ user: null,
417
+ };
418
+ applyDataSetRedirect({ context, session: createSession(), connections: requestConnections });
419
+ return context;
420
+ }
421
+
422
+ beforeEach(() => {
423
+ resolver.mockClear();
424
+ });
425
+
426
+ test('fails and never reaches the resolver when the connection type declares no dataSet', async () => {
427
+ const context = createRequestContext({ connectionType: 'Knex' });
428
+ await expect(
429
+ callRequest(context, { pageId: 'home', requestId: 'get_rows', payload: {} })
430
+ ).rejects.toThrow(
431
+ 'Connection "store" (type Knex) cannot run under data set "staging-sample": its type does not say how to redirect it.'
432
+ );
433
+ expect(resolver).not.toHaveBeenCalled();
434
+ });
435
+
436
+ test('reaches the resolver with the session database when the connection type declares redirect', async () => {
437
+ const context = createRequestContext({ connectionType: 'FolderRuntime' });
438
+ await callRequest(context, { pageId: 'home', requestId: 'get_rows', payload: {} });
439
+ expect(resolver).toHaveBeenCalledTimes(1);
440
+ expect(resolver.mock.calls[0][0].connection).toEqual({
441
+ databaseUri: session.databaseUri,
442
+ databaseName: 'ld_000000000001_1',
443
+ collection: 'rows',
444
+ });
445
+ });
446
+ });
@@ -0,0 +1,207 @@
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 { callRequest } from '@lowdefy/api';
22
+ import createReadConfigFile from '@lowdefy/api/context/createReadConfigFile.js';
23
+
24
+ import applyDataSetRedirect from './applyDataSetRedirect.js';
25
+
26
+ // A walled-client plugin under auth.organizations.policy: tenant, served inside a data session and
27
+ // outside one, in both orders. The unwalled-reach verdict is memoised per process and read from the
28
+ // build, so whichever request evaluates it first, the walled-client plugin is served everywhere and
29
+ // a plugin that really holds the walled database's URI is refused everywhere.
30
+
31
+ const walledUri = 'mongodb://app-host:27017/app';
32
+
33
+ const session = {
34
+ id: 'abc',
35
+ name: 'staging-sample',
36
+ state: 'open',
37
+ databaseUri: 'mongodb://127.0.0.1:5555/?replicaSet=testset',
38
+ prefix: 'ld_000000000001',
39
+ databaseName: 'ld_000000000001_0',
40
+ databases: new Map([[JSON.stringify([`literal:${walledUri}`, null]), 'ld_000000000001_0']]),
41
+ databaseCount: 1,
42
+ };
43
+
44
+ async function WalledRead({ connection, walled }) {
45
+ const bound = await walled(connection.mongoConnectionId);
46
+ return {
47
+ databaseUri: bound.connection.databaseUri,
48
+ databaseName: bound.connection.databaseName ?? null,
49
+ tenant: bound.tenant,
50
+ };
51
+ }
52
+ WalledRead.schema = {};
53
+ WalledRead.meta = { checkRead: true, checkWrite: false };
54
+
55
+ const connections = {
56
+ MongoDBCollection: { meta: { tenant: true, dataSet: 'redirect' }, schema: {}, requests: {} },
57
+ WalledRuntime: {
58
+ meta: { tenant: false, dataSet: 'walled' },
59
+ schema: {},
60
+ requests: { WalledRead },
61
+ },
62
+ };
63
+
64
+ const artifacts = {
65
+ 'tenantConnections.json': [{ connectionId: 'fixture_db', type: 'MongoDBCollection' }],
66
+ 'unwalledConnections.json': [
67
+ { connectionId: 'walled_runtime', type: 'WalledRuntime' },
68
+ { connectionId: 'raw_runtime', type: 'WalledRuntime' },
69
+ ],
70
+ 'connections/fixture_db.json': {
71
+ id: 'connection:fixture_db',
72
+ connectionId: 'fixture_db',
73
+ type: 'MongoDBCollection',
74
+ tenantCapability: true,
75
+ properties: { databaseUri: walledUri, collection: 'items', read: true },
76
+ },
77
+ 'connections/walled_runtime.json': {
78
+ id: 'connection:walled_runtime',
79
+ connectionId: 'walled_runtime',
80
+ type: 'WalledRuntime',
81
+ tenantCapability: false,
82
+ properties: { mongoConnectionId: 'fixture_db', read: true },
83
+ },
84
+ // A plugin that holds the walled database's URI itself while naming a walled connection: the
85
+ // reach check refuses it, in and out of sessions.
86
+ 'connections/raw_runtime.json': {
87
+ id: 'connection:raw_runtime',
88
+ connectionId: 'raw_runtime',
89
+ type: 'WalledRuntime',
90
+ '~k': 'k_raw_runtime',
91
+ tenantCapability: false,
92
+ properties: { mongoConnectionId: 'fixture_db', uri: walledUri, read: true },
93
+ },
94
+ 'pages/home/requests/accept_match.json': {
95
+ id: 'request:home:accept_match',
96
+ type: 'WalledRead',
97
+ requestId: 'accept_match',
98
+ pageId: 'home',
99
+ connectionId: 'walled_runtime',
100
+ auth: { public: true },
101
+ properties: {},
102
+ },
103
+ 'pages/home/requests/raw_read.json': {
104
+ id: 'request:home:raw_read',
105
+ type: 'WalledRead',
106
+ requestId: 'raw_read',
107
+ pageId: 'home',
108
+ connectionId: 'raw_runtime',
109
+ auth: { public: true },
110
+ properties: {},
111
+ },
112
+ };
113
+
114
+ const rawRefusal =
115
+ 'Connection "raw_runtime" (WalledRuntime) holds a URI to the database of walled connection "fixture_db".';
116
+
117
+ let buildDirectory;
118
+
119
+ beforeAll(() => {
120
+ buildDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-walled-reach-'));
121
+ Object.entries(artifacts).forEach(([name, value]) => {
122
+ const filePath = path.join(buildDirectory, name);
123
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
124
+ fs.writeFileSync(filePath, JSON.stringify(value));
125
+ });
126
+ });
127
+
128
+ afterAll(() => {
129
+ fs.rmSync(buildDirectory, { recursive: true, force: true });
130
+ });
131
+
132
+ // The context createLowdefyContext composes: createApiContext's base reader over the build, and
133
+ // for a data-session request the gate over it.
134
+ function createContext({ config, dataSession }) {
135
+ const logger = { debug: () => {}, error: () => {}, info: () => {}, warn: () => {} };
136
+ const context = {
137
+ appMeta: {},
138
+ authorizeOutcome: () => 'allow',
139
+ buildDirectory,
140
+ config,
141
+ connections,
142
+ fileCache: new Map(),
143
+ handleError: async (error) => {
144
+ error.handled = true;
145
+ },
146
+ headers: {},
147
+ logger,
148
+ mode: 'dev',
149
+ operators: {},
150
+ organization: { policy: 'tenant' },
151
+ scrubSecrets: (value) => value,
152
+ secrets: {},
153
+ steps: {},
154
+ user: { id: 'u_member', organization_id: 'org_a' },
155
+ };
156
+ context.readConfigFile = createReadConfigFile(context);
157
+ if (dataSession) {
158
+ applyDataSetRedirect({ context, session, connections });
159
+ }
160
+ return context;
161
+ }
162
+
163
+ async function acceptMatch(context) {
164
+ const result = await callRequest(context, {
165
+ pageId: 'home',
166
+ requestId: 'accept_match',
167
+ payload: {},
168
+ });
169
+ return result.response;
170
+ }
171
+
172
+ const servedInSession = {
173
+ databaseUri: session.databaseUri,
174
+ databaseName: session.databaseName,
175
+ tenant: { field: 'organization_id', value: 'org_a' },
176
+ };
177
+ const servedOutside = {
178
+ databaseUri: walledUri,
179
+ databaseName: null,
180
+ tenant: { field: 'organization_id', value: 'org_a' },
181
+ };
182
+
183
+ test('a walled-client plugin is served in a data session first and outside one afterwards', async () => {
184
+ const config = {};
185
+ expect(await acceptMatch(createContext({ config, dataSession: true }))).toEqual(servedInSession);
186
+ expect(await acceptMatch(createContext({ config, dataSession: false }))).toEqual(servedOutside);
187
+ await expect(
188
+ callRequest(createContext({ config, dataSession: false }), {
189
+ pageId: 'home',
190
+ requestId: 'raw_read',
191
+ payload: {},
192
+ })
193
+ ).rejects.toThrow(rawRefusal);
194
+ });
195
+
196
+ test('a walled-client plugin is served outside a data session first and in one afterwards', async () => {
197
+ const config = {};
198
+ expect(await acceptMatch(createContext({ config, dataSession: false }))).toEqual(servedOutside);
199
+ expect(await acceptMatch(createContext({ config, dataSession: true }))).toEqual(servedInSession);
200
+ await expect(
201
+ callRequest(createContext({ config, dataSession: true }), {
202
+ pageId: 'home',
203
+ requestId: 'raw_read',
204
+ payload: {},
205
+ })
206
+ ).rejects.toThrow(rawRefusal);
207
+ });