@lowdefy/server-dev 6.0.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 +63 -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 +224 -39
- 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/getAuth.js +46 -0
- 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/{getAuthConfig.js → getStrategies.js} +12 -10
- 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 +174 -41
- package/lib/server/createLowdefyContext.test.mjs +437 -0
- 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 +77 -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/scrubSecrets.js +23 -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 +61 -49
- package/package.original.json +61 -49
- 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 +196 -14
- 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 +4 -2
- 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/lib/server/auth/session.js +0 -39
- 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,215 @@
|
|
|
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
|
+
// The real URI helpers drive the challenge strings, so the header assertions
|
|
21
|
+
// below exercise the actual BETTER_AUTH_URL derivation - only createMcpServer
|
|
22
|
+
// is stubbed out.
|
|
23
|
+
const uriHelpers = await import('@lowdefy/api/routes/mcp/getMcpUri.js');
|
|
24
|
+
|
|
25
|
+
const mockCreateMcpServer = jest.fn();
|
|
26
|
+
jest.unstable_mockModule('@lowdefy/api', () => ({
|
|
27
|
+
...uriHelpers,
|
|
28
|
+
createMcpServer: mockCreateMcpServer,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.unstable_mockModule('@hono/mcp', () => ({
|
|
32
|
+
StreamableHTTPTransport: class {
|
|
33
|
+
handleRequest() {
|
|
34
|
+
return new Response(JSON.stringify({ served: true }), {
|
|
35
|
+
status: 200,
|
|
36
|
+
headers: { 'content-type': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const mockAuthJson = { configured: true, oauthProvider: { consentPage: '/oauth/consent' } };
|
|
43
|
+
jest.unstable_mockModule('../../lib/build/auth.js', () => ({
|
|
44
|
+
default: mockAuthJson,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const { default: mcpHandler } = await import('./mcp.js');
|
|
48
|
+
|
|
49
|
+
const originalBetterAuthUrl = process.env.BETTER_AUTH_URL;
|
|
50
|
+
|
|
51
|
+
const mockReadConfigFile = jest.fn();
|
|
52
|
+
|
|
53
|
+
let contextOverrides;
|
|
54
|
+
|
|
55
|
+
function createApp() {
|
|
56
|
+
const app = new Hono();
|
|
57
|
+
app.use('*', async (c, next) => {
|
|
58
|
+
c.set('lowdefyContext', {
|
|
59
|
+
config: { basePath: '' },
|
|
60
|
+
logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
61
|
+
readConfigFile: mockReadConfigFile,
|
|
62
|
+
...contextOverrides,
|
|
63
|
+
});
|
|
64
|
+
await next();
|
|
65
|
+
});
|
|
66
|
+
app.all('/api/mcp', mcpHandler);
|
|
67
|
+
return app;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
process.env.BETTER_AUTH_URL = 'https://app.test.com';
|
|
72
|
+
mockAuthJson.oauthProvider = { consentPage: '/oauth/consent' };
|
|
73
|
+
contextOverrides = {};
|
|
74
|
+
mockReadConfigFile.mockReset();
|
|
75
|
+
mockCreateMcpServer.mockReset();
|
|
76
|
+
mockCreateMcpServer.mockResolvedValue({ connect: jest.fn() });
|
|
77
|
+
mockReadConfigFile.mockResolvedValue({ configured: true, hasPublicTool: true, endpoints: [] });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(() => {
|
|
81
|
+
if (originalBetterAuthUrl === undefined) {
|
|
82
|
+
delete process.env.BETTER_AUTH_URL;
|
|
83
|
+
} else {
|
|
84
|
+
process.env.BETTER_AUTH_URL = originalBetterAuthUrl;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('mcpHandler returns 404 when mcp is not configured', async () => {
|
|
89
|
+
mockReadConfigFile.mockResolvedValue({ configured: false });
|
|
90
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
91
|
+
expect(res.status).toEqual(404);
|
|
92
|
+
expect(await res.json()).toEqual({ error: 'MCP is not configured.' });
|
|
93
|
+
expect(mockCreateMcpServer).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('mcpHandler challenges an invalid token with the resource metadata pointer', async () => {
|
|
97
|
+
contextOverrides.mcpAuth = { tokenStatus: 'invalid', parseableJwt: true };
|
|
98
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
99
|
+
expect(res.status).toEqual(401);
|
|
100
|
+
expect(res.headers.get('WWW-Authenticate')).toEqual(
|
|
101
|
+
'Bearer resource_metadata="https://app.test.com/.well-known/oauth-protected-resource/api/mcp"'
|
|
102
|
+
);
|
|
103
|
+
expect(mockCreateMcpServer).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('mcpHandler extends the challenge with invalid_token when the bearer is not a JWT', async () => {
|
|
107
|
+
contextOverrides.mcpAuth = { tokenStatus: 'invalid', parseableJwt: false };
|
|
108
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
109
|
+
expect(res.status).toEqual(401);
|
|
110
|
+
expect(res.headers.get('WWW-Authenticate')).toEqual(
|
|
111
|
+
'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="https://app.test.com/.well-known/oauth-protected-resource/api/mcp"'
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('mcpHandler tells a revoked grant to reconnect and choose an organization', async () => {
|
|
116
|
+
contextOverrides.mcpAuth = {
|
|
117
|
+
clientId: 'client_1',
|
|
118
|
+
organizationId: 'org_1',
|
|
119
|
+
tokenStatus: 'invalid',
|
|
120
|
+
parseableJwt: true,
|
|
121
|
+
revoked: true,
|
|
122
|
+
};
|
|
123
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
124
|
+
expect(res.status).toEqual(401);
|
|
125
|
+
expect(res.headers.get('WWW-Authenticate')).toEqual(
|
|
126
|
+
'Bearer error="invalid_token", error_description="This connection was disconnected. Reconnect to choose the organization to work in.", resource_metadata="https://app.test.com/.well-known/oauth-protected-resource/api/mcp"'
|
|
127
|
+
);
|
|
128
|
+
expect(mockCreateMcpServer).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('mcpHandler gives a token without membership the same challenge as an invalid token', async () => {
|
|
132
|
+
contextOverrides.mcpAuth = {
|
|
133
|
+
clientId: 'client_1',
|
|
134
|
+
organizationId: 'org_1',
|
|
135
|
+
tokenStatus: 'invalid',
|
|
136
|
+
parseableJwt: true,
|
|
137
|
+
noMembership: true,
|
|
138
|
+
};
|
|
139
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
140
|
+
expect(res.status).toEqual(401);
|
|
141
|
+
expect(res.headers.get('WWW-Authenticate')).toEqual(
|
|
142
|
+
'Bearer resource_metadata="https://app.test.com/.well-known/oauth-protected-resource/api/mcp"'
|
|
143
|
+
);
|
|
144
|
+
expect(mockCreateMcpServer).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('mcpHandler challenges an anonymous request when the surface has no public tool', async () => {
|
|
148
|
+
mockReadConfigFile.mockResolvedValue({ configured: true, hasPublicTool: false, endpoints: [] });
|
|
149
|
+
contextOverrides.mcpAuth = { tokenStatus: 'none', parseableJwt: true };
|
|
150
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
151
|
+
expect(res.status).toEqual(401);
|
|
152
|
+
expect(res.headers.get('WWW-Authenticate')).toEqual(
|
|
153
|
+
'Bearer resource_metadata="https://app.test.com/.well-known/oauth-protected-resource/api/mcp"'
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('mcpHandler serves an anonymous request when the surface has a public tool', async () => {
|
|
158
|
+
contextOverrides.mcpAuth = { tokenStatus: 'none', parseableJwt: true };
|
|
159
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
160
|
+
expect(res.status).toEqual(200);
|
|
161
|
+
expect(await res.json()).toEqual({ served: true });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('mcpHandler serves a valid-token request and never challenges past the boundary', async () => {
|
|
165
|
+
mockReadConfigFile.mockResolvedValue({ configured: true, hasPublicTool: false, endpoints: [] });
|
|
166
|
+
contextOverrides.mcpAuth = {
|
|
167
|
+
tokenStatus: 'valid',
|
|
168
|
+
parseableJwt: true,
|
|
169
|
+
grantedScopes: ['mcp:read'],
|
|
170
|
+
};
|
|
171
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
172
|
+
expect(res.status).toEqual(200);
|
|
173
|
+
expect(await res.json()).toEqual({ served: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('mcpHandler refuses a foreign Origin with a 403', async () => {
|
|
177
|
+
contextOverrides.mcpAuth = { tokenStatus: 'none', parseableJwt: true };
|
|
178
|
+
const res = await createApp().request('/api/mcp', {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: { Origin: 'https://evil.test' },
|
|
181
|
+
});
|
|
182
|
+
expect(res.status).toEqual(403);
|
|
183
|
+
expect(await res.json()).toEqual({ error: 'Origin not allowed.' });
|
|
184
|
+
expect(mockCreateMcpServer).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('mcpHandler accepts the pinned canonical origin', async () => {
|
|
188
|
+
contextOverrides.mcpAuth = { tokenStatus: 'none', parseableJwt: true };
|
|
189
|
+
const res = await createApp().request('/api/mcp', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { Origin: 'https://app.test.com' },
|
|
192
|
+
});
|
|
193
|
+
expect(res.status).toEqual(200);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('mcpHandler accepts an origin registered on BetterAuth trustedOrigins', async () => {
|
|
197
|
+
contextOverrides.mcpAuth = { tokenStatus: 'none', parseableJwt: true };
|
|
198
|
+
contextOverrides.auth = { options: { trustedOrigins: ['https://studio.test.com'] } };
|
|
199
|
+
const res = await createApp().request('/api/mcp', {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { Origin: 'https://studio.test.com' },
|
|
202
|
+
});
|
|
203
|
+
expect(res.status).toEqual(200);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('mcpHandler serves openly with no envelope when oauthProvider is not configured', async () => {
|
|
207
|
+
delete mockAuthJson.oauthProvider;
|
|
208
|
+
contextOverrides.mcpAuth = { tokenStatus: 'invalid', parseableJwt: false };
|
|
209
|
+
const res = await createApp().request('/api/mcp', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { Origin: 'https://evil.test' },
|
|
212
|
+
});
|
|
213
|
+
expect(res.status).toEqual(200);
|
|
214
|
+
expect(await res.json()).toEqual({ served: true });
|
|
215
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
// The real URI helpers drive the challenge strings, so the header assertions
|
|
21
|
+
// below exercise the actual BETTER_AUTH_URL derivation - only createMcpServer
|
|
22
|
+
// is stubbed out.
|
|
23
|
+
const uriHelpers = await import('@lowdefy/api/routes/mcp/getMcpUri.js');
|
|
24
|
+
|
|
25
|
+
const mockCreateMcpServer = jest.fn();
|
|
26
|
+
jest.unstable_mockModule('@lowdefy/api', () => ({
|
|
27
|
+
...uriHelpers,
|
|
28
|
+
createMcpServer: mockCreateMcpServer,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.unstable_mockModule('@hono/mcp', () => ({
|
|
32
|
+
StreamableHTTPTransport: class {
|
|
33
|
+
handleRequest() {
|
|
34
|
+
return new Response(JSON.stringify({ served: true }), {
|
|
35
|
+
status: 200,
|
|
36
|
+
headers: { 'content-type': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const mockAuthJson = { configured: true, oauthProvider: { consentPage: '/oauth/consent' } };
|
|
43
|
+
jest.unstable_mockModule('../../lib/build/auth.js', () => ({
|
|
44
|
+
default: mockAuthJson,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const { default: mcpHandler } = await import('./mcp.js');
|
|
48
|
+
|
|
49
|
+
const originalBetterAuthUrl = process.env.BETTER_AUTH_URL;
|
|
50
|
+
|
|
51
|
+
const mockReadConfigFile = jest.fn();
|
|
52
|
+
|
|
53
|
+
let contextOverrides;
|
|
54
|
+
|
|
55
|
+
function createApp() {
|
|
56
|
+
const app = new Hono();
|
|
57
|
+
app.use('*', async (c, next) => {
|
|
58
|
+
c.set('lowdefyContext', {
|
|
59
|
+
config: { basePath: '' },
|
|
60
|
+
logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
61
|
+
readConfigFile: mockReadConfigFile,
|
|
62
|
+
...contextOverrides,
|
|
63
|
+
});
|
|
64
|
+
await next();
|
|
65
|
+
});
|
|
66
|
+
app.all('/api/mcp', mcpHandler);
|
|
67
|
+
return app;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
process.env.BETTER_AUTH_URL = 'https://app.test.com';
|
|
72
|
+
mockAuthJson.oauthProvider = { consentPage: '/oauth/consent' };
|
|
73
|
+
contextOverrides = {};
|
|
74
|
+
mockReadConfigFile.mockReset();
|
|
75
|
+
mockCreateMcpServer.mockReset();
|
|
76
|
+
mockCreateMcpServer.mockResolvedValue({ connect: jest.fn() });
|
|
77
|
+
mockReadConfigFile.mockResolvedValue({ configured: true, hasPublicTool: true, endpoints: [] });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(() => {
|
|
81
|
+
if (originalBetterAuthUrl === undefined) {
|
|
82
|
+
delete process.env.BETTER_AUTH_URL;
|
|
83
|
+
} else {
|
|
84
|
+
process.env.BETTER_AUTH_URL = originalBetterAuthUrl;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('mcpHandler answers a refused member token with its reason and no resource metadata', async () => {
|
|
89
|
+
contextOverrides.mcpAuth = {
|
|
90
|
+
tokenStatus: 'invalid',
|
|
91
|
+
memberToken: true,
|
|
92
|
+
description: 'This token has expired.',
|
|
93
|
+
};
|
|
94
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
95
|
+
expect(res.status).toEqual(401);
|
|
96
|
+
expect(res.headers.get('WWW-Authenticate')).toEqual(
|
|
97
|
+
'Bearer error="invalid_token", error_description="This token has expired."'
|
|
98
|
+
);
|
|
99
|
+
expect(mockCreateMcpServer).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('mcpHandler answers a member token whose member is gone with that reason', async () => {
|
|
103
|
+
contextOverrides.mcpAuth = {
|
|
104
|
+
tokenStatus: 'invalid',
|
|
105
|
+
memberToken: true,
|
|
106
|
+
description: "This token's member can no longer use it.",
|
|
107
|
+
};
|
|
108
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
109
|
+
expect(res.status).toEqual(401);
|
|
110
|
+
expect(res.headers.get('WWW-Authenticate')).toEqual(
|
|
111
|
+
'Bearer error="invalid_token", error_description="This token\'s member can no longer use it."'
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('mcpHandler serves a valid member token caller', async () => {
|
|
116
|
+
contextOverrides.mcpAuth = {
|
|
117
|
+
tokenStatus: 'valid',
|
|
118
|
+
tokenId: 'token_1',
|
|
119
|
+
organizationId: 'org_1',
|
|
120
|
+
grantedScopes: ['mcp:read', 'mcp:write'],
|
|
121
|
+
};
|
|
122
|
+
const res = await createApp().request('/api/mcp', { method: 'POST' });
|
|
123
|
+
expect(res.status).toEqual(200);
|
|
124
|
+
expect(mockCreateMcpServer).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { getAsIssuer, getMcpResourceUri, MCP_OAUTH_SCOPES } from '@lowdefy/api';
|
|
18
|
+
|
|
19
|
+
import lowdefyConfig from '../../lib/build/config.js';
|
|
20
|
+
|
|
21
|
+
// RFC 9728 protected-resource metadata for the MCP endpoint. Constant per
|
|
22
|
+
// deployment: the URIs derive from the pinned BETTER_AUTH_URL, never a Host
|
|
23
|
+
// header. The organization a token acts in is not part of the resource - it is
|
|
24
|
+
// chosen at authorization and carried as a token claim.
|
|
25
|
+
function mcpProtectedResourceMetadataHandler(c) {
|
|
26
|
+
return c.json({
|
|
27
|
+
resource: getMcpResourceUri({ config: lowdefyConfig }),
|
|
28
|
+
authorization_servers: [getAsIssuer({ config: lowdefyConfig })],
|
|
29
|
+
scopes_supported: MCP_OAUTH_SCOPES,
|
|
30
|
+
bearer_methods_supported: ['header'],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default mcpProtectedResourceMetadataHandler;
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
jest.unstable_mockModule('../../lib/build/config.js', () => ({
|
|
21
|
+
default: { basePath: '' },
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const { default: mcpProtectedResourceMetadataHandler } = await import(
|
|
25
|
+
'./mcpProtectedResourceMetadata.js'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const originalBetterAuthUrl = process.env.BETTER_AUTH_URL;
|
|
29
|
+
|
|
30
|
+
function createApp() {
|
|
31
|
+
const app = new Hono();
|
|
32
|
+
app.get('/.well-known/oauth-protected-resource/api/mcp', mcpProtectedResourceMetadataHandler);
|
|
33
|
+
return app;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
process.env.BETTER_AUTH_URL = 'https://app.test.com';
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
if (originalBetterAuthUrl === undefined) {
|
|
42
|
+
delete process.env.BETTER_AUTH_URL;
|
|
43
|
+
} else {
|
|
44
|
+
process.env.BETTER_AUTH_URL = originalBetterAuthUrl;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('metadata handler serves the single MCP resource document', async () => {
|
|
49
|
+
const res = await createApp().request('/.well-known/oauth-protected-resource/api/mcp');
|
|
50
|
+
expect(res.status).toEqual(200);
|
|
51
|
+
expect(await res.json()).toEqual({
|
|
52
|
+
resource: 'https://app.test.com/api/mcp',
|
|
53
|
+
authorization_servers: ['https://app.test.com/api/auth'],
|
|
54
|
+
scopes_supported: ['mcp:read', 'mcp:write', 'offline_access'],
|
|
55
|
+
bearer_methods_supported: ['header'],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('metadata handler derives the resource from BETTER_AUTH_URL, never the request host', async () => {
|
|
60
|
+
const res = await createApp().request(
|
|
61
|
+
'http://evil.example.com/.well-known/oauth-protected-resource/api/mcp'
|
|
62
|
+
);
|
|
63
|
+
expect(res.status).toEqual(200);
|
|
64
|
+
expect((await res.json()).resource).toEqual('https://app.test.com/api/mcp');
|
|
65
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
// Every page path renders the same dev shell; the client reads the page from
|
|
18
|
+
// the URL. `.*`, not `.+`: under a basePath, `<basePath>/` arrives with an
|
|
19
|
+
// empty rest, and is the app root like `<basePath>` itself.
|
|
20
|
+
function mountDevPageRoutes({ app, renderPage }) {
|
|
21
|
+
app.get('/', renderPage);
|
|
22
|
+
app.get('/:rest{.*}', renderPage);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default mountDevPageRoutes;
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
|
|
19
|
+
import mountDevPageRoutes from './mountDevPageRoutes.js';
|
|
20
|
+
|
|
21
|
+
test.each([
|
|
22
|
+
['/app', '/app'],
|
|
23
|
+
['/app', '/app/'],
|
|
24
|
+
['/app', '/app/invoices'],
|
|
25
|
+
])('with basePath "%s", %s renders the dev shell', async (basePath, path) => {
|
|
26
|
+
const app = basePath === '' ? new Hono() : new Hono().basePath(basePath);
|
|
27
|
+
mountDevPageRoutes({ app, renderPage: (c) => c.text('shell') });
|
|
28
|
+
const res = await app.request(path);
|
|
29
|
+
expect(await res.text()).toEqual('shell');
|
|
30
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { createAsMetadataHandler } from '@lowdefy/api';
|
|
18
|
+
import { type } from '@lowdefy/helpers';
|
|
19
|
+
|
|
20
|
+
import authJson from '../../lib/build/auth.js';
|
|
21
|
+
import mcpProtectedResourceMetadataHandler from './mcpProtectedResourceMetadata.js';
|
|
22
|
+
|
|
23
|
+
// OAuth discovery for the MCP resource. Both documents are public,
|
|
24
|
+
// unauthenticated and constant per deployment, so they mount outside the api
|
|
25
|
+
// context. Nothing mounts when the app is not an authorization server - the
|
|
26
|
+
// build then guaranteed an all-public tool surface with nothing to discover.
|
|
27
|
+
function mountOauthDiscovery({ app, auth }) {
|
|
28
|
+
if (type.isNone(authJson.oauthProvider)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
app.get('/.well-known/oauth-protected-resource/api/mcp', mcpProtectedResourceMetadataHandler);
|
|
32
|
+
// The auth instance is absent only in dev mock mode, which runs no auth
|
|
33
|
+
// engine - the static resource document above still serves there.
|
|
34
|
+
if (type.isNone(auth)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// The oauth-provider serves its AS metadata relative to the auth mount, so
|
|
38
|
+
// the RFC 8414 path-inserted location a client derives from the issuer must
|
|
39
|
+
// be mounted here, re-exposing the same live document.
|
|
40
|
+
const asMetadataHandler = createAsMetadataHandler({ auth });
|
|
41
|
+
app.get('/.well-known/oauth-authorization-server/api/auth', (c) => asMetadataHandler(c.req.raw));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default mountOauthDiscovery;
|
|
@@ -0,0 +1,79 @@
|
|
|
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 mockCreateAsMetadataHandler = jest.fn(
|
|
21
|
+
() => async () =>
|
|
22
|
+
new Response(JSON.stringify({ issuer: 'https://app.test.com/api/auth' }), {
|
|
23
|
+
status: 200,
|
|
24
|
+
headers: { 'content-type': 'application/json' },
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
jest.unstable_mockModule('@lowdefy/api', () => ({
|
|
28
|
+
createAsMetadataHandler: mockCreateAsMetadataHandler,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.unstable_mockModule('./mcpProtectedResourceMetadata.js', () => ({
|
|
32
|
+
default: (c) => c.json({ resource: 'mcp' }),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const mockAuthJson = { configured: true };
|
|
36
|
+
jest.unstable_mockModule('../../lib/build/auth.js', () => ({
|
|
37
|
+
default: mockAuthJson,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const { default: mountOauthDiscovery } = await import('./mountOauthDiscovery.js');
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
delete mockAuthJson.oauthProvider;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('mountOauthDiscovery mounts nothing when oauthProvider is not configured', async () => {
|
|
47
|
+
const app = new Hono();
|
|
48
|
+
mountOauthDiscovery({ app, auth: { api: {} } });
|
|
49
|
+
const resourceRes = await app.request('/.well-known/oauth-protected-resource/api/mcp');
|
|
50
|
+
expect(resourceRes.status).toEqual(404);
|
|
51
|
+
const asRes = await app.request('/.well-known/oauth-authorization-server/api/auth');
|
|
52
|
+
expect(asRes.status).toEqual(404);
|
|
53
|
+
expect(mockCreateAsMetadataHandler).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('mountOauthDiscovery mounts both discovery documents when oauthProvider is configured', async () => {
|
|
57
|
+
mockAuthJson.oauthProvider = { consentPage: '/oauth/consent' };
|
|
58
|
+
const app = new Hono();
|
|
59
|
+
const auth = { api: {} };
|
|
60
|
+
mountOauthDiscovery({ app, auth });
|
|
61
|
+
const resourceRes = await app.request('/.well-known/oauth-protected-resource/api/mcp');
|
|
62
|
+
expect(resourceRes.status).toEqual(200);
|
|
63
|
+
expect(await resourceRes.json()).toEqual({ resource: 'mcp' });
|
|
64
|
+
const asRes = await app.request('/.well-known/oauth-authorization-server/api/auth');
|
|
65
|
+
expect(asRes.status).toEqual(200);
|
|
66
|
+
expect(await asRes.json()).toEqual({ issuer: 'https://app.test.com/api/auth' });
|
|
67
|
+
expect(mockCreateAsMetadataHandler).toHaveBeenCalledWith({ auth });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('mountOauthDiscovery serves only the static resource document without an auth instance', async () => {
|
|
71
|
+
mockAuthJson.oauthProvider = { consentPage: '/oauth/consent' };
|
|
72
|
+
const app = new Hono();
|
|
73
|
+
mountOauthDiscovery({ app, auth: null });
|
|
74
|
+
const resourceRes = await app.request('/.well-known/oauth-protected-resource/api/mcp');
|
|
75
|
+
expect(resourceRes.status).toEqual(200);
|
|
76
|
+
const asRes = await app.request('/.well-known/oauth-authorization-server/api/auth');
|
|
77
|
+
expect(asRes.status).toEqual(404);
|
|
78
|
+
expect(mockCreateAsMetadataHandler).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
// Claude Code merges the protocol version it negotiated (mcp-protocol-version)
|
|
18
|
+
// with a static MCP-Protocol-Version it sets on every request, and the Fetch
|
|
19
|
+
// Headers constructor folds the two spellings of one header into a single
|
|
20
|
+
// comma-joined value: "2025-11-25, 2025-11-25". @hono/mcp compares that raw
|
|
21
|
+
// string against its supported list, so a client that agrees with the server
|
|
22
|
+
// twice over is answered 404 "Unsupported protocol version" - which Claude Code
|
|
23
|
+
// shows as "MCP endpoint not found". Collapsing a repeated value to the one
|
|
24
|
+
// version it names is lossless; a header naming two DIFFERENT versions is left
|
|
25
|
+
// for the transport to refuse. Runs before the transport reads the header: the
|
|
26
|
+
// transport reads c.req.header(), which reads the raw request headers live.
|
|
27
|
+
function normalizeMcpProtocolVersionHeader({ request }) {
|
|
28
|
+
const value = request.headers.get('mcp-protocol-version');
|
|
29
|
+
if (value === null || !value.includes(',')) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const versions = [
|
|
33
|
+
...new Set(
|
|
34
|
+
value
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((version) => version.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
),
|
|
39
|
+
];
|
|
40
|
+
if (versions.length !== 1) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
request.headers.set('mcp-protocol-version', versions[0]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default normalizeMcpProtocolVersionHeader;
|