@xenon-device-management/xenon 2.13.2 → 2.15.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/README.md +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +85 -71
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -1,25 +1,66 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetLinkBase = resetLinkBase;
|
|
3
4
|
exports.buildResetLink = buildResetLink;
|
|
5
|
+
exports.requestBase = requestBase;
|
|
6
|
+
exports.describeLifetime = describeLifetime;
|
|
4
7
|
exports.resetEmail = resetEmail;
|
|
8
|
+
const config_1 = require("../config");
|
|
9
|
+
const publicUrl_1 = require("./publicUrl");
|
|
5
10
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* until it expires or is used: never log the result.
|
|
11
|
+
* Where a password reset link points, and the email that carries it. The raw
|
|
12
|
+
* token in a link is a credential for the account until it expires or is
|
|
13
|
+
* used: never log the result.
|
|
10
14
|
*
|
|
11
15
|
* The token goes in the fragment (#), not the path. Browsers never send the
|
|
12
16
|
* fragment to the server, so opening the link can't put the token in any
|
|
13
17
|
* server-side log. As a path segment it was logged on every open: Appium's
|
|
14
18
|
* [HTTP] request line and Xenon's UI-fallback line both record the URL.
|
|
15
19
|
*/
|
|
16
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The address a link Xenon sends (by email, or to the log fallback) points
|
|
22
|
+
* at: this server's address from XENON_PUBLIC_URL (publicServerBase), or
|
|
23
|
+
* null when it is unset or not a usable address.
|
|
24
|
+
*
|
|
25
|
+
* Never the request's Host header. Through 2.14 the link was built from it,
|
|
26
|
+
* so anyone could ask for a reset of someone else's account naming a host of
|
|
27
|
+
* their own (a matching Origin passes the CSRF check), and Xenon emailed the
|
|
28
|
+
* victim a genuine link to that host, which handed over the token.
|
|
29
|
+
*/
|
|
30
|
+
function resetLinkBase(cfg = config_1.config) {
|
|
31
|
+
return (0, publicUrl_1.publicServerBase)(cfg);
|
|
32
|
+
}
|
|
33
|
+
/** The reset page for `rawToken`, under `base` (resetLinkBase, or requestBase). */
|
|
34
|
+
function buildResetLink(base, rawToken) {
|
|
35
|
+
return `${base}/xenon/reset-password#${rawToken}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The address the caller reached this server on, honouring a TLS-terminating
|
|
39
|
+
* proxy's x-forwarded-proto. Only for a link handed back to the signed-in
|
|
40
|
+
* administrator who asked for it (POST /users/:id/reset-link when it can't be
|
|
41
|
+
* emailed): they see where it points before passing it on, and hold the token
|
|
42
|
+
* anyway. Never for a link Xenon sends to someone else.
|
|
43
|
+
*/
|
|
44
|
+
function requestBase(req) {
|
|
17
45
|
const proto = req.secure || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
|
|
18
46
|
const host = req.headers.host || 'localhost';
|
|
19
|
-
return `${proto}://${host}
|
|
47
|
+
return `${proto}://${host}`;
|
|
48
|
+
}
|
|
49
|
+
/** "1 hour", "30 minutes", "2 hours": a reset link's lifetime, for the email. */
|
|
50
|
+
function describeLifetime(ms) {
|
|
51
|
+
const minutes = Math.max(1, Math.round(ms / 60000));
|
|
52
|
+
if (minutes % 60 === 0) {
|
|
53
|
+
const hours = minutes / 60;
|
|
54
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
55
|
+
}
|
|
56
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
20
57
|
}
|
|
21
|
-
/**
|
|
22
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The reset email, shared by self-service and admin-issued resets. `ttlMs` is
|
|
60
|
+
* how long the link lasts (PasswordResetService.ttlMs(), from
|
|
61
|
+
* XENON_RESET_TOKEN_TTL_MS): the email said "1 hour" whatever it was.
|
|
62
|
+
*/
|
|
63
|
+
function resetEmail(user, link, ttlMs) {
|
|
23
64
|
return {
|
|
24
65
|
to: user.email,
|
|
25
66
|
subject: 'Reset your Xenon password',
|
|
@@ -27,6 +68,6 @@ function resetEmail(user, link) {
|
|
|
27
68
|
`Someone requested a password reset for your Xenon account. ` +
|
|
28
69
|
`If that was you, click the link below to choose a new password:\n\n` +
|
|
29
70
|
`${link}\n\n` +
|
|
30
|
-
`This link expires in
|
|
71
|
+
`This link expires in ${describeLifetime(ttlMs)}. If you did not request this, ignore this email — no action is needed.\n`,
|
|
31
72
|
};
|
|
32
73
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.passwordResetMode = passwordResetMode;
|
|
4
|
+
const passwordResetLink_1 = require("./passwordResetLink");
|
|
4
5
|
function passwordResetMode(cfg) {
|
|
5
|
-
|
|
6
|
+
const smtp = !!(cfg.smtpUrl && cfg.smtpUrl.trim());
|
|
7
|
+
return smtp && (0, passwordResetLink_1.resetLinkBase)(cfg) !== null ? 'email' : 'admin';
|
|
6
8
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.publicServerBase = publicServerBase;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
/**
|
|
6
|
+
* XENON_PUBLIC_URL: the address people reach this Xenon server at, such as
|
|
7
|
+
* https://xenon.example.com or http://lab-mac:4723. Links Xenon hands out
|
|
8
|
+
* (password reset links, a device's dashboard_link) are built from it, never
|
|
9
|
+
* from the request's Host header, which whoever sends the request chooses.
|
|
10
|
+
*
|
|
11
|
+
* The dashboard's own address is taken too: a trailing `/xenon` or `/xenon/`
|
|
12
|
+
* is dropped, so `http://localhost:4723/xenon/` gives `http://localhost:4723`.
|
|
13
|
+
* Kept as given it built `/xenon/xenon/reset-password`, which the dashboard
|
|
14
|
+
* sends to its sign-in page, losing the token.
|
|
15
|
+
*
|
|
16
|
+
* Any other path is refused: the dashboard loads its pages, scripts and API
|
|
17
|
+
* from `/xenon` on the server's root (its base path is fixed at build time),
|
|
18
|
+
* so it can't be served under a reverse-proxy prefix, and a link under one
|
|
19
|
+
* wouldn't open. So are an address that isn't http(s), and one with a query,
|
|
20
|
+
* fragment or user name (a fragment would swallow a reset token).
|
|
21
|
+
*
|
|
22
|
+
* Returns the origin (scheme, host and port, no trailing slash), or null when
|
|
23
|
+
* the variable is unset or not such an address.
|
|
24
|
+
*/
|
|
25
|
+
function publicServerBase(cfg = config_1.config) {
|
|
26
|
+
var _a;
|
|
27
|
+
const raw = (_a = cfg.publicUrl) === null || _a === void 0 ? void 0 : _a.trim();
|
|
28
|
+
if (!raw)
|
|
29
|
+
return null;
|
|
30
|
+
let url;
|
|
31
|
+
try {
|
|
32
|
+
url = new URL(raw);
|
|
33
|
+
}
|
|
34
|
+
catch (_b) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
38
|
+
return null;
|
|
39
|
+
if (url.search || url.hash || url.username || url.password)
|
|
40
|
+
return null;
|
|
41
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
42
|
+
if (path !== '' && path !== '/xenon')
|
|
43
|
+
return null;
|
|
44
|
+
return url.origin;
|
|
45
|
+
}
|
|
@@ -18,10 +18,19 @@ const config_1 = require("../../config");
|
|
|
18
18
|
* and are NOT counted against this gate — they bypass it entirely.
|
|
19
19
|
*/
|
|
20
20
|
let ConcurrencyGate = class ConcurrencyGate {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* An explicit `limit` is fixed. Without one the gate follows
|
|
23
|
+
* `config.maxConcurrentRecordings` as it stands at each admission, not as it
|
|
24
|
+
* stood when the gate was built: the gate is created on first use, and the
|
|
25
|
+
* `maxConcurrentRecordings` option is applied at boot.
|
|
26
|
+
*/
|
|
27
|
+
constructor(fixedLimit) {
|
|
28
|
+
this.fixedLimit = fixedLimit;
|
|
23
29
|
this.active = new Set();
|
|
24
|
-
|
|
30
|
+
}
|
|
31
|
+
get limit() {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
return (_b = (_a = this.fixedLimit) !== null && _a !== void 0 ? _a : config_1.config.maxConcurrentRecordings) !== null && _b !== void 0 ? _b : 4;
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* Atomic admission: either ALL recordingIds are admitted, or none are.
|
|
@@ -21,9 +21,11 @@ function resolveSessionIdentity(input) {
|
|
|
21
21
|
if (!input.sessionToken)
|
|
22
22
|
return NONE;
|
|
23
23
|
// Attribution is decoupled from enforcement: a token that does not verify is
|
|
24
|
-
// IGNORED here, never rejected.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// IGNORED here, never rejected. Whether the session is admitted is decided
|
|
25
|
+
// by the caller: assertSessionTokenGate for a session with no valid
|
|
26
|
+
// credentials, and authorizeSessionRequest's `sessions` scope check for a
|
|
27
|
+
// token that verifies. That separation is what lets a valid token identify
|
|
28
|
+
// its caller even when the gate is switched off.
|
|
27
29
|
try {
|
|
28
30
|
const payload = yield input.verify(input.sessionToken);
|
|
29
31
|
const sub = payload === null || payload === void 0 ? void 0 : payload.sub;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UNEXPECTED_SHUTDOWN_REASON = void 0;
|
|
4
|
+
exports.unexpectedShutdownReason = unexpectedShutdownReason;
|
|
5
|
+
/** The reason recorded when Appium ends a session and gives no cause of its own. */
|
|
6
|
+
exports.UNEXPECTED_SHUTDOWN_REASON = 'Driver shut down unexpectedly';
|
|
7
|
+
/**
|
|
8
|
+
* The failure reason of a session Appium ended by itself
|
|
9
|
+
* (`XenonPlugin.onUnexpectedShutdown`): the message of the cause Appium
|
|
10
|
+
* passed, else UNEXPECTED_SHUTDOWN_REASON.
|
|
11
|
+
*
|
|
12
|
+
* Appium's new-command timeout is the usual cause, and its message ("New
|
|
13
|
+
* Command Timeout of 60 seconds expired. Try customizing the timeout using
|
|
14
|
+
* the 'newCommandTimeout' desired capability") is what files the session
|
|
15
|
+
* under TIMEOUT. A driver that gives up on itself passes its own error, such
|
|
16
|
+
* as base-driver's "The driver was unexpectedly shut down!". The umbrella's
|
|
17
|
+
* placeholder for a missing cause, "Unknown error", says nothing, so it gets
|
|
18
|
+
* the fallback too. Through 2.14 every such session was recorded with the
|
|
19
|
+
* fixed reason, which no failure pattern matches, so an idle session was
|
|
20
|
+
* filed UNKNOWN.
|
|
21
|
+
*/
|
|
22
|
+
function unexpectedShutdownReason(cause) {
|
|
23
|
+
const message = typeof cause === 'string' ? cause : cause === null || cause === void 0 ? void 0 : cause.message;
|
|
24
|
+
const text = typeof message === 'string' ? message.trim() : '';
|
|
25
|
+
return text && text !== 'Unknown error' ? text : exports.UNEXPECTED_SHUTDOWN_REASON;
|
|
26
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SelfHealingSwitch = void 0;
|
|
22
|
+
const typedi_1 = require("typedi");
|
|
23
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
24
|
+
const web_config_service_1 = require("../../data-service/web-config-service");
|
|
25
|
+
const labSettings_1 = require("./labSettings");
|
|
26
|
+
/**
|
|
27
|
+
* Whether self-healing is on, for the command interceptor, which asks at every
|
|
28
|
+
* command and so must never read the database.
|
|
29
|
+
*
|
|
30
|
+
* The value saved on the Settings page (the `WebConfig` row `enableSelfHealing`)
|
|
31
|
+
* is kept here, loaded once at boot and replaced when `POST /config` saves it.
|
|
32
|
+
* The rule is the one every lab setting follows (`selfHealingEnabled`): the
|
|
33
|
+
* saved value, else the plugin option the server started with, else on. The
|
|
34
|
+
* interceptor passes the options it was given, so the startup half is always
|
|
35
|
+
* the live one.
|
|
36
|
+
*
|
|
37
|
+
* Through 2.13 the toggle was stored nowhere and the interceptor read the
|
|
38
|
+
* startup options alone, so switching healing off in the dashboard did nothing.
|
|
39
|
+
*/
|
|
40
|
+
let SelfHealingSwitch = class SelfHealingSwitch {
|
|
41
|
+
/** Whether a failed findElement goes on to the healing tiers, now. */
|
|
42
|
+
isEnabled(startup) {
|
|
43
|
+
return (0, labSettings_1.selfHealingEnabled)(startup.enableSelfHealing, this.saved);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads the saved value, once, at boot. A database that can't be read leaves
|
|
47
|
+
* the startup option in charge, as it does for the cleanup job, and is said
|
|
48
|
+
* once here rather than at every command.
|
|
49
|
+
*/
|
|
50
|
+
load() {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
try {
|
|
53
|
+
const { enableSelfHealing } = yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig();
|
|
54
|
+
this.saved = typeof enableSelfHealing === 'boolean' ? enableSelfHealing : undefined;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
this.saved = undefined;
|
|
58
|
+
logger_1.default.warn(`Could not read the saved self-healing setting, using the startup option: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** What `POST /config` just saved, in force for the next command. */
|
|
63
|
+
set(saved) {
|
|
64
|
+
this.saved = saved;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
exports.SelfHealingSwitch = SelfHealingSwitch;
|
|
68
|
+
exports.SelfHealingSwitch = SelfHealingSwitch = __decorate([
|
|
69
|
+
(0, typedi_1.Service)()
|
|
70
|
+
], SelfHealingSwitch);
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.AiEngineSettings = exports.AI_ENGINE_SETTINGS = exports.AI_PROVIDERS = void 0;
|
|
22
|
+
exports.pickAiEngine = pickAiEngine;
|
|
23
|
+
exports.effectiveAiEngine = effectiveAiEngine;
|
|
24
|
+
exports.validateAiEngineUpdate = validateAiEngineUpdate;
|
|
25
|
+
exports.aiEngineUpdateOf = aiEngineUpdateOf;
|
|
26
|
+
const typedi_1 = require("typedi");
|
|
27
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
28
|
+
const config_1 = require("../../config");
|
|
29
|
+
const web_config_service_1 = require("../../data-service/web-config-service");
|
|
30
|
+
/**
|
|
31
|
+
* The AI engine page's settings: which provider answers the AI calls (failure
|
|
32
|
+
* analysis, the visual and LLM healing tiers, visual assertions), and the model
|
|
33
|
+
* and address it uses.
|
|
34
|
+
*
|
|
35
|
+
* They follow the rule every lab setting does (labSettings.ts): a value saved
|
|
36
|
+
* in the dashboard (`WebConfig`, written by `POST /config`), else the plugin
|
|
37
|
+
* option or environment variable the server was started with, else the
|
|
38
|
+
* default. The result is written to `config`, which every AI call reads, at
|
|
39
|
+
* boot and on each save, so a change applies to the next call without a
|
|
40
|
+
* restart. Through 2.14 `POST /config` changed `config` and saved nothing: a
|
|
41
|
+
* restart went back to the provider the server was started with.
|
|
42
|
+
*
|
|
43
|
+
* API keys are not among them. They come from the server's environment only,
|
|
44
|
+
* are never saved, and are never sent to the dashboard.
|
|
45
|
+
*/
|
|
46
|
+
exports.AI_PROVIDERS = ['gemini', 'openai', 'anthropic', 'ollama'];
|
|
47
|
+
/** The settings decided here, by their names in `config`, `WebConfig` and `/config`. */
|
|
48
|
+
exports.AI_ENGINE_SETTINGS = [
|
|
49
|
+
'aiProvider',
|
|
50
|
+
'aiModel',
|
|
51
|
+
'aiBaseUrl',
|
|
52
|
+
'geminiModel',
|
|
53
|
+
'openaiModel',
|
|
54
|
+
'anthropicModel',
|
|
55
|
+
'ollamaModel',
|
|
56
|
+
];
|
|
57
|
+
const MODEL_SETTINGS = [
|
|
58
|
+
'aiModel',
|
|
59
|
+
'geminiModel',
|
|
60
|
+
'openaiModel',
|
|
61
|
+
'anthropicModel',
|
|
62
|
+
'ollamaModel',
|
|
63
|
+
];
|
|
64
|
+
const DEFAULT_PROVIDER = 'gemini';
|
|
65
|
+
const MODEL_MAX_LENGTH = 200;
|
|
66
|
+
const BASE_URL_MAX_LENGTH = 2048;
|
|
67
|
+
const isProvider = (v) => typeof v === 'string' && exports.AI_PROVIDERS.includes(v);
|
|
68
|
+
const isNonEmpty = (v) => typeof v === 'string' && v.trim() !== '';
|
|
69
|
+
/** The AI engine settings in `source` (the running config, the saved rows), and nothing else. */
|
|
70
|
+
function pickAiEngine(source) {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const name of exports.AI_ENGINE_SETTINGS) {
|
|
73
|
+
if (typeof source[name] === 'string')
|
|
74
|
+
out[name] = source[name];
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The settings in effect, field by field: the saved value when it can work,
|
|
80
|
+
* else the one the server was started with, else the default (gemini; a model
|
|
81
|
+
* or base URL has none, and the provider's own then applies). An empty saved
|
|
82
|
+
* value is the person clearing it. The started-with provider is kept as it
|
|
83
|
+
* is, even one Xenon doesn't know: falling back to another provider would
|
|
84
|
+
* send screenshots somewhere the server's owner didn't choose. AIService then
|
|
85
|
+
* runs with no provider, and says so.
|
|
86
|
+
*/
|
|
87
|
+
function effectiveAiEngine(startup, saved) {
|
|
88
|
+
const out = {
|
|
89
|
+
aiProvider: isProvider(saved.aiProvider)
|
|
90
|
+
? saved.aiProvider
|
|
91
|
+
: isNonEmpty(startup.aiProvider)
|
|
92
|
+
? startup.aiProvider
|
|
93
|
+
: DEFAULT_PROVIDER,
|
|
94
|
+
};
|
|
95
|
+
for (const name of exports.AI_ENGINE_SETTINGS) {
|
|
96
|
+
if (name === 'aiProvider')
|
|
97
|
+
continue;
|
|
98
|
+
const mine = saved[name];
|
|
99
|
+
out[name] = isNonEmpty(mine)
|
|
100
|
+
? mine.trim()
|
|
101
|
+
: isNonEmpty(startup[name])
|
|
102
|
+
? startup[name]
|
|
103
|
+
: undefined;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** Why `url` can't be saved as the base URL, or null. */
|
|
108
|
+
function baseUrlProblem(url) {
|
|
109
|
+
const why = "aiBaseUrl must be an http or https address with no user name, password or query (the API key comes from the server's environment), or empty to use the server's own.";
|
|
110
|
+
if (url.length > BASE_URL_MAX_LENGTH)
|
|
111
|
+
return why;
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = new URL(url);
|
|
115
|
+
}
|
|
116
|
+
catch (_a) {
|
|
117
|
+
return why;
|
|
118
|
+
}
|
|
119
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
120
|
+
return why;
|
|
121
|
+
// Anything that could carry a credential would be one saved in the database.
|
|
122
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
123
|
+
return why;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What is wrong with the AI engine fields of a settings update, or null.
|
|
128
|
+
* Checked before anything is saved: a saved value outlives the restart that
|
|
129
|
+
* used to undo it. Each may be empty, to go back to what the server was
|
|
130
|
+
* started with. Fields not sent are not checked.
|
|
131
|
+
*/
|
|
132
|
+
function validateAiEngineUpdate(body) {
|
|
133
|
+
const has = (key) => body[key] !== undefined;
|
|
134
|
+
if (has('aiProvider') && !(body.aiProvider === '' || isProvider(body.aiProvider))) {
|
|
135
|
+
return {
|
|
136
|
+
field: 'aiProvider',
|
|
137
|
+
message: `aiProvider must be one of ${exports.AI_PROVIDERS.join(', ')}, or empty to use the server's own.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
for (const field of MODEL_SETTINGS) {
|
|
141
|
+
if (!has(field))
|
|
142
|
+
continue;
|
|
143
|
+
const value = body[field];
|
|
144
|
+
const name = typeof value === 'string' ? value.trim() : null;
|
|
145
|
+
if (name === null || name.length > MODEL_MAX_LENGTH || /[\s\u0000-\u001f\u007f]/.test(name)) {
|
|
146
|
+
return {
|
|
147
|
+
field,
|
|
148
|
+
message: `${field} must be a model name with no spaces, of at most ${MODEL_MAX_LENGTH} characters, or empty to use the server's own.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (has('aiBaseUrl')) {
|
|
153
|
+
const value = body.aiBaseUrl;
|
|
154
|
+
const problem = typeof value !== 'string'
|
|
155
|
+
? 'aiBaseUrl must be text.'
|
|
156
|
+
: value.trim() === ''
|
|
157
|
+
? null
|
|
158
|
+
: baseUrlProblem(value.trim());
|
|
159
|
+
if (problem)
|
|
160
|
+
return { field: 'aiBaseUrl', message: problem };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/** The AI engine fields of a checked update, as they are saved: names trimmed. */
|
|
165
|
+
function aiEngineUpdateOf(body) {
|
|
166
|
+
const picked = pickAiEngine(body);
|
|
167
|
+
for (const name of Object.keys(picked))
|
|
168
|
+
picked[name] = picked[name].trim();
|
|
169
|
+
return picked;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Keeps `config`'s AI engine settings at the values in effect. Loaded once at
|
|
173
|
+
* boot, after the startup options were applied and the database is ready, and
|
|
174
|
+
* told of each save by `POST /config`.
|
|
175
|
+
*/
|
|
176
|
+
let AiEngineSettings = class AiEngineSettings {
|
|
177
|
+
constructor() {
|
|
178
|
+
/** What the dashboard saved. */
|
|
179
|
+
this.saved = {};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Reads the saved settings and applies them. A database that can't be read
|
|
183
|
+
* leaves the startup settings in charge, as it does for the other settings.
|
|
184
|
+
*/
|
|
185
|
+
load() {
|
|
186
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
187
|
+
this.startedWith();
|
|
188
|
+
try {
|
|
189
|
+
this.saved = pickAiEngine((yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()));
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
this.saved = {};
|
|
193
|
+
logger_1.default.warn(`Could not read the saved AI engine settings, using the startup options: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
194
|
+
}
|
|
195
|
+
this.apply();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/** What `POST /config` just saved, in force for the next AI call. */
|
|
199
|
+
noteSaved(update) {
|
|
200
|
+
this.startedWith();
|
|
201
|
+
this.saved = Object.assign(Object.assign({}, this.saved), update);
|
|
202
|
+
this.apply();
|
|
203
|
+
}
|
|
204
|
+
startedWith() {
|
|
205
|
+
if (!this.startup)
|
|
206
|
+
this.startup = pickAiEngine(config_1.config);
|
|
207
|
+
return this.startup;
|
|
208
|
+
}
|
|
209
|
+
apply() {
|
|
210
|
+
const before = config_1.config.aiProvider;
|
|
211
|
+
(0, config_1.updateConfig)(effectiveAiEngine(this.startedWith(), this.saved));
|
|
212
|
+
if (config_1.config.aiProvider !== before)
|
|
213
|
+
logger_1.default.info(`AI provider: ${config_1.config.aiProvider}`);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
exports.AiEngineSettings = AiEngineSettings;
|
|
217
|
+
exports.AiEngineSettings = AiEngineSettings = __decorate([
|
|
218
|
+
(0, typedi_1.Service)()
|
|
219
|
+
], AiEngineSettings);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.settingsDefaults = settingsDefaults;
|
|
16
|
+
exports.isValidCron = isValidCron;
|
|
17
|
+
exports.selfHealingEnabled = selfHealingEnabled;
|
|
18
|
+
exports.effectiveSettings = effectiveSettings;
|
|
19
|
+
exports.loadEffectiveSettings = loadEffectiveSettings;
|
|
20
|
+
exports.validateSettingsUpdate = validateSettingsUpdate;
|
|
21
|
+
const typedi_1 = require("typedi");
|
|
22
|
+
const node_schedule_1 = __importDefault(require("node-schedule"));
|
|
23
|
+
const schema_json_1 = __importDefault(require("../../../schema.json"));
|
|
24
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
25
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
26
|
+
const web_config_service_1 = require("../../data-service/web-config-service");
|
|
27
|
+
const schemaProperties = schema_json_1.default.properties;
|
|
28
|
+
function schemaDefault(key) {
|
|
29
|
+
return schemaProperties[key].default;
|
|
30
|
+
}
|
|
31
|
+
/** The defaults schema.json declares, which Appium fills in: the server's own. */
|
|
32
|
+
function settingsDefaults() {
|
|
33
|
+
return {
|
|
34
|
+
enableSelfHealing: schemaDefault('enableSelfHealing'),
|
|
35
|
+
healthCheckIntervalMs: schemaDefault('healthCheckIntervalMs'),
|
|
36
|
+
buildCleanupDays: schemaDefault('buildCleanupDays'),
|
|
37
|
+
buildCleanupMaxCount: schemaDefault('buildCleanupMaxCount'),
|
|
38
|
+
buildCleanupSchedule: schemaDefault('buildCleanupSchedule'),
|
|
39
|
+
deleteBuildAssets: schemaDefault('deleteBuildAssets'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a string is a cron expression the scheduler will run: five or six
|
|
44
|
+
* fields. node-schedule alone also takes a date ("2030-01-01") as a one-off
|
|
45
|
+
* job, which is not a schedule that repeats.
|
|
46
|
+
*/
|
|
47
|
+
function isValidCron(value) {
|
|
48
|
+
if (typeof value !== 'string')
|
|
49
|
+
return false;
|
|
50
|
+
const fields = value.trim().split(/\s+/);
|
|
51
|
+
if (fields.length < 5 || fields.length > 6)
|
|
52
|
+
return false;
|
|
53
|
+
if (!fields.every((field) => /^[0-9A-Za-z*/,\-?#]+$/.test(field)))
|
|
54
|
+
return false;
|
|
55
|
+
const job = new node_schedule_1.default.Job(() => undefined);
|
|
56
|
+
const ok = job.schedule(value.trim());
|
|
57
|
+
job.cancel();
|
|
58
|
+
return ok;
|
|
59
|
+
}
|
|
60
|
+
const wholeNumberAtLeastOne = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 1;
|
|
61
|
+
const positiveNumber = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
|
|
62
|
+
const isBoolean = (v) => typeof v === 'boolean';
|
|
63
|
+
/** The first value that is usable, in the order given. */
|
|
64
|
+
function firstValid(valid, ...candidates) {
|
|
65
|
+
return candidates.find(valid);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether self-healing runs: the value saved in the dashboard, else the plugin
|
|
69
|
+
* option, else the default (on). The same rule `effectiveSettings` applies,
|
|
70
|
+
* on its own because the command interceptor asks at every command and
|
|
71
|
+
* `effectiveSettings` also checks cron expressions. A value that is not a
|
|
72
|
+
* boolean is no choice, so `"false"` as an option does not turn healing off.
|
|
73
|
+
*/
|
|
74
|
+
function selfHealingEnabled(startupOption, saved) {
|
|
75
|
+
var _a;
|
|
76
|
+
const fallback = schemaDefault('enableSelfHealing');
|
|
77
|
+
return (_a = firstValid(isBoolean, saved, startupOption, fallback)) !== null && _a !== void 0 ? _a : fallback;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The settings in effect: saved in the dashboard, else the plugin option the
|
|
81
|
+
* server started with, else the default. A saved or started-with value that
|
|
82
|
+
* could not work (a retention window of 0 would purge everything) is skipped,
|
|
83
|
+
* so a bad row never reaches the cleanup job.
|
|
84
|
+
*/
|
|
85
|
+
function effectiveSettings(startup, saved) {
|
|
86
|
+
var _a, _b, _c, _d, _e;
|
|
87
|
+
const defaults = settingsDefaults();
|
|
88
|
+
const out = {
|
|
89
|
+
enableSelfHealing: selfHealingEnabled(startup.enableSelfHealing, saved.enableSelfHealing),
|
|
90
|
+
healthCheckIntervalMs: (_a = firstValid(positiveNumber, saved.healthCheckIntervalMs, startup.healthCheckIntervalMs, defaults.healthCheckIntervalMs)) !== null && _a !== void 0 ? _a : defaults.healthCheckIntervalMs,
|
|
91
|
+
buildCleanupDays: (_b = firstValid(wholeNumberAtLeastOne, saved.buildCleanupDays, startup.buildCleanupDays, defaults.buildCleanupDays)) !== null && _b !== void 0 ? _b : defaults.buildCleanupDays,
|
|
92
|
+
buildCleanupMaxCount: (_c = firstValid(wholeNumberAtLeastOne, saved.buildCleanupMaxCount, startup.buildCleanupMaxCount, defaults.buildCleanupMaxCount)) !== null && _c !== void 0 ? _c : defaults.buildCleanupMaxCount,
|
|
93
|
+
buildCleanupSchedule: (_d = firstValid(isValidCron, saved.buildCleanupSchedule, startup.buildCleanupSchedule, defaults.buildCleanupSchedule)) !== null && _d !== void 0 ? _d : defaults.buildCleanupSchedule,
|
|
94
|
+
deleteBuildAssets: (_e = firstValid(isBoolean, saved.deleteBuildAssets, startup.deleteBuildAssets, defaults.deleteBuildAssets)) !== null && _e !== void 0 ? _e : defaults.deleteBuildAssets,
|
|
95
|
+
};
|
|
96
|
+
// A saved schedule of '' is the person clearing it, to go back to the
|
|
97
|
+
// interval; only an unsaved one falls through to the startup option.
|
|
98
|
+
const schedule = typeof saved.healthCheckSchedule === 'string'
|
|
99
|
+
? saved.healthCheckSchedule
|
|
100
|
+
: startup.healthCheckSchedule;
|
|
101
|
+
if (typeof schedule === 'string' && schedule.trim() !== '') {
|
|
102
|
+
out.healthCheckSchedule = schedule;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** The settings in effect now: what is saved, over what the server started with. */
|
|
107
|
+
function loadEffectiveSettings() {
|
|
108
|
+
return __awaiter(this, arguments, void 0, function* (startup = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs) {
|
|
109
|
+
let saved = {};
|
|
110
|
+
try {
|
|
111
|
+
saved = yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// A cleanup that can't read the dashboard's values still runs, on the
|
|
115
|
+
// startup options: skipping a night's cleanup helps nobody.
|
|
116
|
+
logger_1.default.warn(`Could not read the saved settings, using the startup options: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
117
|
+
}
|
|
118
|
+
return effectiveSettings(startup, saved);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* What is wrong with a settings update, or null. The cleanup fields are held to
|
|
123
|
+
* a rule because a retention window of 0, or a cap of 0, would purge every
|
|
124
|
+
* build, and these values used to be inert; the self-healing switch because
|
|
125
|
+
* anything but true or false is no answer to "on or off". The other fields are
|
|
126
|
+
* not checked. Fields not sent are not checked either.
|
|
127
|
+
*/
|
|
128
|
+
function validateSettingsUpdate(body) {
|
|
129
|
+
const has = (key) => body[key] !== undefined;
|
|
130
|
+
for (const field of ['buildCleanupDays', 'buildCleanupMaxCount']) {
|
|
131
|
+
if (has(field) && !wholeNumberAtLeastOne(body[field])) {
|
|
132
|
+
return { field, message: `${field} must be a whole number of at least 1.` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (has('buildCleanupSchedule') && !isValidCron(body.buildCleanupSchedule)) {
|
|
136
|
+
return {
|
|
137
|
+
field: 'buildCleanupSchedule',
|
|
138
|
+
message: 'buildCleanupSchedule must be a cron expression with five or six fields, ' +
|
|
139
|
+
"such as '0 0 * * *' for midnight.",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (has('deleteBuildAssets') && typeof body.deleteBuildAssets !== 'boolean') {
|
|
143
|
+
return { field: 'deleteBuildAssets', message: 'deleteBuildAssets must be true or false.' };
|
|
144
|
+
}
|
|
145
|
+
if (has('enableSelfHealing') && typeof body.enableSelfHealing !== 'boolean') {
|
|
146
|
+
return { field: 'enableSelfHealing', message: 'enableSelfHealing must be true or false.' };
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|