@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
|
@@ -56,6 +56,10 @@ const prisma_1 = require("../../prisma");
|
|
|
56
56
|
const SessionManager_1 = require("../../sessions/SessionManager");
|
|
57
57
|
const UniversalMjpegProxy_1 = require("../../helpers/UniversalMjpegProxy");
|
|
58
58
|
const web_config_service_1 = require("../../data-service/web-config-service");
|
|
59
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
60
|
+
const labSettings_1 = require("../../services/settings/labSettings");
|
|
61
|
+
const SelfHealingSwitch_1 = require("../../services/settings/SelfHealingSwitch");
|
|
62
|
+
const aiEngineSettings_1 = require("../../services/settings/aiEngineSettings");
|
|
59
63
|
const typedi_1 = require("typedi");
|
|
60
64
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
61
65
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
@@ -1099,17 +1103,49 @@ function streamLiveSessionVideo(request, response) {
|
|
|
1099
1103
|
}
|
|
1100
1104
|
});
|
|
1101
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* The base URL as the dashboard may see it: a user name, password or query
|
|
1108
|
+
* value it carries is masked. Saving one is refused (aiEngineSettings.ts), but
|
|
1109
|
+
* the server's environment can still hold one, and every admin can read this.
|
|
1110
|
+
*/
|
|
1111
|
+
function maskedBaseUrl(url) {
|
|
1112
|
+
if (!url)
|
|
1113
|
+
return url;
|
|
1114
|
+
let parsed;
|
|
1115
|
+
try {
|
|
1116
|
+
parsed = new URL(url);
|
|
1117
|
+
}
|
|
1118
|
+
catch (_a) {
|
|
1119
|
+
return url;
|
|
1120
|
+
}
|
|
1121
|
+
// Shown as given unless something needs masking: toString() would add a '/'.
|
|
1122
|
+
if (!parsed.username && !parsed.password && !parsed.search && !parsed.hash)
|
|
1123
|
+
return url;
|
|
1124
|
+
if (parsed.username || parsed.password) {
|
|
1125
|
+
parsed.username = '***';
|
|
1126
|
+
parsed.password = '';
|
|
1127
|
+
}
|
|
1128
|
+
for (const name of [...new Set(parsed.searchParams.keys())]) {
|
|
1129
|
+
parsed.searchParams.set(name, '***');
|
|
1130
|
+
}
|
|
1131
|
+
parsed.hash = '';
|
|
1132
|
+
return parsed.toString();
|
|
1133
|
+
}
|
|
1102
1134
|
function getGlobalConfig(request, response) {
|
|
1103
1135
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1104
1136
|
try {
|
|
1105
1137
|
const dbConfig = yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig();
|
|
1106
|
-
//
|
|
1138
|
+
// What the server runs with: a saved value, else the startup option, else
|
|
1139
|
+
// the default. The pages show this (a saved value alone left them to guess
|
|
1140
|
+
// at what a setting never saved was), and `defaults` for "restore defaults".
|
|
1141
|
+
const effective = (0, labSettings_1.effectiveSettings)(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs, dbConfig);
|
|
1142
|
+
// The AI engine settings in effect (aiEngineSettings.ts keeps `config` at
|
|
1143
|
+
// them). Never a key: only whether one is set.
|
|
1107
1144
|
const { config } = yield Promise.resolve().then(() => __importStar(require('../../config')));
|
|
1108
|
-
// Sanitize keys - return boolean existence only
|
|
1109
1145
|
const aiConfig = {
|
|
1110
1146
|
aiProvider: config.aiProvider,
|
|
1111
1147
|
aiModel: config.aiModel,
|
|
1112
|
-
aiBaseUrl: config.aiBaseUrl,
|
|
1148
|
+
aiBaseUrl: maskedBaseUrl(config.aiBaseUrl),
|
|
1113
1149
|
geminiModel: config.geminiModel,
|
|
1114
1150
|
openaiModel: config.openaiModel,
|
|
1115
1151
|
anthropicModel: config.anthropicModel,
|
|
@@ -1118,7 +1154,9 @@ function getGlobalConfig(request, response) {
|
|
|
1118
1154
|
openaiSet: !!config.openaiApiKey,
|
|
1119
1155
|
anthropicSet: !!config.anthropicApiKey,
|
|
1120
1156
|
};
|
|
1121
|
-
return response
|
|
1157
|
+
return response
|
|
1158
|
+
.status(200)
|
|
1159
|
+
.json(Object.assign(Object.assign(Object.assign(Object.assign({}, dbConfig), effective), { defaults: (0, labSettings_1.settingsDefaults)() }), aiConfig));
|
|
1122
1160
|
}
|
|
1123
1161
|
catch (err) {
|
|
1124
1162
|
return response.status(500).json({ error: true, message: err.message });
|
|
@@ -1127,31 +1165,36 @@ function getGlobalConfig(request, response) {
|
|
|
1127
1165
|
}
|
|
1128
1166
|
function updateGlobalConfig(request, response) {
|
|
1129
1167
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1168
|
+
var _a;
|
|
1130
1169
|
try {
|
|
1131
1170
|
const payload = request.body;
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
if (
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1171
|
+
// Before anything is saved: a value the cleanup job would act on, such as a
|
|
1172
|
+
// retention window of 0, is refused rather than stored, and so is an AI
|
|
1173
|
+
// provider or address that could not work, now that it outlives a restart.
|
|
1174
|
+
const problem = (_a = (0, labSettings_1.validateSettingsUpdate)(payload !== null && payload !== void 0 ? payload : {})) !== null && _a !== void 0 ? _a : (0, aiEngineSettings_1.validateAiEngineUpdate)(payload !== null && payload !== void 0 ? payload : {});
|
|
1175
|
+
if (problem) {
|
|
1176
|
+
return response
|
|
1177
|
+
.status(400)
|
|
1178
|
+
.json({ error: 'invalid_setting', field: problem.field, message: problem.message });
|
|
1179
|
+
}
|
|
1180
|
+
// Saved, then in force for the next AI call. Through 2.14 the AI fields
|
|
1181
|
+
// changed the running server only, and a restart undid them.
|
|
1182
|
+
const aiEngine = (0, aiEngineSettings_1.aiEngineUpdateOf)(payload !== null && payload !== void 0 ? payload : {});
|
|
1183
|
+
yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(Object.assign(Object.assign({}, payload), aiEngine));
|
|
1184
|
+
if (Object.keys(aiEngine).length > 0) {
|
|
1185
|
+
typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).noteSaved(aiEngine);
|
|
1186
|
+
}
|
|
1187
|
+
// The command interceptor asks at every command and never reads the
|
|
1188
|
+
// database: hand it the saved value, in force from the next command.
|
|
1189
|
+
if (payload.enableSelfHealing !== undefined) {
|
|
1190
|
+
typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).set(payload.enableSelfHealing);
|
|
1191
|
+
}
|
|
1192
|
+
// The retention values are read at each cleanup run, so they apply by
|
|
1193
|
+
// themselves. A schedule is a timer: replace it now, not at the next restart.
|
|
1194
|
+
if (payload.buildCleanupSchedule !== undefined) {
|
|
1195
|
+
const { rescheduleCleanupBuilds } = yield Promise.resolve().then(() => __importStar(require('../../device-utils')));
|
|
1196
|
+
yield rescheduleCleanupBuilds(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs);
|
|
1152
1197
|
}
|
|
1153
|
-
// Persist Web Configs to DB
|
|
1154
|
-
yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(payload);
|
|
1155
1198
|
return response.status(200).json({ success: true });
|
|
1156
1199
|
}
|
|
1157
1200
|
catch (err) {
|
|
@@ -89,10 +89,12 @@ function getDevices(request, response) {
|
|
|
89
89
|
const [enriched] = yield (0, device_service_1.enrichDevicesWithTeamNames)([match]);
|
|
90
90
|
return response.json(enriched);
|
|
91
91
|
}
|
|
92
|
-
/* dashboard-plugin-url is
|
|
93
|
-
*
|
|
92
|
+
/* dashboard-plugin-url is where this server reaches the appium-dashboard-plugin,
|
|
93
|
+
* and dashboard-plugin-link the base of the link people open; both from
|
|
94
|
+
* configuration, never from the request (app/dashboardPluginLink.ts).
|
|
94
95
|
*/
|
|
95
96
|
const dashboardPluginUrl = request['dashboard-plugin-url'];
|
|
97
|
+
const dashboardPluginLink = request['dashboard-plugin-link'];
|
|
96
98
|
if (dashboardPluginUrl) {
|
|
97
99
|
const response = yield InternalHttpClient_1.InternalHttpClient.get(`${dashboardPluginUrl}/api/sessions?start_time=${SERVER_UP_TIME}`);
|
|
98
100
|
const sessions = ((_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.rows) || [];
|
|
@@ -105,7 +107,7 @@ function getDevices(request, response) {
|
|
|
105
107
|
});
|
|
106
108
|
devices = devices.map((d) => {
|
|
107
109
|
var _a;
|
|
108
|
-
d.dashboard_link = `${
|
|
110
|
+
d.dashboard_link = `${dashboardPluginLink}?device_udid=${d.udid}&start_time=${SERVER_UP_TIME}`;
|
|
109
111
|
d.total_session_count = ((_a = deviceSessionMap[d.udid]) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
110
112
|
return d;
|
|
111
113
|
});
|
|
@@ -402,10 +404,33 @@ function updateTags(request, response) {
|
|
|
402
404
|
if (!udid || !host || !Array.isArray(tags)) {
|
|
403
405
|
return response.status(400).json({ error: 'Missing udid, host, or tags array' });
|
|
404
406
|
}
|
|
407
|
+
// The tags are saved for the phone (deviceSettings.ts): only for one that
|
|
408
|
+
// is here. Through 2.13 any udid and host answered 200 and wrote nothing.
|
|
409
|
+
if (!(yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid, host }))) {
|
|
410
|
+
return response.status(404).json({ error: 'Device not found' });
|
|
411
|
+
}
|
|
405
412
|
yield (0, device_service_1.updateDeviceTags)(udid, host, tags);
|
|
406
413
|
response.status(200).json({ success: true });
|
|
407
414
|
});
|
|
408
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Every phone of this udid that this server knows: those connected now, and
|
|
418
|
+
* those whose settings are saved while they are away (deviceSettings.ts), so
|
|
419
|
+
* a phone that is not connected can still be moved, and its team deleted.
|
|
420
|
+
*/
|
|
421
|
+
function phonesOfUdid(udid) {
|
|
422
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
423
|
+
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
424
|
+
const phones = new Map();
|
|
425
|
+
for (const phone of [
|
|
426
|
+
...(yield store.findDevices({ udid })),
|
|
427
|
+
...(yield store.findSavedPhones(udid)),
|
|
428
|
+
]) {
|
|
429
|
+
phones.set(`${phone.udid}\u0000${phone.host}`, { udid: phone.udid, host: phone.host });
|
|
430
|
+
}
|
|
431
|
+
return [...phones.values()];
|
|
432
|
+
});
|
|
433
|
+
}
|
|
409
434
|
function assignDeviceToTeam(request, response) {
|
|
410
435
|
return __awaiter(this, void 0, void 0, function* () {
|
|
411
436
|
const { udid } = request.params;
|
|
@@ -415,20 +440,19 @@ function assignDeviceToTeam(request, response) {
|
|
|
415
440
|
if (!team)
|
|
416
441
|
return response.status(404).json({ error: 'team not found' });
|
|
417
442
|
}
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
data: { teamId: teamId || null },
|
|
421
|
-
});
|
|
422
|
-
if (result.count === 0)
|
|
443
|
+
const phones = yield phonesOfUdid(udid);
|
|
444
|
+
if (phones.length === 0)
|
|
423
445
|
return response.status(404).json({ error: 'device not found' });
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
for (const
|
|
427
|
-
yield
|
|
446
|
+
// Through the store, which saves the team for the phone as it writes the
|
|
447
|
+
// row: a phone keeps it when it disconnects and comes back.
|
|
448
|
+
for (const phone of phones) {
|
|
449
|
+
yield device_store_1.DeviceStoreFactory.getStore().updateDevice(phone.udid, phone.host, {
|
|
450
|
+
teamId: teamId || null,
|
|
451
|
+
});
|
|
428
452
|
}
|
|
429
453
|
// The live dashboard events follow the new team now, not after the cache's TTL.
|
|
430
454
|
typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver).note(udid, teamId || null);
|
|
431
|
-
response.json({ ok: true, updated:
|
|
455
|
+
response.json({ ok: true, updated: phones.length });
|
|
432
456
|
});
|
|
433
457
|
}
|
|
434
458
|
/**
|
|
@@ -17,6 +17,7 @@ const ApiKeyService_1 = require("../../services/ApiKeyService");
|
|
|
17
17
|
const UserService_1 = require("../../services/UserService");
|
|
18
18
|
const prisma_1 = require("../../prisma");
|
|
19
19
|
const profileIdentity_1 = require("./profileIdentity");
|
|
20
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
20
21
|
const ROLE_SCOPES = {
|
|
21
22
|
SUPER_ADMIN: ['admin'],
|
|
22
23
|
ADMIN: ['devices', 'sessions', 'read'],
|
|
@@ -79,10 +80,23 @@ function profileRouter() {
|
|
|
79
80
|
return res.status(404).json({ error: 'user not found' });
|
|
80
81
|
return res.json({ accessKey: user.accessKey });
|
|
81
82
|
}));
|
|
83
|
+
// Rotating the access key ends every key pair its owner has, whatever their
|
|
84
|
+
// scopes, so it takes the person themselves (a dashboard sign-in) or a
|
|
85
|
+
// credential with the admin scope. Through 2.14 any credential could, a
|
|
86
|
+
// read-only token included, and cut off every one of its owner's clients.
|
|
82
87
|
r.post('/access-key/rotate', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
var _a;
|
|
83
89
|
const auth = requireAuth(req, res);
|
|
84
90
|
if (!auth)
|
|
85
91
|
return;
|
|
92
|
+
const credentialScopes = String((_a = auth.scopes) !== null && _a !== void 0 ? _a : '')
|
|
93
|
+
.split(',')
|
|
94
|
+
.map((s) => s.trim());
|
|
95
|
+
if (auth.kind !== 'user-session' && !credentialScopes.includes('admin')) {
|
|
96
|
+
return res.status(403).json({
|
|
97
|
+
error: 'rotating the access key needs a dashboard sign-in or a credential with the admin scope',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
86
100
|
const userId = yield effectiveUserId(auth.userId);
|
|
87
101
|
if (!userId)
|
|
88
102
|
return res.status(404).json({ error: 'user not found' });
|
|
@@ -132,6 +146,13 @@ function profileRouter() {
|
|
|
132
146
|
}
|
|
133
147
|
expiresAtDate = d;
|
|
134
148
|
}
|
|
149
|
+
// A token made with a credential that expires (a Bearer token, an API key
|
|
150
|
+
// with an expiry) ends no later than it. Through 2.14 a 1-hour Bearer
|
|
151
|
+
// token could make itself a key that never expired.
|
|
152
|
+
const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, auth.credentialExpiresAt);
|
|
153
|
+
if ('error' in within)
|
|
154
|
+
return res.status(400).json({ error: within.error });
|
|
155
|
+
expiresAtDate = within.expiresAt;
|
|
135
156
|
const allowed = (_a = ROLE_SCOPES[auth.role]) !== null && _a !== void 0 ? _a : ROLE_SCOPES.MEMBER;
|
|
136
157
|
if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(isScope))) {
|
|
137
158
|
return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
|
|
@@ -9,10 +9,15 @@ exports.resolveStreamType = resolveStreamType;
|
|
|
9
9
|
* MJPEG path so the recording pipeline (which reads the MJPEG server) is
|
|
10
10
|
* unaffected (spec phase-1 rule). iOS/tvOS always use MJPEG (WDA emits it
|
|
11
11
|
* directly).
|
|
12
|
+
*
|
|
13
|
+
* `clientCanPlayH264` is false when the page that starts the stream shows
|
|
14
|
+
* MJPEG: device control, or a browser without WebCodecs (exposed only on
|
|
15
|
+
* https and localhost). Starting an H.264 capture for it would run a second
|
|
16
|
+
* capture beside the screencap loop its `<img>` starts.
|
|
12
17
|
*/
|
|
13
|
-
function resolveStreamType(platform, flagOn, recording) {
|
|
18
|
+
function resolveStreamType(platform, flagOn, recording, clientCanPlayH264 = true) {
|
|
14
19
|
const isAndroid = platform === 'android' || platform === 'androidtv' || platform === 'android-tv';
|
|
15
|
-
if (!isAndroid || !flagOn || recording)
|
|
20
|
+
if (!isAndroid || !flagOn || recording || !clientCanPlayH264)
|
|
16
21
|
return 'mjpeg';
|
|
17
22
|
return 'h264';
|
|
18
23
|
}
|
|
@@ -141,8 +141,11 @@ function usersRouter() {
|
|
|
141
141
|
});
|
|
142
142
|
}));
|
|
143
143
|
// Admin-issued password reset. Replaces relaying a link out of the server
|
|
144
|
-
// log: with SMTP the link is emailed to the user
|
|
145
|
-
//
|
|
144
|
+
// log: with SMTP and XENON_PUBLIC_URL the link is emailed to the user;
|
|
145
|
+
// otherwise it is returned once, to the admin who asked, and never logged.
|
|
146
|
+
// An emailed link points at XENON_PUBLIC_URL only, never the request's
|
|
147
|
+
// Host: the admin's request could name any host, and the user would get a
|
|
148
|
+
// genuine Xenon email sending their token there.
|
|
146
149
|
r.post('/:id/reset-link', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
147
150
|
const auth = getAuth(req);
|
|
148
151
|
const targetId = req.params.id;
|
|
@@ -168,10 +171,11 @@ function usersRouter() {
|
|
|
168
171
|
const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
|
|
169
172
|
const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
|
|
170
173
|
const { raw } = yield resetSvc.createToken(target.id);
|
|
171
|
-
const
|
|
174
|
+
const publicBase = (0, passwordResetLink_1.resetLinkBase)();
|
|
172
175
|
const expiresAt = new Date(Date.now() + resetSvc.ttlMs()).toISOString();
|
|
173
|
-
if (emailSvc.hasSmtp()) {
|
|
174
|
-
|
|
176
|
+
if (emailSvc.hasSmtp() && publicBase) {
|
|
177
|
+
const link = (0, passwordResetLink_1.buildResetLink)(publicBase, raw);
|
|
178
|
+
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link, resetSvc.ttlMs()));
|
|
175
179
|
auditLog.info(`${auth.userId} emailed a password-reset link to user ${target.id}`);
|
|
176
180
|
return res.json({ emailed: true, expiresAt });
|
|
177
181
|
}
|
|
@@ -179,6 +183,7 @@ function usersRouter() {
|
|
|
179
183
|
auditLog.warn(`${auth.userId} issued a password-reset link for user ${target.id} (returned to the admin)`);
|
|
180
184
|
// The body is a credential: keep it out of browser and proxy caches.
|
|
181
185
|
res.set('Cache-Control', 'no-store');
|
|
186
|
+
const link = (0, passwordResetLink_1.buildResetLink)(publicBase !== null && publicBase !== void 0 ? publicBase : (0, passwordResetLink_1.requestBase)(req), raw);
|
|
182
187
|
return res.json({ emailed: false, link, expiresAt });
|
|
183
188
|
}));
|
|
184
189
|
r.delete('/:id', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
const express_1 = require("express");
|
|
16
16
|
const NotificationService_1 = require("../../services/NotificationService");
|
|
17
|
+
const webhookEvents_1 = require("../../services/webhookEvents");
|
|
17
18
|
const typedi_1 = require("typedi");
|
|
18
19
|
const logger_1 = __importDefault(require("../../logger"));
|
|
19
20
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
@@ -63,17 +64,20 @@ function deleteConfig(req, res) {
|
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
|
-
// Sends a sample event the way the webhook's real events go out (its
|
|
67
|
-
// and template), and says whether it was delivered.
|
|
67
|
+
// Sends a sample of an event the way the webhook's real events go out (its
|
|
68
|
+
// type and template), and says whether it was delivered.
|
|
68
69
|
function testWebhook(req, res) {
|
|
69
70
|
return __awaiter(this, void 0, void 0, function* () {
|
|
70
71
|
var _a, _b;
|
|
71
|
-
const { url, type, payloadTemplate } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
|
|
72
|
+
const { url, type, payloadTemplate, event } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
|
|
72
73
|
if (!url || typeof url !== 'string') {
|
|
73
74
|
return res.status(400).json({ error: 'url is required' });
|
|
74
75
|
}
|
|
76
|
+
if (event !== undefined && !(0, webhookEvents_1.isEventType)(event)) {
|
|
77
|
+
return res.status(400).json({ error: `event must be one of: ${webhookEvents_1.EVENT_TYPES.join(', ')}` });
|
|
78
|
+
}
|
|
75
79
|
try {
|
|
76
|
-
yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate);
|
|
80
|
+
yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate, event);
|
|
77
81
|
res.json({ success: true });
|
|
78
82
|
}
|
|
79
83
|
catch (err) {
|
package/lib/src/app/swagger.js
CHANGED
|
@@ -75,6 +75,8 @@ Appium's WebDriver API (\`POST /session\` and the session's commands, under Appi
|
|
|
75
75
|
- \`accessKey\` and \`token\`;
|
|
76
76
|
- \`sessionToken\`, minted by \`POST /api/auth/token\`.
|
|
77
77
|
|
|
78
|
+
Either names the session's owner only while that user exists and is active, and a session token needs the \`sessions\` scope, as a key does. A key or token that doesn't check out counts as none: with \`XENON_REQUIRE_SESSION_TOKEN\` the session is refused, without it the session runs with no owner.
|
|
79
|
+
|
|
78
80
|
A leased device also takes \`leaseId\` and \`leaseToken\`, which the lease's \`appiumCapabilities\` already carry. Xenon removes these credentials before the driver or any stored record sees them.
|
|
79
81
|
|
|
80
82
|
## Live streams
|
|
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.STREAM_ENDED = void 0;
|
|
15
16
|
exports.encodeWsFrame = encodeWsFrame;
|
|
16
17
|
exports.parseH264WsPath = parseH264WsPath;
|
|
17
18
|
exports.attachH264Ws = attachH264Ws;
|
|
@@ -29,6 +30,12 @@ const nodeSocketRelay_1 = require("./nodeSocketRelay");
|
|
|
29
30
|
* so it is not put on the wire).
|
|
30
31
|
*/
|
|
31
32
|
const TYPE_CODE = { config: 0, key: 1, delta: 2 };
|
|
33
|
+
/**
|
|
34
|
+
* The close code for a stream whose capture stopped under its viewer, as the
|
|
35
|
+
* logcat socket uses it for an upstream that died. The player then shows MJPEG
|
|
36
|
+
* without asking stream/start: there is no H.264 capture left to end.
|
|
37
|
+
*/
|
|
38
|
+
exports.STREAM_ENDED = 1012;
|
|
32
39
|
function encodeWsFrame(p) {
|
|
33
40
|
return Buffer.concat([Buffer.from([TYPE_CODE[p.type]]), p.data]);
|
|
34
41
|
}
|
|
@@ -139,7 +146,9 @@ function attachH264Ws(server, deps) {
|
|
|
139
146
|
if (p.type === 'delta' && ws.bufferedAmount > maxBuffered)
|
|
140
147
|
return;
|
|
141
148
|
ws.send(encodeWsFrame(p));
|
|
142
|
-
}
|
|
149
|
+
},
|
|
150
|
+
// The capture stopped under this viewer (a recording, a stream/stop).
|
|
151
|
+
() => ws.close(exports.STREAM_ENDED, 'stream ended'));
|
|
143
152
|
logger_1.default.info(`[${parsed.udid}] H.264 WS client connected`);
|
|
144
153
|
}));
|
|
145
154
|
}
|
package/lib/src/config.js
CHANGED
|
@@ -32,14 +32,20 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var _a;
|
|
36
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.config = void 0;
|
|
36
|
+
exports.config = exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = void 0;
|
|
38
37
|
exports.updateConfig = updateConfig;
|
|
39
38
|
exports.resolveAuthDisabled = resolveAuthDisabled;
|
|
39
|
+
exports.resolveMaxConcurrentRecordings = resolveMaxConcurrentRecordings;
|
|
40
|
+
exports.resolveRecordingsAssetsPath = resolveRecordingsAssetsPath;
|
|
41
|
+
exports.recordingConfigFrom = recordingConfigFrom;
|
|
40
42
|
const os = __importStar(require("os"));
|
|
41
43
|
const path = __importStar(require("path"));
|
|
42
44
|
const basePath = path.join(os.homedir(), '.cache', 'xenon');
|
|
45
|
+
/** Where free-form recordings go unless the option or the environment says otherwise. */
|
|
46
|
+
const DEFAULT_RECORDINGS_ASSETS_PATH = path.join(basePath, 'assets', 'sessions', 'recordings');
|
|
47
|
+
/** The server-wide cap on simultaneous free-form recordings, unless set. */
|
|
48
|
+
exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = 4;
|
|
43
49
|
exports.config = {
|
|
44
50
|
cacheDir: basePath,
|
|
45
51
|
databaseProvider: process.env.XENON_DB_PROVIDER || 'sqlite',
|
|
@@ -81,15 +87,15 @@ exports.config = {
|
|
|
81
87
|
userSessionTtlMs: Number(process.env.XENON_USER_SESSION_TTL_MS) || 24 * 60 * 60 * 1000,
|
|
82
88
|
smtpUrl: process.env.XENON_SMTP_URL,
|
|
83
89
|
smtpFrom: process.env.XENON_SMTP_FROM,
|
|
90
|
+
publicUrl: process.env.XENON_PUBLIC_URL,
|
|
84
91
|
resetTokenTtlMs: Number(process.env.XENON_RESET_TOKEN_TTL_MS) || 60 * 60 * 1000,
|
|
85
92
|
// Opt-in since 1.20.7: it writes the raw reset link (a credential) to the
|
|
86
93
|
// server log. Without SMTP, admins issue links from the Users page instead.
|
|
87
94
|
passwordResetLogFallback: process.env.XENON_PASSWORD_RESET_LOG_FALLBACK === 'true',
|
|
88
95
|
resetRateLimitAttempts: Number(process.env.XENON_RESET_RATE_LIMIT_ATTEMPTS) || 3,
|
|
89
96
|
resetRateLimitWindowMs: Number(process.env.XENON_RESET_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
|
90
|
-
recordingsAssetsPath: process.env.XENON_RECORDINGS_ASSETS_PATH
|
|
91
|
-
|
|
92
|
-
maxConcurrentRecordings: Number((_a = process.env.XENON_MAX_CONCURRENT_RECORDINGS) !== null && _a !== void 0 ? _a : 4),
|
|
97
|
+
recordingsAssetsPath: resolveRecordingsAssetsPath(undefined, process.env.XENON_RECORDINGS_ASSETS_PATH),
|
|
98
|
+
maxConcurrentRecordings: resolveMaxConcurrentRecordings(undefined, process.env.XENON_MAX_CONCURRENT_RECORDINGS),
|
|
93
99
|
};
|
|
94
100
|
function updateConfig(newConfig) {
|
|
95
101
|
Object.assign(exports.config, newConfig);
|
|
@@ -113,3 +119,43 @@ function updateConfig(newConfig) {
|
|
|
113
119
|
function resolveAuthDisabled(pluginArgValue, envDisabled) {
|
|
114
120
|
return pluginArgValue === true || envDisabled === true;
|
|
115
121
|
}
|
|
122
|
+
function wholeNumberAtLeastOne(value) {
|
|
123
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 ? value : undefined;
|
|
124
|
+
}
|
|
125
|
+
function nonBlankString(value) {
|
|
126
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The cap on simultaneous free-form recordings: the `maxConcurrentRecordings`
|
|
130
|
+
* plugin option when it is set, else XENON_MAX_CONCURRENT_RECORDINGS, else 4.
|
|
131
|
+
* Both options are `schema.json` options Appium accepts, but only the variable
|
|
132
|
+
* was ever read. A value that is not a whole number of at least 1 is ignored,
|
|
133
|
+
* so the next source answers: the variable used to be taken as `Number(env)`,
|
|
134
|
+
* and `many` made that NaN, which no recording count exceeds (no cap at all).
|
|
135
|
+
* Pure, so the precedence is testable without booting a server.
|
|
136
|
+
*/
|
|
137
|
+
function resolveMaxConcurrentRecordings(optionValue, envValue) {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
const fromEnv = (envValue === null || envValue === void 0 ? void 0 : envValue.trim()) ? Number(envValue) : undefined;
|
|
140
|
+
return ((_b = (_a = wholeNumberAtLeastOne(optionValue)) !== null && _a !== void 0 ? _a : wholeNumberAtLeastOne(fromEnv)) !== null && _b !== void 0 ? _b : exports.DEFAULT_MAX_CONCURRENT_RECORDINGS);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Where free-form recordings are stored: the `recordingsAssetsPath` plugin
|
|
144
|
+
* option when it is set, else XENON_RECORDINGS_ASSETS_PATH, else
|
|
145
|
+
* `~/.cache/xenon/assets/sessions/recordings`. A blank value counts as unset.
|
|
146
|
+
*/
|
|
147
|
+
function resolveRecordingsAssetsPath(optionValue, envValue) {
|
|
148
|
+
var _a, _b;
|
|
149
|
+
return (_b = (_a = nonBlankString(optionValue)) !== null && _a !== void 0 ? _a : nonBlankString(envValue)) !== null && _b !== void 0 ? _b : DEFAULT_RECORDINGS_ASSETS_PATH;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The part of `config` the recording plugin options set, from the options the
|
|
153
|
+
* server started with and the environment as it is now. Applied at boot,
|
|
154
|
+
* before the artifact store and the concurrency gate read them.
|
|
155
|
+
*/
|
|
156
|
+
function recordingConfigFrom(pluginArgs, env = process.env) {
|
|
157
|
+
return {
|
|
158
|
+
maxConcurrentRecordings: resolveMaxConcurrentRecordings(pluginArgs.maxConcurrentRecordings, env.XENON_MAX_CONCURRENT_RECORDINGS),
|
|
159
|
+
recordingsAssetsPath: resolveRecordingsAssetsPath(pluginArgs.recordingsAssetsPath, env.XENON_RECORDINGS_ASSETS_PATH),
|
|
160
|
+
};
|
|
161
|
+
}
|