@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,77 @@
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 { Hono } from 'hono';
18
+ import { jest } from '@jest/globals';
19
+
20
+ const mockGetEndpointConfig = jest.fn();
21
+ const mockRunWebhookEndpoint = jest.fn();
22
+ jest.unstable_mockModule('@lowdefy/api', () => ({
23
+ callEndpoint: jest.fn(),
24
+ getEndpointConfig: mockGetEndpointConfig,
25
+ parseRequestBody: jest.fn(),
26
+ runWebhookEndpoint: mockRunWebhookEndpoint,
27
+ }));
28
+
29
+ const { default: endpointsHandler } = await import('./endpoints.js');
30
+
31
+ const logger = { debug: jest.fn(), error: jest.fn(), info: jest.fn(), warn: jest.fn() };
32
+
33
+ function createApp() {
34
+ const app = new Hono();
35
+ app.use('*', async (c, next) => {
36
+ c.set('lowdefyContext', { logger, handleError: jest.fn() });
37
+ await next();
38
+ });
39
+ app.all('/api/endpoints/*', endpointsHandler);
40
+ return app;
41
+ }
42
+
43
+ beforeEach(() => {
44
+ mockGetEndpointConfig.mockResolvedValue({ webhook: true });
45
+ });
46
+
47
+ test('a webhook answer with contentType text/plain is sent as the plain-text body', async () => {
48
+ mockRunWebhookEndpoint.mockResolvedValue({
49
+ status: 200,
50
+ body: 'Validation: Testing client application',
51
+ contentType: 'text/plain',
52
+ });
53
+ const res = await createApp().request(
54
+ '/api/endpoints/graph_notifications?validationToken=Validation%3A+Testing+client+application',
55
+ { method: 'POST' }
56
+ );
57
+ expect(res.status).toBe(200);
58
+ expect(res.headers.get('content-type')).toMatch(/^text\/plain\b/);
59
+ expect(res.headers.get('x-content-type-options')).toBe('nosniff');
60
+ expect(await res.text()).toBe('Validation: Testing client application');
61
+ expect(mockRunWebhookEndpoint).toHaveBeenCalledWith(
62
+ expect.anything(),
63
+ expect.objectContaining({
64
+ endpointId: 'graph_notifications',
65
+ query: { validationToken: 'Validation: Testing client application' },
66
+ })
67
+ );
68
+ });
69
+
70
+ test('a webhook answer without contentType is still sent as JSON', async () => {
71
+ mockRunWebhookEndpoint.mockResolvedValue({ status: 200, body: 'token' });
72
+ const res = await createApp().request('/api/endpoints/hook', { method: 'POST', body: '{}' });
73
+ expect(res.status).toBe(200);
74
+ expect(res.headers.get('content-type')).toMatch(/^application\/json\b/);
75
+ expect(res.headers.get('x-content-type-options')).toBeNull();
76
+ expect(await res.json()).toBe('token');
77
+ });
@@ -14,11 +14,15 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { parseRequestBody } from '@lowdefy/api';
18
+
17
19
  import enrichFeedback from '../../lib/docs/enrichFeedback.js';
18
20
  import formatFeedback from '../../lib/docs/formatFeedback.js';
19
21
  import createLogger from '../../lib/server/log/createLogger.js';
22
+ import createSameOriginGuard from '../middleware/createSameOriginGuard.js';
20
23
 
21
24
  const logger = createLogger({ server: 'lowdefy-dev-feedback' });
25
+ const guardSameOrigin = createSameOriginGuard();
22
26
 
23
27
  // Receives annotation batches from the dev browser overlay (a developer
24
28
  // draws on a live page and comments), enriches each annotation with its
@@ -26,22 +30,15 @@ const logger = createLogger({ server: 'lowdefy-dev-feedback' });
26
30
  // text. The overlay copies that text to the clipboard — the developer
27
31
  // pastes it into whichever agent session they choose, which is also what
28
32
  // makes delivery unambiguous when several sessions share one dev server.
