@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
|
@@ -69,7 +69,6 @@ const device_utils_1 = require("../device-utils");
|
|
|
69
69
|
const TracingService_1 = require("./TracingService");
|
|
70
70
|
const PortAllocator_1 = require("./PortAllocator");
|
|
71
71
|
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
72
|
-
const JwtKeyService_1 = require("./token/JwtKeyService");
|
|
73
72
|
const sessionIdentity_1 = require("./session/sessionIdentity");
|
|
74
73
|
const leaseSessionCaps_1 = require("./lease/leaseSessionCaps");
|
|
75
74
|
const sessionCredentials_1 = require("./session/sessionCredentials");
|
|
@@ -80,6 +79,7 @@ const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
|
80
79
|
const queueVisibility_1 = require("./device-access/queueVisibility");
|
|
81
80
|
const appVisibility_1 = require("./device-access/appVisibility");
|
|
82
81
|
const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
|
|
82
|
+
const sessionMemory_1 = require("../sessions/sessionMemory");
|
|
83
83
|
const SessionMetricsService_1 = require("./metrics/SessionMetricsService");
|
|
84
84
|
const appCapability_1 = require("./session/appCapability");
|
|
85
85
|
const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
|
|
@@ -98,6 +98,23 @@ const SessionStatus_1 = require("../types/SessionStatus");
|
|
|
98
98
|
const SessionType_1 = __importDefault(require("../enums/SessionType"));
|
|
99
99
|
const async_lock_1 = __importDefault(require("async-lock"));
|
|
100
100
|
const base_driver_1 = require("@appium/base-driver");
|
|
101
|
+
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
102
|
+
const interceptorOptions_1 = require("./interceptor/interceptorOptions");
|
|
103
|
+
/**
|
|
104
|
+
* A session token admits a session only with the `sessions` scope, as a key
|
|
105
|
+
* pair does. Tokens minted before this check carry no `scopes` claim, and
|
|
106
|
+
* POST /auth/token gave one to any credential, a read-only one included:
|
|
107
|
+
* they are refused too.
|
|
108
|
+
*/
|
|
109
|
+
function sessionTokenGrantsSessions(scopes) {
|
|
110
|
+
if (typeof scopes !== 'string')
|
|
111
|
+
return false;
|
|
112
|
+
const owned = scopes.split(',').map((s) => s.trim());
|
|
113
|
+
return owned.includes('sessions') || owned.includes('admin');
|
|
114
|
+
}
|
|
115
|
+
const SESSION_TOKEN_LACKS_SCOPE = 'session rejected: xe:options.sessionToken carries no `sessions` scope; tokens minted by ' +
|
|
116
|
+
'Xenon 2.14 or earlier carry none — mint a new one with POST /xenon/api/auth/token ' +
|
|
117
|
+
'({"audience":"xenon-mcp"}) using a credential that has the `sessions` scope.';
|
|
101
118
|
/**
|
|
102
119
|
* How long the hub waits for a node (or cloud provider) to create a session.
|
|
103
120
|
* A create is sent once (InternalRequestConfig `retry: false`): the node may
|
|
@@ -261,6 +278,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
261
278
|
apiKeyId: authResult.apiKeyId,
|
|
262
279
|
userId: authResult.userId,
|
|
263
280
|
appDownload,
|
|
281
|
+
allocatedAt: Date.now(),
|
|
264
282
|
};
|
|
265
283
|
// The grant opens one phone, on this node. A token taken to another node
|
|
266
284
|
// (an emulator's udid repeats across machines), or naming a phone this
|
|
@@ -389,7 +407,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
389
407
|
this.logger.debug(`📱 Removing pending session with capability_id: ${allocation.pendingSessionId}`);
|
|
390
408
|
yield (0, pending_sessions_service_1.removePendingSession)(allocation.pendingSessionId);
|
|
391
409
|
if (this.isCreateSessionResponseInternal(session)) {
|
|
392
|
-
yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId);
|
|
410
|
+
yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId, allocation.allocatedAt);
|
|
393
411
|
}
|
|
394
412
|
else {
|
|
395
413
|
yield this.handleSessionFailure(session, device, remote);
|
|
@@ -503,9 +521,11 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
503
521
|
// took out of `xe:options` (or its `xenon:options` alias): an access-key +
|
|
504
522
|
// token pair, or a session token. Today this is best-effort: if none is
|
|
505
523
|
// supplied we log a warning and let the request through to avoid breaking
|
|
506
|
-
// pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on.
|
|
507
|
-
//
|
|
508
|
-
//
|
|
524
|
+
// pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on.
|
|
525
|
+
// Credentials that don't verify — a wrong, revoked or expired key or token,
|
|
526
|
+
// or one whose user is deleted or not ACTIVE — count as none. Credentials
|
|
527
|
+
// that verify without the `sessions` scope are rejected. Respects the
|
|
528
|
+
// global `authDisabled` flag.
|
|
509
529
|
//
|
|
510
530
|
// Returns the caller's { apiKeyId, callerTeamIds } so allocation can filter
|
|
511
531
|
// devices, and app resolution uploaded apps, by team. `apiKeyId` is null
|
|
@@ -547,23 +567,47 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
547
567
|
}
|
|
548
568
|
const { ApiKeyService } = yield Promise.resolve().then(() => __importStar(require('./ApiKeyService')));
|
|
549
569
|
const svc = typedi_1.Container.get(ApiKeyService);
|
|
570
|
+
const { verifyKeyPairCredential, verifySessionTokenCredential, isWrongSessionToken } = yield Promise.resolve().then(() => __importStar(require('../middleware/verifyCredential')));
|
|
550
571
|
// xe:options.{accessKey, token} — the only key-pair credential shape.
|
|
572
|
+
// Checked by REST's rule (verifyKeyPairCredential): a live key whose owner
|
|
573
|
+
// exists and is ACTIVE. The key alone (verifyPair) let a user set to
|
|
574
|
+
// Inactive, or deleted, keep creating sessions as themselves. The owner
|
|
575
|
+
// found here is the one lookup the session's scope and lease use.
|
|
551
576
|
const pair = credentials.pair;
|
|
552
|
-
const
|
|
577
|
+
const verifiedPair = pair ? yield verifyKeyPairCredential(pair.accessKey, pair.token) : null;
|
|
578
|
+
// The session token, verified once for both the gate and attribution, by
|
|
579
|
+
// REST's rule too: a live signature and an ACTIVE subject, looked up now.
|
|
580
|
+
let tokenCheck;
|
|
581
|
+
const verifyToken = (t) => {
|
|
582
|
+
if (!tokenCheck)
|
|
583
|
+
tokenCheck = verifySessionTokenCredential(t);
|
|
584
|
+
return tokenCheck;
|
|
585
|
+
};
|
|
586
|
+
// Both readers below treat any failure as a token that doesn't check out:
|
|
587
|
+
// the gate refuses it as "invalid or expired", attribution ignores it. A
|
|
588
|
+
// check that could not run (the database or the signing key unavailable)
|
|
589
|
+
// is not that: it refuses the create here, as a key whose owner can't be
|
|
590
|
+
// looked up does, rather than run the session with no owner.
|
|
591
|
+
if (!verifiedPair && credentials.sessionToken) {
|
|
592
|
+
yield verifyToken(credentials.sessionToken).catch((err) => {
|
|
593
|
+
if (!isWrongSessionToken(err))
|
|
594
|
+
throw err;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
553
597
|
const { assertSessionTokenGate, sessionTokenGateEnabled } = yield Promise.resolve().then(() => __importStar(require('./sessionTokenGate')));
|
|
554
598
|
try {
|
|
555
599
|
yield assertSessionTokenGate({
|
|
556
600
|
enabled: sessionTokenGateEnabled(),
|
|
557
|
-
hasValidKeyPair: !!
|
|
601
|
+
hasValidKeyPair: !!verifiedPair,
|
|
558
602
|
token: credentials.sessionToken,
|
|
559
|
-
verify:
|
|
603
|
+
verify: verifyToken,
|
|
560
604
|
});
|
|
561
605
|
}
|
|
562
606
|
catch (err) {
|
|
563
607
|
this.logger.error(`❌ ${err.message}`);
|
|
564
608
|
throw new base_driver_1.errors.InvalidArgumentError(err.message);
|
|
565
609
|
}
|
|
566
|
-
if (!
|
|
610
|
+
if (!verifiedPair) {
|
|
567
611
|
// No key pair. An xe:options.sessionToken still identifies the caller,
|
|
568
612
|
// so read it for attribution even when the gate is off — otherwise the
|
|
569
613
|
// ownership guard fails closed on a session whose owner we actually know.
|
|
@@ -571,23 +615,36 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
571
615
|
// The token's teamId claim narrows it the way a key's team does; kept
|
|
572
616
|
// from the one verify rather than verifying again.
|
|
573
617
|
let tokenTeamId = null;
|
|
618
|
+
let tokenScopes;
|
|
619
|
+
let tokenOwner;
|
|
574
620
|
const identity = yield (0, sessionIdentity_1.resolveSessionIdentity)({
|
|
575
621
|
row: null,
|
|
576
622
|
sessionToken: credentials.sessionToken,
|
|
577
623
|
verify: (t) => __awaiter(this, void 0, void 0, function* () {
|
|
578
|
-
const payload = yield
|
|
579
|
-
audience: 'xenon-session',
|
|
580
|
-
});
|
|
624
|
+
const { payload, user } = yield verifyToken(t);
|
|
581
625
|
tokenTeamId = typeof (payload === null || payload === void 0 ? void 0 : payload.teamId) === 'string' ? payload.teamId : null;
|
|
626
|
+
tokenScopes = payload === null || payload === void 0 ? void 0 : payload.scopes;
|
|
627
|
+
tokenOwner = user;
|
|
582
628
|
return payload;
|
|
583
629
|
}),
|
|
584
630
|
});
|
|
631
|
+
// A token that names its owner may create sessions only with the
|
|
632
|
+
// `sessions` scope, as a key pair may: refused, like a key without it.
|
|
633
|
+
if (identity.userId && !sessionTokenGrantsSessions(tokenScopes)) {
|
|
634
|
+
this.logger.error('Rejecting session: xe:options.sessionToken lacks the `sessions` scope');
|
|
635
|
+
throw new base_driver_1.errors.InvalidArgumentError(SESSION_TOKEN_LACKS_SCOPE);
|
|
636
|
+
}
|
|
585
637
|
if (!identity.userId) {
|
|
586
638
|
this.logger.warn('Session created without valid credentials. Pass `xe:options.accessKey` + ' +
|
|
587
639
|
'`xe:options.token`, or `xe:options.sessionToken`.');
|
|
588
640
|
}
|
|
589
|
-
const leaseAccess = this.leaseAccessFor(identity.userId
|
|
590
|
-
? {
|
|
641
|
+
const leaseAccess = this.leaseAccessFor(identity.userId && tokenOwner
|
|
642
|
+
? {
|
|
643
|
+
kind: 'session-token',
|
|
644
|
+
scopes: String(tokenScopes),
|
|
645
|
+
user: tokenOwner,
|
|
646
|
+
teamId: tokenTeamId,
|
|
647
|
+
}
|
|
591
648
|
: { kind: 'none' });
|
|
592
649
|
// A token's user is scoped as REST scopes them. No credentials at all
|
|
593
650
|
// stays unscoped (the back-compat path), with no lookup.
|
|
@@ -597,6 +654,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
597
654
|
teamId: identity.userId ? tokenTeamId : null,
|
|
598
655
|
} });
|
|
599
656
|
}
|
|
657
|
+
const { row, user: owner } = verifiedPair;
|
|
600
658
|
if (!svc.hasScope(row, ['sessions'])) {
|
|
601
659
|
this.logger.error('Rejecting session: credentials lack the `sessions` scope');
|
|
602
660
|
throw new base_driver_1.errors.InvalidArgumentError('credentials are invalid, revoked, or lack the `sessions` scope');
|
|
@@ -605,7 +663,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
605
663
|
const leaseAccess = this.leaseAccessFor({
|
|
606
664
|
kind: 'api-key',
|
|
607
665
|
scopes: row.scopes,
|
|
608
|
-
|
|
666
|
+
user: owner,
|
|
609
667
|
teamId: (_a = row.teamId) !== null && _a !== void 0 ? _a : null,
|
|
610
668
|
});
|
|
611
669
|
// The phones and apps this session may use: REST's rule
|
|
@@ -640,7 +698,8 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
640
698
|
// How a session that names a lease is judged, looked up at most once and
|
|
641
699
|
// only when asked. Who may override follows canOverrideLease; which phones
|
|
642
700
|
// it can see follows REST's computeTeamIds. Both need the owner's live role,
|
|
643
|
-
// as authMiddleware reads it for every REST request
|
|
701
|
+
// as authMiddleware reads it for every REST request: the owner the
|
|
702
|
+
// credential check looked up a moment ago, ACTIVE then.
|
|
644
703
|
leaseAccessFor(credential) {
|
|
645
704
|
let looked;
|
|
646
705
|
return () => {
|
|
@@ -657,24 +716,24 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
657
716
|
if (credential.kind === 'none') {
|
|
658
717
|
return { canOverride: (0, leaseOverride_1.canOverrideLease)({ kind: 'none' }), teamIds: undefined };
|
|
659
718
|
}
|
|
719
|
+
const { user } = credential;
|
|
660
720
|
try {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const role = (user === null || user === void 0 ? void 0 : user.role) === 'SUPER_ADMIN' || (user === null || user === void 0 ? void 0 : user.role) === 'ADMIN' ? user.role : 'MEMBER';
|
|
721
|
+
const canOverride = (0, leaseOverride_1.canOverrideLease)({
|
|
722
|
+
kind: credential.kind,
|
|
723
|
+
scopes: credential.scopes,
|
|
724
|
+
user,
|
|
725
|
+
});
|
|
726
|
+
const role = user.role === 'SUPER_ADMIN' || user.role === 'ADMIN' ? user.role : 'MEMBER';
|
|
668
727
|
const teamIds = yield (0, callerTeamIds_1.computeTeamIds)({
|
|
669
728
|
role,
|
|
670
|
-
userId:
|
|
729
|
+
userId: user.id,
|
|
671
730
|
apiKeyTeamId: credential.teamId,
|
|
672
731
|
});
|
|
673
732
|
return { canOverride, teamIds };
|
|
674
733
|
}
|
|
675
734
|
catch (err) {
|
|
676
735
|
// Fail closed: no override, and the shared pool only.
|
|
677
|
-
this.logger.warn(`Could not look up how user ${
|
|
736
|
+
this.logger.warn(`Could not look up how user ${user.id} may use a lease: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
678
737
|
return { canOverride: false, teamIds: [] };
|
|
679
738
|
}
|
|
680
739
|
});
|
|
@@ -788,7 +847,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
788
847
|
}
|
|
789
848
|
}
|
|
790
849
|
finalizeSession(session_1, device_1, caps_1, driver_1, isRemote_1) {
|
|
791
|
-
return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null) {
|
|
850
|
+
return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null, allocatedAt) {
|
|
792
851
|
var _a;
|
|
793
852
|
const sessionId = session.value[0];
|
|
794
853
|
const sessionResponse = session.value[1];
|
|
@@ -824,6 +883,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
824
883
|
const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver, webDriverUrl);
|
|
825
884
|
sessionInstance.apiKeyId = apiKeyId;
|
|
826
885
|
sessionInstance.userId = userId;
|
|
886
|
+
sessionInstance.allocatedAt = allocatedAt;
|
|
827
887
|
// A session this server drives: its owner is known while it runs, row or
|
|
828
888
|
// not (a node writes none for the hub's sessions; LiveSessionOwners).
|
|
829
889
|
if (sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
@@ -877,26 +937,11 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
877
937
|
applyPostSessionLogic(session, caps, device) {
|
|
878
938
|
return __awaiter(this, void 0, void 0, function* () {
|
|
879
939
|
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]) {
|
|
886
|
-
try {
|
|
887
|
-
const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
|
|
888
|
-
yield typedi_1.Container.get(InterceptorService).start(session.getId(), device, {
|
|
889
|
-
enabled: true,
|
|
890
|
-
bufferSize: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE],
|
|
891
|
-
captureBodies: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] !== false,
|
|
892
|
-
mocks: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS] || [],
|
|
893
|
-
includeHosts: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS] || [],
|
|
894
|
-
excludeHosts: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS] || [],
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
catch (err) {
|
|
898
|
-
this.logger.warn(`🕸️ Failed to start interceptor for ${session.getId()}: ${err.message}`);
|
|
899
|
-
}
|
|
940
|
+
// Only the server that drives the phone changes its network: a node
|
|
941
|
+
// applies its own sessions' profile and interceptor, and a hub that did
|
|
942
|
+
// it too ran its own adb against the node's phone.
|
|
943
|
+
if (session.getType() === SessionType_1.default.LOCAL) {
|
|
944
|
+
yield this.applySessionNetwork(session.getId(), caps, device, context.pluginArgs);
|
|
900
945
|
}
|
|
901
946
|
const isDashboardEnabled = !!context.pluginArgs.enableDashboard;
|
|
902
947
|
const shouldSaveLogs = session.getType() !== SessionType_1.default.CLOUD;
|
|
@@ -930,6 +975,36 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
930
975
|
}
|
|
931
976
|
});
|
|
932
977
|
}
|
|
978
|
+
/**
|
|
979
|
+
* The session's network profile and network capture, on a phone this
|
|
980
|
+
* server drives. First what an earlier session left on the phone and
|
|
981
|
+
* couldn't put back is undone, so this session starts from the phone's own
|
|
982
|
+
* settings and records those as the ones to put back. Each ending undoes
|
|
983
|
+
* these through PhoneNetworkRestore.
|
|
984
|
+
*/
|
|
985
|
+
applySessionNetwork(sessionId, caps, device, pluginArgs) {
|
|
986
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
987
|
+
var _a;
|
|
988
|
+
if (((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android') {
|
|
989
|
+
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreLeftovers({ udid: device.udid });
|
|
990
|
+
}
|
|
991
|
+
const networkProfile = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE];
|
|
992
|
+
if (networkProfile) {
|
|
993
|
+
const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
|
|
994
|
+
yield typedi_1.Container.get(NetworkConditioningService).applyProfile(sessionId, device, networkProfile);
|
|
995
|
+
}
|
|
996
|
+
const interceptor = (0, interceptorOptions_1.resolveInterceptorOptions)(caps, pluginArgs.interceptor);
|
|
997
|
+
if (interceptor.enabled) {
|
|
998
|
+
try {
|
|
999
|
+
const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
|
|
1000
|
+
yield typedi_1.Container.get(InterceptorService).start(sessionId, device, interceptor);
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
this.logger.warn(`🕸️ Failed to start interceptor for ${sessionId}: ${err.message}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
933
1008
|
/**
|
|
934
1009
|
* The row a hub keeps for a session it routes to a node or a cloud when the
|
|
935
1010
|
* dashboard's own (onSessionStarted) isn't written: with the dashboard off,
|
|
@@ -1097,6 +1172,10 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1097
1172
|
// which releases ports and archives video twice.
|
|
1098
1173
|
if (sessionId) {
|
|
1099
1174
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
1175
|
+
// The phone's network (profile, interceptor proxy) and the capture,
|
|
1176
|
+
// before the phone is released to the next session. Whether or not
|
|
1177
|
+
// SESSION_MANAGER holds the session.
|
|
1178
|
+
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, 'session deleted');
|
|
1100
1179
|
yield (0, device_service_1.releaseSessionDevices)(sessionId);
|
|
1101
1180
|
this.logger.info(`📱 Unblocking the device that is blocked for session ${sessionId}`);
|
|
1102
1181
|
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
@@ -1127,6 +1206,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1127
1206
|
finally {
|
|
1128
1207
|
if (sessionId) {
|
|
1129
1208
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1209
|
+
(0, sessionMemory_1.forgetSessionMemory)(sessionId);
|
|
1130
1210
|
// Before the lock's "still in memory?" check, which a dashboard-off
|
|
1131
1211
|
// node fails. On a node this ends the figures it holds for its hub;
|
|
1132
1212
|
// on a hub it collects a node session's last ones. Idempotent.
|
|
@@ -1137,37 +1217,10 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1137
1217
|
// Another concurrent deleteSession already cleaned up.
|
|
1138
1218
|
return;
|
|
1139
1219
|
}
|
|
1140
|
-
|
|
1141
|
-
try {
|
|
1142
|
-
const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
|
|
1143
|
-
yield typedi_1.Container.get(NetworkConditioningService).reset(sessionId, device);
|
|
1144
|
-
}
|
|
1145
|
-
catch (resetErr) {
|
|
1146
|
-
this.logger.warn(`⚠️ NetworkConditioningService.reset failed for session ${sessionId}: ${resetErr.message}`);
|
|
1147
|
-
}
|
|
1148
|
-
try {
|
|
1149
|
-
const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
|
|
1150
|
-
const interceptor = typedi_1.Container.get(InterceptorService);
|
|
1151
|
-
if (interceptor.isActive(sessionId))
|
|
1152
|
-
yield interceptor.stop(sessionId);
|
|
1153
|
-
}
|
|
1154
|
-
catch (interceptorErr) {
|
|
1155
|
-
this.logger.warn(`⚠️ InterceptorService.stop failed for session ${sessionId}: ${interceptorErr.message}`);
|
|
1156
|
-
}
|
|
1220
|
+
// The phone's network was put back before it was released (above).
|
|
1157
1221
|
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, status, reason);
|
|
1158
1222
|
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
1159
1223
|
typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).forget(sessionId);
|
|
1160
|
-
try {
|
|
1161
|
-
const { getSessionById } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/session-service')));
|
|
1162
|
-
const sessionData = yield getSessionById(sessionId);
|
|
1163
|
-
if (sessionData && (sessionData.status === 'failed' || sessionData.failure_reason)) {
|
|
1164
|
-
const { NotificationService } = yield Promise.resolve().then(() => __importStar(require('./NotificationService')));
|
|
1165
|
-
yield typedi_1.Container.get(NotificationService).dispatchEvent('session_failed', sessionData);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
catch (err) {
|
|
1169
|
-
/* ignore notification errors */
|
|
1170
|
-
}
|
|
1171
1224
|
}));
|
|
1172
1225
|
}
|
|
1173
1226
|
}
|
|
@@ -1240,6 +1293,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1240
1293
|
stopSessionForShutdown(sessionId, reason) {
|
|
1241
1294
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1242
1295
|
yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
|
|
1296
|
+
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, reason);
|
|
1243
1297
|
try {
|
|
1244
1298
|
yield (0, device_service_1.releaseSessionDevices)(sessionId);
|
|
1245
1299
|
}
|
|
@@ -1258,13 +1312,17 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1258
1312
|
}
|
|
1259
1313
|
}
|
|
1260
1314
|
try {
|
|
1261
|
-
|
|
1315
|
+
// A shutdown fails the sessions it drains; no test caused it, so no webhook.
|
|
1316
|
+
yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, SessionStatus_1.SessionStatus.FAILED, reason, {
|
|
1317
|
+
notify: false,
|
|
1318
|
+
});
|
|
1262
1319
|
}
|
|
1263
1320
|
catch (err) {
|
|
1264
1321
|
this.logger.warn(`[shutdown] onSessionStopped failed for ${sessionId}: ${err.message}`);
|
|
1265
1322
|
}
|
|
1266
1323
|
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
1267
1324
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1325
|
+
(0, sessionMemory_1.forgetSessionMemory)(sessionId);
|
|
1268
1326
|
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1269
1327
|
}));
|
|
1270
1328
|
});
|
|
@@ -24,6 +24,7 @@ const logger_1 = __importDefault(require("../logger"));
|
|
|
24
24
|
const SessionManager_1 = require("../sessions/SessionManager");
|
|
25
25
|
const SessionLifecycleService_1 = require("./SessionLifecycleService");
|
|
26
26
|
const SessionType_1 = __importDefault(require("../enums/SessionType"));
|
|
27
|
+
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
27
28
|
// Graceful shutdown phase that runs BEFORE the existing infrastructure
|
|
28
29
|
// teardown in index.ts:cleanup(). Active sessions get a bounded chance to
|
|
29
30
|
// archive their video + release ports + mark themselves failed, so we don't
|
|
@@ -57,6 +58,7 @@ let ShutdownCoordinator = class ShutdownCoordinator {
|
|
|
57
58
|
const sessions = SessionManager_1.SESSION_MANAGER.getAllSessions().filter((session) => session.getType() === SessionType_1.default.LOCAL);
|
|
58
59
|
if (sessions.length === 0) {
|
|
59
60
|
this.logger.info('No active sessions to drain');
|
|
61
|
+
yield this.restorePhoneNetworks(timeoutMs);
|
|
60
62
|
return { attempted: 0, completed: 0 };
|
|
61
63
|
}
|
|
62
64
|
this.logger.info(`Draining ${sessions.length} active session(s) (overall=${timeoutMs}ms, per-session=${perSessionTimeoutMs}ms)`);
|
|
@@ -84,10 +86,41 @@ let ShutdownCoordinator = class ShutdownCoordinator {
|
|
|
84
86
|
}));
|
|
85
87
|
const results = yield Promise.allSettled(tasks);
|
|
86
88
|
const completed = results.filter((r) => r.status === 'fulfilled' && r.value).length;
|
|
89
|
+
yield this.restorePhoneNetworks(Math.max(0, deadline - Date.now()));
|
|
87
90
|
this.logger.info(`Drain complete: ${completed}/${sessions.length} session(s) finalized`);
|
|
88
91
|
return { attempted: sessions.length, completed };
|
|
89
92
|
});
|
|
90
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The network of every phone a session of this server still has changed
|
|
96
|
+
* (profile, interceptor proxy), sessions SESSION_MANAGER doesn't hold
|
|
97
|
+
* included, within what is left of the budget. A shutdown that never gets
|
|
98
|
+
* here (Appium exits on SIGTERM first) leaves it to the next start, from
|
|
99
|
+
* the ledger (PhoneNetworkRestore.cleanUpAtBoot).
|
|
100
|
+
*/
|
|
101
|
+
restorePhoneNetworks(budgetMs) {
|
|
102
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
103
|
+
var _a;
|
|
104
|
+
if (budgetMs <= 0)
|
|
105
|
+
return;
|
|
106
|
+
let timer;
|
|
107
|
+
try {
|
|
108
|
+
yield Promise.race([
|
|
109
|
+
typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreAll('Xenon is shutting down'),
|
|
110
|
+
new Promise((resolve) => {
|
|
111
|
+
timer = setTimeout(resolve, budgetMs);
|
|
112
|
+
}),
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
this.logger.warn(`[shutdown] Putting phone networks back failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
if (timer)
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
91
124
|
};
|
|
92
125
|
exports.ShutdownCoordinator = ShutdownCoordinator;
|
|
93
126
|
exports.ShutdownCoordinator = ShutdownCoordinator = __decorate([
|
|
@@ -33,9 +33,14 @@ const SocketEvents_1 = require("../enums/SocketEvents");
|
|
|
33
33
|
const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
34
34
|
const DeviceTeamResolver_1 = require("./device-access/DeviceTeamResolver");
|
|
35
35
|
const upgradeRouter_1 = require("../app/ws/upgradeRouter");
|
|
36
|
+
const sessionCommandSummary_1 = require("../dashboard/sessionCommandSummary");
|
|
36
37
|
/** socket.io's default path, which the dashboard and nodes connect to. */
|
|
37
38
|
const SOCKET_IO_PATH = '/socket.io';
|
|
38
39
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
40
|
+
const EVENT_LOG_KEEPS = new Map([
|
|
41
|
+
[SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, null],
|
|
42
|
+
[SocketEvents_1.SocketEvents.SESSION_COMMAND, sessionCommandSummary_1.sessionCommandSummary],
|
|
43
|
+
]);
|
|
39
44
|
function readCookie(cookieHeader, name) {
|
|
40
45
|
if (!cookieHeader)
|
|
41
46
|
return undefined;
|
|
@@ -242,12 +247,23 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
242
247
|
if (this.io) {
|
|
243
248
|
this.io.to('dashboard').emit(event, data);
|
|
244
249
|
}
|
|
245
|
-
|
|
250
|
+
this.logEvent(event, data);
|
|
251
|
+
}
|
|
252
|
+
/** Once per event, unscoped: whole, or what EVENT_LOG_KEEPS keeps of it. */
|
|
253
|
+
logEvent(event, data) {
|
|
254
|
+
const keep = EVENT_LOG_KEEPS.get(event);
|
|
255
|
+
if (keep === null)
|
|
256
|
+
return;
|
|
257
|
+
typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({
|
|
258
|
+
type: event,
|
|
259
|
+
payload: keep ? keep(data) : data,
|
|
260
|
+
});
|
|
246
261
|
}
|
|
247
262
|
/**
|
|
248
263
|
* A dashboard event about one or more phones: each dashboard socket gets it
|
|
249
264
|
* only if its caller can see the phone ({@link DeviceEventScope}), by the
|
|
250
|
-
* same team rule as REST. The event log records it once, unscoped
|
|
265
|
+
* same team rule as REST. The event log records it once, unscoped, as
|
|
266
|
+
* EVENT_LOG_KEEPS says.
|
|
251
267
|
*
|
|
252
268
|
* Each phone's events are delivered through one chain, in the order they
|
|
253
269
|
* were emitted, whatever their scope shape: one that waits on a team lookup
|
|
@@ -262,7 +278,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
262
278
|
*/
|
|
263
279
|
emitToDashboardForDevices(event, data, scope) {
|
|
264
280
|
var _a, _b;
|
|
265
|
-
|
|
281
|
+
this.logEvent(event, data);
|
|
266
282
|
const udids = 'udids' in scope ? scope.udids : [(_a = scope.udid) !== null && _a !== void 0 ? _a : ''];
|
|
267
283
|
if (!this.hasScopedDashboard() && udids.every((udid) => !this.deliveries.has(udid))) {
|
|
268
284
|
this.noteRow(scope);
|
|
@@ -52,20 +52,37 @@ let TeamService = class TeamService {
|
|
|
52
52
|
delete(id) {
|
|
53
53
|
return __awaiter(this, void 0, void 0, function* () {
|
|
54
54
|
// Apps too: App.teamId is onDelete SetNull, so deleting a team that owns
|
|
55
|
-
// apps would quietly share them with every member.
|
|
56
|
-
|
|
55
|
+
// apps would quietly share them with every member. So is a phone's saved
|
|
56
|
+
// team (DeviceSetting): a phone of the team that is not connected now
|
|
57
|
+
// would come back in the shared pool.
|
|
58
|
+
const [connected, saved, members, apps] = yield Promise.all([
|
|
57
59
|
prisma_1.prisma.device.count({ where: { teamId: id } }),
|
|
60
|
+
prisma_1.prisma.deviceSetting.findMany({ where: { teamId: id }, select: { udid: true, host: true } }),
|
|
58
61
|
prisma_1.prisma.teamMember.count({ where: { teamId: id } }),
|
|
59
62
|
prisma_1.prisma.app.count({ where: { teamId: id } }),
|
|
60
63
|
]);
|
|
64
|
+
const away = yield this.notConnected(saved);
|
|
65
|
+
const devices = connected + away;
|
|
61
66
|
if (devices > 0 || members > 0 || apps > 0) {
|
|
62
|
-
|
|
67
|
+
const phones = away > 0 ? `${devices} device(s) (${away} not connected now)` : `${devices} device(s)`;
|
|
68
|
+
throw new Error(`Team still has ${phones}, ${members} member(s) and ${apps} app(s). Reassign them before deleting.`);
|
|
63
69
|
}
|
|
64
70
|
// Clear teamId on any (revoked) ApiKey rows so the FK doesn't block delete.
|
|
65
71
|
yield prisma_1.prisma.apiKey.updateMany({ where: { teamId: id }, data: { teamId: null } });
|
|
66
72
|
yield prisma_1.prisma.team.delete({ where: { id } });
|
|
67
73
|
});
|
|
68
74
|
}
|
|
75
|
+
/** How many of these saved phones have no Device row: not connected now. */
|
|
76
|
+
notConnected(saved) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
if (saved.length === 0)
|
|
79
|
+
return 0;
|
|
80
|
+
const here = yield prisma_1.prisma.device.count({
|
|
81
|
+
where: { OR: saved.map(({ udid, host }) => ({ udid, host })) },
|
|
82
|
+
});
|
|
83
|
+
return saved.length - here;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
69
86
|
listMembers(teamId) {
|
|
70
87
|
return __awaiter(this, void 0, void 0, function* () {
|
|
71
88
|
const rows = yield prisma_1.prisma.teamMember.findMany({
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.sliceVideo = sliceVideo;
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const ffmpegPath_1 = require("../../helpers/ffmpegPath");
|
|
8
9
|
const logger_1 = __importDefault(require("../../logger"));
|
|
9
10
|
function sliceVideo(inputPath, startSec, endSec, outPath, spawnFn = child_process_1.spawn) {
|
|
10
11
|
return new Promise((resolve) => {
|
|
@@ -16,7 +17,9 @@ function sliceVideo(inputPath, startSec, endSec, outPath, spawnFn = child_proces
|
|
|
16
17
|
'-c', 'copy',
|
|
17
18
|
outPath,
|
|
18
19
|
];
|
|
19
|
-
|
|
20
|
+
// Xenon's own ffmpeg, as everywhere else: a bare name isn't found by a
|
|
21
|
+
// server the Mac app started, and finds a system ffmpeg first otherwise.
|
|
22
|
+
const proc = spawnFn((0, ffmpegPath_1.resolveFfmpegPath)(), args);
|
|
20
23
|
let stderr = '';
|
|
21
24
|
if (proc.stderr) {
|
|
22
25
|
proc.stderr.on('data', (chunk) => {
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.canOverrideLease = canOverrideLease;
|
|
4
4
|
function canOverrideLease(credential) {
|
|
5
|
-
var _a, _b;
|
|
6
5
|
switch (credential.kind) {
|
|
7
6
|
case 'auth-disabled':
|
|
8
7
|
return true;
|
|
9
|
-
case 'api-key':
|
|
8
|
+
case 'api-key':
|
|
9
|
+
case 'session-token': {
|
|
10
10
|
if (!credential.user)
|
|
11
11
|
return false;
|
|
12
12
|
const scopes = new Set(credential.scopes.split(',').map((s) => s.trim()));
|
|
13
|
-
|
|
13
|
+
const { role } = credential.user;
|
|
14
|
+
if (credential.kind === 'session-token') {
|
|
15
|
+
return role === 'SUPER_ADMIN' || (role === 'ADMIN' && scopes.has('admin'));
|
|
16
|
+
}
|
|
17
|
+
return scopes.has('admin') || role === 'SUPER_ADMIN';
|
|
14
18
|
}
|
|
15
|
-
case 'session-token':
|
|
16
|
-
return ((_a = credential.user) === null || _a === void 0 ? void 0 : _a.role) === 'SUPER_ADMIN' || ((_b = credential.user) === null || _b === void 0 ? void 0 : _b.role) === 'ADMIN';
|
|
17
19
|
default:
|
|
18
20
|
return false;
|
|
19
21
|
}
|