@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
|
@@ -187,11 +187,19 @@ paths:
|
|
|
187
187
|
operationId: getLabSettings
|
|
188
188
|
summary: Read the lab's settings and AI provider
|
|
189
189
|
description: |
|
|
190
|
-
The lab-wide settings
|
|
191
|
-
with the AI settings
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
The lab-wide settings in effect (health checks, build cleanup and the AI self-healing
|
|
191
|
+
switch), merged with the AI settings in effect (provider, models and base URL). Each
|
|
192
|
+
setting is the value saved in the dashboard, else the plugin option or environment
|
|
193
|
+
variable the server was started with, else the default, which is also what the server
|
|
194
|
+
runs with. A model or base URL set nowhere is absent: the provider's own applies. A user
|
|
195
|
+
name, password or query value in the base URL is sent as `***`. `defaults` holds the defaults themselves, which
|
|
196
|
+
is what "restore defaults" goes back to. Only `healthCheckSchedule` can be absent: no
|
|
197
|
+
schedule is set. Through 2.13 `enableSelfHealing` was neither stored nor sent, so the
|
|
198
|
+
Settings page always showed it on.
|
|
199
|
+
API keys are never returned: `geminiSet`, `openaiSet` and `anthropicSet` say only
|
|
200
|
+
whether one is configured. Needs the `ADMIN` role or above, as the dashboard's settings
|
|
201
|
+
pages do; through 2.12 any signed-in user could read it. Through 2.13 a setting never
|
|
202
|
+
saved was absent, and `defaults` was not sent.
|
|
195
203
|
tags:
|
|
196
204
|
- Configuration
|
|
197
205
|
responses:
|
|
@@ -202,8 +210,19 @@ paths:
|
|
|
202
210
|
schema:
|
|
203
211
|
$ref: '#/components/schemas/PlatformSettings'
|
|
204
212
|
example:
|
|
213
|
+
enableSelfHealing: true
|
|
205
214
|
healthCheckIntervalMs: 300000
|
|
206
215
|
buildCleanupDays: 30
|
|
216
|
+
buildCleanupMaxCount: 100
|
|
217
|
+
buildCleanupSchedule: 0 0 * * *
|
|
218
|
+
deleteBuildAssets: true
|
|
219
|
+
defaults:
|
|
220
|
+
enableSelfHealing: true
|
|
221
|
+
healthCheckIntervalMs: 300000
|
|
222
|
+
buildCleanupDays: 30
|
|
223
|
+
buildCleanupMaxCount: 100
|
|
224
|
+
buildCleanupSchedule: 0 0 * * *
|
|
225
|
+
deleteBuildAssets: true
|
|
207
226
|
aiProvider: gemini
|
|
208
227
|
aiModel: gemini-3-flash-preview
|
|
209
228
|
geminiModel: gemini-3-flash-preview
|
|
@@ -233,11 +252,34 @@ paths:
|
|
|
233
252
|
Saves the lab-wide settings and changes the AI provider settings. Every field is
|
|
234
253
|
optional; fields not sent are left as they are, and unknown fields are ignored.
|
|
235
254
|
|
|
236
|
-
- Health-check and build-cleanup fields are stored in the database.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
255
|
+
- Health-check and build-cleanup fields are stored in the database. A stored value
|
|
256
|
+
replaces the plugin option the server was started with, and applies without a
|
|
257
|
+
restart: the health check within a minute, the retention values at the next cleanup
|
|
258
|
+
run, and a new `buildCleanupSchedule` at once. The cleanup fields are checked first
|
|
259
|
+
and refused with `400` when they could not work: `buildCleanupDays` and
|
|
260
|
+
`buildCleanupMaxCount` must be whole numbers of at least 1, `buildCleanupSchedule` a
|
|
261
|
+
cron expression of five or six fields, `deleteBuildAssets` true or false. Nothing is
|
|
262
|
+
saved then. Send only the fields to change: a field sent is saved as the lab's own.
|
|
263
|
+
Through 2.13 only one setting could be saved: saving a second failed with a `500`.
|
|
264
|
+
- `enableSelfHealing` (true or false; anything else is refused with `400`) is the AI
|
|
265
|
+
self-healing switch. A stored value replaces the plugin option the server was started
|
|
266
|
+
with, and applies to the next command this server runs, with no restart. A session's
|
|
267
|
+
own `xe:options.healingTiers` still limits the tiers it may use. Through 2.13 the
|
|
268
|
+
switch was accepted and ignored. The value belongs to the server it is saved on:
|
|
269
|
+
saving it on a hub changes the hub's own sessions, not those on a node's phones,
|
|
270
|
+
which the node's own setting decides.
|
|
271
|
+
- AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) are stored
|
|
272
|
+
too. A stored value replaces the plugin option or environment variable the server was
|
|
273
|
+
started with, and applies to the next AI call, with no restart. An empty value removes
|
|
274
|
+
the stored one, and the server's own applies again. They are checked first and refused
|
|
275
|
+
with `400`: `aiProvider` must be `gemini`, `openai`, `anthropic` or `ollama`; a model
|
|
276
|
+
name has no spaces and at most 200 characters; `aiBaseUrl` is an http or https address
|
|
277
|
+
with no user name, password or query. Through 2.14 they changed the running server
|
|
278
|
+
only, and a restart undid them. Like the self-healing switch, they belong to the server
|
|
279
|
+
they are saved on.
|
|
280
|
+
- API keys can't be set here, and are never stored: they come from the server's
|
|
281
|
+
environment. `aiTemperature`, `aiMaxTokens` and `aiTopP`, which the AI engine page sent
|
|
282
|
+
through 2.14, were never used by any AI call and are ignored like any unknown field.
|
|
241
283
|
|
|
242
284
|
Needs the `SUPER_ADMIN` role and the `admin` scope; an `ADMIN` gets `403` with a message
|
|
243
285
|
saying so (through 2.12 an `ADMIN` could). With the dashboard cookie the request must
|
|
@@ -254,6 +296,7 @@ paths:
|
|
|
254
296
|
healthCheckIntervalMs: 300000
|
|
255
297
|
buildCleanupDays: 14
|
|
256
298
|
deleteBuildAssets: true
|
|
299
|
+
enableSelfHealing: false
|
|
257
300
|
aiProvider: anthropic
|
|
258
301
|
anthropicModel: claude-sonnet-4-6
|
|
259
302
|
responses:
|
|
@@ -265,6 +308,16 @@ paths:
|
|
|
265
308
|
$ref: '#/components/schemas/Success'
|
|
266
309
|
example:
|
|
267
310
|
success: true
|
|
311
|
+
'400':
|
|
312
|
+
description: A build-cleanup field, the self-healing switch or an AI field has a value that could not work. Nothing was saved.
|
|
313
|
+
content:
|
|
314
|
+
application/json:
|
|
315
|
+
schema:
|
|
316
|
+
$ref: '#/components/schemas/Error'
|
|
317
|
+
example:
|
|
318
|
+
error: invalid_setting
|
|
319
|
+
field: buildCleanupDays
|
|
320
|
+
message: buildCleanupDays must be a whole number of at least 1.
|
|
268
321
|
'401':
|
|
269
322
|
$ref: '#/components/responses/Unauthorized'
|
|
270
323
|
'403':
|
|
@@ -279,7 +332,7 @@ paths:
|
|
|
279
332
|
$ref: '#/components/schemas/Error'
|
|
280
333
|
example:
|
|
281
334
|
error: true
|
|
282
|
-
message:
|
|
335
|
+
message: Can't reach database server
|
|
283
336
|
/api/config/reset-metrics:
|
|
284
337
|
post:
|
|
285
338
|
operationId: resetDeviceHealCounts
|
|
@@ -415,21 +468,39 @@ paths:
|
|
|
415
468
|
description: |
|
|
416
469
|
Adds a webhook, active at once. When one of its `events` happens Xenon POSTs to `url`:
|
|
417
470
|
|
|
418
|
-
- with `payloadTemplate`: the template, with each `{{
|
|
419
|
-
the event (`eventType` plus the event's fields
|
|
420
|
-
JSON,
|
|
471
|
+
- with `payloadTemplate`: the template, with each `{{name}}` (or `{{a.b}}`, `{{list.0.b}}`)
|
|
472
|
+
replaced from the event (`eventType` plus the event's fields, below). A template that
|
|
473
|
+
is JSON as written is filled in string by string, so a value holding a quote or a line
|
|
474
|
+
break can't break it, and is sent as that JSON. Any other template is filled in as
|
|
475
|
+
text and sent as JSON if the result parses, otherwise as `{ "text": "<result>" }`. A
|
|
476
|
+
`{{name}}` the event doesn't have is left as written;
|
|
421
477
|
- otherwise, `type: slack`: a Slack message with an attachment;
|
|
422
478
|
- otherwise: `{ "event": "<event>", "payload": { ... } }`.
|
|
423
479
|
|
|
424
|
-
Each delivery is one POST, never retried. A
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
480
|
+
Each delivery is one POST, never retried. A webhook that refuses an event or can't be
|
|
481
|
+
reached doesn't fail the event: the failure is logged on the server and the other
|
|
482
|
+
webhooks still get it. Use `POST /api/webhook/test` to see whether a webhook accepts
|
|
483
|
+
deliveries.
|
|
428
484
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
485
|
+
What each event carries (a device event also has the device's other fields, which may
|
|
486
|
+
change; only the names listed are promised):
|
|
487
|
+
|
|
488
|
+
- `device_offline`, `device_new`: `udid`, `name`, `host`, `platform`.
|
|
489
|
+
Sent when a device stops being reported, or is first detected.
|
|
490
|
+
|
|
491
|
+
- `session_failed`: `sessionId`, `sessionName`, `failureReason`, `udid`, `deviceName`,
|
|
492
|
+
`platform`, `osVersion`, `startTime`, `endTime` (ISO 8601; unknown values are empty
|
|
493
|
+
text). Sent once per session when it ends as failed: the test marked it failed, a command
|
|
494
|
+
failed, it timed out for inactivity, its driver crashed, or its heartbeat stopped. Not
|
|
495
|
+
when the server itself shuts down, not for a session already failed when the server
|
|
496
|
+
started, and not when the dashboard is off, since the session's record is what it is
|
|
497
|
+
built from.
|
|
498
|
+
|
|
499
|
+
- `selector_health_digest`: `windowDays`, `totalHeals`, `distinctSelectors` and
|
|
500
|
+
`hotspots`, a list of `healCount`, `originalSelector` and `suggestedRewrite`.
|
|
501
|
+
|
|
502
|
+
Neither `url` nor the event names are checked. Needs the `ADMIN` role and the `admin`
|
|
503
|
+
scope. With the dashboard cookie the request must come from the same host.
|
|
433
504
|
tags:
|
|
434
505
|
- Webhooks
|
|
435
506
|
requestBody:
|
|
@@ -536,11 +607,12 @@ paths:
|
|
|
536
607
|
operationId: testWebhook
|
|
537
608
|
summary: Send a test message to a URL
|
|
538
609
|
description: |
|
|
539
|
-
Sends a sample `device_new`
|
|
540
|
-
`test-device-udid`, host `127.0.0.1
|
|
541
|
-
`payloadTemplate` gets its real events (see
|
|
542
|
-
|
|
543
|
-
|
|
610
|
+
Sends a sample of `event` (`device_new` unless named; a device named `Test Device`, udid
|
|
611
|
+
`test-device-udid`, host `127.0.0.1`, or for `session_failed` a failed session on it) to
|
|
612
|
+
`url` the way a webhook with this `type` and `payloadTemplate` gets its real events (see
|
|
613
|
+
`POST /api/webhook`, which lists what each event carries): the filled-in template if
|
|
614
|
+
there is one, else a Slack message for `type: slack` (the default), else
|
|
615
|
+
`{ "event": "<event>", "payload": { ... } }`. No stored webhook is involved.
|
|
544
616
|
|
|
545
617
|
Answers `200` when `url` accepted the delivery (a 2xx), and `502 delivery_failed`, with
|
|
546
618
|
the reason, when it refused it or couldn't be reached. Needs the `ADMIN` role and the
|
|
@@ -575,6 +647,16 @@ paths:
|
|
|
575
647
|
The webhook's template, filled in from the sample event as a real event's
|
|
576
648
|
would be.
|
|
577
649
|
example: '{"text": "{{eventType}}: {{name}} ({{udid}})"}'
|
|
650
|
+
event:
|
|
651
|
+
type: string
|
|
652
|
+
default: device_new
|
|
653
|
+
enum:
|
|
654
|
+
- device_offline
|
|
655
|
+
- session_failed
|
|
656
|
+
- device_new
|
|
657
|
+
- selector_health_digest
|
|
658
|
+
description: Which event to send a sample of.
|
|
659
|
+
example: session_failed
|
|
578
660
|
example:
|
|
579
661
|
url: https://hooks.slack.com/services/T000/B000/XXXXXXXX
|
|
580
662
|
type: slack
|
|
@@ -588,13 +670,18 @@ paths:
|
|
|
588
670
|
example:
|
|
589
671
|
success: true
|
|
590
672
|
'400':
|
|
591
|
-
description: '`url` is missing or not a string.'
|
|
673
|
+
description: '`url` is missing or not a string, or `event` is not one of the four.'
|
|
592
674
|
content:
|
|
593
675
|
application/json:
|
|
594
676
|
schema:
|
|
595
677
|
$ref: '#/components/schemas/Error'
|
|
596
|
-
|
|
597
|
-
|
|
678
|
+
examples:
|
|
679
|
+
noUrl:
|
|
680
|
+
value:
|
|
681
|
+
error: url is required
|
|
682
|
+
badEvent:
|
|
683
|
+
value:
|
|
684
|
+
error: 'event must be one of: device_offline, session_failed, device_new, selector_health_digest'
|
|
598
685
|
'401':
|
|
599
686
|
$ref: '#/components/responses/Unauthorized'
|
|
600
687
|
'403':
|
|
@@ -2264,8 +2351,12 @@ components:
|
|
|
2264
2351
|
example: 734120
|
|
2265
2352
|
PlatformSettings:
|
|
2266
2353
|
type: object
|
|
2267
|
-
description: The lab's
|
|
2354
|
+
description: The lab's settings in effect (saved, else the startup option, else the default), merged with the AI settings in effect. Only `healthCheckSchedule` can be absent.
|
|
2268
2355
|
properties:
|
|
2356
|
+
enableSelfHealing:
|
|
2357
|
+
type: boolean
|
|
2358
|
+
description: Whether a failed findElement goes on to the self-healing tiers.
|
|
2359
|
+
example: true
|
|
2269
2360
|
healthCheckIntervalMs:
|
|
2270
2361
|
type: integer
|
|
2271
2362
|
example: 300000
|
|
@@ -2283,8 +2374,30 @@ components:
|
|
|
2283
2374
|
example: 0 3 * * *
|
|
2284
2375
|
deleteBuildAssets:
|
|
2285
2376
|
type: boolean
|
|
2377
|
+
defaults:
|
|
2378
|
+
type: object
|
|
2379
|
+
description: The server's own defaults for the settings above, which is what "restore defaults" goes back to.
|
|
2380
|
+
properties:
|
|
2381
|
+
enableSelfHealing:
|
|
2382
|
+
type: boolean
|
|
2383
|
+
example: true
|
|
2384
|
+
healthCheckIntervalMs:
|
|
2385
|
+
type: integer
|
|
2386
|
+
example: 300000
|
|
2387
|
+
buildCleanupDays:
|
|
2388
|
+
type: integer
|
|
2389
|
+
example: 30
|
|
2390
|
+
buildCleanupMaxCount:
|
|
2391
|
+
type: integer
|
|
2392
|
+
example: 100
|
|
2393
|
+
buildCleanupSchedule:
|
|
2394
|
+
type: string
|
|
2395
|
+
example: 0 0 * * *
|
|
2396
|
+
deleteBuildAssets:
|
|
2397
|
+
type: boolean
|
|
2286
2398
|
aiProvider:
|
|
2287
2399
|
type: string
|
|
2400
|
+
description: The provider in effect, saved on the AI engine page, else the server's option or environment, else gemini.
|
|
2288
2401
|
example: gemini
|
|
2289
2402
|
aiModel:
|
|
2290
2403
|
type: string
|
|
@@ -2311,6 +2424,10 @@ components:
|
|
|
2311
2424
|
type: object
|
|
2312
2425
|
description: Any subset. Unknown fields are ignored.
|
|
2313
2426
|
properties:
|
|
2427
|
+
enableSelfHealing:
|
|
2428
|
+
type: boolean
|
|
2429
|
+
description: The AI self-healing switch. Takes effect at the next command, with no restart.
|
|
2430
|
+
example: false
|
|
2314
2431
|
healthCheckIntervalMs:
|
|
2315
2432
|
type: integer
|
|
2316
2433
|
example: 300000
|
|
@@ -2332,30 +2449,37 @@ components:
|
|
|
2332
2449
|
type: boolean
|
|
2333
2450
|
aiProvider:
|
|
2334
2451
|
type: string
|
|
2335
|
-
description:
|
|
2452
|
+
description: The provider every AI call uses. Stored; empty goes back to the server's own.
|
|
2336
2453
|
enum:
|
|
2337
2454
|
- gemini
|
|
2338
2455
|
- openai
|
|
2339
2456
|
- anthropic
|
|
2340
2457
|
- ollama
|
|
2458
|
+
- ''
|
|
2341
2459
|
aiModel:
|
|
2342
2460
|
type: string
|
|
2343
|
-
description:
|
|
2461
|
+
description: The model for a provider with no model of its own below. Stored; empty goes back to the server's own.
|
|
2462
|
+
maxLength: 200
|
|
2344
2463
|
aiBaseUrl:
|
|
2345
2464
|
type: string
|
|
2346
|
-
description:
|
|
2465
|
+
description: Where OpenAI-compatible and Ollama calls go. An http or https address with no user name, password or query. Stored; empty goes back to the server's own.
|
|
2466
|
+
example: http://gpu-box.lab:11434
|
|
2347
2467
|
geminiModel:
|
|
2348
2468
|
type: string
|
|
2349
|
-
description:
|
|
2469
|
+
description: Stored; empty goes back to the server's own.
|
|
2470
|
+
maxLength: 200
|
|
2350
2471
|
openaiModel:
|
|
2351
2472
|
type: string
|
|
2352
|
-
description:
|
|
2473
|
+
description: Stored; empty goes back to the server's own.
|
|
2474
|
+
maxLength: 200
|
|
2353
2475
|
anthropicModel:
|
|
2354
2476
|
type: string
|
|
2355
|
-
description:
|
|
2477
|
+
description: Stored; empty goes back to the server's own.
|
|
2478
|
+
maxLength: 200
|
|
2356
2479
|
ollamaModel:
|
|
2357
2480
|
type: string
|
|
2358
|
-
description:
|
|
2481
|
+
description: Stored; empty goes back to the server's own.
|
|
2482
|
+
maxLength: 200
|
|
2359
2483
|
PlatformAiTestRequest:
|
|
2360
2484
|
type: object
|
|
2361
2485
|
description: Every field is optional and falls back to the server's current setting.
|
|
@@ -2448,7 +2572,9 @@ components:
|
|
|
2448
2572
|
example: slack
|
|
2449
2573
|
payloadTemplate:
|
|
2450
2574
|
type: string
|
|
2451
|
-
description:
|
|
2575
|
+
description: >-
|
|
2576
|
+
A body template with `{{name}}` placeholders, used in place of the default body (see
|
|
2577
|
+
`POST /api/webhook` for the names each event has).
|
|
2452
2578
|
example: '{"text":"{{eventType}} on {{udid}}"}'
|
|
2453
2579
|
PlatformApp:
|
|
2454
2580
|
type: object
|
|
@@ -108,13 +108,13 @@ paths:
|
|
|
108
108
|
name: Checkout with saved card
|
|
109
109
|
status: failed
|
|
110
110
|
node_id: 3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d
|
|
111
|
-
has_live_video:
|
|
111
|
+
has_live_video: false
|
|
112
112
|
video_recording_enabled: true
|
|
113
113
|
video_recording: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.mp4
|
|
114
114
|
startTime: '2026-10-03T14:20:11.004Z'
|
|
115
115
|
endTime: '2026-10-03T14:22:05.120Z'
|
|
116
116
|
failure_reason: 'NoSuchElementError: An element could not be located'
|
|
117
|
-
failure_category:
|
|
117
|
+
failure_category: ELEMENT_NOT_FOUND
|
|
118
118
|
device_udid: emulator-5554
|
|
119
119
|
device_platform: android
|
|
120
120
|
device_version: '14'
|
|
@@ -923,7 +923,7 @@ paths:
|
|
|
923
923
|
type: string
|
|
924
924
|
example: |
|
|
925
925
|
id,build_id,status,failure_category,failure_reason,device_platform,device_version,device_name,node_id,startTime,endTime,name
|
|
926
|
-
6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,
|
|
926
|
+
6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,ELEMENT_NOT_FOUND,"NoSuchElementError: An element could not be located",android,14,Pixel 7,3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d,2026-10-03T14:20:11.004Z,2026-10-03T14:22:05.120Z,Checkout with saved card
|
|
927
927
|
'400':
|
|
928
928
|
description: '`format` is neither `json` nor `csv`.'
|
|
929
929
|
content:
|
|
@@ -2688,6 +2688,13 @@ components:
|
|
|
2688
2688
|
failure_category:
|
|
2689
2689
|
type: string
|
|
2690
2690
|
nullable: true
|
|
2691
|
+
description: >-
|
|
2692
|
+
What kind of failure it was: `ELEMENT_NOT_FOUND`, `APP_CRASH`, `TIMEOUT`,
|
|
2693
|
+
`PERMISSION_BLOCKED`, `WDA_FAILURE`, `XENON_COMMAND_FAILURE`, `SYSTEM_OVERLOAD` or
|
|
2694
|
+
`UNKNOWN`, read from the failure reason and the last failed commands when the
|
|
2695
|
+
session fails; or `HUB_RESTART` for a session the server's restart ended. Null for a
|
|
2696
|
+
session that hasn't failed, and for a failure recorded without that analysis.
|
|
2697
|
+
example: ELEMENT_NOT_FOUND
|
|
2691
2698
|
is_profiling_available:
|
|
2692
2699
|
type: boolean
|
|
2693
2700
|
device_info:
|
|
@@ -15,6 +15,8 @@ const typedi_1 = require("typedi");
|
|
|
15
15
|
const ApiKeyService_1 = require("../../services/ApiKeyService");
|
|
16
16
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
17
17
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
18
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
19
|
+
const KNOWN_SCOPES = ['read', 'sessions', 'devices', 'admin'];
|
|
18
20
|
function apiKeysRouter() {
|
|
19
21
|
const r = (0, express_1.Router)();
|
|
20
22
|
r.use((0, roleGuard_1.roleGuard)('ADMIN'));
|
|
@@ -23,11 +25,16 @@ function apiKeysRouter() {
|
|
|
23
25
|
res.json(yield svc.list());
|
|
24
26
|
}));
|
|
25
27
|
r.post('/', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
26
|
-
var _a;
|
|
28
|
+
var _a, _b;
|
|
27
29
|
const { name, scopes, rateLimit, teamId, expiresAt } = req.body;
|
|
28
30
|
if (!name || !Array.isArray(scopes) || scopes.length === 0) {
|
|
29
31
|
return res.status(400).json({ error: 'name and scopes required' });
|
|
30
32
|
}
|
|
33
|
+
// Only the scopes that exist. Any string was stored, so a typo made a key
|
|
34
|
+
// with none of the scopes its maker meant.
|
|
35
|
+
if (!scopes.every((s) => KNOWN_SCOPES.includes(s))) {
|
|
36
|
+
return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
|
|
37
|
+
}
|
|
31
38
|
let expiresAtDate;
|
|
32
39
|
if (expiresAt !== undefined && expiresAt !== null) {
|
|
33
40
|
const d = new Date(expiresAt);
|
|
@@ -39,6 +46,11 @@ function apiKeysRouter() {
|
|
|
39
46
|
}
|
|
40
47
|
expiresAtDate = d;
|
|
41
48
|
}
|
|
49
|
+
// Made with a credential that expires, a key ends no later than it.
|
|
50
|
+
const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.credentialExpiresAt);
|
|
51
|
+
if ('error' in within)
|
|
52
|
+
return res.status(400).json({ error: within.error });
|
|
53
|
+
expiresAtDate = within.expiresAt;
|
|
42
54
|
const { id, raw } = yield svc.create({
|
|
43
55
|
name,
|
|
44
56
|
scopes,
|
|
@@ -47,7 +59,7 @@ function apiKeysRouter() {
|
|
|
47
59
|
userId: req.auth.userId,
|
|
48
60
|
expiresAt: expiresAtDate,
|
|
49
61
|
});
|
|
50
|
-
res.json({ id, key: raw, expiresAt: (
|
|
62
|
+
res.json({ id, key: raw, expiresAt: (_b = expiresAtDate === null || expiresAtDate === void 0 ? void 0 : expiresAtDate.toISOString()) !== null && _b !== void 0 ? _b : null });
|
|
51
63
|
}));
|
|
52
64
|
r.delete('/:id', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
53
65
|
yield svc.revoke(req.params.id);
|
|
@@ -28,11 +28,15 @@ const JwtKeyService_1 = require("../../services/token/JwtKeyService");
|
|
|
28
28
|
const mcpScopes_1 = require("../../services/token/mcpScopes");
|
|
29
29
|
const passwordResetMode_1 = require("../../services/passwordResetMode");
|
|
30
30
|
const passwordResetLink_1 = require("../../services/passwordResetLink");
|
|
31
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
31
32
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
32
33
|
const isSecureFromReq = (req) => req.secure || req.headers['x-forwarded-proto'] === 'https';
|
|
33
34
|
const MCP_TTL_SEC = Number(process.env.XENON_MCP_TOKEN_TTL_SEC || 86400);
|
|
34
35
|
const REST_TTL_SEC = 3600;
|
|
35
36
|
const MINTABLE_AUDIENCES = ['xenon-rest', 'xenon-mcp'];
|
|
37
|
+
function grantsScope(scopesCsv, scope) {
|
|
38
|
+
return scopesCsv.split(',').some((s) => s.trim() === scope);
|
|
39
|
+
}
|
|
36
40
|
function issueToken(auth, body) {
|
|
37
41
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
42
|
var _a, _b, _c, _d;
|
|
@@ -40,7 +44,7 @@ function issueToken(auth, body) {
|
|
|
40
44
|
if (!MINTABLE_AUDIENCES.includes(audience)) {
|
|
41
45
|
throw new Error(`unsupported audience: ${audience}`);
|
|
42
46
|
}
|
|
43
|
-
const expiresIn = audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC;
|
|
47
|
+
const expiresIn = (0, mintedLifetime_1.mintedLifetimeSec)(audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC, auth.credentialExpiresAt);
|
|
44
48
|
const svc = typedi_1.Container.get(JwtKeyService_1.JwtKeyService);
|
|
45
49
|
if (audience === 'xenon-mcp') {
|
|
46
50
|
// Validate the untrusted `scopes` body field before it reaches the mapper:
|
|
@@ -66,7 +70,23 @@ function issueToken(auth, body) {
|
|
|
66
70
|
// Session-token capability (spec §3 item 6 / R9): a sibling credential the
|
|
67
71
|
// client injects as `xe:options.sessionToken` so the Appium createSession
|
|
68
72
|
// interceptor can refuse tokenless direct-connect sessions when the gate is on.
|
|
69
|
-
|
|
73
|
+
//
|
|
74
|
+
// It creates Appium sessions, so it comes only with a grant that includes
|
|
75
|
+
// `appium:use`, which needs the `sessions` scope. Through 2.14 every
|
|
76
|
+
// credential got one, a read-only key included. It carries the scopes a
|
|
77
|
+
// session is judged by: `sessions`, and `admin` (a lease override) only
|
|
78
|
+
// when the minting credential has it and the grant isn't narrower than
|
|
79
|
+
// it: the default grant (no scopes asked for), or a full-admin one.
|
|
80
|
+
if (!grant.granular.includes('appium:use')) {
|
|
81
|
+
return { token, expiresIn, audience, scopes: grant.granular };
|
|
82
|
+
}
|
|
83
|
+
const askedForScopes = Array.isArray(body.scopes) && body.scopes.length > 0;
|
|
84
|
+
const sessionAdmin = grantsScope(auth.scopes, 'admin') && (!askedForScopes || grant.roles.includes('admin'));
|
|
85
|
+
const sessionToken = yield svc.sign({
|
|
86
|
+
sub: auth.userId,
|
|
87
|
+
teamId: (_c = auth.teamId) !== null && _c !== void 0 ? _c : null,
|
|
88
|
+
scopes: sessionAdmin ? 'admin,sessions' : 'sessions',
|
|
89
|
+
}, { audience: 'xenon-session', ttlSeconds: expiresIn });
|
|
70
90
|
return { token, expiresIn, audience, scopes: grant.granular, sessionToken };
|
|
71
91
|
}
|
|
72
92
|
const token = yield svc.sign({ sub: auth.userId, role: auth.role, scopes: auth.scopes, teamId: (_d = auth.teamId) !== null && _d !== void 0 ? _d : null }, { audience, ttlSeconds: expiresIn });
|
|
@@ -139,11 +159,15 @@ function authPublicRouter() {
|
|
|
139
159
|
const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
|
|
140
160
|
// No SMTP and no opt-in log fallback: nothing can reach the user, so
|
|
141
161
|
// don't mint a live credential. Admins issue links from the Users page.
|
|
142
|
-
|
|
162
|
+
// No XENON_PUBLIC_URL: nothing says where the link may point. The
|
|
163
|
+
// request's Host is the asker's to choose, and the asker may not be
|
|
164
|
+
// the account's owner, so it is never used here.
|
|
165
|
+
const base = (0, passwordResetLink_1.resetLinkBase)();
|
|
166
|
+
if (user && user.status === 'ACTIVE' && emailSvc.canDeliver() && base) {
|
|
143
167
|
const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
|
|
144
168
|
const { raw } = yield resetSvc.createToken(user.id);
|
|
145
|
-
const link = (0, passwordResetLink_1.buildResetLink)(
|
|
146
|
-
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link));
|
|
169
|
+
const link = (0, passwordResetLink_1.buildResetLink)(base, raw);
|
|
170
|
+
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link, resetSvc.ttlMs()));
|
|
147
171
|
}
|
|
148
172
|
}
|
|
149
173
|
catch (e) {
|
|
@@ -625,7 +625,7 @@ const MJPEG_PROXY_CACHE = new Map();
|
|
|
625
625
|
* Start stream endpoint - Initiates WDA and MJPEG streaming for iOS devices
|
|
626
626
|
*/
|
|
627
627
|
router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
628
|
-
var _a, _b, _c, _d, _e;
|
|
628
|
+
var _a, _b, _c, _d, _e, _f;
|
|
629
629
|
const { udid } = req.params;
|
|
630
630
|
// Someone is about to watch: a page that just left (a reload, or another tab
|
|
631
631
|
// closing) must not stop the stream this page is starting.
|
|
@@ -709,7 +709,11 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
709
709
|
// A recording device keeps MJPEG preview so we never run screenrecord and
|
|
710
710
|
// the recording's screencap pipeline against the same device at once.
|
|
711
711
|
const recording = yield typedi_1.Container.get(recording_store_1.RecordingStore).isRecording(udid);
|
|
712
|
-
|
|
712
|
+
// A page that shows MJPEG says so (`player: 'mjpeg'`): device control, a
|
|
713
|
+
// browser with no WebCodecs, or a page whose H.264 player just failed.
|
|
714
|
+
// Anything else is the server's choice.
|
|
715
|
+
const clientShowsMjpeg = ((_f = req.body) === null || _f === void 0 ? void 0 : _f.player) === 'mjpeg';
|
|
716
|
+
const streamType = (0, streamType_1.resolveStreamType)(device.platform, flagOn, recording, !clientShowsMjpeg);
|
|
713
717
|
let mjpegPort;
|
|
714
718
|
if (device.platform === 'ios' || device.platform === 'tvos') {
|
|
715
719
|
mjpegPort = (yield typedi_1.Container.get(IOSStreamService_1.default).startStream(udid)).mjpegPort;
|
|
@@ -720,6 +724,20 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
720
724
|
yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid, { source: h264Cfg.source });
|
|
721
725
|
}
|
|
722
726
|
else {
|
|
727
|
+
// One capture pipeline per device where possible.
|
|
728
|
+
const h264 = typedi_1.Container.get(AndroidH264StreamService_1.default);
|
|
729
|
+
if (recording) {
|
|
730
|
+
// A recording reads the MJPEG capture, and H.264 never runs beside it.
|
|
731
|
+
if (h264.getMultiplexer(udid))
|
|
732
|
+
yield h264.stop(udid);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// This page shows MJPEG. An H.264 capture still running for the phone
|
|
736
|
+
// (this page's own before its player failed, or a tile's) ends before
|
|
737
|
+
// the screencap loop starts if nobody watches it, else when its last
|
|
738
|
+
// viewer leaves: another tile or tab may still be playing it.
|
|
739
|
+
yield h264.endWhenUnwatched(udid);
|
|
740
|
+
}
|
|
723
741
|
mjpegPort = (yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(udid)).mjpegPort;
|
|
724
742
|
}
|
|
725
743
|
// Lazy-load the screen size if missing: tiles need it to accept taps.
|