29
- // Same-origin check cloned from routes/clientError.js: only a page served
30
- // by this dev server (not an arbitrary site) should be able to use it.
33
+ // The same-origin guard the browser POST routes share: only a page served by
34
+ // this dev server (not an arbitrary site) should be able to use it.
31
35
  async function feedbackHandler(c) {
32
- const origin = c.req.header('origin');
33
- if (!origin) {
34
- return c.json({ error: 'Forbidden' }, 403);
35
- }
36
- try {
37
- if (new URL(origin).host !== c.req.header('host')) {
38
- return c.json({ error: 'Forbidden' }, 403);
39
- }
40
- } catch {
41
- return c.json({ error: 'Forbidden' }, 403);
36
+ const forbidden = guardSameOrigin(c);
37
+ if (forbidden) {
38
+ return forbidden;
42
39
  }
43
40
 
44
- const batch = await c.req.json();
41
+ const batch = parseRequestBody({ text: await c.req.text() });
45
42
  if (!Array.isArray(batch?.annotations)) {
46
43
  return c.json(
47
44
  {
@@ -15,49 +15,69 @@
15
15
  */
16
16
 
17
17
  import { getPageConfig } from '@lowdefy/api';
18
+ import { matchPagePath } from '@lowdefy/node-utils';
18
19
 
19
- import buildPageIfNeeded, { getPageJitEnrichment } from '../../lib/server/jitPageBuilder.js';
20
- import getPathSegments from '../lib/getPathSegments.js';
20
+ import authJson from '../../lib/build/auth.js';
21
+ import { buildPageWithContext, getPageJitEnrichment } from '../../lib/server/jitPageBuilder.js';
22
+ import getBuildId from '../../lib/docs/getBuildId.js';
23
+ import getRequestPath from '../lib/getRequestPath.js';
24
+ import lowdefyConfig from '../../lib/build/config.js';
25
+ import servedBuilds from '../../lib/server/recording/servedBuilds.js';
26
+
27
+ const basePath = lowdefyConfig.basePath ?? '';
21
28
 
22
29
  // JIT page build + config response. The response shapes are a frozen contract
23
30
  // with the dev client:
24
31
  // 200 { installing: true, packages } — plugin install in progress, client polls
25
32
  // 500 { buildError: true, errors, message, source } — build failed
33
+ // 401 { redirect } — logged-out navigation to a protected page
34
+ // 403 { redirect } — authorised but second factor not yet enrolled
26
35
  // 404 'Page not found.'
27
- // 200 pageConfig (+ _warnings, + _jsEntries module text, + _dynamicIcons data)
36
+ // 200 { pageId, pathParams, matchedPath, pageConfig } (pageConfig + _buildId,
37
+ // + _warnings, + _jsEntries module text, + _dynamicIcons data)
38
+ //
39
+ // The request path is matched against the skeleton build's route table, and
40
+ // the matched page is built before getPageConfig reads its file.
28
41
  async function jitPageHandler(c) {
29
42
  const context = c.get('lowdefyContext');
30
- const pageId = getPathSegments(c, '/api/page/').join('/');
43
+ const { path, matchedPath } = getRequestPath({ c, basePath, prefix: '/api/page/' });
44
+ // The callbackUrl of the sign-in and enrolment redirects: the requested page,
45
+ // query included.
46
+ const callbackUrl = `${basePath}/${path}${new URL(c.req.url).search}`;
47
+ const match = matchPagePath({ routes: await context.readConfigFile('routes.json'), path });
31
48
 
32
49
  let buildResult;
33
- try {
34
- buildResult = await buildPageIfNeeded({
35
- pageId,
36
- buildDirectory: context.buildDirectory,
37
- configDirectory: context.configDirectory,
38
- });
39
- } catch (error) {
40
- const rawErrors = error.buildErrors ?? [error];
41
- const errors = [];
42
- for (const err of rawErrors) {
43
- await context.handleError(err);
44
- errors.push({
45
- type: err.name ?? 'Error',
46
- message: err.message,
47
- source: err.source ?? null,
48
- stack: err.stack ?? null,
49
- });
50
+ let buildContext;
51
+ if (match !== null) {
52
+ try {
53
+ ({ result: buildResult, buildContext } = await buildPageWithContext({
54
+ pageId: match.pageId,
55
+ buildDirectory: context.buildDirectory,
56
+ configDirectory: context.configDirectory,
57
+ }));
58
+ } catch (error) {
59
+ const rawErrors = error.buildErrors ?? [error];
60
+ const errors = [];
61
+ for (const err of rawErrors) {
62
+ await context.handleError(err);
63
+ errors.push({
64
+ type: err.name ?? 'Error',
65
+ message: err.message,
66
+ source: err.source ?? null,
67
+ stack: err.stack ?? null,
68
+ });
69
+ }
70
+ return c.json(
71
+ {
72
+ buildError: true,
73
+ errors,
74
+ // Keep top-level message/source for backward compatibility
75
+ message: error.message,
76
+ source: error.source ?? null,
77
+ },
78
+ 500
79
+ );
50
80
  }
51
- return c.json(
52
- {
53
- buildError: true,
54
- errors,
55
- // Keep top-level message/source for backward compatibility
56
- message: error.message,
57
- source: error.source ?? null,
58
- },
59
- 500
60
- );
61
81
  }
62
82
 
63
83
  if (buildResult && buildResult.installing) {
@@ -67,20 +87,64 @@ async function jitPageHandler(c) {
67
87
  });
68
88
  }
69
89
 
70
- const pageConfig = await getPageConfig(context, { pageId, urlQuery: c.req.query() });
71
- if (pageConfig === null) {
90
+ const result = await getPageConfig(context, { path, urlQuery: c.req.query() });
91
+ if (result.status === 'unauthenticated') {
92
+ // The client follows this redirect with a full page load, so the login
93
+ // page can return to the requested page after sign-in.
94
+ context.logger.debug(
95
+ `Page config request for "/${path}" resolved unauthenticated - returning a sign-in redirect.`
96
+ );
97
+ return c.json(
98
+ {
99
+ redirect: `${basePath}${authJson.authPages.signIn}?callbackUrl=${encodeURIComponent(
100
+ callbackUrl
101
+ )}`,
102
+ },
103
+ 401
104
+ );
105
+ }
106
+ if (result.status === 'enrol_required') {
107
+ // 403, not the 401 the signed-out branch above uses: a 401 is the client's
108
+ // dead-session signal and would bounce the user to sign-in, which is the loop
109
+ // the enrolment gate exists to avoid.
110
+ context.logger.debug(
111
+ `Page config request for "/${path}" resolved enrol_required - returning a two-factor enrolment redirect.`
112
+ );
113
+ return c.json(
114
+ {
115
+ redirect: `${basePath}${authJson.authPages.twoFactorEnrol}?callbackUrl=${encodeURIComponent(
116
+ callbackUrl
117
+ )}`,
118
+ },
119
+ 403
120
+ );
121
+ }
122
+ if (result.status !== 'ok') {
72
123
  return c.text('Page not found.', 404);
73
124
  }
125
+ const pageConfig = result.pageConfig;
126
+ // The build this config was served under. The dev recorder stamps it on each
127
+ // record, and the recording route trusts only a build it finds in
128
+ // servedBuilds.
129
+ pageConfig._buildId = getBuildId();
130
+ servedBuilds.add(pageConfig._buildId);
74
131
  if (buildResult?.warnings?.length > 0) {
75
132
  pageConfig._warnings = buildResult.warnings;
76
133
  }
77
134
  // Fold this page's JIT-discovered _js entries and dynamic icons into the
78
135
  // response the client already awaits, so first paint has everything it needs
79
136
  // without the two secondary fetches that stalled in Vite's transform window.
80
- const { jsEntries, dynamicIcons } = getPageJitEnrichment({ pageConfig });
137
+ // Read from the context the page was built on: another request can discard
138
+ // it while getPageConfig runs.
139
+ const { jsEntries, dynamicIcons } = getPageJitEnrichment({ pageConfig, buildContext });
81
140
  if (jsEntries) pageConfig._jsEntries = jsEntries;
82
141
  if (dynamicIcons) pageConfig._dynamicIcons = dynamicIcons;
83
- return c.json(pageConfig);
142
+ return c.json({
143
+ pageId: result.pageId,
144
+ pathParams: result.pathParams,
145
+ matchedPath,
146
+ pageConfig,
147
+ });
84
148
  }
85
149
 
86
150
  export default jitPageHandler;
@@ -0,0 +1,248 @@
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
+ import { Hono } from 'hono';
21
+ import { jest } from '@jest/globals';
22
+
23
+ const BUILD_A = '2026-10-03T14:02:00.000Z';
24
+ const BUILD_B = '2026-10-03T14:05:00.000Z';
25
+ let mockBuildId = BUILD_A;
26
+ jest.unstable_mockModule('../../lib/docs/getBuildId.js', () => ({
27
+ default: () => mockBuildId,
28
+ }));
29
+
30
+ const mockGetPageConfig = jest.fn();
31
+ jest.unstable_mockModule('@lowdefy/api', () => ({
32
+ getPageConfig: mockGetPageConfig,
33
+ }));
34
+
35
+ // No JIT build work in these tests — the route's build branch is exercised
36
+ // elsewhere; here we only drive the getPageConfig status fork.
37
+ const mockGetPageJitEnrichment = jest.fn(() => ({}));
38
+ const mockBuildContext = { jsMap: { client: {} } };
39
+ const mockBuildPageWithContext = jest.fn(async () => ({
40
+ result: true,
41
+ buildContext: mockBuildContext,
42
+ }));
43
+ jest.unstable_mockModule('../../lib/server/jitPageBuilder.js', () => ({
44
+ buildPageWithContext: mockBuildPageWithContext,
45
+ getPageJitEnrichment: mockGetPageJitEnrichment,
46
+ }));
47
+
48
+ jest.unstable_mockModule('../../lib/build/auth.js', () => ({
49
+ default: { authPages: { signIn: '/auth/login', twoFactorEnrol: '/two-factor-enrol' } },
50
+ }));
51
+
52
+ jest.unstable_mockModule('../../lib/build/config.js', () => ({
53
+ default: { basePath: '' },
54
+ }));
55
+
56
+ const { default: jitPageHandler } = await import('./jitPage.js');
57
+ const { default: devRecordingHandler } = await import('./devRecording.js');
58
+ const { default: servedBuilds } = await import('../../lib/server/recording/servedBuilds.js');
59
+
60
+ const routes = [
61
+ { pageId: 'dashboard', path: 'dashboard' },
62
+ { pageId: 'ticket', path: '{space}/tickets/{ticket_id}' },
63
+ ];
64
+
65
+ function createApp() {
66
+ const app = new Hono();
67
+ app.use('*', async (c, next) => {
68
+ c.set('lowdefyContext', {
69
+ buildDirectory: '/build',
70
+ configDirectory: '/config',
71
+ readConfigFile: async (file) => (file === 'routes.json' ? routes : null),
72
+ logger: { debug: jest.fn(), info: jest.fn(), error: jest.fn() },
73
+ handleError: jest.fn(),
74
+ });
75
+ await next();
76
+ });
77
+ app.all('/api/page/*', jitPageHandler);
78
+ app.post('/api/dev-recording', devRecordingHandler);
79
+ return app;
80
+ }
81
+
82
+ afterEach(() => {
83
+ mockGetPageConfig.mockReset();
84
+ mockBuildPageWithContext.mockClear();
85
+ mockBuildId = BUILD_A;
86
+ });
87
+
88
+ test('jitPageHandler redirects to the two-factor enrolment page with a 403 when enrol_required', async () => {
89
+ mockGetPageConfig.mockResolvedValue({ status: 'enrol_required' });
90
+ const res = await createApp().request('/api/page/dashboard');
91
+ expect(res.status).toEqual(403);
92
+ const body = await res.json();
93
+ expect(body).toEqual({ redirect: '/two-factor-enrol?callbackUrl=%2Fdashboard' });
94
+ });
95
+
96
+ test('jitPageHandler still returns a 401 sign-in redirect when unauthenticated', async () => {
97
+ mockGetPageConfig.mockResolvedValue({ status: 'unauthenticated' });
98
+ const res = await createApp().request('/api/page/dashboard');
99
+ expect(res.status).toEqual(401);
100
+ const body = await res.json();
101
+ expect(body).toEqual({ redirect: '/auth/login?callbackUrl=%2Fdashboard' });
102
+ });
103
+
104
+ test('jitPageHandler still returns a 404 when the page is not found', async () => {
105
+ mockGetPageConfig.mockResolvedValue({ status: 'not_found' });
106
+ const res = await createApp().request('/api/page/dashboard');
107
+ expect(res.status).toEqual(404);
108
+ expect(await res.text()).toEqual('Page not found.');
109
+ });
110
+
111
+ test('jitPageHandler returns the pageConfig when status is ok', async () => {
112
+ mockGetPageConfig.mockResolvedValue({
113
+ status: 'ok',
114
+ pageId: 'dashboard',
115
+ pathParams: {},
116
+ pageConfig: { id: 'dashboard' },
117
+ });
118
+ const res = await createApp().request('/api/page/dashboard');
119
+ expect(res.status).toEqual(200);
120
+ const body = await res.json();
121
+ expect(body).toEqual({
122
+ pageId: 'dashboard',
123
+ pathParams: {},
124
+ matchedPath: 'dashboard',
125
+ pageConfig: { id: 'dashboard', _buildId: BUILD_A },
126
+ });
127
+ });
128
+
129
+ test('jitPageHandler builds the page a patterned path matches and returns its path values', async () => {
130
+ mockGetPageConfig.mockResolvedValue({
131
+ status: 'ok',
132
+ pageId: 'ticket',
133
+ pathParams: { space: 'support', ticket_id: '1234' },
134
+ pageConfig: { id: 'ticket' },
135
+ });
136
+ const res = await createApp().request('/api/page/support/tickets/1234/?tab=2');
137
+ expect(mockBuildPageWithContext).toHaveBeenCalledWith({
138
+ pageId: 'ticket',
139
+ buildDirectory: '/build',
140
+ configDirectory: '/config',
141
+ });
142
+ expect(mockGetPageConfig.mock.calls[0][1]).toEqual({
143
+ path: 'support/tickets/1234/',
144
+ urlQuery: { tab: '2' },
145
+ });
146
+ expect(res.status).toEqual(200);
147
+ const body = await res.json();
148
+ expect(body).toEqual({
149
+ pageId: 'ticket',
150
+ pathParams: { space: 'support', ticket_id: '1234' },
151
+ matchedPath: 'support/tickets/1234',
152
+ pageConfig: { id: 'ticket', _buildId: BUILD_A },
153
+ });
154
+ });
155
+
156
+ test('jitPageHandler builds nothing for a path no page matches', async () => {
157
+ mockGetPageConfig.mockResolvedValue({ status: 'not_found' });
158
+ const res = await createApp().request('/api/page/support/tickets');
159
+ expect(mockBuildPageWithContext).not.toHaveBeenCalled();
160
+ expect(res.status).toEqual(404);
161
+ });
162
+
163
+ test('jitPageHandler sign-in redirect carries the request path and query on callbackUrl', async () => {
164
+ mockGetPageConfig.mockResolvedValue({ status: 'unauthenticated' });
165
+ const res = await createApp().request('/api/page/support/tickets?tab=2');
166
+ expect(res.status).toEqual(401);
167
+ expect(await res.json()).toEqual({
168
+ redirect: `/auth/login?callbackUrl=${encodeURIComponent('/support/tickets?tab=2')}`,
169
+ });
170
+ });
171
+
172
+ test('jitPageHandler folds _jsEntries and _dynamicIcons onto the ok response', async () => {
173
+ mockGetPageConfig.mockResolvedValue({ status: 'ok', pageConfig: { id: 'dashboard' } });
174
+ mockGetPageJitEnrichment.mockReturnValueOnce({
175
+ jsEntries: 'export default {};',
176
+ dynamicIcons: { Zap: { node: [['path', { d: 'M0 0' }]] } },
177
+ });
178
+ const res = await createApp().request('/api/page/dashboard');
179
+ expect(res.status).toEqual(200);
180
+ const body = await res.json();
181
+ expect(body.pageConfig).toEqual({
182
+ id: 'dashboard',
183
+ _buildId: BUILD_A,
184
+ _jsEntries: 'export default {};',
185
+ _dynamicIcons: { Zap: { node: [['path', { d: 'M0 0' }]] } },
186
+ });
187
+ });
188
+
189
+ test('jitPageHandler reads the enrichment from the context the page was built on', async () => {
190
+ const pageConfig = { id: 'dashboard' };
191
+ mockGetPageConfig.mockResolvedValue({ status: 'ok', pageConfig });
192
+ await createApp().request('/api/page/dashboard');
193
+ expect(mockGetPageJitEnrichment).toHaveBeenLastCalledWith({
194
+ pageConfig,
195
+ buildContext: mockBuildContext,
196
+ });
197
+ });
198
+
199
+ test('jitPageHandler names the build it served, and the recording route keeps that build after a rebuild', async () => {
200
+ const configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-jit-page-build-'));
201
+ process.env.LOWDEFY_DIRECTORY_CONFIG = configDirectory;
202
+ try {
203
+ mockGetPageConfig.mockResolvedValue({ status: 'ok', pageConfig: { id: 'dashboard' } });
204
+ const app = createApp();
205
+ const { pageConfig: page } = await (await app.request('/api/page/dashboard')).json();
206
+ expect(page._buildId).toBe(BUILD_A);
207
+ expect(servedBuilds.has(BUILD_A)).toBe(true);
208
+
209
+ mockBuildId = BUILD_B;
210
+ const session = '20261003T140311Z-k3x9qa';
211
+ const res = await app.request('/api/dev-recording', {
212
+ method: 'POST',
213
+ headers: {
214
+ host: 'localhost:3001',
215
+ origin: 'http://localhost:3001',
216
+ 'content-type': 'application/json',
217
+ },
218
+ body: JSON.stringify({
219
+ session,
220
+ records: [
221
+ {
222
+ v: 1,
223
+ session,
224
+ t: '2026-10-03T14:04:00.000Z',
225
+ kind: 'click',
226
+ scope: 'page',
227
+ page_id: 'dashboard',
228
+ event: null,
229
+ build: page._buildId,
230
+ },
231
+ ],
232
+ }),
233
+ });
234
+ expect(res.status).toBe(204);
235
+ const file = path.join(
236
+ configDirectory,
237
+ '.lowdefy',
238
+ 'traces',
239
+ 'dev',
240
+ '2026-10-03',
241
+ `${session}.jsonl`
242
+ );
243
+ expect(JSON.parse(fs.readFileSync(file, 'utf8').trim()).build).toBe(BUILD_A);
244
+ } finally {
245
+ fs.rmSync(configDirectory, { recursive: true, force: true });
246
+ delete process.env.LOWDEFY_DIRECTORY_CONFIG;
247
+ }
248
+ });
package/src/routes/mcp.js CHANGED
@@ -15,17 +15,94 @@
15
15
  */
16
16
 
17
17
  import { StreamableHTTPTransport } from '@hono/mcp';
18
- import { createMcpServer } from '@lowdefy/api';
18
+ import { createMcpServer, getMcpResourceMetadataUri, getMcpResourceUri } from '@lowdefy/api';
19
+ import { type } from '@lowdefy/helpers';
20
+
21
+ import authJson from '../../lib/build/auth.js';
22
+ import normalizeMcpProtocolVersionHeader from './normalizeMcpProtocolVersionHeader.js';
23
+
24
+ // The RFC 6750 challenge for the MCP resource, pointing at its RFC 9728
25
+ // metadata. The invalid_token extension exists for the two failures a client
26
+ // can act on: BetterAuth mints opaque tokens when a client omits the RFC 8707
27
+ // resource parameter, and those can never verify here - the description names
28
+ // the parameter, never app state. A revoked grant (the member switched
29
+ // organization or disconnected the assistant) is told to reconnect, which
30
+ // re-runs the authorization and its organization choice. A refused member
31
+ // token gets its reason and no resource_metadata: a script cannot sign in, so
32
+ // pointing it at the authorization server would only mislead it.
33
+ function getChallengeHeader({ context, description, memberToken, parseableJwt, revoked }) {
34
+ if (memberToken === true) {
35
+ return `Bearer error="invalid_token", error_description="${description}"`;
36
+ }
37
+ const metadataUri = getMcpResourceMetadataUri({ config: context.config });
38
+ if (parseableJwt === false) {
39
+ return `Bearer error="invalid_token", error_description="The access token is not a JWT. Connect with a client that sends the RFC 8707 resource parameter.", resource_metadata="${metadataUri}"`;
40
+ }
41
+ if (revoked === true) {
42
+ return `Bearer error="invalid_token", error_description="This connection was disconnected. Reconnect to choose the organization to work in.", resource_metadata="${metadataUri}"`;
43
+ }
44
+ return `Bearer resource_metadata="${metadataUri}"`;
45
+ }
46
+
47
+ // Browser cross-site defence: an Origin that is neither the pinned canonical
48
+ // origin nor one the app registered on BetterAuth's trustedOrigins is
49
+ // refused. Non-browser MCP clients send no Origin and pass.
50
+ function isOriginAllowed({ c, context }) {
51
+ const origin = c.req.header('origin');
52
+ if (type.isNone(origin)) {
53
+ return true;
54
+ }
55
+ const canonicalOrigin = new URL(getMcpResourceUri({ config: context.config })).origin;
56
+ if (origin === canonicalOrigin) {
57
+ return true;
58
+ }
59
+ // Exact strings only - a trustedOrigins function or wildcard entry is not
60
+ // consulted here.
61
+ const trustedOrigins = context.auth?.options?.trustedOrigins;
62
+ return type.isArray(trustedOrigins) && trustedOrigins.includes(origin);
63
+ }
19
64
 
20
65
  // Stateless per-request MCP server over the app's configured endpoints. The
21
- // apiContext middleware has already resolved the caller's session, so tool
22
- // listing and calls are authorized per request.
66
+ // apiContext middleware has already resolved the caller from the bearer token
67
+ // (context.mcpAuth), so tool listing and calls are authorized per request.
68
+ //
69
+ // The 401 challenge is decided here, before the transport runs -
70
+ // transport.handleRequest writes tool results over HTTP 200, so past this
71
+ // boundary a role or scope shortfall stays opaque inside the tool surface,
72
+ // never a 403 or insufficient_scope.
23
73
  async function mcpHandler(c) {
74
+ normalizeMcpProtocolVersionHeader({ request: c.req.raw });
24
75
  const context = c.get('lowdefyContext');
25
- const server = await createMcpServer({ context });
26
- if (!server) {
76
+ const mcpConfig = await context.readConfigFile('mcp.json');
77
+ if (type.isNone(mcpConfig) || mcpConfig.configured !== true) {
27
78
  return c.json({ error: 'MCP is not configured.' }, 404);
28
79
  }
80
+ // Without an authorization server the build guaranteed an all-public tool
81
+ // surface, so the route serves openly - no challenge envelope at all.
82
+ if (!type.isNone(authJson.oauthProvider)) {
83
+ if (!isOriginAllowed({ c, context })) {
84
+ return c.json({ error: 'Origin not allowed.' }, 403);
85
+ }
86
+ const { tokenStatus, description, memberToken, parseableJwt, revoked } = context.mcpAuth ?? {};
87
+ if (tokenStatus === 'invalid') {
88
+ return c.json({ error: 'Unauthorized.' }, 401, {
89
+ 'WWW-Authenticate': getChallengeHeader({
90
+ context,
91
+ description,
92
+ memberToken,
93
+ parseableJwt,
94
+ revoked,
95
+ }),
96
+ });
97
+ }
98
+ // Nothing to serve anonymously - challenge instead of an empty tool list.
99
+ if (tokenStatus === 'none' && mcpConfig.hasPublicTool === false) {
100
+ return c.json({ error: 'Unauthorized.' }, 401, {
101
+ 'WWW-Authenticate': getChallengeHeader({ context, parseableJwt: true }),
102
+ });
103
+ }
104
+ }
105
+ const server = await createMcpServer({ context });
29
106
  const transport = new StreamableHTTPTransport();
30
107
  await server.connect(transport);
31
108
  return transport.handleRequest(c);