@xenon-device-management/xenon 2.14.0 → 2.16.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 +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- 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 +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- 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 +57 -72
- 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/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -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 +83 -56
- 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/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- 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/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- 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/selector-state-flow.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/XenonCapabilityManager.spec.js +23 -0
- 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-adb-retry.spec.js +112 -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/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/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -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 +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -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/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -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/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/queue-requester.spec.js +1 -1
- 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/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- 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-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -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 +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -188,9 +188,11 @@ paths:
|
|
|
188
188
|
summary: Read the lab's settings and AI provider
|
|
189
189
|
description: |
|
|
190
190
|
The lab-wide settings in effect (health checks, build cleanup and the AI self-healing
|
|
191
|
-
switch), merged with the AI settings
|
|
192
|
-
in the dashboard, else the plugin option
|
|
193
|
-
|
|
191
|
+
switch), merged with the AI settings in effect (provider, models and base URL). Each
|
|
192
|
+
setting is the value saved in the dashboard, else the plugin option or environment
|
|
193
|
+
variable the server was started with, else the default, which is also what the server
|
|
194
|
+
runs with. A model or base URL set nowhere is absent: the provider's own applies. A user
|
|
195
|
+
name, password or query value in the base URL is sent as `***`. `defaults` holds the defaults themselves, which
|
|
194
196
|
is what "restore defaults" goes back to. Only `healthCheckSchedule` can be absent: no
|
|
195
197
|
schedule is set. Through 2.13 `enableSelfHealing` was neither stored nor sent, so the
|
|
196
198
|
Settings page always showed it on.
|
|
@@ -266,10 +268,18 @@ paths:
|
|
|
266
268
|
switch was accepted and ignored. The value belongs to the server it is saved on:
|
|
267
269
|
saving it on a hub changes the hub's own sessions, not those on a node's phones,
|
|
268
270
|
which the node's own setting decides.
|
|
269
|
-
- AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
- AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) are stored
|
|
272
|
+
too. A stored value replaces the plugin option or environment variable the server was
|
|
273
|
+
started with, and applies to the next AI call, with no restart. An empty value removes
|
|
274
|
+
the stored one, and the server's own applies again. They are checked first and refused
|
|
275
|
+
with `400`: `aiProvider` must be `gemini`, `openai`, `anthropic` or `ollama`; a model
|
|
276
|
+
name has no spaces and at most 200 characters; `aiBaseUrl` is an http or https address
|
|
277
|
+
with no user name, password or query. Through 2.14 they changed the running server
|
|
278
|
+
only, and a restart undid them. Like the self-healing switch, they belong to the server
|
|
279
|
+
they are saved on.
|
|
280
|
+
- API keys can't be set here, and are never stored: they come from the server's
|
|
281
|
+
environment. `aiTemperature`, `aiMaxTokens` and `aiTopP`, which the AI engine page sent
|
|
282
|
+
through 2.14, were never used by any AI call and are ignored like any unknown field.
|
|
273
283
|
|
|
274
284
|
Needs the `SUPER_ADMIN` role and the `admin` scope; an `ADMIN` gets `403` with a message
|
|
275
285
|
saying so (through 2.12 an `ADMIN` could). With the dashboard cookie the request must
|
|
@@ -299,7 +309,7 @@ paths:
|
|
|
299
309
|
example:
|
|
300
310
|
success: true
|
|
301
311
|
'400':
|
|
302
|
-
description: A build-cleanup field
|
|
312
|
+
description: A build-cleanup field, the self-healing switch or an AI field has a value that could not work. Nothing was saved.
|
|
303
313
|
content:
|
|
304
314
|
application/json:
|
|
305
315
|
schema:
|
|
@@ -2387,6 +2397,7 @@ components:
|
|
|
2387
2397
|
type: boolean
|
|
2388
2398
|
aiProvider:
|
|
2389
2399
|
type: string
|
|
2400
|
+
description: The provider in effect, saved on the AI engine page, else the server's option or environment, else gemini.
|
|
2390
2401
|
example: gemini
|
|
2391
2402
|
aiModel:
|
|
2392
2403
|
type: string
|
|
@@ -2438,30 +2449,37 @@ components:
|
|
|
2438
2449
|
type: boolean
|
|
2439
2450
|
aiProvider:
|
|
2440
2451
|
type: string
|
|
2441
|
-
description:
|
|
2452
|
+
description: The provider every AI call uses. Stored; empty goes back to the server's own.
|
|
2442
2453
|
enum:
|
|
2443
2454
|
- gemini
|
|
2444
2455
|
- openai
|
|
2445
2456
|
- anthropic
|
|
2446
2457
|
- ollama
|
|
2458
|
+
- ''
|
|
2447
2459
|
aiModel:
|
|
2448
2460
|
type: string
|
|
2449
|
-
description:
|
|
2461
|
+
description: The model for a provider with no model of its own below. Stored; empty goes back to the server's own.
|
|
2462
|
+
maxLength: 200
|
|
2450
2463
|
aiBaseUrl:
|
|
2451
2464
|
type: string
|
|
2452
|
-
description:
|
|
2465
|
+
description: Where OpenAI-compatible and Ollama calls go. An http or https address with no user name, password or query. Stored; empty goes back to the server's own.
|
|
2466
|
+
example: http://gpu-box.lab:11434
|
|
2453
2467
|
geminiModel:
|
|
2454
2468
|
type: string
|
|
2455
|
-
description:
|
|
2469
|
+
description: Stored; empty goes back to the server's own.
|
|
2470
|
+
maxLength: 200
|
|
2456
2471
|
openaiModel:
|
|
2457
2472
|
type: string
|
|
2458
|
-
description:
|
|
2473
|
+
description: Stored; empty goes back to the server's own.
|
|
2474
|
+
maxLength: 200
|
|
2459
2475
|
anthropicModel:
|
|
2460
2476
|
type: string
|
|
2461
|
-
description:
|
|
2477
|
+
description: Stored; empty goes back to the server's own.
|
|
2478
|
+
maxLength: 200
|
|
2462
2479
|
ollamaModel:
|
|
2463
2480
|
type: string
|
|
2464
|
-
description:
|
|
2481
|
+
description: Stored; empty goes back to the server's own.
|
|
2482
|
+
maxLength: 200
|
|
2465
2483
|
PlatformAiTestRequest:
|
|
2466
2484
|
type: object
|
|
2467
2485
|
description: Every field is optional and falls back to the server's current setting.
|
|
@@ -108,13 +108,13 @@ paths:
|
|
|
108
108
|
name: Checkout with saved card
|
|
109
109
|
status: failed
|
|
110
110
|
node_id: 3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d
|
|
111
|
-
has_live_video:
|
|
111
|
+
has_live_video: false
|
|
112
112
|
video_recording_enabled: true
|
|
113
113
|
video_recording: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.mp4
|
|
114
114
|
startTime: '2026-10-03T14:20:11.004Z'
|
|
115
115
|
endTime: '2026-10-03T14:22:05.120Z'
|
|
116
116
|
failure_reason: 'NoSuchElementError: An element could not be located'
|
|
117
|
-
failure_category:
|
|
117
|
+
failure_category: ELEMENT_NOT_FOUND
|
|
118
118
|
device_udid: emulator-5554
|
|
119
119
|
device_platform: android
|
|
120
120
|
device_version: '14'
|
|
@@ -923,7 +923,7 @@ paths:
|
|
|
923
923
|
type: string
|
|
924
924
|
example: |
|
|
925
925
|
id,build_id,status,failure_category,failure_reason,device_platform,device_version,device_name,node_id,startTime,endTime,name
|
|
926
|
-
6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,
|
|
926
|
+
6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,ELEMENT_NOT_FOUND,"NoSuchElementError: An element could not be located",android,14,Pixel 7,3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d,2026-10-03T14:20:11.004Z,2026-10-03T14:22:05.120Z,Checkout with saved card
|
|
927
927
|
'400':
|
|
928
928
|
description: '`format` is neither `json` nor `csv`.'
|
|
929
929
|
content:
|
|
@@ -2688,6 +2688,15 @@ components:
|
|
|
2688
2688
|
failure_category:
|
|
2689
2689
|
type: string
|
|
2690
2690
|
nullable: true
|
|
2691
|
+
description: >-
|
|
2692
|
+
What kind of failure it was, read from the failure reason and the last failed
|
|
2693
|
+
commands' errors when the session fails: `SESSION_LOST`, `APP_CRASH`,
|
|
2694
|
+
`SYSTEM_OVERLOAD`, `STALE_ELEMENT`, `TIMEOUT`, `ELEMENT_NOT_FOUND`,
|
|
2695
|
+
`PERMISSION_BLOCKED` or `UNKNOWN`; or `HUB_RESTART` for a session the server's
|
|
2696
|
+
restart or shutdown ended. Sessions filed through 2.15 may also hold `WDA_FAILURE` or
|
|
2697
|
+
`XENON_COMMAND_FAILURE`, no longer written. Null for a session that hasn't failed,
|
|
2698
|
+
and for a failure recorded without that analysis.
|
|
2699
|
+
example: ELEMENT_NOT_FOUND
|
|
2691
2700
|
is_profiling_available:
|
|
2692
2701
|
type: boolean
|
|
2693
2702
|
device_info:
|
|
@@ -15,6 +15,8 @@ const typedi_1 = require("typedi");
|
|
|
15
15
|
const ApiKeyService_1 = require("../../services/ApiKeyService");
|
|
16
16
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
17
17
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
18
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
19
|
+
const KNOWN_SCOPES = ['read', 'sessions', 'devices', 'admin'];
|
|
18
20
|
function apiKeysRouter() {
|
|
19
21
|
const r = (0, express_1.Router)();
|
|
20
22
|
r.use((0, roleGuard_1.roleGuard)('ADMIN'));
|
|
@@ -23,11 +25,16 @@ function apiKeysRouter() {
|
|
|
23
25
|
res.json(yield svc.list());
|
|
24
26
|
}));
|
|
25
27
|
r.post('/', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
26
|
-
var _a;
|
|
28
|
+
var _a, _b;
|
|
27
29
|
const { name, scopes, rateLimit, teamId, expiresAt } = req.body;
|
|
28
30
|
if (!name || !Array.isArray(scopes) || scopes.length === 0) {
|
|
29
31
|
return res.status(400).json({ error: 'name and scopes required' });
|
|
30
32
|
}
|
|
33
|
+
// Only the scopes that exist. Any string was stored, so a typo made a key
|
|
34
|
+
// with none of the scopes its maker meant.
|
|
35
|
+
if (!scopes.every((s) => KNOWN_SCOPES.includes(s))) {
|
|
36
|
+
return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
|
|
37
|
+
}
|
|
31
38
|
let expiresAtDate;
|
|
32
39
|
if (expiresAt !== undefined && expiresAt !== null) {
|
|
33
40
|
const d = new Date(expiresAt);
|
|
@@ -39,6 +46,11 @@ function apiKeysRouter() {
|
|
|
39
46
|
}
|
|
40
47
|
expiresAtDate = d;
|
|
41
48
|
}
|
|
49
|
+
// Made with a credential that expires, a key ends no later than it.
|
|
50
|
+
const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.credentialExpiresAt);
|
|
51
|
+
if ('error' in within)
|
|
52
|
+
return res.status(400).json({ error: within.error });
|
|
53
|
+
expiresAtDate = within.expiresAt;
|
|
42
54
|
const { id, raw } = yield svc.create({
|
|
43
55
|
name,
|
|
44
56
|
scopes,
|
|
@@ -47,7 +59,7 @@ function apiKeysRouter() {
|
|
|
47
59
|
userId: req.auth.userId,
|
|
48
60
|
expiresAt: expiresAtDate,
|
|
49
61
|
});
|
|
50
|
-
res.json({ id, key: raw, expiresAt: (
|
|
62
|
+
res.json({ id, key: raw, expiresAt: (_b = expiresAtDate === null || expiresAtDate === void 0 ? void 0 : expiresAtDate.toISOString()) !== null && _b !== void 0 ? _b : null });
|
|
51
63
|
}));
|
|
52
64
|
r.delete('/:id', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
53
65
|
yield svc.revoke(req.params.id);
|
|
@@ -28,11 +28,16 @@ const JwtKeyService_1 = require("../../services/token/JwtKeyService");
|
|
|
28
28
|
const mcpScopes_1 = require("../../services/token/mcpScopes");
|
|
29
29
|
const passwordResetMode_1 = require("../../services/passwordResetMode");
|
|
30
30
|
const passwordResetLink_1 = require("../../services/passwordResetLink");
|
|
31
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
32
|
+
const mcpTokenTtl_1 = require("../../services/token/mcpTokenTtl");
|
|
31
33
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
32
34
|
const isSecureFromReq = (req) => req.secure || req.headers['x-forwarded-proto'] === 'https';
|
|
33
|
-
const MCP_TTL_SEC =
|
|
35
|
+
const MCP_TTL_SEC = (0, mcpTokenTtl_1.mcpTokenTtlSec)();
|
|
34
36
|
const REST_TTL_SEC = 3600;
|
|
35
37
|
const MINTABLE_AUDIENCES = ['xenon-rest', 'xenon-mcp'];
|
|
38
|
+
function grantsScope(scopesCsv, scope) {
|
|
39
|
+
return scopesCsv.split(',').some((s) => s.trim() === scope);
|
|
40
|
+
}
|
|
36
41
|
function issueToken(auth, body) {
|
|
37
42
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
43
|
var _a, _b, _c, _d;
|
|
@@ -40,7 +45,7 @@ function issueToken(auth, body) {
|
|
|
40
45
|
if (!MINTABLE_AUDIENCES.includes(audience)) {
|
|
41
46
|
throw new Error(`unsupported audience: ${audience}`);
|
|
42
47
|
}
|
|
43
|
-
const expiresIn = audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC;
|
|
48
|
+
const expiresIn = (0, mintedLifetime_1.mintedLifetimeSec)(audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC, auth.credentialExpiresAt);
|
|
44
49
|
const svc = typedi_1.Container.get(JwtKeyService_1.JwtKeyService);
|
|
45
50
|
if (audience === 'xenon-mcp') {
|
|
46
51
|
// Validate the untrusted `scopes` body field before it reaches the mapper:
|
|
@@ -66,7 +71,23 @@ function issueToken(auth, body) {
|
|
|
66
71
|
// Session-token capability (spec §3 item 6 / R9): a sibling credential the
|
|
67
72
|
// client injects as `xe:options.sessionToken` so the Appium createSession
|
|
68
73
|
// interceptor can refuse tokenless direct-connect sessions when the gate is on.
|
|
69
|
-
|
|
74
|
+
//
|
|
75
|
+
// It creates Appium sessions, so it comes only with a grant that includes
|
|
76
|
+
// `appium:use`, which needs the `sessions` scope. Through 2.14 every
|
|
77
|
+
// credential got one, a read-only key included. It carries the scopes a
|
|
78
|
+
// session is judged by: `sessions`, and `admin` (a lease override) only
|
|
79
|
+
// when the minting credential has it and the grant isn't narrower than
|
|
80
|
+
// it: the default grant (no scopes asked for), or a full-admin one.
|
|
81
|
+
if (!grant.granular.includes('appium:use')) {
|
|
82
|
+
return { token, expiresIn, audience, scopes: grant.granular };
|
|
83
|
+
}
|
|
84
|
+
const askedForScopes = Array.isArray(body.scopes) && body.scopes.length > 0;
|
|
85
|
+
const sessionAdmin = grantsScope(auth.scopes, 'admin') && (!askedForScopes || grant.roles.includes('admin'));
|
|
86
|
+
const sessionToken = yield svc.sign({
|
|
87
|
+
sub: auth.userId,
|
|
88
|
+
teamId: (_c = auth.teamId) !== null && _c !== void 0 ? _c : null,
|
|
89
|
+
scopes: sessionAdmin ? 'admin,sessions' : 'sessions',
|
|
90
|
+
}, { audience: 'xenon-session', ttlSeconds: expiresIn });
|
|
70
91
|
return { token, expiresIn, audience, scopes: grant.granular, sessionToken };
|
|
71
92
|
}
|
|
72
93
|
const token = yield svc.sign({ sub: auth.userId, role: auth.role, scopes: auth.scopes, teamId: (_d = auth.teamId) !== null && _d !== void 0 ? _d : null }, { audience, ttlSeconds: expiresIn });
|
|
@@ -139,11 +160,15 @@ function authPublicRouter() {
|
|
|
139
160
|
const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
|
|
140
161
|
// No SMTP and no opt-in log fallback: nothing can reach the user, so
|
|
141
162
|
// don't mint a live credential. Admins issue links from the Users page.
|
|
142
|
-
|
|
163
|
+
// No XENON_PUBLIC_URL: nothing says where the link may point. The
|
|
164
|
+
// request's Host is the asker's to choose, and the asker may not be
|
|
165
|
+
// the account's owner, so it is never used here.
|
|
166
|
+
const base = (0, passwordResetLink_1.resetLinkBase)();
|
|
167
|
+
if (user && user.status === 'ACTIVE' && emailSvc.canDeliver() && base) {
|
|
143
168
|
const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
|
|
144
169
|
const { raw } = yield resetSvc.createToken(user.id);
|
|
145
|
-
const link = (0, passwordResetLink_1.buildResetLink)(
|
|
146
|
-
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link));
|
|
170
|
+
const link = (0, passwordResetLink_1.buildResetLink)(base, raw);
|
|
171
|
+
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link, resetSvc.ttlMs()));
|
|
147
172
|
}
|
|
148
173
|
}
|
|
149
174
|
catch (e) {
|
|
@@ -724,12 +724,20 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
724
724
|
yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid, { source: h264Cfg.source });
|
|
725
725
|
}
|
|
726
726
|
else {
|
|
727
|
-
// One capture pipeline per device
|
|
728
|
-
// (a tile's, or this page's own before its player failed) ends before the
|
|
729
|
-
// screencap loop starts.
|
|
727
|
+
// One capture pipeline per device where possible.
|
|
730
728
|
const h264 = typedi_1.Container.get(AndroidH264StreamService_1.default);
|
|
731
|
-
if (
|
|
732
|
-
|
|
729
|
+
if (recording) {
|
|
730
|
+
// A recording reads the MJPEG capture, and H.264 never runs beside it.
|
|
731
|
+
if (h264.getMultiplexer(udid))
|
|
732
|
+
yield h264.stop(udid);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// This page shows MJPEG. An H.264 capture still running for the phone
|
|
736
|
+
// (this page's own before its player failed, or a tile's) ends before
|
|
737
|
+
// the screencap loop starts if nobody watches it, else when its last
|
|
738
|
+
// viewer leaves: another tile or tab may still be playing it.
|
|
739
|
+
yield h264.endWhenUnwatched(udid);
|
|
740
|
+
}
|
|
733
741
|
mjpegPort = (yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(udid)).mjpegPort;
|
|
734
742
|
}
|
|
735
743
|
// Lazy-load the screen size if missing: tiles need it to accept taps.
|
|
@@ -59,6 +59,7 @@ const web_config_service_1 = require("../../data-service/web-config-service");
|
|
|
59
59
|
const PluginContext_1 = require("../../PluginContext");
|
|
60
60
|
const labSettings_1 = require("../../services/settings/labSettings");
|
|
61
61
|
const SelfHealingSwitch_1 = require("../../services/settings/SelfHealingSwitch");
|
|
62
|
+
const aiEngineSettings_1 = require("../../services/settings/aiEngineSettings");
|
|
62
63
|
const typedi_1 = require("typedi");
|
|
63
64
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
64
65
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
@@ -1102,6 +1103,34 @@ function streamLiveSessionVideo(request, response) {
|
|
|
1102
1103
|
}
|
|
1103
1104
|
});
|
|
1104
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
|
+
}
|
|
1105
1134
|
function getGlobalConfig(request, response) {
|
|
1106
1135
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1107
1136
|
try {
|
|
@@ -1110,13 +1139,13 @@ function getGlobalConfig(request, response) {
|
|
|
1110
1139
|
// the default. The pages show this (a saved value alone left them to guess
|
|
1111
1140
|
// at what a setting never saved was), and `defaults` for "restore defaults".
|
|
1112
1141
|
const effective = (0, labSettings_1.effectiveSettings)(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs, dbConfig);
|
|
1113
|
-
//
|
|
1142
|
+
// The AI engine settings in effect (aiEngineSettings.ts keeps `config` at
|
|
1143
|
+
// them). Never a key: only whether one is set.
|
|
1114
1144
|
const { config } = yield Promise.resolve().then(() => __importStar(require('../../config')));
|
|
1115
|
-
// Sanitize keys - return boolean existence only
|
|
1116
1145
|
const aiConfig = {
|
|
1117
1146
|
aiProvider: config.aiProvider,
|
|
1118
1147
|
aiModel: config.aiModel,
|
|
1119
|
-
aiBaseUrl: config.aiBaseUrl,
|
|
1148
|
+
aiBaseUrl: maskedBaseUrl(config.aiBaseUrl),
|
|
1120
1149
|
geminiModel: config.geminiModel,
|
|
1121
1150
|
openaiModel: config.openaiModel,
|
|
1122
1151
|
anthropicModel: config.anthropicModel,
|
|
@@ -1136,39 +1165,25 @@ function getGlobalConfig(request, response) {
|
|
|
1136
1165
|
}
|
|
1137
1166
|
function updateGlobalConfig(request, response) {
|
|
1138
1167
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1168
|
+
var _a;
|
|
1139
1169
|
try {
|
|
1140
1170
|
const payload = request.body;
|
|
1141
1171
|
// Before anything is saved: a value the cleanup job would act on, such as a
|
|
1142
|
-
// retention window of 0, is refused rather than stored
|
|
1143
|
-
|
|
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 : {});
|
|
1144
1175
|
if (problem) {
|
|
1145
1176
|
return response
|
|
1146
1177
|
.status(400)
|
|
1147
1178
|
.json({ error: 'invalid_setting', field: problem.field, message: problem.message });
|
|
1148
1179
|
}
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
runtimeOverrides.aiModel = payload.aiModel;
|
|
1156
|
-
if (payload.aiBaseUrl !== undefined)
|
|
1157
|
-
runtimeOverrides.aiBaseUrl = payload.aiBaseUrl;
|
|
1158
|
-
if (payload.geminiModel !== undefined)
|
|
1159
|
-
runtimeOverrides.geminiModel = payload.geminiModel;
|
|
1160
|
-
if (payload.openaiModel !== undefined)
|
|
1161
|
-
runtimeOverrides.openaiModel = payload.openaiModel;
|
|
1162
|
-
if (payload.anthropicModel !== undefined)
|
|
1163
|
-
runtimeOverrides.anthropicModel = payload.anthropicModel;
|
|
1164
|
-
if (payload.ollamaModel !== undefined)
|
|
1165
|
-
runtimeOverrides.ollamaModel = payload.ollamaModel;
|
|
1166
|
-
if (Object.keys(runtimeOverrides).length > 0) {
|
|
1167
|
-
const { updateConfig } = yield Promise.resolve().then(() => __importStar(require('../../config')));
|
|
1168
|
-
updateConfig(runtimeOverrides);
|
|
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);
|
|
1169
1186
|
}
|
|
1170
|
-
// Persist Web Configs to DB
|
|
1171
|
-
yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(payload);
|
|
1172
1187
|
// The command interceptor asks at every command and never reads the
|
|
1173
1188
|
// database: hand it the saved value, in force from the next command.
|
|
1174
1189
|
if (payload.enableSelfHealing !== undefined) {
|
|
@@ -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
|
});
|
|
@@ -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(', ')}` });
|
|
@@ -18,6 +18,7 @@ const express_1 = require("express");
|
|
|
18
18
|
const typedi_1 = require("typedi");
|
|
19
19
|
const crypto_1 = __importDefault(require("crypto"));
|
|
20
20
|
const UserService_1 = require("../../services/UserService");
|
|
21
|
+
const UserSessionService_1 = require("../../services/UserSessionService");
|
|
21
22
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
22
23
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
23
24
|
const userAuthorization_1 = require("../../services/identity/userAuthorization");
|
|
@@ -130,7 +131,7 @@ function usersRouter() {
|
|
|
130
131
|
});
|
|
131
132
|
if (status === 'INACTIVE') {
|
|
132
133
|
// updateUser sets status; we additionally revoke sessions.
|
|
133
|
-
yield
|
|
134
|
+
yield typedi_1.Container.get(UserSessionService_1.UserSessionService).revokeAllForUser(targetId);
|
|
134
135
|
}
|
|
135
136
|
res.json({
|
|
136
137
|
id: updated.id,
|
|
@@ -141,8 +142,11 @@ function usersRouter() {
|
|
|
141
142
|
});
|
|
142
143
|
}));
|
|
143
144
|
// Admin-issued password reset. Replaces relaying a link out of the server
|
|
144
|
-
// log: with SMTP the link is emailed to the user
|
|
145
|
-
//
|
|
145
|
+
// log: with SMTP and XENON_PUBLIC_URL the link is emailed to the user;
|
|
146
|
+
// otherwise it is returned once, to the admin who asked, and never logged.
|
|
147
|
+
// An emailed link points at XENON_PUBLIC_URL only, never the request's
|
|
148
|
+
// Host: the admin's request could name any host, and the user would get a
|
|
149
|
+
// genuine Xenon email sending their token there.
|
|
146
150
|
r.post('/:id/reset-link', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
147
151
|
const auth = getAuth(req);
|
|
148
152
|
const targetId = req.params.id;
|
|
@@ -168,10 +172,11 @@ function usersRouter() {
|
|
|
168
172
|
const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
|
|
169
173
|
const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
|
|
170
174
|
const { raw } = yield resetSvc.createToken(target.id);
|
|
171
|
-
const
|
|
175
|
+
const publicBase = (0, passwordResetLink_1.resetLinkBase)();
|
|
172
176
|
const expiresAt = new Date(Date.now() + resetSvc.ttlMs()).toISOString();
|
|
173
|
-
if (emailSvc.hasSmtp()) {
|
|
174
|
-
|
|
177
|
+
if (emailSvc.hasSmtp() && publicBase) {
|
|
178
|
+
const link = (0, passwordResetLink_1.buildResetLink)(publicBase, raw);
|
|
179
|
+
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link, resetSvc.ttlMs()));
|
|
175
180
|
auditLog.info(`${auth.userId} emailed a password-reset link to user ${target.id}`);
|
|
176
181
|
return res.json({ emailed: true, expiresAt });
|
|
177
182
|
}
|
|
@@ -179,6 +184,7 @@ function usersRouter() {
|
|
|
179
184
|
auditLog.warn(`${auth.userId} issued a password-reset link for user ${target.id} (returned to the admin)`);
|
|
180
185
|
// The body is a credential: keep it out of browser and proxy caches.
|
|
181
186
|
res.set('Cache-Control', 'no-store');
|
|
187
|
+
const link = (0, passwordResetLink_1.buildResetLink)(publicBase !== null && publicBase !== void 0 ? publicBase : (0, passwordResetLink_1.requestBase)(req), raw);
|
|
182
188
|
return res.json({ emailed: false, link, expiresAt });
|
|
183
189
|
}));
|
|
184
190
|
r.delete('/:id', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
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
|
}
|