@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.
- package/client/App.jsx +52 -24
- package/client/Inspector.jsx +73 -30
- package/client/JourneyObserver.jsx +47 -0
- package/client/Page.jsx +54 -22
- package/client/Recorder.jsx +73 -0
- package/client/Routing.jsx +37 -13
- package/client/clientBuildImports.test.mjs +2 -0
- package/client/devStreamConnections.test.mjs +40 -0
- package/client/feedback/FeedbackMount.jsx +2 -2
- package/client/feedback/FeedbackOverlay.jsx +7 -4
- package/client/feedback/feedbackStyles.js +3 -0
- package/client/journeyObserver/startJourneyObserver.js +101 -0
- package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
- package/client/openInEditor/OpenInEditorListener.jsx +2 -0
- package/client/recorder/buildEngineRecord.js +37 -0
- package/client/recorder/buildInteractionRecord.js +64 -0
- package/client/recorder/buildTraceEvent.js +57 -0
- package/client/recorder/buildTraceEvent.test.mjs +81 -0
- package/client/recorder/chunkRecords.js +69 -0
- package/client/recorder/chunkRecords.test.mjs +59 -0
- package/client/recorder/collectEventActions.js +30 -0
- package/client/recorder/collectEventEndpoints.js +40 -0
- package/client/recorder/collectEventRequests.js +39 -0
- package/client/recorder/createPairingBuffer.js +187 -0
- package/client/recorder/createPairingBuffer.test.mjs +405 -0
- package/client/recorder/createPasswordRedactor.js +84 -0
- package/client/recorder/createRecordBatcher.js +54 -0
- package/client/recorder/createRecordBatcher.test.mjs +76 -0
- package/client/recorder/createRecorder.js +216 -0
- package/client/recorder/createRecorder.test.mjs +318 -0
- package/client/recorder/createRecordingSession.js +61 -0
- package/client/recorder/createRecordingSession.test.mjs +72 -0
- package/client/recorder/diffStateWrites.js +77 -0
- package/client/recorder/diffStateWrites.test.mjs +66 -0
- package/client/recorder/isEventCall.js +30 -0
- package/client/recorder/jsonType.js +39 -0
- package/client/recorder/keyChord.js +47 -0
- package/client/recorder/keyChord.test.mjs +50 -0
- package/client/recorder/sendRecords.js +40 -0
- package/client/recorder/toJsonSafe.js +29 -0
- package/lib/client/BuildingPage.jsx +3 -1
- package/lib/client/ErrorBar.jsx +17 -13
- package/lib/client/RedirectingPage.jsx +56 -0
- package/lib/client/auth/Auth.jsx +11 -6
- package/lib/client/auth/AuthConfigured.jsx +222 -36
- package/lib/client/auth/AuthNotConfigured.js +45 -4
- package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
- package/lib/client/auth/authTemplateSync.test.mjs +40 -0
- package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
- package/lib/client/auth/createFetchCheckingBuild.js +45 -0
- package/lib/client/bundleBuildId.js +25 -0
- package/lib/client/collectVisibleBlockIds.js +51 -0
- package/lib/client/getCspNonce.js +25 -0
- package/lib/client/getPagePath.js +36 -0
- package/lib/client/getPagePath.test.mjs +51 -0
- package/lib/client/utils/formatErrorsForCopy.js +30 -0
- package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
- package/lib/client/utils/getErrorBarColor.js +25 -0
- package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
- package/lib/client/utils/request.js +8 -1
- package/lib/client/utils/usePageConfig.js +89 -25
- package/lib/client/utils/usePageConfig.test.mjs +198 -13
- package/lib/configTree/buildConfigTree.mjs +58 -0
- package/lib/configTree/buildConfigTree.test.mjs +77 -0
- package/lib/configTree/collectConfigText.mjs +101 -0
- package/lib/configTree/collectConfigText.test.mjs +116 -0
- package/lib/configTree/prepareScratchServer.mjs +46 -0
- package/lib/configTree/runConfigTreeBuild.mjs +88 -0
- package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
- package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
- package/lib/docs/JourneyStepError.js +30 -0
- package/lib/docs/acquireBrowserSlot.js +30 -0
- package/lib/docs/addNestedEndpoints.js +54 -0
- package/lib/docs/addNestedEndpoints.test.mjs +185 -0
- package/lib/docs/addWriteFlags.js +77 -0
- package/lib/docs/buildEditedPages.js +50 -0
- package/lib/docs/buildWaitHeader.js +21 -0
- package/lib/docs/captureAnnotatedScreenshot.js +23 -12
- package/lib/docs/checkAgentFiles.js +148 -0
- package/lib/docs/checkAgentFiles.test.mjs +225 -0
- package/lib/docs/checkStepReferences.js +239 -0
- package/lib/docs/checkStepReferences.test.mjs +173 -0
- package/lib/docs/checkpointStore.js +3 -0
- package/lib/docs/clientErrorStore.js +18 -1
- package/lib/docs/clientErrorStore.test.mjs +46 -0
- package/lib/docs/collectExercised.js +52 -0
- package/lib/docs/createBrowserLifecycle.js +102 -0
- package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
- package/lib/docs/createDocsMcpServer.js +345 -388
- package/lib/docs/createDocsMcpServer.test.mjs +644 -4
- package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
- package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
- package/lib/docs/createJourneyActors.js +213 -0
- package/lib/docs/createJourneyActors.test.mjs +79 -0
- package/lib/docs/createJourneyObservations.js +80 -0
- package/lib/docs/createLeftOriginError.js +26 -0
- package/lib/docs/createNetworkCounter.js +101 -0
- package/lib/docs/dataSets/checkSharedCollectionIds.js +67 -0
- package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
- package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
- package/lib/docs/dataSets/describeDataSetResult.js +25 -0
- package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
- package/lib/docs/dataSets/deserializeFixture.js +34 -0
- package/lib/docs/dataSets/drainSessionWork.js +37 -0
- package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
- package/lib/docs/dataSets/duplicateIndexName.js +24 -0
- package/lib/docs/dataSets/findCollectionCollisions.js +52 -0
- package/lib/docs/dataSets/getDataStore.js +106 -0
- package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
- package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
- package/lib/docs/dataSets/groupDataSetByCollection.js +44 -0
- package/lib/docs/dataSets/insertFixture.js +57 -0
- package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
- package/lib/docs/dataSets/isDataStoreGone.js +29 -0
- package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
- package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
- package/lib/docs/dataSets/openDataSession.js +119 -0
- package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -0
- package/lib/docs/dataSets/pickDataStorePort.js +58 -0
- package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
- package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
- package/lib/docs/dataSets/readDataSession.js +41 -0
- package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
- package/lib/docs/dataSets/readDataSet.js +68 -0
- package/lib/docs/dataSets/readDataSet.test.mjs +220 -0
- package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
- package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
- package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
- package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
- package/lib/docs/dataSets/sweepOrphanDatabases.js +33 -0
- package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
- package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
- package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
- package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
- package/lib/docs/devEventBus.js +136 -0
- package/lib/docs/devEventBus.test.mjs +269 -0
- package/lib/docs/devToolDefinitions.js +529 -0
- package/lib/docs/docSources.js +21 -0
- package/lib/docs/docs.test.mjs +171 -15
- package/lib/docs/docsIndex.test.mjs +377 -0
- package/lib/docs/evalOperator.js +5 -3
- package/lib/docs/evalOperatorHeadless.js +84 -12
- package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
- package/lib/docs/evalOperatorInTab.js +2 -1
- package/lib/docs/findConfig.js +95 -49
- package/lib/docs/fitResponse.js +81 -0
- package/lib/docs/fitResponse.test.mjs +128 -0
- package/lib/docs/formatFeedback.js +26 -5
- package/lib/docs/formatFeedback.test.mjs +48 -0
- package/lib/docs/getAppMap.js +7 -1
- package/lib/docs/getBrowser.js +150 -80
- package/lib/docs/getBrowser.test.mjs +407 -16
- package/lib/docs/getBuildId.js +61 -0
- package/lib/docs/getBuildId.test.mjs +84 -0
- package/lib/docs/getBuildStatus.js +54 -6
- package/lib/docs/getBuildStatus.test.mjs +133 -0
- package/lib/docs/getBuildStatusAfterEdits.js +35 -0
- package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
- package/lib/docs/getCoreDoc.js +9 -1
- package/lib/docs/getDoc.js +100 -0
- package/lib/docs/getDocsIndex.js +152 -0
- package/lib/docs/getHazards.js +115 -0
- package/lib/docs/getHazards.test.mjs +175 -0
- package/lib/docs/getMailOutboxDirectory.js +26 -0
- package/lib/docs/getOverview.js +79 -13
- package/lib/docs/getOverviewBasePath.test.mjs +37 -0
- package/lib/docs/getPageBuildStatus.js +46 -0
- package/lib/docs/getPluginDoc.js +21 -23
- package/lib/docs/getSchema.js +33 -1
- package/lib/docs/getStaleStatus.js +36 -0
- package/lib/docs/getStaleStatus.test.mjs +74 -0
- package/lib/docs/guardJourneyOrigin.js +39 -0
- package/lib/docs/inspectState.js +5 -3
- package/lib/docs/inspectStateFromTab.js +2 -2
- package/lib/docs/inspectStateHeadless.js +89 -27
- package/lib/docs/inspectStateHeadless.test.mjs +20 -1
- package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
- package/lib/docs/installHeadlessShell.js +123 -0
- package/lib/docs/installHeadlessShell.test.mjs +70 -0
- package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
- package/lib/docs/isHeadlessShellIncomplete.js +34 -0
- package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
- package/lib/docs/isPageReady.js +77 -0
- package/lib/docs/isPageReady.test.mjs +235 -0
- package/lib/docs/isRunErrorEntry.js +25 -0
- package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
- package/lib/docs/journeyRunRecording.js +35 -0
- package/lib/docs/journeyRunRecording.test.mjs +39 -0
- package/lib/docs/keepEventStreamAlive.js +68 -0
- package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
- package/lib/docs/launchBrowser.js +83 -0
- package/lib/docs/launchBrowser.test.mjs +175 -0
- package/lib/docs/listModuleDocEntries.js +69 -0
- package/lib/docs/listPackageDocFiles.js +67 -0
- package/lib/docs/listPluginDocEntries.js +111 -0
- package/lib/docs/listPlugins.js +9 -58
- package/lib/docs/listTypes.js +14 -6
- package/lib/docs/loadFrameworkHazards.js +40 -0
- package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
- package/lib/docs/loadState.js +103 -35
- package/lib/docs/makeModuleManifestDoc.js +174 -0
- package/lib/docs/matchExercisedPages.js +35 -0
- package/lib/docs/matchJourneyRoute.js +69 -0
- package/lib/docs/matchJourneyRoute.test.mjs +178 -0
- package/lib/docs/mergeNetworkSnapshots.js +49 -0
- package/lib/docs/nextJourneySequence.js +30 -0
- package/lib/docs/noBrowserError.js +22 -0
- package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
- package/lib/docs/observe/claimExpectedErrors.js +67 -0
- package/lib/docs/observe/createJourneyAppErrors.js +236 -0
- package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
- package/lib/docs/observe/evaluateInvariants.js +183 -0
- package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
- package/lib/docs/observe/findingKey.js +29 -0
- package/lib/docs/observe/hasNoEffect.js +31 -0
- package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
- package/lib/docs/observe/installStepObserver.js +72 -0
- package/lib/docs/observe/isAppApiUrl.js +33 -0
- package/lib/docs/observe/parseAppApiUrl.js +37 -0
- package/lib/docs/observe/readStepObserver.js +42 -0
- package/lib/docs/observe/relativeSource.js +29 -0
- package/lib/docs/observe/resolveConfigKeySource.js +33 -0
- package/lib/docs/observe/takeJourneyEvents.js +36 -0
- package/lib/docs/observe/toAppErrorFailure.js +61 -0
- package/lib/docs/observe/usesSearchStage.js +45 -0
- package/lib/docs/observe/waitForClientErrorReports.js +31 -0
- package/lib/docs/observe/watchJourneyContext.js +75 -0
- package/lib/docs/observe/withAppErrors.js +31 -0
- package/lib/docs/observe/withAppErrors.test.mjs +101 -0
- package/lib/docs/openJourney.js +94 -0
- package/lib/docs/openJourneyEmail.js +41 -0
- package/lib/docs/openJourneyEmail.test.mjs +187 -0
- package/lib/docs/pageBlocks.js +47 -0
- package/lib/docs/pageBuildStatus.test.mjs +103 -0
- package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
- package/lib/docs/rails/authEngineActions.js +53 -0
- package/lib/docs/rails/authEngineActions.test.mjs +69 -0
- package/lib/docs/rails/authEngineMethods.js +52 -0
- package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
- package/lib/docs/rails/checkAuthActionsRan.js +66 -0
- package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
- package/lib/docs/rails/checkClickRails.js +56 -0
- package/lib/docs/rails/createDataSetRails.js +82 -0
- package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
- package/lib/docs/rails/findExternalBlocks.js +147 -0
- package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
- package/lib/docs/rails/installAuthActionRecorder.js +56 -0
- package/lib/docs/rails/readAuthActionRecorder.js +32 -0
- package/lib/docs/rails/readBuiltArtifact.js +30 -0
- package/lib/docs/readConnectionPlugins.js +26 -0
- package/lib/docs/readDevAuthMode.js +32 -0
- package/lib/docs/readDocEntry.js +40 -0
- package/lib/docs/readDocTitle.js +26 -0
- package/lib/docs/readJourneyEmailMatch.js +57 -0
- package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
- package/lib/docs/readJourneySession.js +56 -0
- package/lib/docs/readMailOutbox.js +37 -0
- package/lib/docs/readPagePath.js +27 -0
- package/lib/docs/readPluginPackages.js +99 -0
- package/lib/docs/readProxyBuildWait.js +38 -0
- package/lib/docs/readRoutineSteps.js +45 -0
- package/lib/docs/registerShownTextEngine.js +118 -0
- package/lib/docs/requestRestart.js +30 -0
- package/lib/docs/requestRestart.test.mjs +49 -0
- package/lib/docs/resolvePageInstance.js +46 -0
- package/lib/docs/resolvePageInstance.test.mjs +81 -0
- package/lib/docs/resolveSource.js +11 -7
- package/lib/docs/resolveSource.test.mjs +1 -1
- package/lib/docs/resolveToolCaller.js +62 -0
- package/lib/docs/reviewPage.js +51 -0
- package/lib/docs/reviewPageBuilds.js +56 -0
- package/lib/docs/reviewPageBuilds.test.mjs +207 -0
- package/lib/docs/runCheck.js +75 -0
- package/lib/docs/runCheck.test.mjs +91 -0
- package/lib/docs/runCheckChild.js +49 -0
- package/lib/docs/runEndpoint.js +185 -0
- package/lib/docs/runEndpoint.test.mjs +509 -0
- package/lib/docs/runErrorBuffers.js +99 -0
- package/lib/docs/runErrorBuffers.test.mjs +95 -0
- package/lib/docs/runJourney.js +347 -0
- package/lib/docs/runJourney.test.mjs +2383 -0
- package/lib/docs/runJourneyAsUsers.js +44 -0
- package/lib/docs/runJourneyDataSet.test.mjs +274 -0
- package/lib/docs/runJourneySteps.js +1320 -0
- package/lib/docs/runRequest.js +100 -59
- package/lib/docs/runRequest.test.mjs +190 -0
- package/lib/docs/saveAnnotatedScreenshot.js +1 -1
- package/lib/docs/scaffoldPage.js +3 -1
- package/lib/docs/screenshotPage.js +163 -11
- package/lib/docs/screenshotPage.test.mjs +263 -1
- package/lib/docs/screenshotPageClip.test.mjs +4 -0
- package/lib/docs/searchDocs.js +82 -30
- package/lib/docs/searchDocs.test.mjs +100 -0
- package/lib/docs/searchIcons.js +90 -0
- package/lib/docs/searchIcons.test.mjs +127 -0
- package/lib/docs/selectExercised.js +39 -0
- package/lib/docs/selectFinalState.js +66 -0
- package/lib/docs/selectFinalState.test.mjs +51 -0
- package/lib/docs/serverErrorStore.js +54 -0
- package/lib/docs/serverErrorStore.test.mjs +50 -0
- package/lib/docs/setupTestFixtures.mjs +133 -39
- package/lib/docs/shownTextSelector.js +28 -0
- package/lib/docs/snapshotState.js +13 -5
- package/lib/docs/splitSearchTerms.js +62 -0
- package/lib/docs/splitSearchTerms.test.mjs +50 -0
- package/lib/docs/stateCheckpoints.test.mjs +77 -0
- package/lib/docs/tabAvailable.js +6 -5
- package/lib/docs/tabChannel.js +127 -21
- package/lib/docs/tabChannel.test.mjs +187 -29
- package/lib/docs/unsettledPageNote.js +24 -0
- package/lib/docs/validateJourneyMail.js +36 -0
- package/lib/docs/validateJourneyTimeout.js +38 -0
- package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
- package/lib/docs/validateRecordingParam.js +42 -0
- package/lib/docs/validateStateSelection.js +33 -0
- package/lib/docs/validateStateSelection.test.mjs +33 -0
- package/lib/docs/validateViewport.js +51 -0
- package/lib/docs/validateViewport.test.mjs +54 -0
- package/lib/docs/waitForBuild.js +50 -0
- package/lib/docs/waitForBuild.test.mjs +75 -0
- package/lib/docs/waitForJourneyEmail.js +77 -0
- package/lib/docs/withBrowserSlot.js +45 -0
- package/lib/docs/withBrowserSlot.test.mjs +75 -0
- package/lib/docs/withDataSession.js +53 -0
- package/lib/server/applyDataSetRedirect.js +83 -0
- package/lib/server/applyDataSetRedirect.test.mjs +340 -0
- package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
- package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
- package/lib/server/auth/createSystemContext.js +73 -0
- package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
- package/lib/server/auth/getHeadlessUser.js +9 -8
- package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
- package/lib/server/auth/getMockUser.js +18 -14
- package/lib/server/auth/{session.js → getStrategies.js} +17 -16
- package/lib/server/auth/headlessUser.js +3 -2
- package/lib/server/auth/journeyActor.js +35 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
- package/lib/server/checkPageRecord.js +44 -0
- package/lib/server/contextMapBudget.js +24 -0
- package/lib/server/contextMapBudget.test.mjs +98 -0
- package/lib/server/continueJitKeys.js +28 -0
- package/lib/server/continueJitKeys.test.mjs +43 -0
- package/lib/server/createDataSessionEndedError.js +33 -0
- package/lib/server/createDataSetRefusal.js +26 -0
- package/lib/server/createLowdefyContext.js +168 -41
- package/lib/server/createLowdefyContext.test.mjs +328 -8
- package/lib/server/getClientAddress.js +51 -0
- package/lib/server/getClientAddress.test.mjs +56 -0
- package/lib/server/getPageJitEnrichment.test.mjs +29 -11
- package/lib/server/hashConfigContent.js +26 -0
- package/lib/server/importAppCode.test.mjs +57 -0
- package/lib/server/isRoutineStep.js +27 -0
- package/lib/server/jitPageBuilder.js +309 -126
- package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
- package/lib/server/jitPageBuilder.test.mjs +218 -0
- package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
- package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
- package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
- package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
- package/lib/server/journeyCookies.js +22 -0
- package/lib/server/journeyCookies.test.mjs +145 -0
- package/lib/server/loadDynamicJsMap.js +47 -0
- package/lib/server/log/createHandleError.js +38 -2
- package/lib/server/log/createHandleError.test.mjs +125 -2
- package/lib/server/log/createLogger.js +3 -0
- package/lib/server/log/createLogger.test.mjs +15 -0
- package/lib/server/log/logRequest.js +2 -3
- package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
- package/lib/server/mutants/applyMutant.js +39 -0
- package/lib/server/mutants/artifactPatterns.js +25 -0
- package/lib/server/mutants/buildAnchor.js +45 -0
- package/lib/server/mutants/collectSharedIds.js +41 -0
- package/lib/server/mutants/configPathSegments.js +38 -0
- package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
- package/lib/server/mutants/describeEvent.js +22 -0
- package/lib/server/mutants/enumerateArtifact.js +56 -0
- package/lib/server/mutants/findNodeByKey.js +45 -0
- package/lib/server/mutants/findNodeByPointer.js +49 -0
- package/lib/server/mutants/getArtifactKind.js +32 -0
- package/lib/server/mutants/groupMutantCopies.js +56 -0
- package/lib/server/mutants/hashArtifact.js +25 -0
- package/lib/server/mutants/hashMutantTarget.js +34 -0
- package/lib/server/mutants/listMutants.js +122 -0
- package/lib/server/mutants/listMutants.test.mjs +502 -0
- package/lib/server/mutants/mutantId.js +30 -0
- package/lib/server/mutants/mutantRuns.js +57 -0
- package/lib/server/mutants/mutantRuns.test.mjs +136 -0
- package/lib/server/mutants/operators/dropAction.js +47 -0
- package/lib/server/mutants/operators/dropBlock.js +44 -0
- package/lib/server/mutants/operators/dropPayload.js +52 -0
- package/lib/server/mutants/operators/dropStep.js +44 -0
- package/lib/server/mutants/operators/flipVisible.js +53 -0
- package/lib/server/mutants/operators/index.js +37 -0
- package/lib/server/mutants/operators/retargetLink.js +72 -0
- package/lib/server/mutants/operators/skipValidate.js +77 -0
- package/lib/server/mutants/operators/swapIf.js +112 -0
- package/lib/server/mutants/operators.test.mjs +396 -0
- package/lib/server/mutants/positionFreePath.js +38 -0
- package/lib/server/mutants/removeFromParent.js +28 -0
- package/lib/server/mutants/test/fixtures.mjs +173 -0
- package/lib/server/mutants/test/keyArtifact.mjs +63 -0
- package/lib/server/mutants/validateMutantParam.js +69 -0
- package/lib/server/mutants/validateMutantsBody.js +88 -0
- package/lib/server/mutants/walkArtifact.js +179 -0
- package/lib/server/pageBuildRecords.js +131 -0
- package/lib/server/pageBuildRecords.test.mjs +258 -0
- package/lib/server/pageCache.mjs +22 -7
- package/lib/server/pageCache.test.mjs +22 -7
- package/lib/server/pageRebuilds.test.mjs +543 -0
- package/lib/server/pruneJitMaps.js +36 -0
- package/lib/server/pruneJitMaps.test.mjs +55 -0
- package/lib/server/readInstanceRecord.js +33 -0
- package/lib/server/readInstanceRecord.test.mjs +99 -0
- package/lib/server/readMergedMaps.js +86 -0
- package/lib/server/readMergedMaps.test.mjs +79 -0
- package/lib/server/recording/appendRecords.js +33 -0
- package/lib/server/recording/errorRecordingStamp.js +34 -0
- package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
- package/lib/server/recording/isRecordingEnabled.js +31 -0
- package/lib/server/recording/readRecordingCookie.js +71 -0
- package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
- package/lib/server/recording/recordingCookiePayload.js +33 -0
- package/lib/server/recording/servedBuilds.js +46 -0
- package/lib/server/recording/servedBuilds.test.mjs +42 -0
- package/lib/server/sinkExternalConnections.js +95 -0
- package/lib/server/sinkExternalConnections.test.mjs +135 -0
- package/lib/server/skipStaleMapWrites.js +62 -0
- package/lib/server/skipStaleMapWrites.test.mjs +113 -0
- package/lib/server/startIdleGc.js +67 -0
- package/lib/server/startIdleGc.test.mjs +138 -0
- package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
- package/lib/server/trackSessionWork.js +30 -0
- package/lib/vite/createDevServerExclude.js +40 -0
- package/lib/vite/createDevServerExclude.test.mjs +79 -0
- package/manager/getContext.mjs +24 -2
- package/manager/processes/initialBuild.mjs +14 -0
- package/manager/processes/initialBuild.test.mjs +101 -0
- package/manager/processes/lowdefyBuild.mjs +30 -7
- package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
- package/manager/processes/lowdefyBuild.test.mjs +136 -1
- package/manager/processes/optimizeDependencies.mjs +59 -0
- package/manager/processes/restartServer.mjs +34 -6
- package/manager/processes/restartServer.test.mjs +162 -0
- package/manager/processes/shutdownServer.mjs +11 -0
- package/manager/processes/shutdownServer.test.mjs +45 -0
- package/manager/processes/startFirstServer.mjs +46 -0
- package/manager/processes/startFirstServer.test.mjs +326 -0
- package/manager/processes/startMailSink.mjs +135 -0
- package/manager/processes/startMailSink.test.mjs +146 -0
- package/manager/processes/startProxy.mjs +129 -12
- package/manager/processes/startProxy.test.mjs +366 -0
- package/manager/processes/startServer.mjs +47 -6
- package/manager/processes/startServer.test.mjs +155 -0
- package/manager/processes/startWatchers.mjs +6 -4
- package/manager/processes/syncServer.mjs +69 -0
- package/manager/processes/syncServer.test.mjs +141 -0
- package/manager/processes/waitForDevServer.mjs +48 -0
- package/manager/processes/waitForDevServer.test.mjs +112 -0
- package/manager/run.mjs +128 -43
- package/manager/utils/BatchChanges.mjs +21 -5
- package/manager/utils/BatchChanges.test.mjs +77 -0
- package/manager/utils/acquireDevInstance.mjs +99 -0
- package/manager/utils/acquireDevInstance.test.mjs +160 -0
- package/manager/utils/createBuildActivity.mjs +67 -0
- package/manager/utils/createBuildActivity.test.mjs +58 -0
- package/manager/utils/createChangeSignal.mjs +39 -0
- package/manager/utils/createChangeSignal.test.mjs +62 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
- package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
- package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
- package/manager/utils/createDotPathIgnore.mjs +44 -0
- package/manager/utils/createDotPathIgnore.test.mjs +65 -0
- package/manager/utils/createIdleGc.mjs +51 -0
- package/manager/utils/createIdleGc.test.mjs +77 -0
- package/manager/utils/createRequestActivity.mjs +100 -0
- package/manager/utils/createRequestActivity.test.mjs +121 -0
- package/manager/utils/createServerArtifactTracker.mjs +83 -0
- package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
- package/manager/utils/createServerEnv.mjs +39 -0
- package/manager/utils/filesHaveSameBytes.mjs +41 -0
- package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
- package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
- package/manager/utils/importFresh.mjs +46 -0
- package/manager/utils/importFresh.test.mjs +72 -0
- package/manager/utils/importFreshWorker.mjs +20 -0
- package/manager/utils/killTaggedBrowser.mjs +35 -0
- package/manager/utils/pagePathChanged.mjs +56 -0
- package/manager/utils/pruneRecordings.mjs +85 -0
- package/manager/utils/pruneRecordings.test.mjs +117 -0
- package/manager/utils/publishBuildDirectory.mjs +121 -0
- package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
- package/manager/utils/publishBuildDirectory.test.mjs +262 -0
- package/manager/utils/readBasePath.mjs +27 -0
- package/manager/utils/readBasePath.test.mjs +43 -0
- package/manager/utils/readBuildStatusWait.mjs +67 -0
- package/manager/utils/readManagerVersion.mjs +30 -0
- package/manager/utils/readManagerVersion.test.mjs +35 -0
- package/manager/utils/readPluginDefinitions.mjs +36 -0
- package/manager/utils/resolveDevAuthUrl.mjs +58 -0
- package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
- package/manager/utils/resolvePorts.mjs +49 -0
- package/manager/utils/setupWatcher.mjs +19 -11
- package/manager/utils/startRecordingPruner.mjs +45 -0
- package/manager/utils/startRecordingPruner.test.mjs +66 -0
- package/manager/utils/waitForChildExit.mjs +53 -0
- package/manager/utils/waitForChildExit.test.mjs +70 -0
- package/manager/utils/waitForServer.mjs +44 -0
- package/manager/utils/writeBuildStatus.mjs +1 -1
- package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
- package/manager/watchers/envWatcher.mjs +9 -2
- package/manager/watchers/envWatcher.test.mjs +87 -0
- package/manager/watchers/listServerSidePackages.mjs +47 -0
- package/manager/watchers/listServerSidePackages.test.mjs +38 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
- package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
- package/manager/watchers/pluginSourceWatcher.mjs +79 -0
- package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
- package/manager/watchers/restartRequestWatcher.mjs +71 -0
- package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
- package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
- package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
- package/manager/watchers/serverPackageWatcher.mjs +38 -0
- package/package.json +60 -47
- package/package.original.json +60 -47
- package/src/app.js +131 -20
- package/src/html/renderDevPage.js +63 -11
- package/src/html/renderDevPage.test.mjs +138 -0
- package/src/lib/getRequestPath.js +29 -0
- package/src/middleware/createSameOriginGuard.js +37 -0
- package/src/middleware/createSameOriginGuard.test.mjs +102 -0
- package/src/middleware/errorHandler.js +51 -1
- package/src/middleware/errorHandler.test.mjs +173 -3
- package/src/middleware/isRebindingSafeHost.js +44 -0
- package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
- package/src/middleware/localDevToolsOnly.js +38 -0
- package/src/middleware/localDevToolsOnly.test.mjs +61 -0
- package/src/middleware/noStore.js +27 -0
- package/src/middleware/noStore.test.mjs +48 -0
- package/src/middleware/requestTimeout.js +38 -0
- package/src/middleware/staleFlag.js +76 -0
- package/src/middleware/staleFlag.test.mjs +127 -0
- package/src/middleware/stampCaller.js +38 -0
- package/src/middleware/stampCaller.test.mjs +64 -0
- package/src/routes/agent.js +15 -3
- package/src/routes/auth.js +29 -20
- package/src/routes/auth.test.mjs +30 -56
- package/src/routes/clientError.js +13 -13
- package/src/routes/cron.js +3 -1
- package/src/routes/cronForward.js +3 -1
- package/src/routes/detached.js +22 -9
- package/src/routes/devInspect.js +17 -8
- package/src/routes/devInspect.test.mjs +44 -7
- package/src/routes/devRecording.js +113 -0
- package/src/routes/devRecording.test.mjs +253 -0
- package/src/routes/docs/buildStatus.js +10 -2
- package/src/routes/docs/buildStatus.test.mjs +46 -0
- package/src/routes/docs/content.js +5 -3
- package/src/routes/docs/evalOperator.js +15 -1
- package/src/routes/docs/events.js +50 -0
- package/src/routes/docs/events.test.mjs +117 -0
- package/src/routes/docs/find.js +9 -1
- package/src/routes/docs/icons.js +30 -0
- package/src/routes/docs/inspectState.js +16 -1
- package/src/routes/docs/journey.js +194 -0
- package/src/routes/docs/journey.test.mjs +401 -0
- package/src/routes/docs/journeyRefusals.test.mjs +71 -0
- package/src/routes/docs/loadState.js +4 -2
- package/src/routes/docs/mcp.js +34 -3
- package/src/routes/docs/mcp.test.mjs +130 -0
- package/src/routes/docs/mutants.js +78 -0
- package/src/routes/docs/mutants.test.mjs +180 -0
- package/src/routes/docs/parsePathParamsParam.js +41 -0
- package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
- package/src/routes/docs/parseUserParam.js +27 -21
- package/src/routes/docs/parseUserParam.test.mjs +17 -5
- package/src/routes/docs/restart.js +35 -0
- package/src/routes/docs/restart.test.mjs +64 -0
- package/src/routes/docs/runEndpoint.js +54 -0
- package/src/routes/docs/runEndpoint.test.mjs +165 -0
- package/src/routes/docs/runRequest.js +25 -8
- package/src/routes/docs/runRequest.test.mjs +92 -0
- package/src/routes/docs/screenshot.js +50 -2
- package/src/routes/docs/screenshot.test.mjs +117 -0
- package/src/routes/docs/search.js +15 -2
- package/src/routes/docs/snapshotState.js +17 -3
- package/src/routes/endpoints.js +27 -26
- package/src/routes/endpointsTextAnswer.test.mjs +77 -0
- package/src/routes/feedback.js +10 -13
- package/src/routes/jitPage.js +99 -35
- package/src/routes/jitPage.test.mjs +248 -0
- package/src/routes/mcp.js +82 -5
- package/src/routes/mcp.test.mjs +215 -0
- package/src/routes/mcpMemberToken.test.mjs +125 -0
- package/src/routes/mcpProtectedResourceMetadata.js +34 -0
- package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
- package/src/routes/mountDevPageRoutes.js +25 -0
- package/src/routes/mountDevPageRoutes.test.mjs +30 -0
- package/src/routes/mountOauthDiscovery.js +44 -0
- package/src/routes/mountOauthDiscovery.test.mjs +79 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
- package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
- package/src/routes/reload.js +16 -0
- package/src/routes/request.js +6 -4
- package/src/routes/request.test.mjs +9 -5
- package/src/routes/requestBody.test.mjs +159 -0
- package/src/routes/usage.js +6 -2
- package/src/routes/user.js +27 -0
- package/src/routes/websocket.js +29 -8
- package/src/routes/websocket.test.mjs +65 -0
- package/src/routes/wellKnownFallback.js +30 -0
- package/src/routes/wellKnownFallback.test.mjs +32 -0
- package/src/websocket/devWebSocket.js +41 -26
- package/src/websocket/devWebSocket.test.mjs +157 -0
- package/vite.config.js +53 -23
- package/lib/server/auth/getDevSession.js +0 -70
- package/lib/server/auth/getDevSession.test.mjs +0 -138
- package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
- package/manager/watchers/moduleBuildWatcher.mjs +0 -79
- package/manager/watchers/serverArtifactWatcher.mjs +0 -120
- 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
|
+
});
|
package/src/routes/feedback.js
CHANGED
|
@@ -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
|
-
//
|
|
30
|
-
//
|
|
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
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
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.
|
|
41
|
+
const batch = parseRequestBody({ text: await c.req.text() });
|
|
45
42
|
if (!Array.isArray(batch?.annotations)) {
|
|
46
43
|
return c.json(
|
|
47
44
|
{
|
package/src/routes/jitPage.js
CHANGED
|
@@ -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
|
|
20
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
71
|
-
if (
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
26
|
-
if (
|
|
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);
|