@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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const SessionLifecycleService_1 = require("../../../src/services/SessionLifecycleService");
|
|
20
|
+
const NetworkConditioningService_1 = require("../../../src/services/NetworkConditioningService");
|
|
21
|
+
const InterceptorService_1 = require("../../../src/services/InterceptorService");
|
|
22
|
+
const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
|
|
23
|
+
const PluginContext_1 = require("../../../src/PluginContext");
|
|
24
|
+
const IPluginArgs_1 = require("../../../src/interfaces/IPluginArgs");
|
|
25
|
+
const XenonCapabilityManager_1 = require("../../../src/XenonCapabilityManager");
|
|
26
|
+
const SessionManager_1 = require("../../../src/sessions/SessionManager");
|
|
27
|
+
const SessionType_1 = __importDefault(require("../../../src/enums/SessionType"));
|
|
28
|
+
const container_registration_1 = require("../../helpers/container-registration");
|
|
29
|
+
const scratch_database_1 = require("../../helpers/scratch-database");
|
|
30
|
+
const android = { udid: 'R5CT', platform: 'android', deviceType: 'real', host: 'h' };
|
|
31
|
+
const sessionOf = (type) => ({
|
|
32
|
+
getId: () => 's1',
|
|
33
|
+
getType: () => type,
|
|
34
|
+
getCapabilities: () => ({}),
|
|
35
|
+
apiKeyId: null,
|
|
36
|
+
userId: null,
|
|
37
|
+
startVideoRecording: sinon_1.default.stub().resolves(),
|
|
38
|
+
});
|
|
39
|
+
const capsWith = (extra) => (Object.assign({ [XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING]: false, [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS]: [] }, extra));
|
|
40
|
+
/**
|
|
41
|
+
* Where a session's network changes are made. Only the server that drives the
|
|
42
|
+
* phone makes them: a hub used to run its own adb against a node's phone (on
|
|
43
|
+
* one Mac, the very same phone), and start a second proxy for it.
|
|
44
|
+
*/
|
|
45
|
+
describe('a session’s network changes at its start', function () {
|
|
46
|
+
this.timeout(30000);
|
|
47
|
+
// The row a hub writes for a session another server runs (recordRoutedSession).
|
|
48
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
49
|
+
let restoreRegs;
|
|
50
|
+
let order;
|
|
51
|
+
let applyProfile;
|
|
52
|
+
let start;
|
|
53
|
+
let restoreLeftovers;
|
|
54
|
+
let context;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(NetworkConditioningService_1.NetworkConditioningService, InterceptorService_1.InterceptorService, PhoneNetworkRestore_1.PhoneNetworkRestore, PluginContext_1.PluginContext);
|
|
57
|
+
order = [];
|
|
58
|
+
applyProfile = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('profile'); }));
|
|
59
|
+
start = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('interceptor'); }));
|
|
60
|
+
restoreLeftovers = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
order.push('leftovers');
|
|
62
|
+
return 0;
|
|
63
|
+
}));
|
|
64
|
+
typedi_1.Container.set(NetworkConditioningService_1.NetworkConditioningService, { applyProfile });
|
|
65
|
+
typedi_1.Container.set(InterceptorService_1.InterceptorService, { start });
|
|
66
|
+
typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreLeftovers });
|
|
67
|
+
context = new PluginContext_1.PluginContext();
|
|
68
|
+
context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
|
|
69
|
+
typedi_1.Container.set(PluginContext_1.PluginContext, context);
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
sinon_1.default.restore();
|
|
73
|
+
SessionManager_1.SESSION_MANAGER.removeSession('s1');
|
|
74
|
+
restoreRegs();
|
|
75
|
+
yield scratch.db.session.deleteMany({});
|
|
76
|
+
}));
|
|
77
|
+
const apply = (type, caps) => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).applyPostSessionLogic(sessionOf(type), caps, android);
|
|
78
|
+
it('puts back what an earlier session left on the phone, then applies its own', () => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
yield apply(SessionType_1.default.LOCAL, capsWith({
|
|
80
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
|
|
81
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
|
|
82
|
+
}));
|
|
83
|
+
(0, chai_1.expect)(restoreLeftovers.calledOnceWith({ udid: 'R5CT' })).to.equal(true);
|
|
84
|
+
(0, chai_1.expect)(order).to.deep.equal(['leftovers', 'profile', 'interceptor']);
|
|
85
|
+
}));
|
|
86
|
+
it("doesn't touch a phone another server drives", () => __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
for (const type of [SessionType_1.default.REMOTE, SessionType_1.default.CLOUD]) {
|
|
88
|
+
yield apply(type, capsWith({
|
|
89
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
|
|
90
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
|
|
91
|
+
}));
|
|
92
|
+
SessionManager_1.SESSION_MANAGER.removeSession('s1');
|
|
93
|
+
}
|
|
94
|
+
(0, chai_1.expect)(order).to.deep.equal([]);
|
|
95
|
+
}));
|
|
96
|
+
it("starts the interceptor by the server's option when the session doesn't say", () => __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true, bufferSize: 9 } });
|
|
98
|
+
yield apply(SessionType_1.default.LOCAL, capsWith({}));
|
|
99
|
+
(0, chai_1.expect)(start.calledOnce).to.equal(true);
|
|
100
|
+
const [sessionId, device, opts] = start.firstCall.args;
|
|
101
|
+
(0, chai_1.expect)(sessionId).to.equal('s1');
|
|
102
|
+
(0, chai_1.expect)(device).to.equal(android);
|
|
103
|
+
(0, chai_1.expect)(opts).to.include({ enabled: true, bufferSize: 9, captureBodies: true });
|
|
104
|
+
}));
|
|
105
|
+
it("leaves the interceptor off when the session turns it off over the server's option", () => __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true } });
|
|
107
|
+
yield apply(SessionType_1.default.LOCAL, capsWith({ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: false }));
|
|
108
|
+
(0, chai_1.expect)(start.called).to.equal(false);
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const net_1 = __importDefault(require("net"));
|
|
18
|
+
const xenonProxy_1 = require("../../../src/services/network/xenonProxy");
|
|
19
|
+
describe('a phone proxy setting, read', () => {
|
|
20
|
+
it('parses host:port', () => {
|
|
21
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('127.0.0.1:11100')).to.deep.equal({ host: '127.0.0.1', port: 11100 });
|
|
22
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(' 10.0.2.2:11105 ')).to.deep.equal({ host: '10.0.2.2', port: 11105 });
|
|
23
|
+
});
|
|
24
|
+
it('reads no proxy from what Android stores for none', () => {
|
|
25
|
+
for (const none of [null, undefined, '', 'null', ':0', ' ']) {
|
|
26
|
+
(0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)(none), String(none)).to.equal(true);
|
|
27
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(none)).to.equal(null);
|
|
28
|
+
}
|
|
29
|
+
(0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)('proxy.corp:3128')).to.equal(false);
|
|
30
|
+
});
|
|
31
|
+
it('refuses a value that is not host:port', () => {
|
|
32
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('proxy.corp')).to.equal(null);
|
|
33
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:b')).to.equal(null);
|
|
34
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:0')).to.equal(null);
|
|
35
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:70000')).to.equal(null);
|
|
36
|
+
});
|
|
37
|
+
it('knows the addresses a phone uses to reach this machine', () => {
|
|
38
|
+
const own = ['192.168.1.20'];
|
|
39
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('127.0.0.1', own)).to.equal(true);
|
|
40
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('localhost', own)).to.equal(true);
|
|
41
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('10.0.2.2', own)).to.equal(true);
|
|
42
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.20', own)).to.equal(true);
|
|
43
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.21', own)).to.equal(false);
|
|
44
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('proxy.corp', own)).to.equal(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('isPortListening', function () {
|
|
48
|
+
let server;
|
|
49
|
+
let livePort;
|
|
50
|
+
let deadPort;
|
|
51
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
server = net_1.default.createServer((s) => s.destroy());
|
|
53
|
+
yield new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
|
|
54
|
+
livePort = server.address().port;
|
|
55
|
+
const closed = net_1.default.createServer();
|
|
56
|
+
yield new Promise((r) => closed.listen(0, '127.0.0.1', () => r()));
|
|
57
|
+
deadPort = closed.address().port;
|
|
58
|
+
yield new Promise((r) => closed.close(() => r()));
|
|
59
|
+
}));
|
|
60
|
+
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
yield new Promise((r) => server.close(() => r()));
|
|
62
|
+
}));
|
|
63
|
+
it('says a port with a listener answers', () => __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
(0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(livePort)).to.equal(true);
|
|
65
|
+
}));
|
|
66
|
+
it('says a closed port does not', () => __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
(0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(deadPort)).to.equal(false);
|
|
68
|
+
}));
|
|
69
|
+
});
|
|
@@ -81,7 +81,7 @@ describe('Device filter tests', () => {
|
|
|
81
81
|
(0, chai_1.expect)(filter).to.deep.equal({
|
|
82
82
|
platform: 'ios',
|
|
83
83
|
platformVersion: '14.0',
|
|
84
|
-
|
|
84
|
+
appleFamily: 'iphone',
|
|
85
85
|
deviceType: 'real',
|
|
86
86
|
udid: ['21112-1111-1111-111'],
|
|
87
87
|
minSDK: undefined,
|
|
@@ -113,7 +113,7 @@ describe('Device filter tests', () => {
|
|
|
113
113
|
(0, chai_1.expect)(filter).to.deep.equal({
|
|
114
114
|
platform: 'ios',
|
|
115
115
|
platformVersion: '14.0',
|
|
116
|
-
|
|
116
|
+
appleFamily: 'iphone',
|
|
117
117
|
filterByHost: undefined,
|
|
118
118
|
deviceType: 'simulator',
|
|
119
119
|
udid: [],
|
|
@@ -140,7 +140,7 @@ describe('Device filter tests', () => {
|
|
|
140
140
|
platform: 'ios',
|
|
141
141
|
filterByHost: undefined,
|
|
142
142
|
platformVersion: undefined,
|
|
143
|
-
|
|
143
|
+
appleFamily: 'iphone',
|
|
144
144
|
deviceType: 'simulator',
|
|
145
145
|
udid: [],
|
|
146
146
|
minSDK: '10.2.0',
|
|
@@ -39,7 +39,12 @@ describe('profile router', () => {
|
|
|
39
39
|
}));
|
|
40
40
|
it('POST /profile/access-key/rotate returns a new accessKey', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
41
|
sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey').resolves({ id: 'u1', accessKey: 'xen_NEW' });
|
|
42
|
-
const app = appWithAuth({
|
|
42
|
+
const app = appWithAuth({
|
|
43
|
+
kind: 'user-session',
|
|
44
|
+
userId: 'u1',
|
|
45
|
+
role: 'ADMIN',
|
|
46
|
+
scopes: 'admin,devices,sessions,read',
|
|
47
|
+
});
|
|
43
48
|
const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
|
|
44
49
|
(0, chai_1.expect)(r.body.accessKey).to.equal('xen_NEW');
|
|
45
50
|
}));
|
|
@@ -147,4 +152,120 @@ describe('profile router', () => {
|
|
|
147
152
|
const r = yield (0, loopbackRequest_1.default)(app).delete('/profile/tokens/k1');
|
|
148
153
|
(0, chai_1.expect)(r.status).to.equal(404);
|
|
149
154
|
}));
|
|
155
|
+
// A credential can't do more to its owner's account than it could itself
|
|
156
|
+
// have been made to do.
|
|
157
|
+
describe('what the credential in use limits', () => {
|
|
158
|
+
// Rotating the access key ends every key pair its owner has, whatever
|
|
159
|
+
// their scopes, so it takes the person themselves (a dashboard sign-in)
|
|
160
|
+
// or a credential with the admin scope. Any credential, a read-only
|
|
161
|
+
// token included, could do it.
|
|
162
|
+
describe('POST /profile/access-key/rotate', () => {
|
|
163
|
+
let rotate;
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
rotate = sinon_1.default
|
|
166
|
+
.stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey')
|
|
167
|
+
.resolves({ id: 'u1', accessKey: 'xen_NEW' });
|
|
168
|
+
});
|
|
169
|
+
for (const [kind, scopes] of [
|
|
170
|
+
['bearer', 'read'],
|
|
171
|
+
['api-key', 'read'],
|
|
172
|
+
['api-key', 'devices,sessions,read'],
|
|
173
|
+
['bearer', 'sessions'],
|
|
174
|
+
]) {
|
|
175
|
+
it(`is refused to ${kind === 'bearer' ? 'a Bearer token' : 'an API key'} with ${scopes}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
176
|
+
const app = appWithAuth({ kind, userId: 'u1', role: 'ADMIN', scopes });
|
|
177
|
+
const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
|
|
178
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
179
|
+
(0, chai_1.expect)(r.body.error).to.match(/dashboard sign-in or a credential with the admin scope/);
|
|
180
|
+
(0, chai_1.expect)(rotate.called).to.equal(false);
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
it('is allowed to a dashboard sign-in, a member included', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
+
const app = appWithAuth({
|
|
185
|
+
kind: 'user-session',
|
|
186
|
+
userId: 'u1',
|
|
187
|
+
role: 'MEMBER',
|
|
188
|
+
scopes: 'devices,sessions,read',
|
|
189
|
+
});
|
|
190
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
|
|
191
|
+
}));
|
|
192
|
+
it('is allowed to a credential with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
const app = appWithAuth({
|
|
194
|
+
kind: 'api-key',
|
|
195
|
+
userId: 'u1',
|
|
196
|
+
role: 'SUPER_ADMIN',
|
|
197
|
+
scopes: 'admin',
|
|
198
|
+
});
|
|
199
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
|
|
200
|
+
}));
|
|
201
|
+
});
|
|
202
|
+
// A token made with a credential that expires ends no later than it: a
|
|
203
|
+
// 1-hour Bearer token made itself a key that never expired.
|
|
204
|
+
describe('POST /profile/tokens from a credential that expires', () => {
|
|
205
|
+
const inAnHour = new Date(Date.now() + 3600000);
|
|
206
|
+
const bearer = () => appWithAuth({
|
|
207
|
+
kind: 'bearer',
|
|
208
|
+
userId: 'u1',
|
|
209
|
+
role: 'MEMBER',
|
|
210
|
+
scopes: 'sessions,read',
|
|
211
|
+
credentialExpiresAt: inAnHour.getTime(),
|
|
212
|
+
});
|
|
213
|
+
let create;
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
create = sinon_1.default
|
|
216
|
+
.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
|
|
217
|
+
.resolves({ id: 'k9', raw: 'raw' });
|
|
218
|
+
});
|
|
219
|
+
it('expires with that credential when no expiresAt is asked for', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
220
|
+
var _a;
|
|
221
|
+
const r = yield (0, loopbackRequest_1.default)(bearer()).post('/profile/tokens').send({ name: 'CI' });
|
|
222
|
+
(0, chai_1.expect)(r.status).to.equal(201);
|
|
223
|
+
(0, chai_1.expect)(r.body.expiresAt).to.equal(inAnHour.toISOString());
|
|
224
|
+
(0, chai_1.expect)((_a = create.firstCall.args[0].expiresAt) === null || _a === void 0 ? void 0 : _a.getTime()).to.equal(inAnHour.getTime());
|
|
225
|
+
}));
|
|
226
|
+
it('refuses an expiresAt later than that credential', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
227
|
+
const later = new Date(inAnHour.getTime() + 60000).toISOString();
|
|
228
|
+
const r = yield (0, loopbackRequest_1.default)(bearer())
|
|
229
|
+
.post('/profile/tokens')
|
|
230
|
+
.send({ name: 'CI', expiresAt: later });
|
|
231
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
232
|
+
(0, chai_1.expect)(r.body.error).to.match(/later than the credential/);
|
|
233
|
+
(0, chai_1.expect)(create.called).to.equal(false);
|
|
234
|
+
}));
|
|
235
|
+
// Verification allows 60 s of clock skew, so a token can still be
|
|
236
|
+
// accepted just after its exp: it must not make a key already expired.
|
|
237
|
+
it('refuses when that credential has already expired', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
238
|
+
const app = appWithAuth({
|
|
239
|
+
kind: 'bearer',
|
|
240
|
+
userId: 'u1',
|
|
241
|
+
role: 'MEMBER',
|
|
242
|
+
scopes: 'sessions,read',
|
|
243
|
+
credentialExpiresAt: Date.now() - 1000,
|
|
244
|
+
});
|
|
245
|
+
const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
|
|
246
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
247
|
+
(0, chai_1.expect)(r.body.error).to.match(/has expired/);
|
|
248
|
+
(0, chai_1.expect)(create.called).to.equal(false);
|
|
249
|
+
}));
|
|
250
|
+
it('keeps an earlier expiresAt', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
251
|
+
const sooner = new Date(Date.now() + 600000).toISOString();
|
|
252
|
+
const r = yield (0, loopbackRequest_1.default)(bearer())
|
|
253
|
+
.post('/profile/tokens')
|
|
254
|
+
.send({ name: 'CI', expiresAt: sooner });
|
|
255
|
+
(0, chai_1.expect)(r.status).to.equal(201);
|
|
256
|
+
(0, chai_1.expect)(r.body.expiresAt).to.equal(sooner);
|
|
257
|
+
}));
|
|
258
|
+
it('leaves a dashboard sign-in free to make a key that never expires', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
259
|
+
const app = appWithAuth({
|
|
260
|
+
kind: 'user-session',
|
|
261
|
+
userId: 'u1',
|
|
262
|
+
role: 'MEMBER',
|
|
263
|
+
scopes: 'devices,sessions,read',
|
|
264
|
+
});
|
|
265
|
+
const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
|
|
266
|
+
(0, chai_1.expect)(r.status).to.equal(201);
|
|
267
|
+
(0, chai_1.expect)(r.body.expiresAt).to.equal(null);
|
|
268
|
+
}));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
150
271
|
});
|
|
@@ -126,7 +126,7 @@ describe('createSession records who asked on the pending-session row', () => {
|
|
|
126
126
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
|
|
127
127
|
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
128
|
const [sub, teamId] = t.replace(/^jwt:/, '').split('@');
|
|
129
|
-
return { sub, teamId: teamId !== null && teamId !== void 0 ? teamId : null };
|
|
129
|
+
return { sub, teamId: teamId !== null && teamId !== void 0 ? teamId : null, scopes: 'sessions' };
|
|
130
130
|
}),
|
|
131
131
|
});
|
|
132
132
|
teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const plugin_1 = require("../../src/plugin");
|
|
20
|
+
const ServerManager_1 = require("../../src/services/ServerManager");
|
|
21
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
|
+
const concurrency_gate_1 = require("../../src/services/recording/concurrency-gate");
|
|
23
|
+
const config_1 = require("../../src/config");
|
|
24
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
25
|
+
/**
|
|
26
|
+
* `maxConcurrentRecordings` and `recordingsAssetsPath` are declared in
|
|
27
|
+
* schema.json, so Appium accepts them in a config file, but nothing read them:
|
|
28
|
+
* only the XENON_MAX_CONCURRENT_RECORDINGS and XENON_RECORDINGS_ASSETS_PATH
|
|
29
|
+
* environment variables reached src/config.ts. The option now wins, the
|
|
30
|
+
* variable is the fallback, and both are applied at boot before anything that
|
|
31
|
+
* reads them (the artifact store, the concurrency gate).
|
|
32
|
+
*/
|
|
33
|
+
describe('the recording options', () => {
|
|
34
|
+
let saved;
|
|
35
|
+
let env;
|
|
36
|
+
let statics;
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
saved = { max: config_1.config.maxConcurrentRecordings, path: config_1.config.recordingsAssetsPath };
|
|
39
|
+
env = {
|
|
40
|
+
max: process.env.XENON_MAX_CONCURRENT_RECORDINGS,
|
|
41
|
+
path: process.env.XENON_RECORDINGS_ASSETS_PATH,
|
|
42
|
+
};
|
|
43
|
+
delete process.env.XENON_MAX_CONCURRENT_RECORDINGS;
|
|
44
|
+
delete process.env.XENON_RECORDINGS_ASSETS_PATH;
|
|
45
|
+
statics = {
|
|
46
|
+
nodeId: plugin_1.XenonPlugin.NODE_ID,
|
|
47
|
+
port: plugin_1.XenonPlugin.port,
|
|
48
|
+
basePath: plugin_1.XenonPlugin.nodeBasePath,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
sinon_1.default.restore();
|
|
53
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: saved.max, recordingsAssetsPath: saved.path });
|
|
54
|
+
for (const [name, value] of [
|
|
55
|
+
['XENON_MAX_CONCURRENT_RECORDINGS', env.max],
|
|
56
|
+
['XENON_RECORDINGS_ASSETS_PATH', env.path],
|
|
57
|
+
]) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
delete process.env[name];
|
|
60
|
+
else
|
|
61
|
+
process.env[name] = value;
|
|
62
|
+
}
|
|
63
|
+
plugin_1.XenonPlugin.NODE_ID = statics.nodeId;
|
|
64
|
+
plugin_1.XenonPlugin.port = statics.port;
|
|
65
|
+
plugin_1.XenonPlugin.nodeBasePath = statics.basePath;
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Boots as far as the database step and says what the options were when it
|
|
69
|
+
* began: the artifact store is built there from `recordingsAssetsPath`.
|
|
70
|
+
*/
|
|
71
|
+
function seenAtDatabaseStep(pluginArgs) {
|
|
72
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
const seen = {};
|
|
74
|
+
const boot = ServerManager_1.ServerManager.prototype;
|
|
75
|
+
sinon_1.default.stub(boot, 'resolvePluginArgs').resolves(pluginArgs);
|
|
76
|
+
sinon_1.default.stub(logger_1.default, 'banner');
|
|
77
|
+
sinon_1.default.stub(PluginContext_1.PluginContext.prototype, 'setContext');
|
|
78
|
+
sinon_1.default.stub(boot, 'syncDatabaseAndAIConfig').resolves();
|
|
79
|
+
sinon_1.default.stub(boot, 'initializeCoreSubsystems').callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
seen.max = config_1.config.maxConcurrentRecordings;
|
|
81
|
+
seen.path = config_1.config.recordingsAssetsPath;
|
|
82
|
+
throw new Error('boot stopped by the test');
|
|
83
|
+
}));
|
|
84
|
+
yield typedi_1.Container.get(ServerManager_1.ServerManager)
|
|
85
|
+
.updateServer({}, null, { port: 4723 })
|
|
86
|
+
.catch((err) => (0, chai_1.expect)(err.message).to.equal('boot stopped by the test'));
|
|
87
|
+
return seen;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
describe('applied at boot', () => {
|
|
91
|
+
it('takes the maxConcurrentRecordings option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: 2 });
|
|
93
|
+
(0, chai_1.expect)(seen.max).to.equal(2);
|
|
94
|
+
}));
|
|
95
|
+
it('takes the recordingsAssetsPath option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: '/srv/xenon/recordings' });
|
|
97
|
+
(0, chai_1.expect)(seen.path).to.equal('/srv/xenon/recordings');
|
|
98
|
+
}));
|
|
99
|
+
it('falls back to the environment variables when the options are not set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
|
|
101
|
+
process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
|
|
102
|
+
const seen = yield seenAtDatabaseStep({});
|
|
103
|
+
(0, chai_1.expect)(seen).to.deep.equal({ max: 7, path: '/mnt/recordings' });
|
|
104
|
+
}));
|
|
105
|
+
it('lets the option win over the environment variable', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
|
+
process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
|
|
107
|
+
process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
|
|
108
|
+
const seen = yield seenAtDatabaseStep({
|
|
109
|
+
maxConcurrentRecordings: 3,
|
|
110
|
+
recordingsAssetsPath: '/srv/xenon/recordings',
|
|
111
|
+
});
|
|
112
|
+
(0, chai_1.expect)(seen).to.deep.equal({ max: 3, path: '/srv/xenon/recordings' });
|
|
113
|
+
}));
|
|
114
|
+
it('keeps the default cap of 4 when neither is set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
const seen = yield seenAtDatabaseStep({});
|
|
116
|
+
(0, chai_1.expect)(seen.max).to.equal(4);
|
|
117
|
+
}));
|
|
118
|
+
it('ignores a cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
for (const bad of [0, -2, 2.5, NaN, '3', null]) {
|
|
120
|
+
const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: bad });
|
|
121
|
+
(0, chai_1.expect)(seen.max, `option ${JSON.stringify(bad)}`).to.equal(4);
|
|
122
|
+
sinon_1.default.restore();
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
it('ignores an environment cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
process.env.XENON_MAX_CONCURRENT_RECORDINGS = 'many';
|
|
127
|
+
const seen = yield seenAtDatabaseStep({});
|
|
128
|
+
(0, chai_1.expect)(seen.max).to.equal(4);
|
|
129
|
+
}));
|
|
130
|
+
it('ignores a blank recordingsAssetsPath', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
|
+
const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: ' ' });
|
|
132
|
+
(0, chai_1.expect)(seen.path).to.equal(saved.path);
|
|
133
|
+
}));
|
|
134
|
+
});
|
|
135
|
+
describe('the concurrency gate', () => {
|
|
136
|
+
it('reads the cap when it admits, not when it is built', () => {
|
|
137
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
|
|
138
|
+
const gate = new concurrency_gate_1.ConcurrencyGate();
|
|
139
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: 1 });
|
|
140
|
+
(0, chai_1.expect)(gate.getLimit()).to.equal(1);
|
|
141
|
+
(0, chai_1.expect)(gate.tryAcquire(['r1'])).to.equal(true);
|
|
142
|
+
(0, chai_1.expect)(gate.tryAcquire(['r2'])).to.equal(false);
|
|
143
|
+
});
|
|
144
|
+
it('keeps an explicit limit over the configured one', () => {
|
|
145
|
+
(0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
|
|
146
|
+
(0, chai_1.expect)(new concurrency_gate_1.ConcurrencyGate(2).getLimit()).to.equal(2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('resolving the recording options', () => {
|
|
151
|
+
describe('maxConcurrentRecordings', () => {
|
|
152
|
+
it('takes the option, then the variable, then 4', () => {
|
|
153
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(2, '7')).to.equal(2);
|
|
154
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, '7')).to.equal(7);
|
|
155
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, undefined)).to.equal(4);
|
|
156
|
+
});
|
|
157
|
+
it('passes over a value that is not a whole number of at least 1', () => {
|
|
158
|
+
for (const bad of [0, -1, 1.5, NaN, Infinity, '2', null, {}]) {
|
|
159
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(bad, '7'), JSON.stringify(bad)).to.equal(7);
|
|
160
|
+
}
|
|
161
|
+
for (const bad of ['', ' ', '0', '-3', '2.5', 'many', 'NaN']) {
|
|
162
|
+
(0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, bad), JSON.stringify(bad)).to.equal(4);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('recordingsAssetsPath', () => {
|
|
167
|
+
it('takes the option, then the variable, then the default folder', () => {
|
|
168
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)('/a', '/b')).to.equal('/a');
|
|
169
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '/b')).to.equal('/b');
|
|
170
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, undefined)).to.match(/\.cache\/xenon\/assets\/sessions\/recordings$/);
|
|
171
|
+
});
|
|
172
|
+
it('counts a blank value as unset', () => {
|
|
173
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' ', '/b')).to.equal('/b');
|
|
174
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '')).to.match(/recordings$/);
|
|
175
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(42, undefined)).to.match(/recordings$/);
|
|
176
|
+
});
|
|
177
|
+
it('trims what it is given', () => {
|
|
178
|
+
(0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' /a ', undefined)).to.equal('/a');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|