@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
|
@@ -18,109 +18,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
exports.WebConfigService = void 0;
|
|
19
19
|
const prisma_1 = require("../prisma");
|
|
20
20
|
const typedi_1 = require("typedi");
|
|
21
|
+
/** How each saved setting is read back: they are all stored as text. */
|
|
22
|
+
const SETTINGS = {
|
|
23
|
+
enableSelfHealing: 'boolean',
|
|
24
|
+
healthCheckIntervalMs: 'number',
|
|
25
|
+
healthCheckSchedule: 'text',
|
|
26
|
+
buildCleanupDays: 'number',
|
|
27
|
+
buildCleanupMaxCount: 'number',
|
|
28
|
+
buildCleanupSchedule: 'text',
|
|
29
|
+
deleteBuildAssets: 'boolean',
|
|
30
|
+
aiProvider: 'text',
|
|
31
|
+
aiModel: 'text',
|
|
32
|
+
aiBaseUrl: 'text',
|
|
33
|
+
geminiModel: 'text',
|
|
34
|
+
openaiModel: 'text',
|
|
35
|
+
anthropicModel: 'text',
|
|
36
|
+
ollamaModel: 'text',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The lab settings the dashboard saves, one `WebConfig` row each, keyed by the
|
|
40
|
+
* setting's name.
|
|
41
|
+
*
|
|
42
|
+
* A row's `id` is the table's primary key, and used to be written as 'global'
|
|
43
|
+
* for every setting, so the table held one: the first setting saved was kept,
|
|
44
|
+
* and saving a second failed with "Unique constraint failed on the fields
|
|
45
|
+
* (`id`)". A setting's row now has its own name as its id, as the metrics
|
|
46
|
+
* counters kept in the same table do. A row an earlier version saved under
|
|
47
|
+
* 'global' is still read, and still the one the next save of that setting
|
|
48
|
+
* updates.
|
|
49
|
+
*/
|
|
21
50
|
let WebConfigService = class WebConfigService {
|
|
22
|
-
constructor() {
|
|
23
|
-
this.CONFIG_ID = 'global';
|
|
24
|
-
}
|
|
25
51
|
getConfig() {
|
|
26
52
|
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
-
const
|
|
28
|
-
where: {
|
|
53
|
+
const rows = yield prisma_1.prisma.webConfig.findMany({
|
|
54
|
+
where: { name: { in: Object.keys(SETTINGS) } },
|
|
29
55
|
});
|
|
30
56
|
const result = {};
|
|
31
|
-
for (const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
else if (
|
|
36
|
-
result.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
result.buildCleanupDays = parseInt(config.value);
|
|
40
|
-
}
|
|
41
|
-
else if (config.name === 'buildCleanupMaxCount') {
|
|
42
|
-
result.buildCleanupMaxCount = parseInt(config.value);
|
|
43
|
-
}
|
|
44
|
-
else if (config.name === 'buildCleanupSchedule') {
|
|
45
|
-
result.buildCleanupSchedule = config.value;
|
|
46
|
-
}
|
|
47
|
-
else if (config.name === 'deleteBuildAssets') {
|
|
48
|
-
result.deleteBuildAssets = config.value === 'true';
|
|
49
|
-
}
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const kind = SETTINGS[row.name];
|
|
59
|
+
if (kind === 'number')
|
|
60
|
+
result[row.name] = parseInt(row.value);
|
|
61
|
+
else if (kind === 'boolean')
|
|
62
|
+
result[row.name] = row.value === 'true';
|
|
63
|
+
else if (kind === 'text')
|
|
64
|
+
result[row.name] = row.value;
|
|
50
65
|
}
|
|
51
66
|
return result;
|
|
52
67
|
});
|
|
53
68
|
}
|
|
54
69
|
setConfig(config) {
|
|
55
70
|
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (config.healthCheckSchedule !== undefined) {
|
|
69
|
-
promises.push(prisma_1.prisma.webConfig.upsert({
|
|
70
|
-
where: { name: 'healthCheckSchedule' },
|
|
71
|
-
update: { value: config.healthCheckSchedule },
|
|
72
|
-
create: {
|
|
73
|
-
id: this.CONFIG_ID,
|
|
74
|
-
name: 'healthCheckSchedule',
|
|
75
|
-
value: config.healthCheckSchedule,
|
|
76
|
-
},
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
if (config.buildCleanupDays !== undefined) {
|
|
80
|
-
promises.push(prisma_1.prisma.webConfig.upsert({
|
|
81
|
-
where: { name: 'buildCleanupDays' },
|
|
82
|
-
update: { value: config.buildCleanupDays.toString() },
|
|
83
|
-
create: {
|
|
84
|
-
id: this.CONFIG_ID,
|
|
85
|
-
name: 'buildCleanupDays',
|
|
86
|
-
value: config.buildCleanupDays.toString(),
|
|
87
|
-
},
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
if (config.buildCleanupMaxCount !== undefined) {
|
|
91
|
-
promises.push(prisma_1.prisma.webConfig.upsert({
|
|
92
|
-
where: { name: 'buildCleanupMaxCount' },
|
|
93
|
-
update: { value: config.buildCleanupMaxCount.toString() },
|
|
94
|
-
create: {
|
|
95
|
-
id: this.CONFIG_ID,
|
|
96
|
-
name: 'buildCleanupMaxCount',
|
|
97
|
-
value: config.buildCleanupMaxCount.toString(),
|
|
98
|
-
},
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
if (config.buildCleanupSchedule !== undefined) {
|
|
102
|
-
promises.push(prisma_1.prisma.webConfig.upsert({
|
|
103
|
-
where: { name: 'buildCleanupSchedule' },
|
|
104
|
-
update: { value: config.buildCleanupSchedule },
|
|
105
|
-
create: {
|
|
106
|
-
id: this.CONFIG_ID,
|
|
107
|
-
name: 'buildCleanupSchedule',
|
|
108
|
-
value: config.buildCleanupSchedule,
|
|
109
|
-
},
|
|
110
|
-
}));
|
|
111
|
-
}
|
|
112
|
-
if (config.deleteBuildAssets !== undefined) {
|
|
113
|
-
promises.push(prisma_1.prisma.webConfig.upsert({
|
|
114
|
-
where: { name: 'deleteBuildAssets' },
|
|
115
|
-
update: { value: config.deleteBuildAssets.toString() },
|
|
116
|
-
create: {
|
|
117
|
-
id: this.CONFIG_ID,
|
|
118
|
-
name: 'deleteBuildAssets',
|
|
119
|
-
value: config.deleteBuildAssets.toString(),
|
|
120
|
-
},
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
yield Promise.all(promises);
|
|
71
|
+
const writes = Object.keys(SETTINGS)
|
|
72
|
+
.filter((name) => config[name] !== undefined && config[name] !== null)
|
|
73
|
+
.map((name) => {
|
|
74
|
+
const value = String(config[name]);
|
|
75
|
+
return prisma_1.prisma.webConfig.upsert({
|
|
76
|
+
where: { name },
|
|
77
|
+
update: { value },
|
|
78
|
+
create: { id: name, name, value },
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
yield Promise.all(writes);
|
|
124
82
|
});
|
|
125
83
|
}
|
|
126
84
|
};
|
|
@@ -77,6 +77,7 @@ const device_service_1 = require("../data-service/device-service");
|
|
|
77
77
|
const device_store_1 = require("../data-service/device-store");
|
|
78
78
|
const Devices_1 = __importDefault(require("./cloud/Devices"));
|
|
79
79
|
const DeviceLockManager_1 = require("./android/DeviceLockManager");
|
|
80
|
+
const shellCommands_1 = require("./shellCommands");
|
|
80
81
|
const androidIdentity_1 = require("./android/androidIdentity");
|
|
81
82
|
const deviceIdentity_1 = require("./deviceIdentity");
|
|
82
83
|
const localDeviceHosts_1 = require("./localDeviceHosts");
|
|
@@ -1151,43 +1152,20 @@ let AndroidDeviceManager = class AndroidDeviceManager {
|
|
|
1151
1152
|
}
|
|
1152
1153
|
executeShell(udid, command) {
|
|
1153
1154
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
'dumpsys power',
|
|
1161
|
-
'whoami',
|
|
1162
|
-
'getprop',
|
|
1163
|
-
'pm list packages',
|
|
1164
|
-
'ip addr',
|
|
1165
|
-
'cat /proc/meminfo',
|
|
1166
|
-
'cat /proc/cpuinfo',
|
|
1167
|
-
'date',
|
|
1168
|
-
'uptime',
|
|
1169
|
-
'netstat',
|
|
1170
|
-
];
|
|
1171
|
-
// Basic sanitation
|
|
1172
|
-
const safeCommand = command.trim();
|
|
1173
|
-
// Check if the command starts with any allowed prefix
|
|
1174
|
-
const isAllowed = ALLOWED_COMMANDS.some((prefix) => safeCommand.startsWith(prefix));
|
|
1175
|
-
if (!isAllowed) {
|
|
1176
|
-
logger_1.default.warn(`Blocked potentially unsafe shell command on ${udid}: ${safeCommand}`);
|
|
1177
|
-
throw new Error(`Command '${safeCommand}' is not allowed for security reasons.`);
|
|
1155
|
+
// An allowed command word for word, in words `sh` reads nothing into:
|
|
1156
|
+
// `adb shell` hands the joined line to the phone's shell (shellCommands.ts).
|
|
1157
|
+
const parsed = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.ANDROID_SHELL_COMMANDS);
|
|
1158
|
+
if ('refused' in parsed) {
|
|
1159
|
+
logger_1.default.warn(`Blocked shell command on ${udid}: ${command.trim()}`);
|
|
1160
|
+
throw new Error(parsed.refused);
|
|
1178
1161
|
}
|
|
1179
|
-
|
|
1180
|
-
// This is a naive split, but safe enough for the allowed commands which don't use complex quoting
|
|
1181
|
-
const args = safeCommand.split(/\s+/);
|
|
1182
|
-
logger_1.default.info(`Executing shell command on ${udid}: ${safeCommand}`);
|
|
1162
|
+
logger_1.default.info(`Executing shell command on ${udid}: ${parsed.argv.join(' ')}`);
|
|
1183
1163
|
const { adbInstance } = yield this.getAdb();
|
|
1184
1164
|
if (!adbInstance)
|
|
1185
1165
|
throw new Error('ADB is not available');
|
|
1186
1166
|
// Use device lock to ensure thread safety
|
|
1187
1167
|
return yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
|
|
1188
|
-
|
|
1189
|
-
// except we passed them as array, so standard child_process rules apply.
|
|
1190
|
-
return yield adbInstance.adbExec(['-s', udid, 'shell', ...args], { timeout: 10000 });
|
|
1168
|
+
return yield adbInstance.adbExec(['-s', udid, 'shell', ...parsed.argv], { timeout: 10000 });
|
|
1191
1169
|
}));
|
|
1192
1170
|
});
|
|
1193
1171
|
}
|
|
@@ -93,27 +93,34 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
93
93
|
startWatchdog() {
|
|
94
94
|
// unref()ed below: the server's listener keeps the process alive; this only
|
|
95
95
|
// needs to fire while it runs.
|
|
96
|
-
setInterval(() =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// good. stop() itself must not release (a recording starting calls
|
|
112
|
-
// it); an idle stop does, if nothing else uses the device.
|
|
113
|
-
void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
|
|
114
|
-
}
|
|
96
|
+
setInterval(() => this.sweep(Date.now()), 60000).unref();
|
|
97
|
+
}
|
|
98
|
+
/** One look at every capture: ends those nobody has watched for long enough. */
|
|
99
|
+
sweep(now) {
|
|
100
|
+
for (const [udid, s] of this.sessions.entries()) {
|
|
101
|
+
if (s.status !== 'running')
|
|
102
|
+
continue;
|
|
103
|
+
if (s.mux.clientCount > 0) {
|
|
104
|
+
s.emptyAt = undefined;
|
|
105
|
+
}
|
|
106
|
+
else if (s.endWhenUnwatched && !this.startPromises.has(udid)) {
|
|
107
|
+
// A page asked for MJPEG while this capture was starting for a viewer
|
|
108
|
+
// who then never came (endWhenUnwatched leaves a start alone).
|
|
109
|
+
logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
|
|
110
|
+
void this.stop(udid);
|
|
115
111
|
}
|
|
116
|
-
|
|
112
|
+
else if (s.emptyAt === undefined) {
|
|
113
|
+
s.emptyAt = now;
|
|
114
|
+
}
|
|
115
|
+
else if (now - s.emptyAt > IDLE_TIMEOUT_MS) {
|
|
116
|
+
logger_1.default.info(`[${udid}] Stopping idle H.264 stream (no viewers for ${IDLE_TIMEOUT_MS}ms)`);
|
|
117
|
+
// The stream went, but the live-preview hold stayed: with H.264
|
|
118
|
+
// preview, a tab closed without releasing kept the device busy for
|
|
119
|
+
// good. stop() itself must not release (a recording starting calls
|
|
120
|
+
// it); an idle stop does, if nothing else uses the device.
|
|
121
|
+
void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
117
124
|
}
|
|
118
125
|
getMultiplexer(udid) {
|
|
119
126
|
var _a;
|
|
@@ -131,6 +138,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
131
138
|
var _a, _b;
|
|
132
139
|
const mux = new H264Multiplexer_1.H264Multiplexer();
|
|
133
140
|
const session = { status: 'running', mux };
|
|
141
|
+
mux.onEmpty(() => this.endIfUnwatched(udid, session));
|
|
134
142
|
this.sessions.set(udid, session);
|
|
135
143
|
try {
|
|
136
144
|
let resolveConfig = () => undefined;
|
|
@@ -178,6 +186,44 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
178
186
|
}
|
|
179
187
|
});
|
|
180
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* A page that showed this phone's H.264 preview now shows MJPEG (its player
|
|
191
|
+
* failed, or it cannot play H.264), and the MJPEG capture is starting for
|
|
192
|
+
* it. One capture per phone where possible, but never at the expense of a
|
|
193
|
+
* viewer still playing H.264: another tile, tab or admin may be. So the
|
|
194
|
+
* capture ends now if nobody watches it, else the moment its last viewer
|
|
195
|
+
* leaves, never after the idle wait. A viewer who joins meanwhile is served
|
|
196
|
+
* and ends it in turn. A capture still starting is left to the viewer it is
|
|
197
|
+
* starting for, and the watchdog ends it if that viewer never comes.
|
|
198
|
+
*/
|
|
199
|
+
endWhenUnwatched(udid) {
|
|
200
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
201
|
+
const session = this.sessions.get(udid);
|
|
202
|
+
if (!session || session.status !== 'running')
|
|
203
|
+
return;
|
|
204
|
+
session.endWhenUnwatched = true;
|
|
205
|
+
const viewers = session.mux.clientCount;
|
|
206
|
+
if (viewers > 0) {
|
|
207
|
+
logger_1.default.info(`[${udid}] H.264 stream kept beside MJPEG for its ${viewers} viewer(s); ` +
|
|
208
|
+
'it ends when the last one leaves');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (this.startPromises.has(udid)) {
|
|
212
|
+
logger_1.default.info(`[${udid}] H.264 stream still starting: kept for the viewer it is starting for`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
|
|
216
|
+
yield this.stop(udid);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** The last viewer left `session`: end it if a page asked (endWhenUnwatched). */
|
|
220
|
+
endIfUnwatched(udid, session) {
|
|
221
|
+
// A capture replaced since (a stop and a new start) is not this one.
|
|
222
|
+
if (this.sessions.get(udid) !== session || !session.endWhenUnwatched)
|
|
223
|
+
return;
|
|
224
|
+
logger_1.default.info(`[${udid}] Stopping the H.264 stream: its last viewer left, the phone is shown as MJPEG`);
|
|
225
|
+
void this.stop(udid);
|
|
226
|
+
}
|
|
181
227
|
stop(udid) {
|
|
182
228
|
return __awaiter(this, void 0, void 0, function* () {
|
|
183
229
|
var _a;
|
|
@@ -192,6 +238,9 @@ let AndroidH264StreamService = class AndroidH264StreamService {
|
|
|
192
238
|
/* best-effort */
|
|
193
239
|
}
|
|
194
240
|
this.sessions.delete(udid);
|
|
241
|
+
// Viewers still on it (a recording starting, a stream/stop) are told, so
|
|
242
|
+
// their players fall back to MJPEG instead of freezing on the last frame.
|
|
243
|
+
session.mux.close();
|
|
195
244
|
logger_1.default.info(`[${udid}] H.264 stream terminated.`);
|
|
196
245
|
});
|
|
197
246
|
}
|
|
@@ -9,6 +9,7 @@ class H264Multiplexer {
|
|
|
9
9
|
constructor() {
|
|
10
10
|
this.clients = new Set();
|
|
11
11
|
this.gop = []; // current GOP: [keyframe, ...deltas-since]
|
|
12
|
+
this.closed = false;
|
|
12
13
|
}
|
|
13
14
|
setConfig(p) {
|
|
14
15
|
this.config = p;
|
|
@@ -16,9 +17,21 @@ class H264Multiplexer {
|
|
|
16
17
|
get clientCount() {
|
|
17
18
|
return this.clients.size;
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** Called each time the last client leaves. */
|
|
21
|
+
onEmpty(listener) {
|
|
22
|
+
this.emptyListener = listener;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Register a client sink, and how to end it when the stream does (see
|
|
26
|
+
* `close`). Returns a remover (safe to call twice).
|
|
27
|
+
*/
|
|
28
|
+
addClient(send, end) {
|
|
29
|
+
const c = { send, started: false, end };
|
|
30
|
+
if (this.closed) {
|
|
31
|
+
// Joined a stream already stopped: end it now rather than starve it.
|
|
32
|
+
end === null || end === void 0 ? void 0 : end();
|
|
33
|
+
return () => undefined;
|
|
34
|
+
}
|
|
22
35
|
this.clients.add(c);
|
|
23
36
|
if (this.config)
|
|
24
37
|
send(this.config);
|
|
@@ -29,9 +42,30 @@ class H264Multiplexer {
|
|
|
29
42
|
c.started = true;
|
|
30
43
|
}
|
|
31
44
|
return () => {
|
|
32
|
-
|
|
45
|
+
var _a;
|
|
46
|
+
if (this.clients.delete(c) && this.clients.size === 0)
|
|
47
|
+
(_a = this.emptyListener) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
33
48
|
};
|
|
34
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The capture stopped: end every client (its socket closes, so its player
|
|
52
|
+
* falls back to MJPEG instead of freezing on its last frame). Not an
|
|
53
|
+
* `onEmpty`: nobody left, the stream did.
|
|
54
|
+
*/
|
|
55
|
+
close() {
|
|
56
|
+
var _a;
|
|
57
|
+
this.closed = true;
|
|
58
|
+
const ending = [...this.clients];
|
|
59
|
+
this.clients.clear();
|
|
60
|
+
for (const c of ending) {
|
|
61
|
+
try {
|
|
62
|
+
(_a = c.end) === null || _a === void 0 ? void 0 : _a.call(c);
|
|
63
|
+
}
|
|
64
|
+
catch (_b) {
|
|
65
|
+
/* one client's end must not keep the others open */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
35
69
|
/** Feed one upstream packet; fans out per the join semantics above. */
|
|
36
70
|
push(p) {
|
|
37
71
|
if (p.type === 'config') {
|
|
@@ -27,6 +27,7 @@ const typedi_1 = require("typedi");
|
|
|
27
27
|
const ProcessRegistry_1 = require("../../services/ProcessRegistry");
|
|
28
28
|
const IOSStreamService_1 = __importDefault(require("./IOSStreamService"));
|
|
29
29
|
const IOSTunnels_1 = require("./IOSTunnels");
|
|
30
|
+
const shellCommands_1 = require("../shellCommands");
|
|
30
31
|
const device_store_1 = require("../../data-service/device-store");
|
|
31
32
|
const child_process_1 = require("child_process");
|
|
32
33
|
const util_1 = require("util");
|
|
@@ -1005,57 +1006,57 @@ let WDAClient = WDAClient_1 = class WDAClient {
|
|
|
1005
1006
|
}
|
|
1006
1007
|
executeShell(udid, command) {
|
|
1007
1008
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const GENERIC_COMMANDS = ['ls', 'ps', 'top', 'whoami', 'date', 'uptime', 'netstat', 'id'];
|
|
1012
|
-
const safeCommand = command.trim();
|
|
1013
|
-
const commandWord = safeCommand.split(/\s+/)[0]; // First word is the command
|
|
1014
|
-
// Determine device type
|
|
1009
|
+
// Simulators: a few simctl commands, and generic commands spawned in the
|
|
1010
|
+
// simulator. Real iPhones: go-ios commands for this phone only. Every
|
|
1011
|
+
// command is matched word for word, in plain words (shellCommands.ts).
|
|
1015
1012
|
const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
1016
|
-
const isSimulator = device && !device.realDevice;
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
if (isSimctlAllowed && isSimulator) {
|
|
1029
|
-
// Simulator-specific: xcrun simctl <command> <udid> [args...]
|
|
1030
|
-
const [cmd, ...rest] = args;
|
|
1031
|
-
const { stdout } = yield execFilePromise('xcrun', ['simctl', cmd, udid, ...rest]);
|
|
1032
|
-
return stdout;
|
|
1033
|
-
}
|
|
1034
|
-
if (isGoiosAllowed) {
|
|
1035
|
-
// Real device: go-ios <command> --udid <udid> [args...]
|
|
1036
|
-
if (yield s.isGoIOSAvailable()) {
|
|
1037
|
-
const { stdout } = yield execFilePromise(s.goIOSPath, [...args, '--udid', udid], {
|
|
1038
|
-
env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
|
|
1013
|
+
const isSimulator = !!device && !device.realDevice;
|
|
1014
|
+
const word = command.trim().split(/\s+/)[0];
|
|
1015
|
+
const refuse = (reason) => {
|
|
1016
|
+
this.log.warn(`Blocked shell command on ${udid}: ${command.trim()}`);
|
|
1017
|
+
return new Error(reason);
|
|
1018
|
+
};
|
|
1019
|
+
if (isSimulator) {
|
|
1020
|
+
const simctl = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_SIMCTL_COMMANDS);
|
|
1021
|
+
if ('argv' in simctl) {
|
|
1022
|
+
const [cmd, ...rest] = simctl.argv;
|
|
1023
|
+
const { stdout } = yield execFilePromise('xcrun', ['simctl', cmd, udid, ...rest], {
|
|
1024
|
+
timeout: 10000,
|
|
1039
1025
|
});
|
|
1040
1026
|
return stdout;
|
|
1041
1027
|
}
|
|
1042
|
-
|
|
1028
|
+
const spawned = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_SPAWN_COMMANDS);
|
|
1029
|
+
if ('refused' in spawned)
|
|
1030
|
+
throw refuse(spawned.refused);
|
|
1031
|
+
const { stdout } = yield execFilePromise('xcrun', ['simctl', 'spawn', udid, ...spawned.argv], {
|
|
1032
|
+
timeout: 10000,
|
|
1033
|
+
});
|
|
1034
|
+
return stdout;
|
|
1043
1035
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1036
|
+
const onDevice = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_DEVICE_COMMANDS);
|
|
1037
|
+
if ('refused' in onDevice) {
|
|
1038
|
+
const simulatorOnly = [...shellCommands_1.IOS_SIMCTL_COMMANDS, ...shellCommands_1.IOS_SPAWN_COMMANDS].some((c) => c.words[0] === word);
|
|
1039
|
+
if (simulatorOnly) {
|
|
1040
|
+
throw refuse(`'${word}' runs on simulators only. On a real iPhone, try: ${shellCommands_1.IOS_DEVICE_COMMANDS.map((c) => c.words.join(' ')).join(', ')}.`);
|
|
1041
|
+
}
|
|
1042
|
+
throw refuse(onDevice.refused);
|
|
1047
1043
|
}
|
|
1048
|
-
//
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1051
|
-
return stdout;
|
|
1044
|
+
// Xenon names the phone; a command may not name another one.
|
|
1045
|
+
if (onDevice.argv.some((arg) => arg.startsWith('--udid'))) {
|
|
1046
|
+
throw refuse(`Command '${command.trim()}' is not allowed: Xenon chooses the device.`);
|
|
1052
1047
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
throw new Error(`System command '${commandWord}' is not available on real devices. Use device-specific commands: ${GOIOS_COMMANDS.join(', ')}`);
|
|
1048
|
+
const s = typedi_1.Container.get(IOSStreamService_1.default);
|
|
1049
|
+
if (!(yield s.isGoIOSAvailable())) {
|
|
1050
|
+
throw new Error(`'${word}' needs go-ios, which is not installed on this server.`);
|
|
1057
1051
|
}
|
|
1058
|
-
|
|
1052
|
+
// On iOS 17+ go-ios reaches the phone only through its tunnel.
|
|
1053
|
+
const tunnels = typedi_1.Container.get(IOSTunnels_1.IOSTunnels);
|
|
1054
|
+
yield tunnels.borrow(udid);
|
|
1055
|
+
const { stdout } = yield execFilePromise(s.goIOSPath, [...onDevice.argv, '--udid', udid], {
|
|
1056
|
+
env: tunnels.envFor(udid),
|
|
1057
|
+
timeout: 10000,
|
|
1058
|
+
});
|
|
1059
|
+
return stdout;
|
|
1059
1060
|
});
|
|
1060
1061
|
}
|
|
1061
1062
|
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What the device control Shell tab may run.
|
|
4
|
+
*
|
|
5
|
+
* Android runs a command through `adb shell`, which joins the words and hands
|
|
6
|
+
* them to the phone's `sh`. Through 2.13.2 the allow-list was a `startsWith`
|
|
7
|
+
* test on the whole line, so `ls; reboot`, `ls && pm uninstall …` and `lsof`
|
|
8
|
+
* all ran. Now a command must begin with an allowed command word for word, and
|
|
9
|
+
* every word of it may only use characters a shell gives no meaning to, so the
|
|
10
|
+
* phone's shell can't read anything into it. A command that changes or reads
|
|
11
|
+
* more than the entry is for (`dumpsys battery set …`, `cat` of another file)
|
|
12
|
+
* takes no arguments.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.IOS_DEVICE_COMMANDS = exports.IOS_SIMULATOR_COMMANDS = exports.IOS_SPAWN_COMMANDS = exports.IOS_SIMCTL_COMMANDS = exports.ANDROID_SHELL_COMMANDS = void 0;
|
|
16
|
+
exports.parseShellCommand = parseShellCommand;
|
|
17
|
+
/** Letters, digits and `._/:=@%+,-`: nothing `sh` expands, splits or redirects on. */
|
|
18
|
+
const PLAIN_WORD = /^[A-Za-z0-9._/:=@%+,-]+$/;
|
|
19
|
+
exports.ANDROID_SHELL_COMMANDS = [
|
|
20
|
+
{ words: ['ls'], args: 'plain' },
|
|
21
|
+
{ words: ['ps'], args: 'plain' },
|
|
22
|
+
{ words: ['top'], args: 'plain' },
|
|
23
|
+
{ words: ['dumpsys', 'battery'], args: 'none' },
|
|
24
|
+
{ words: ['dumpsys', 'wifi'], args: 'none' },
|
|
25
|
+
{ words: ['dumpsys', 'power'], args: 'none' },
|
|
26
|
+
{ words: ['whoami'], args: 'none' },
|
|
27
|
+
{ words: ['getprop'], args: 'plain' },
|
|
28
|
+
{ words: ['pm', 'list', 'packages'], args: 'plain' },
|
|
29
|
+
{ words: ['ip', 'addr'], args: 'plain' },
|
|
30
|
+
{ words: ['cat', '/proc/meminfo'], args: 'none' },
|
|
31
|
+
{ words: ['cat', '/proc/cpuinfo'], args: 'none' },
|
|
32
|
+
{ words: ['date'], args: 'plain' },
|
|
33
|
+
{ words: ['uptime'], args: 'none' },
|
|
34
|
+
{ words: ['netstat'], args: 'plain' },
|
|
35
|
+
];
|
|
36
|
+
/** `xcrun simctl <command> <udid> [args]` on a simulator. */
|
|
37
|
+
exports.IOS_SIMCTL_COMMANDS = [
|
|
38
|
+
{ words: ['listapps'], args: 'none' },
|
|
39
|
+
{ words: ['get_app_container'], args: 'plain' },
|
|
40
|
+
{ words: ['getenv'], args: 'plain' },
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* `xcrun simctl spawn <udid> <command>` on a simulator. A simulator's processes
|
|
44
|
+
* see this Mac's file system, so these take no arguments.
|
|
45
|
+
*/
|
|
46
|
+
exports.IOS_SPAWN_COMMANDS = [
|
|
47
|
+
{ words: ['ls'], args: 'none' },
|
|
48
|
+
{ words: ['ps'], args: 'none' },
|
|
49
|
+
{ words: ['whoami'], args: 'none' },
|
|
50
|
+
{ words: ['date'], args: 'none' },
|
|
51
|
+
{ words: ['uptime'], args: 'none' },
|
|
52
|
+
{ words: ['id'], args: 'none' },
|
|
53
|
+
];
|
|
54
|
+
exports.IOS_SIMULATOR_COMMANDS = [
|
|
55
|
+
...exports.IOS_SIMCTL_COMMANDS,
|
|
56
|
+
...exports.IOS_SPAWN_COMMANDS,
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* go-ios commands on a real iPhone (`ios <command> [args] --udid <udid>`). None
|
|
60
|
+
* may name a device itself: Xenon adds this phone's `--udid`.
|
|
61
|
+
*/
|
|
62
|
+
exports.IOS_DEVICE_COMMANDS = [
|
|
63
|
+
{ words: ['apps'], args: 'plain' },
|
|
64
|
+
{ words: ['info'], args: 'none' },
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* The command's argv when it is allowed, or why it isn't. Words are split on
|
|
68
|
+
* spaces; quoting is not supported, since a quote is refused anyway.
|
|
69
|
+
*/
|
|
70
|
+
function parseShellCommand(input, commands) {
|
|
71
|
+
const shown = input.trim();
|
|
72
|
+
// Words are separated by spaces only: a newline or tab means another line.
|
|
73
|
+
if (/[^\S ]/.test(shown)) {
|
|
74
|
+
return {
|
|
75
|
+
refused: `Command '${shown.split(/\s/)[0]}…' is not allowed: one line, words separated by spaces.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const argv = shown.split(/ +/).filter(Boolean);
|
|
79
|
+
if (argv.length === 0)
|
|
80
|
+
return { refused: 'Type a command.' };
|
|
81
|
+
const bad = argv.find((word) => !PLAIN_WORD.test(word));
|
|
82
|
+
if (bad !== undefined) {
|
|
83
|
+
return {
|
|
84
|
+
refused: `Command '${shown}' is not allowed: '${bad}' contains characters the shell would interpret.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const match = commands.find((command) => command.words.length <= argv.length && command.words.every((word, i) => argv[i] === word));
|
|
88
|
+
if (!match)
|
|
89
|
+
return { refused: `Command '${shown}' is not allowed for security reasons.` };
|
|
90
|
+
if (match.args === 'none' && argv.length > match.words.length) {
|
|
91
|
+
return {
|
|
92
|
+
refused: `Command '${shown}' is not allowed: '${match.words.join(' ')}' takes no arguments here.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { argv };
|
|
96
|
+
}
|