@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,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Hono } from 'hono';
|
|
18
|
+
|
|
19
|
+
import normalizeMcpProtocolVersionHeader from './normalizeMcpProtocolVersionHeader.js';
|
|
20
|
+
|
|
21
|
+
function createApp() {
|
|
22
|
+
const app = new Hono();
|
|
23
|
+
app.post('/api/mcp', (c) => {
|
|
24
|
+
normalizeMcpProtocolVersionHeader({ request: c.req.raw });
|
|
25
|
+
return c.json({ protocolVersion: c.req.header('mcp-protocol-version') ?? null });
|
|
26
|
+
});
|
|
27
|
+
return app;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function send(headerValue) {
|
|
31
|
+
const headers = new Headers();
|
|
32
|
+
if (headerValue !== undefined) {
|
|
33
|
+
headers.set('mcp-protocol-version', headerValue);
|
|
34
|
+
}
|
|
35
|
+
const res = await createApp().request('/api/mcp', { method: 'POST', headers });
|
|
36
|
+
return (await res.json()).protocolVersion;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('collapses a repeated protocol version to the single version it names', async () => {
|
|
40
|
+
expect(await send('2025-11-25, 2025-11-25')).toEqual('2025-11-25');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('collapses the value the Fetch Headers constructor builds from both header spellings', async () => {
|
|
44
|
+
const headers = new Headers({
|
|
45
|
+
'mcp-protocol-version': '2025-11-25',
|
|
46
|
+
'MCP-Protocol-Version': '2025-11-25',
|
|
47
|
+
});
|
|
48
|
+
const res = await createApp().request('/api/mcp', { method: 'POST', headers });
|
|
49
|
+
expect((await res.json()).protocolVersion).toEqual('2025-11-25');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('leaves a single protocol version untouched', async () => {
|
|
53
|
+
expect(await send('2025-06-18')).toEqual('2025-06-18');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('leaves a header naming two different versions for the transport to refuse', async () => {
|
|
57
|
+
expect(await send('2025-11-25, 2025-06-18')).toEqual('2025-11-25, 2025-06-18');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('leaves an absent header absent', async () => {
|
|
61
|
+
expect(await send(undefined)).toEqual(null);
|
|
62
|
+
});
|
package/src/routes/reload.js
CHANGED
|
@@ -20,6 +20,21 @@ import chokidar from 'chokidar';
|
|
|
20
20
|
import { streamSSE } from 'hono/streaming';
|
|
21
21
|
|
|
22
22
|
import { registerTab, unregisterTab } from '../../lib/docs/tabChannel.js';
|
|
23
|
+
import readRecordingCookie from '../../lib/server/recording/readRecordingCookie.js';
|
|
24
|
+
|
|
25
|
+
// A verified recording cookie means the dev server's own headless browser
|
|
26
|
+
// opened the tab: a journey run ({ source }), or a headless
|
|
27
|
+
// tool ('off'). A developer's tab carries none.
|
|
28
|
+
function tabOrigin(cookieHeader) {
|
|
29
|
+
const recording = readRecordingCookie(cookieHeader);
|
|
30
|
+
if (recording === null) {
|
|
31
|
+
return { source: 'dev', automated: false };
|
|
32
|
+
}
|
|
33
|
+
if (recording === 'off') {
|
|
34
|
+
return { source: 'headless', automated: true };
|
|
35
|
+
}
|
|
36
|
+
return { source: recording.source, automated: true };
|
|
37
|
+
}
|
|
23
38
|
|
|
24
39
|
// SSE endpoint — notifies the client when build/reload is written so it can
|
|
25
40
|
// mutate the SWR cache and refetch config. Also doubles as the transport for
|
|
@@ -46,6 +61,7 @@ async function reloadHandler(c) {
|
|
|
46
61
|
registerTab({
|
|
47
62
|
id: tabId,
|
|
48
63
|
send: (event, data) => stream.writeSSE({ event, data: JSON.stringify(data) }),
|
|
64
|
+
...tabOrigin(c.req.header('cookie')),
|
|
49
65
|
});
|
|
50
66
|
|
|
51
67
|
let open = true;
|
package/src/routes/request.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { callRequest, redactErrorResponse } from '@lowdefy/api';
|
|
17
|
+
import { callRequest, parseRequestBody, redactErrorResponse } from '@lowdefy/api';
|
|
18
18
|
import { serializer } from '@lowdefy/helpers';
|
|
19
19
|
|
|
20
20
|
import buildPageIfNeeded from '../../lib/server/jitPageBuilder.js';
|
|
@@ -23,7 +23,9 @@ import { getMock } from '../../lib/docs/devMockRegistry.js';
|
|
|
23
23
|
|
|
24
24
|
async function requestHandler(c) {
|
|
25
25
|
if (c.req.method !== 'POST') {
|
|
26
|
-
|
|
26
|
+
// A wrong-method request is client-caused: answer 405 rather than raising a
|
|
27
|
+
// fault that would be logged at error level and answered with a 500.
|
|
28
|
+
return c.json({ error: 'Method not allowed.' }, 405);
|
|
27
29
|
}
|
|
28
30
|
const context = c.get('lowdefyContext');
|
|
29
31
|
const segments = getPathSegments(c, '/api/request/');
|
|
@@ -32,7 +34,7 @@ async function requestHandler(c) {
|
|
|
32
34
|
}
|
|
33
35
|
const requestId = segments[segments.length - 1];
|
|
34
36
|
const pageId = segments.slice(0, -1).join('/');
|
|
35
|
-
const { actionId, blockId, payload } = await c.req.
|
|
37
|
+
const { actionId, blockId, payload } = parseRequestBody({ text: await c.req.text() });
|
|
36
38
|
|
|
37
39
|
// Dev-only agent tooling: once a state checkpoint has been loaded
|
|
38
40
|
// (lib/docs/loadState.js), its recorded request/response pairs live in
|
|
@@ -55,7 +57,7 @@ async function requestHandler(c) {
|
|
|
55
57
|
|
|
56
58
|
// Page artifacts (including pages/{pageId}/requests/{requestId}.json) are
|
|
57
59
|
// built JIT by GET /api/page/* and thrown away on every page invalidation
|
|
58
|
-
// (lowdefyBuildWatcher
|
|
60
|
+
// (lowdefyBuildWatcher). A client that already holds
|
|
59
61
|
// the page config keeps firing requests across that window — and a request
|
|
60
62
|
// that arrived before the page was rebuilt failed with
|
|
61
63
|
// `Request "x" does not exist.` Run the same idempotent build the page route
|
|
@@ -20,6 +20,7 @@ import { jest } from '@jest/globals';
|
|
|
20
20
|
const mockCallRequest = jest.fn();
|
|
21
21
|
jest.unstable_mockModule('@lowdefy/api', () => ({
|
|
22
22
|
callRequest: mockCallRequest,
|
|
23
|
+
parseRequestBody: ({ text }) => JSON.parse(text),
|
|
23
24
|
redactErrorResponse: jest.fn((context, error) => ({ error: error.message })),
|
|
24
25
|
}));
|
|
25
26
|
|
|
@@ -115,10 +116,13 @@ test('requestHandler replays a dev mock without building or calling the request'
|
|
|
115
116
|
expect(mockCallRequest).not.toHaveBeenCalled();
|
|
116
117
|
});
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const res = await
|
|
122
|
-
|
|
119
|
+
// A GET to the request route is client-caused, so it must answer 405 rather
|
|
120
|
+
// than throwing into the error handler and being logged as a fault with a 500.
|
|
121
|
+
test('requestHandler returns 405 without building or calling the request for a non-POST method', async () => {
|
|
122
|
+
const res = await createApp().request('/api/request/dashboard/get_rows');
|
|
123
|
+
|
|
124
|
+
expect(res.status).toEqual(405);
|
|
125
|
+
expect(await res.json()).toEqual({ error: 'Method not allowed.' });
|
|
123
126
|
expect(mockBuildPageIfNeeded).not.toHaveBeenCalled();
|
|
127
|
+
expect(mockCallRequest).not.toHaveBeenCalled();
|
|
124
128
|
});
|
|
@@ -0,0 +1,159 @@
|
|
|
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 { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
import { Hono } from 'hono';
|
|
20
|
+
import { jest } from '@jest/globals';
|
|
21
|
+
|
|
22
|
+
import { isSameOriginRequest, parseRequestBody } from '@lowdefy/api';
|
|
23
|
+
|
|
24
|
+
// usage.js reads package.json from the working directory as it loads, and a suite that
|
|
25
|
+
// ran earlier in the same jest worker may have left the working directory in a temporary
|
|
26
|
+
// fixture directory.
|
|
27
|
+
process.chdir(fileURLToPath(new URL('../..', import.meta.url)));
|
|
28
|
+
|
|
29
|
+
const mockGetEndpointConfig = jest.fn().mockRejectedValue(new Error('not found'));
|
|
30
|
+
const mockRunWebhookEndpoint = jest.fn();
|
|
31
|
+
jest.unstable_mockModule('@lowdefy/api', () => ({
|
|
32
|
+
acceptDetachedEndpoint: jest.fn(),
|
|
33
|
+
callAgent: jest.fn(),
|
|
34
|
+
callEndpoint: jest.fn(),
|
|
35
|
+
callRequest: jest.fn(),
|
|
36
|
+
getEndpointConfig: mockGetEndpointConfig,
|
|
37
|
+
logClientError: jest.fn(),
|
|
38
|
+
isSameOriginRequest,
|
|
39
|
+
parseRequestBody,
|
|
40
|
+
redactErrorResponse: jest.fn(),
|
|
41
|
+
runWebhookEndpoint: mockRunWebhookEndpoint,
|
|
42
|
+
}));
|
|
43
|
+
jest.unstable_mockModule('../../lib/server/jitPageBuilder.js', () => ({ default: jest.fn() }));
|
|
44
|
+
jest.unstable_mockModule('../../lib/docs/devMockRegistry.js', () => ({
|
|
45
|
+
getMock: jest.fn(),
|
|
46
|
+
loadMocks: jest.fn(),
|
|
47
|
+
}));
|
|
48
|
+
jest.unstable_mockModule('../../lib/docs/loadState.js', () => ({ default: jest.fn() }));
|
|
49
|
+
jest.unstable_mockModule('../../lib/docs/snapshotState.js', () => ({ default: jest.fn() }));
|
|
50
|
+
|
|
51
|
+
const { default: createErrorHandler } = await import('../middleware/errorHandler.js');
|
|
52
|
+
const routes = {
|
|
53
|
+
'/api/agent/*': (await import('./agent.js')).default,
|
|
54
|
+
'/api/client-error': (await import('./clientError.js')).default,
|
|
55
|
+
'/api/detached/*': (await import('./detached.js')).default,
|
|
56
|
+
'/api/dev-inspect': (await import('./devInspect.js')).default,
|
|
57
|
+
'/api/dev-inspect/*': (await import('./devInspect.js')).default,
|
|
58
|
+
'/api/endpoints/*': (await import('./endpoints.js')).default,
|
|
59
|
+
'/api/request/*': (await import('./request.js')).default,
|
|
60
|
+
'/api/usage': (await import('./usage.js')).default,
|
|
61
|
+
'/lowdefy-docs/state-checkpoints/load': (await import('./docs/loadState.js')).default,
|
|
62
|
+
'/lowdefy-docs/state-checkpoints/snapshot': (await import('./docs/snapshotState.js')).default,
|
|
63
|
+
'/lowdefy-feedback': (await import('./feedback.js')).default,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const logger = { debug: jest.fn(), error: jest.fn(), info: jest.fn(), warn: jest.fn() };
|
|
67
|
+
|
|
68
|
+
function createApp() {
|
|
69
|
+
const app = new Hono();
|
|
70
|
+
app.use('*', async (c, next) => {
|
|
71
|
+
c.set('lowdefyContext', { logger, handleError: jest.fn() });
|
|
72
|
+
await next();
|
|
73
|
+
});
|
|
74
|
+
Object.entries(routes).forEach(([path, handler]) => app.all(path, handler));
|
|
75
|
+
app.onError(createErrorHandler({ logger }));
|
|
76
|
+
return app;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const originalCronSecret = process.env.CRON_SECRET;
|
|
80
|
+
beforeAll(() => {
|
|
81
|
+
process.env.CRON_SECRET = 'secret';
|
|
82
|
+
});
|
|
83
|
+
afterAll(() => {
|
|
84
|
+
if (originalCronSecret === undefined) {
|
|
85
|
+
delete process.env.CRON_SECRET;
|
|
86
|
+
} else {
|
|
87
|
+
process.env.CRON_SECRET = originalCronSecret;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const sameOrigin = { host: 'localhost', origin: 'http://localhost' };
|
|
92
|
+
const notJson = JSON.stringify({ name: 'UserError', message: 'Request body is not valid JSON.' });
|
|
93
|
+
|
|
94
|
+
// Paths outside /api/ get the error handler's plain-text answer.
|
|
95
|
+
test.each([
|
|
96
|
+
['/api/endpoints/save', {}, notJson],
|
|
97
|
+
['/api/request/orders/load', {}, notJson],
|
|
98
|
+
['/api/agent/orders/assistant', {}, notJson],
|
|
99
|
+
['/api/detached/save', { authorization: 'Bearer secret' }, notJson],
|
|
100
|
+
['/api/usage', {}, notJson],
|
|
101
|
+
['/api/client-error', sameOrigin, notJson],
|
|
102
|
+
['/api/dev-inspect', sameOrigin, notJson],
|
|
103
|
+
['/api/dev-inspect/page', sameOrigin, notJson],
|
|
104
|
+
['/lowdefy-docs/state-checkpoints/load', {}, 'Bad Request'],
|
|
105
|
+
['/lowdefy-docs/state-checkpoints/snapshot', {}, 'Bad Request'],
|
|
106
|
+
['/lowdefy-feedback', sameOrigin, 'Bad Request'],
|
|
107
|
+
])(
|
|
108
|
+
'POST %s with a body that is not JSON answers 400 and logs a warning',
|
|
109
|
+
async (path, headers, text) => {
|
|
110
|
+
const res = await createApp().request(path, { method: 'POST', body: 'nope', headers });
|
|
111
|
+
expect(res.status).toBe(400);
|
|
112
|
+
expect(await res.text()).toBe(text);
|
|
113
|
+
expect(logger.warn).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
test('POST /api/endpoints with a null body answers 400', async () => {
|
|
119
|
+
const res = await createApp().request('/api/endpoints/save', { method: 'POST', body: 'null' });
|
|
120
|
+
expect(res.status).toBe(400);
|
|
121
|
+
expect(await res.json()).toEqual({
|
|
122
|
+
name: 'UserError',
|
|
123
|
+
message: 'Request body must be a JSON object.',
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('POST /api/endpoints to a webhook endpoint passes the body text exactly as sent as rawBody', async () => {
|
|
128
|
+
mockGetEndpointConfig.mockResolvedValueOnce({ webhook: { verify: {} } });
|
|
129
|
+
mockRunWebhookEndpoint.mockResolvedValue({ status: 200, body: { ok: true } });
|
|
130
|
+
const body = '{ "zeta": 1, "alpha": "caf\\u00e9", "beta": "café" }';
|
|
131
|
+
const res = await createApp().request('/api/endpoints/signed_hook?t=1', {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
body,
|
|
134
|
+
headers: { 'x-hub-signature-256': 'sha256=abc' },
|
|
135
|
+
});
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
expect(mockRunWebhookEndpoint).toHaveBeenCalledWith(expect.anything(), {
|
|
138
|
+
endpointId: 'signed_hook',
|
|
139
|
+
rawBody: body,
|
|
140
|
+
query: { t: '1' },
|
|
141
|
+
headers: expect.objectContaining({ 'x-hub-signature-256': 'sha256=abc' }),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test.each([
|
|
146
|
+
[200, { received: true }],
|
|
147
|
+
[401, { error: { code: 'unauthorized', message: 'Webhook verification failed.' } }],
|
|
148
|
+
[404, { error: 'not_found', id: 'ticket-1' }],
|
|
149
|
+
[500, { error: { code: 'internal_error', message: 'Webhook failed.' } }],
|
|
150
|
+
])(
|
|
151
|
+
'POST /api/endpoints to a webhook endpoint answers status %s with its body',
|
|
152
|
+
async (status, body) => {
|
|
153
|
+
mockGetEndpointConfig.mockResolvedValueOnce({ webhook: true });
|
|
154
|
+
mockRunWebhookEndpoint.mockResolvedValue({ status, body });
|
|
155
|
+
const res = await createApp().request('/api/endpoints/hook', { method: 'POST', body: '{}' });
|
|
156
|
+
expect(res.status).toBe(status);
|
|
157
|
+
expect(await res.json()).toEqual(body);
|
|
158
|
+
}
|
|
159
|
+
);
|
package/src/routes/usage.js
CHANGED
|
@@ -17,14 +17,18 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
|
|
20
|
+
import { parseRequestBody } from '@lowdefy/api';
|
|
21
|
+
|
|
20
22
|
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
21
23
|
|
|
22
24
|
async function usageHandler(c) {
|
|
23
25
|
if (c.req.method !== 'POST') {
|
|
24
|
-
|
|
26
|
+
// A wrong-method request is client-caused: answer 405 rather than raising a
|
|
27
|
+
// fault that would be logged at error level and answered with a 500.
|
|
28
|
+
return c.json({ error: 'Method not allowed.' }, 405);
|
|
25
29
|
}
|
|
26
30
|
const context = c.get('lowdefyContext');
|
|
27
|
-
const { user, machine } = await c.req.
|
|
31
|
+
const { user, machine } = parseRequestBody({ text: await c.req.text() });
|
|
28
32
|
const host = c.req.header('host');
|
|
29
33
|
context.logger.info({ event: 'log_usage', user, machine });
|
|
30
34
|
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
// The resolved caller for the client - the same resolveAuthentication output
|
|
18
|
+
// the first page load embeds. The base session carries neither roles (read
|
|
19
|
+
// live from the active member row) nor the merged attributes bag, so the
|
|
20
|
+
// client re-syncs from here (UpdateSession) after a session change such as
|
|
21
|
+
// SetActiveOrganization.
|
|
22
|
+
async function userHandler(c) {
|
|
23
|
+
const context = c.get('lowdefyContext');
|
|
24
|
+
return c.json({ user: context.user ?? null });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default userHandler;
|
package/src/routes/websocket.js
CHANGED
|
@@ -14,19 +14,40 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { isWebSocketOriginAllowed } from '@lowdefy/api';
|
|
18
|
+
import { type } from '@lowdefy/helpers';
|
|
19
|
+
|
|
20
|
+
import getStrategies from '../../lib/server/auth/getStrategies.js';
|
|
21
|
+
import isRebindingSafeHost from '../middleware/isRebindingSafeHost.js';
|
|
18
22
|
|
|
19
23
|
// In dev, Vite owns the HTTP server, so websocket upgrades can't flow through
|
|
20
|
-
// @hono/node-server. The
|
|
21
|
-
// with the upgrade request's headers
|
|
22
|
-
// session, apiContext)
|
|
23
|
-
//
|
|
24
|
+
// @hono/node-server. The upgrade handler (src/websocket/devWebSocket.js) runs
|
|
25
|
+
// this route with the upgrade request's headers so the full middleware chain
|
|
26
|
+
// (auth session, apiContext) builds the request context, and passes a
|
|
27
|
+
// websocketUpgrade object in the Hono env; this handler puts the context and
|
|
28
|
+
// auth strategies on it. A plain HTTP request to this route carries no such object.
|
|
24
29
|
function websocketHandler(c) {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
30
|
+
const websocketUpgrade = c.env?.websocketUpgrade;
|
|
31
|
+
if (type.isNone(websocketUpgrade)) {
|
|
27
32
|
return c.json({ message: 'WebSocket upgrade required.' }, 400);
|
|
28
33
|
}
|
|
29
|
-
|
|
34
|
+
// Vite's host check guards HTTP requests against DNS rebinding, but upgrades
|
|
35
|
+
// reach this route through a plugin's upgrade listener, past that check - and
|
|
36
|
+
// a rebound page's Origin matches its Host. So the Host must be one no DNS
|
|
37
|
+
// answer can rebind, as Vite requires of every HTTP request.
|
|
38
|
+
if (!isRebindingSafeHost({ host: c.req.header('host') })) {
|
|
39
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
40
|
+
}
|
|
41
|
+
const context = c.get('lowdefyContext');
|
|
42
|
+
// Upgrades are accepted from the app's own pages and from clients that send
|
|
43
|
+
// no Origin; isWebSocketOriginAllowed holds the rule and logs a refusal.
|
|
44
|
+
if (!isWebSocketOriginAllowed({ context, getHeader: (name) => c.req.header(name) })) {
|
|
45
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
46
|
+
}
|
|
47
|
+
websocketUpgrade.context = context;
|
|
48
|
+
// The connection resolves its caller again while the socket is open, with
|
|
49
|
+
// the upgrade's headers and the strategies of the current build.
|
|
50
|
+
websocketUpgrade.strategies = getStrategies({ logger: context.logger });
|
|
30
51
|
return c.json({ ok: true });
|
|
31
52
|
}
|
|
32
53
|
|
|
@@ -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
|
+
const mockStrategies = [];
|
|
21
|
+
jest.unstable_mockModule('../../lib/server/auth/getStrategies.js', () => ({
|
|
22
|
+
default: () => mockStrategies,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const { default: websocketHandler } = await import('./websocket.js');
|
|
26
|
+
|
|
27
|
+
// The dev upgrade listener (src/websocket/devWebSocket.js) runs this route
|
|
28
|
+
// through the app and opens the websocket only on a 200 that carries the
|
|
29
|
+
// context - so a refusal here is a refused upgrade.
|
|
30
|
+
async function upgrade({ headers }) {
|
|
31
|
+
const app = new Hono().basePath('/app');
|
|
32
|
+
app.use('*', async (c, next) => {
|
|
33
|
+
c.set('lowdefyContext', { config: {}, logger: { warn: () => {} }, rid: 'request' });
|
|
34
|
+
await next();
|
|
35
|
+
});
|
|
36
|
+
app.get('/api/websocket', websocketHandler);
|
|
37
|
+
const websocketUpgrade = { context: null, strategies: null };
|
|
38
|
+
const res = await app.request('/app/api/websocket', { headers }, { websocketUpgrade });
|
|
39
|
+
return {
|
|
40
|
+
status: res.status,
|
|
41
|
+
context: websocketUpgrade.context,
|
|
42
|
+
strategies: websocketUpgrade.strategies,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test.each([
|
|
47
|
+
['the dev app page opens it', { host: 'localhost:4100', origin: 'http://localhost:4100' }],
|
|
48
|
+
['a client sends no origin', { host: 'localhost:4100' }],
|
|
49
|
+
])('the dev websocket route opens the upgrade when %s', async (_, headers) => {
|
|
50
|
+
expect(await upgrade({ headers })).toEqual({
|
|
51
|
+
status: 200,
|
|
52
|
+
context: expect.any(Object),
|
|
53
|
+
strategies: mockStrategies,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test.each([
|
|
58
|
+
['the origin is another site', { host: 'localhost:4100', origin: 'https://other.test' }],
|
|
59
|
+
[
|
|
60
|
+
'a rebound domain names itself in both host and origin',
|
|
61
|
+
{ host: 'rebound.test:4100', origin: 'http://rebound.test:4100' },
|
|
62
|
+
],
|
|
63
|
+
])('the dev websocket route refuses the upgrade when %s', async (_, headers) => {
|
|
64
|
+
expect(await upgrade({ headers })).toEqual({ status: 403, context: null, strategies: null });
|
|
65
|
+
});
|
|
@@ -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
|
+
// RFC 8615 well-known URIs are machine-facing: a path this deployment does not
|
|
18
|
+
// serve must answer 404, never the app's HTML shell. Serving 200 HTML breaks
|
|
19
|
+
// clients that probe discovery documents - an MCP client's OAuth probe, for
|
|
20
|
+
// example, fails with "Failed to parse JSON" instead of learning there is no
|
|
21
|
+
// auth to discover. Mounted after the real discovery documents, before the
|
|
22
|
+
// page catch-all.
|
|
23
|
+
function wellKnownFallbackHandler(c) {
|
|
24
|
+
return c.json(
|
|
25
|
+
{ name: 'NotFoundError', message: `No well-known document at ${c.req.path}.` },
|
|
26
|
+
404
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default wellKnownFallbackHandler;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
import wellKnownFallbackHandler from './wellKnownFallback.js';
|
|
20
|
+
|
|
21
|
+
test('an unserved well-known path answers 404 JSON, never the app shell', () => {
|
|
22
|
+
const json = jest.fn((data, status) => ({ data, status }));
|
|
23
|
+
const result = wellKnownFallbackHandler({
|
|
24
|
+
req: { path: '/.well-known/oauth-protected-resource' },
|
|
25
|
+
json,
|
|
26
|
+
});
|
|
27
|
+
expect(result.status).toBe(404);
|
|
28
|
+
expect(result.data).toEqual({
|
|
29
|
+
name: 'NotFoundError',
|
|
30
|
+
message: 'No well-known document at /.well-known/oauth-protected-resource.',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -14,54 +14,69 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { STATUS_CODES } from 'node:http';
|
|
18
18
|
import { createChannelRegistry, createWebSocketConnection } from '@lowdefy/api';
|
|
19
|
+
import { type } from '@lowdefy/helpers';
|
|
19
20
|
|
|
20
|
-
import
|
|
21
|
+
import createLogger from '../../lib/server/log/createLogger.js';
|
|
22
|
+
|
|
23
|
+
const logger = createLogger();
|
|
21
24
|
|
|
22
25
|
// One registry per dev server process, shared by all websocket connections.
|
|
23
26
|
const registry = createChannelRegistry();
|
|
24
27
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function takePendingWebSocketContext(rid) {
|
|
34
|
-
const context = pendingContexts.get(rid);
|
|
35
|
-
pendingContexts.delete(rid);
|
|
36
|
-
return context;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Called by the Vite plugin on an HTTP upgrade for /api/websocket. Runs the
|
|
40
|
-
// dev Hono app for the upgrade request (auth session + apiContext), then
|
|
41
|
-
// completes the websocket handshake and wires the connection to the registry.
|
|
42
|
-
async function handleWebSocketUpgrade({ request, socket, head, wss }) {
|
|
43
|
-
const rid = randomUUID();
|
|
28
|
+
// Called by the Vite plugin (see vite.config.js) on an HTTP upgrade for
|
|
29
|
+
// /api/websocket, with the dev Hono app. Runs the app for the upgrade request
|
|
30
|
+
// (auth session + apiContext) and receives the request context through the
|
|
31
|
+
// websocketUpgrade object in the Hono env, then completes the websocket
|
|
32
|
+
// handshake and wires the connection to the registry. The context travels
|
|
33
|
+
// with the request, not through state shared between modules, so the upgrade
|
|
34
|
+
// works however Vite has re-evaluated the app's modules after a rebuild.
|
|
35
|
+
async function handleWebSocketUpgrade({ app, request, socket, head, wss }) {
|
|
44
36
|
const headers = new Headers();
|
|
45
37
|
Object.entries(request.headers).forEach(([key, value]) => {
|
|
46
38
|
if (value) {
|
|
47
39
|
headers.set(key, Array.isArray(value) ? value[0] : value);
|
|
48
40
|
}
|
|
49
41
|
});
|
|
50
|
-
headers.set('x-lowdefy-websocket-rid', rid);
|
|
51
42
|
|
|
52
43
|
const url = `http://${request.headers.host ?? 'localhost'}${request.url}`;
|
|
53
|
-
const
|
|
54
|
-
const
|
|
44
|
+
const websocketUpgrade = { context: null, strategies: null };
|
|
45
|
+
const response = await app.fetch(new Request(url, { headers }), { websocketUpgrade });
|
|
46
|
+
|
|
47
|
+
if (response.status !== 200) {
|
|
48
|
+
const body = await response.text();
|
|
49
|
+
logger.warn(
|
|
50
|
+
{ event: 'ws_upgrade_refused', status: response.status },
|
|
51
|
+
`WebSocket upgrade refused: ${request.url} answered ${response.status}. ${body}`
|
|
52
|
+
);
|
|
53
|
+
socket.end(
|
|
54
|
+
`HTTP/1.1 ${response.status} ${STATUS_CODES[response.status]}\r\nConnection: close\r\n\r\n`
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
// A 200 without a context means the request reached another handler than
|
|
60
|
+
// the websocket route (a route or middleware answered first).
|
|
61
|
+
const { context, strategies } = websocketUpgrade;
|
|
62
|
+
if (type.isNone(context)) {
|
|
63
|
+
logger.warn(
|
|
64
|
+
{ event: 'ws_upgrade_refused', status: response.status },
|
|
65
|
+
`WebSocket upgrade refused: ${request.url} answered ${response.status} without a request context; the websocket route did not run.`
|
|
66
|
+
);
|
|
57
67
|
socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
58
68
|
return;
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
72
|
+
// The upgrade's headers stay with the connection: its caller is resolved
|
|
73
|
+
// from them again while the socket is open.
|
|
62
74
|
const connection = createWebSocketConnection(context, {
|
|
75
|
+
closeSocket: ({ code, reason }) => ws.close(code, reason),
|
|
76
|
+
headers,
|
|
63
77
|
registry,
|
|
64
78
|
send: (message) => ws.send(message),
|
|
79
|
+
strategies,
|
|
65
80
|
});
|
|
66
81
|
ws.on('message', (data) => {
|
|
67
82
|
connection.handleMessage(data.toString());
|
|
@@ -75,4 +90,4 @@ async function handleWebSocketUpgrade({ request, socket, head, wss }) {
|
|
|
75
90
|
});
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
export { handleWebSocketUpgrade
|
|
93
|
+
export { handleWebSocketUpgrade };
|