@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
|
@@ -49,12 +49,19 @@ let AICommandService = class AICommandService {
|
|
|
49
49
|
return __awaiter(this, void 0, void 0, function* () {
|
|
50
50
|
this.logger.info(`Testing AI locator: ${locator.strategy}=${locator.selector}`);
|
|
51
51
|
const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
|
|
52
|
+
// A session's own route: its test may act on what it found.
|
|
53
|
+
const keep = (found) => {
|
|
54
|
+
if (driver === null || driver === void 0 ? void 0 : driver.sessionId)
|
|
55
|
+
for (const el of found)
|
|
56
|
+
omniService.remember(driver.sessionId, el);
|
|
57
|
+
return found;
|
|
58
|
+
};
|
|
52
59
|
if (locator.strategy === '-custom:ai-text') {
|
|
53
|
-
return yield omniService.findByText(driver, locator.selector);
|
|
60
|
+
return keep(yield omniService.findByText(driver, locator.selector));
|
|
54
61
|
}
|
|
55
62
|
else if (locator.strategy === '-custom:ai-icon') {
|
|
56
63
|
const result = yield omniService.findByIcon(driver, locator.selector);
|
|
57
|
-
return result ? [result] : [];
|
|
64
|
+
return keep(result ? [result] : []);
|
|
58
65
|
}
|
|
59
66
|
throw new Error(`Unsupported AI strategy: ${locator.strategy}`);
|
|
60
67
|
});
|
|
@@ -45,7 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.AI_SERVICE = exports.AIService = void 0;
|
|
48
|
+
exports.AI_SERVICE = exports.AIService = exports.FAILURE_ANALYSIS_TIMEOUT_MS = exports.AI_CALL_TIMEOUT_MS = exports.AITimeoutError = exports.AIProviderPausedError = exports.AIRateLimitedError = void 0;
|
|
49
49
|
exports.parseVisualVerdict = parseVisualVerdict;
|
|
50
50
|
const generative_ai_1 = require("@google/generative-ai");
|
|
51
51
|
const openai_1 = __importDefault(require("openai"));
|
|
@@ -56,8 +56,88 @@ const fs = __importStar(require("fs"));
|
|
|
56
56
|
const path = __importStar(require("path"));
|
|
57
57
|
const config_1 = require("../config");
|
|
58
58
|
const CircuitBreaker_1 = require("./CircuitBreaker");
|
|
59
|
-
/**
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A provider refused a call for its rate limit or quota (HTTP 429), whichever
|
|
61
|
+
* provider it is. A failed call like any other: its caller gets no answer and
|
|
62
|
+
* the circuit breaker counts it (`status`). Through 2.14 Gemini's provider
|
|
63
|
+
* answered a 429 with the text 'CONNECTION_OK_RATE_LIMITED', which was saved
|
|
64
|
+
* as a failed session's AI analysis and counted by the breaker as a success.
|
|
65
|
+
*/
|
|
66
|
+
class AIRateLimitedError extends Error {
|
|
67
|
+
constructor(detail) {
|
|
68
|
+
super('The AI provider is rate-limited or out of quota');
|
|
69
|
+
this.name = 'AIRateLimitedError';
|
|
70
|
+
this.status = 429;
|
|
71
|
+
this.detail = detail;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.AIRateLimitedError = AIRateLimitedError;
|
|
75
|
+
/** A provider's 429, as its SDK (`status`) or axios (`response.status`) reports it. */
|
|
76
|
+
function isRateLimit(err) {
|
|
77
|
+
var _a;
|
|
78
|
+
return err instanceof AIRateLimitedError || (err === null || err === void 0 ? void 0 : err.status) === 429 || ((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) === 429;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The circuit breaker didn't send the call: the provider's recent calls
|
|
82
|
+
* failed. Still a CircuitOpenError, in words for testers: the Omni-Scan, Test
|
|
83
|
+
* locator and visual assertion answers show it, and "Circuit open for
|
|
84
|
+
* 'ai:gemini:<model>'; retry in 43120ms" isn't.
|
|
85
|
+
*/
|
|
86
|
+
class AIProviderPausedError extends CircuitBreaker_1.CircuitOpenError {
|
|
87
|
+
constructor(from) {
|
|
88
|
+
super(from.key, from.retryAfterMs);
|
|
89
|
+
this.seconds = Math.max(1, Math.ceil(from.retryAfterMs / 1000));
|
|
90
|
+
this.message = `The AI provider has been failing, so Xenon will call it again in ${this.seconds} s`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.AIProviderPausedError = AIProviderPausedError;
|
|
94
|
+
/** A call that outlasted its time limit; its request was cancelled. The circuit breaker counts it (`code`). */
|
|
95
|
+
class AITimeoutError extends Error {
|
|
96
|
+
constructor(ms) {
|
|
97
|
+
super(`The AI provider didn't answer within ${Math.round(ms / 1000)} s`);
|
|
98
|
+
this.name = 'AITimeoutError';
|
|
99
|
+
this.code = 'ETIMEDOUT';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.AITimeoutError = AITimeoutError;
|
|
103
|
+
/**
|
|
104
|
+
* How long an AI call may take, retries included, unless it says otherwise
|
|
105
|
+
* (failure analysis). Most are made while a test command runs: the LLM and
|
|
106
|
+
* visual healing tiers inside a failing findElement (both: 2 of these), the
|
|
107
|
+
* visual assertion and the screen description inside an execute script, an
|
|
108
|
+
* ai-icon find. The OpenAI and Anthropic SDKs otherwise wait up to 10 minutes
|
|
109
|
+
* a try, three tries, and Gemini has no limit, which held the command past
|
|
110
|
+
* the client's own timeout. Ollama's generate call already had 30 s.
|
|
111
|
+
*/
|
|
112
|
+
exports.AI_CALL_TIMEOUT_MS = 30000;
|
|
113
|
+
/**
|
|
114
|
+
* How long a failed session's AI analysis may take, retries included. The
|
|
115
|
+
* OpenAI and Anthropic SDKs otherwise wait up to 10 minutes a try, three
|
|
116
|
+
* tries. The analysis runs after the session has ended (onSessionStopped
|
|
117
|
+
* doesn't wait for it), so this bounds the work left running, not a client's
|
|
118
|
+
* wait: an answer with a screenshot usually takes seconds.
|
|
119
|
+
*/
|
|
120
|
+
exports.FAILURE_ANALYSIS_TIMEOUT_MS = 120000;
|
|
121
|
+
/** `call`, given a signal that cancels it after `ms`, when it fails with AITimeoutError. */
|
|
122
|
+
function withTimeLimit(ms, call) {
|
|
123
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
let timer;
|
|
126
|
+
const expired = new Promise((_, reject) => {
|
|
127
|
+
timer = setTimeout(() => {
|
|
128
|
+
// The time-out first, so it, not the cancelled request's error, is the answer.
|
|
129
|
+
reject(new AITimeoutError(ms));
|
|
130
|
+
controller.abort();
|
|
131
|
+
}, ms);
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
return yield Promise.race([call(controller.signal), expired]);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
61
141
|
/**
|
|
62
142
|
* A provider's answer to the assertion prompt, `{ "result": true|false,
|
|
63
143
|
* "reason": "..." }`, read leniently (code fences, text around the JSON,
|
|
@@ -97,7 +177,7 @@ class GeminiProvider {
|
|
|
97
177
|
this.modelName =
|
|
98
178
|
modelName && modelName.trim() !== '' ? modelName.trim() : 'gemini-3-flash-preview';
|
|
99
179
|
}
|
|
100
|
-
analyze(prompt, screenshotBase64) {
|
|
180
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
101
181
|
return __awaiter(this, void 0, void 0, function* () {
|
|
102
182
|
const parts = [prompt];
|
|
103
183
|
if (screenshotBase64) {
|
|
@@ -116,17 +196,17 @@ class GeminiProvider {
|
|
|
116
196
|
for (const version of versions) {
|
|
117
197
|
try {
|
|
118
198
|
const model = this.genAI.getGenerativeModel({ model: this.modelName }, { apiVersion: version });
|
|
119
|
-
const result = yield model.generateContent(parts);
|
|
199
|
+
const result = yield model.generateContent(parts, signal ? { signal } : undefined);
|
|
120
200
|
const response = yield result.response;
|
|
121
201
|
return response.text();
|
|
122
202
|
}
|
|
123
203
|
catch (err) {
|
|
124
204
|
lastError = err;
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
205
|
+
// The other endpoint has the same quota. By status only: the message
|
|
206
|
+
// holds the URL and the provider's text, where "429" can be a token
|
|
207
|
+
// count or part of the model's name.
|
|
208
|
+
if (err.status === 429)
|
|
209
|
+
throw new AIRateLimitedError(err.message);
|
|
130
210
|
if (err.message.includes('404') ||
|
|
131
211
|
err.message.includes('not found') ||
|
|
132
212
|
err.message.includes('unsupported')) {
|
|
@@ -145,7 +225,7 @@ class OpenAIProvider {
|
|
|
145
225
|
this.client = new openai_1.default({ apiKey, baseURL });
|
|
146
226
|
this.model = model;
|
|
147
227
|
}
|
|
148
|
-
analyze(prompt, screenshotBase64) {
|
|
228
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
149
229
|
return __awaiter(this, void 0, void 0, function* () {
|
|
150
230
|
const messages = [
|
|
151
231
|
{
|
|
@@ -163,7 +243,7 @@ class OpenAIProvider {
|
|
|
163
243
|
model: this.model,
|
|
164
244
|
messages,
|
|
165
245
|
max_tokens: 500,
|
|
166
|
-
});
|
|
246
|
+
}, { signal });
|
|
167
247
|
return response.choices[0].message.content || '';
|
|
168
248
|
});
|
|
169
249
|
}
|
|
@@ -173,7 +253,7 @@ class AnthropicProvider {
|
|
|
173
253
|
this.client = new sdk_1.default({ apiKey });
|
|
174
254
|
this.model = model;
|
|
175
255
|
}
|
|
176
|
-
analyze(prompt, screenshotBase64) {
|
|
256
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
177
257
|
return __awaiter(this, void 0, void 0, function* () {
|
|
178
258
|
const content = [{ type: 'text', text: prompt }];
|
|
179
259
|
if (screenshotBase64) {
|
|
@@ -190,7 +270,7 @@ class AnthropicProvider {
|
|
|
190
270
|
model: this.model,
|
|
191
271
|
max_tokens: 500,
|
|
192
272
|
messages: [{ role: 'user', content }],
|
|
193
|
-
});
|
|
273
|
+
}, { signal });
|
|
194
274
|
return response.content[0].text || '';
|
|
195
275
|
});
|
|
196
276
|
}
|
|
@@ -206,10 +286,11 @@ class OllamaProvider {
|
|
|
206
286
|
checkAvailability() {
|
|
207
287
|
return __awaiter(this, void 0, void 0, function* () {
|
|
208
288
|
const now = Date.now();
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
289
|
+
// An answer is kept for a while; "down" is checked again at the next call
|
|
290
|
+
// (a refused local connection costs nothing), so Ollama is used as soon as
|
|
291
|
+
// it is back.
|
|
292
|
+
if (this.isAvailable && now - this.lastAvailabilityCheck < this.AVAILABILITY_CHECK_INTERVAL) {
|
|
293
|
+
return true;
|
|
213
294
|
}
|
|
214
295
|
try {
|
|
215
296
|
// Quick health check - Ollama has a /api/tags endpoint
|
|
@@ -225,7 +306,7 @@ class OllamaProvider {
|
|
|
225
306
|
}
|
|
226
307
|
});
|
|
227
308
|
}
|
|
228
|
-
analyze(prompt, screenshotBase64) {
|
|
309
|
+
analyze(prompt, screenshotBase64, signal) {
|
|
229
310
|
return __awaiter(this, void 0, void 0, function* () {
|
|
230
311
|
var _a;
|
|
231
312
|
// Check if Ollama is available before attempting
|
|
@@ -239,7 +320,7 @@ class OllamaProvider {
|
|
|
239
320
|
prompt: prompt,
|
|
240
321
|
images: screenshotBase64 ? [screenshotBase64] : [],
|
|
241
322
|
stream: false,
|
|
242
|
-
}, { timeout: 30000 });
|
|
323
|
+
}, { timeout: 30000, signal });
|
|
243
324
|
return response.data.response || '';
|
|
244
325
|
}
|
|
245
326
|
catch (err) {
|
|
@@ -257,6 +338,8 @@ class AIService {
|
|
|
257
338
|
constructor() {
|
|
258
339
|
this.provider = null;
|
|
259
340
|
this.isMock = false;
|
|
341
|
+
/** The settings `provider` was set up from (initializeProvider). */
|
|
342
|
+
this.setUpFrom = null;
|
|
260
343
|
this.initializeProvider();
|
|
261
344
|
}
|
|
262
345
|
// Key the breaker by provider + resolved model name so a misbehaving model
|
|
@@ -276,16 +359,60 @@ class AIService {
|
|
|
276
359
|
return `ai:${provider}:${model || 'default'}`;
|
|
277
360
|
}
|
|
278
361
|
// Single choke point for every LLM call so circuit-breaker state is shared
|
|
279
|
-
// across analyzeFailure / visualFind / healLocator.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
362
|
+
// across analyzeFailure / visualFind / healLocator. A rate limit is
|
|
363
|
+
// AIRateLimitedError whichever provider sent it; a call is cancelled after
|
|
364
|
+
// `timeoutMs` (AI_CALL_TIMEOUT_MS unless given: AITimeoutError). The breaker
|
|
365
|
+
// counts both, and a call it holds back is AIProviderPausedError.
|
|
366
|
+
callProvider(prompt_1, screenshotBase64_1) {
|
|
367
|
+
return __awaiter(this, arguments, void 0, function* (prompt, screenshotBase64, opts = {}) {
|
|
368
|
+
var _a;
|
|
369
|
+
const provider = this.provider;
|
|
370
|
+
try {
|
|
371
|
+
return yield CircuitBreaker_1.CIRCUIT_BREAKERS.execute(this.breakerKey(), () => {
|
|
372
|
+
var _a;
|
|
373
|
+
return withTimeLimit((_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : exports.AI_CALL_TIMEOUT_MS, (signal) => provider.analyze(prompt, screenshotBase64, signal));
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
if (isRateLimit(err) && !(err instanceof AIRateLimitedError)) {
|
|
378
|
+
throw new AIRateLimitedError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
|
|
379
|
+
}
|
|
380
|
+
if (err instanceof CircuitBreaker_1.CircuitOpenError && !(err instanceof AIProviderPausedError)) {
|
|
381
|
+
throw new AIProviderPausedError(err);
|
|
382
|
+
}
|
|
383
|
+
throw err;
|
|
384
|
+
}
|
|
283
385
|
});
|
|
284
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Sets the provider up from the server's AI settings, which the AI engine
|
|
389
|
+
* page changes while the server runs (`POST /config`). Again only when they
|
|
390
|
+
* changed, so a provider's client, and Ollama's availability check, carry
|
|
391
|
+
* over from one call to the next.
|
|
392
|
+
*/
|
|
285
393
|
initializeProvider() {
|
|
286
394
|
const providerType = config_1.config.aiProvider;
|
|
287
395
|
const genericModel = config_1.config.aiModel;
|
|
288
396
|
const baseUrl = config_1.config.aiBaseUrl;
|
|
397
|
+
const settings = JSON.stringify([
|
|
398
|
+
providerType,
|
|
399
|
+
genericModel,
|
|
400
|
+
baseUrl,
|
|
401
|
+
config_1.config.geminiApiKey,
|
|
402
|
+
config_1.config.openaiApiKey,
|
|
403
|
+
config_1.config.anthropicApiKey,
|
|
404
|
+
config_1.config.geminiModel,
|
|
405
|
+
config_1.config.openaiModel,
|
|
406
|
+
config_1.config.anthropicModel,
|
|
407
|
+
config_1.config.ollamaModel,
|
|
408
|
+
process.env.GEMINI_API_KEY === 'mock',
|
|
409
|
+
]);
|
|
410
|
+
if (settings === this.setUpFrom)
|
|
411
|
+
return;
|
|
412
|
+
this.setUpFrom = settings;
|
|
413
|
+
// A provider chosen without a key is no provider, never the previous one.
|
|
414
|
+
this.provider = null;
|
|
415
|
+
this.isMock = false;
|
|
289
416
|
logger_1.default.info(`[AIService] Initializing with provider: ${providerType}`);
|
|
290
417
|
if (process.env.GEMINI_API_KEY === 'mock') {
|
|
291
418
|
this.isMock = true;
|
|
@@ -321,7 +448,15 @@ class AIService {
|
|
|
321
448
|
logger_1.default.error(`[AIService] Initialization error: ${err.message}`);
|
|
322
449
|
}
|
|
323
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Whether a provider is set up for the settings in force now. Failure
|
|
453
|
+
* analysis and the LLM and visual healing tiers ask this first; it used to
|
|
454
|
+
* answer for the provider set up last, so choosing a configured provider on
|
|
455
|
+
* the AI engine page never turned them on, and choosing one without a key
|
|
456
|
+
* left the previous provider answering.
|
|
457
|
+
*/
|
|
324
458
|
isEnabled() {
|
|
459
|
+
this.initializeProvider();
|
|
325
460
|
return this.provider !== null || this.isMock;
|
|
326
461
|
}
|
|
327
462
|
analyzeFailure(context) {
|
|
@@ -341,7 +476,9 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
341
476
|
try {
|
|
342
477
|
const prompt = this.constructPrompt(context);
|
|
343
478
|
const screenshotBase64 = this.getScreenshotBase64(context.screenshotPath);
|
|
344
|
-
const text = yield this.callProvider(prompt, screenshotBase64 || undefined
|
|
479
|
+
const text = yield this.callProvider(prompt, screenshotBase64 || undefined, {
|
|
480
|
+
timeoutMs: exports.FAILURE_ANALYSIS_TIMEOUT_MS,
|
|
481
|
+
});
|
|
345
482
|
logger_1.default.info(`[AIService] Analysis complete for ${context.sessionId}`);
|
|
346
483
|
return text;
|
|
347
484
|
}
|
|
@@ -349,6 +486,12 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
|
|
|
349
486
|
if (err instanceof CircuitBreaker_1.CircuitOpenError) {
|
|
350
487
|
logger_1.default.debug(`[AIService] Analysis skipped: ${err.message}`);
|
|
351
488
|
}
|
|
489
|
+
else if (err instanceof AIRateLimitedError) {
|
|
490
|
+
logger_1.default.warn(`[AIService] No analysis for ${context.sessionId}: ${err.message}: ${err.detail}`);
|
|
491
|
+
}
|
|
492
|
+
else if (err instanceof AITimeoutError) {
|
|
493
|
+
logger_1.default.warn(`[AIService] No analysis for ${context.sessionId}: ${err.message}`);
|
|
494
|
+
}
|
|
352
495
|
else {
|
|
353
496
|
logger_1.default.error(`[AIService] Analysis failed: ${err.message}`);
|
|
354
497
|
}
|
|
@@ -429,11 +572,14 @@ Answer with JSON only, exactly: {"result": true or false, "reason": "one sentenc
|
|
|
429
572
|
answer = yield this.callProvider(prompt, screenshotBase64);
|
|
430
573
|
}
|
|
431
574
|
catch (err) {
|
|
575
|
+
if (err instanceof AIRateLimitedError || err instanceof AITimeoutError) {
|
|
576
|
+
throw new Error(`${err.message}, ${notChecked}. Try again later.`);
|
|
577
|
+
}
|
|
578
|
+
if (err instanceof AIProviderPausedError) {
|
|
579
|
+
throw new Error(`The AI provider has been failing, ${notChecked}. Xenon will call it again in ${err.seconds} s.`);
|
|
580
|
+
}
|
|
432
581
|
throw new Error(`The AI provider failed, ${notChecked}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
433
582
|
}
|
|
434
|
-
if (answer === RATE_LIMITED) {
|
|
435
|
-
throw new Error(`The AI provider is rate-limited, ${notChecked}. Try again later.`);
|
|
436
|
-
}
|
|
437
583
|
const verdict = parseVisualVerdict(answer);
|
|
438
584
|
if (!verdict) {
|
|
439
585
|
const said = answer.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
@@ -446,7 +592,9 @@ Answer with JSON only, exactly: {"result": true or false, "reason": "one sentenc
|
|
|
446
592
|
/**
|
|
447
593
|
* A short description of the screen in the screenshot, for
|
|
448
594
|
* `xenon: analyzeScreen` and device control's Omni-Scan. Throws when there
|
|
449
|
-
* is no provider or the call fails
|
|
595
|
+
* is no provider or the call fails (AIRateLimitedError for a rate limit,
|
|
596
|
+
* AITimeoutError after AI_CALL_TIMEOUT_MS), so the caller can say why there
|
|
597
|
+
* is none.
|
|
450
598
|
*/
|
|
451
599
|
describeScreen(screenshotBase64) {
|
|
452
600
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -458,10 +606,7 @@ You are looking at a screenshot of a mobile app, for a software tester.
|
|
|
458
606
|
Describe the screen in a few short sentences: which screen it is, its main elements and their state (for example filled in, empty, selected or disabled), and anything unexpected, such as an error message, a system dialog, a crash or a loading indicator.
|
|
459
607
|
Describe only what is visible. Answer in plain text.
|
|
460
608
|
`.trim();
|
|
461
|
-
|
|
462
|
-
if (answer === RATE_LIMITED)
|
|
463
|
-
throw new Error('The AI provider is rate-limited');
|
|
464
|
-
return answer;
|
|
609
|
+
return this.callProvider(prompt, screenshotBase64);
|
|
465
610
|
});
|
|
466
611
|
}
|
|
467
612
|
/**
|
|
@@ -512,9 +657,10 @@ Describe only what is visible. Answer in plain text.
|
|
|
512
657
|
}
|
|
513
658
|
testConnection(testConfig) {
|
|
514
659
|
return __awaiter(this, void 0, void 0, function* () {
|
|
660
|
+
var _a;
|
|
661
|
+
const providerType = testConfig.aiProvider || config_1.config.aiProvider;
|
|
515
662
|
try {
|
|
516
663
|
let testProvider = null;
|
|
517
|
-
const providerType = testConfig.aiProvider || config_1.config.aiProvider;
|
|
518
664
|
const model = testConfig.aiModel || config_1.config.aiModel;
|
|
519
665
|
const baseUrl = testConfig.aiBaseUrl || config_1.config.aiBaseUrl;
|
|
520
666
|
logger_1.default.info(`[AIService] Testing connection for provider: ${providerType}`);
|
|
@@ -547,17 +693,21 @@ Describe only what is visible. Answer in plain text.
|
|
|
547
693
|
}
|
|
548
694
|
if (!testProvider)
|
|
549
695
|
throw new Error('Failed to initialize provider for testing');
|
|
550
|
-
// Send a minimal ping command
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
success: true,
|
|
555
|
-
message: `Successfully connected to ${providerType}! (Note: You are currently out of quota/rate-limited, but the setup is correct)`,
|
|
556
|
-
};
|
|
557
|
-
}
|
|
696
|
+
// Send a minimal ping command. Not through the circuit breaker (a test
|
|
697
|
+
// shouldn't pause the provider for everyone), but with the time limit.
|
|
698
|
+
const pinged = testProvider;
|
|
699
|
+
yield withTimeLimit(exports.AI_CALL_TIMEOUT_MS, (signal) => pinged.analyze('Hello. Response: OK', undefined, signal));
|
|
558
700
|
return { success: true, message: `Successfully connected to ${providerType}!` };
|
|
559
701
|
}
|
|
560
702
|
catch (err) {
|
|
703
|
+
// Not a success: every AI call fails the same way until it has quota again.
|
|
704
|
+
if (isRateLimit(err)) {
|
|
705
|
+
logger_1.default.warn(`[AIService] Connection test: ${providerType} is rate-limited: ${(_a = err.detail) !== null && _a !== void 0 ? _a : err.message}`);
|
|
706
|
+
return {
|
|
707
|
+
success: false,
|
|
708
|
+
message: `${providerType} answered, but it is rate-limited or out of quota. AI features won't work with it until it has quota again.`,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
561
711
|
logger_1.default.error(`[AIService] Connection test failed: ${err.message}`);
|
|
562
712
|
return { success: false, message: `Connection failed: ${err.message}` };
|
|
563
713
|
}
|
|
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
|
|
|
23
23
|
const crypto_1 = __importDefault(require("crypto"));
|
|
24
24
|
const prisma_1 = require("../prisma");
|
|
25
25
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
26
27
|
let ApiKeyService = class ApiKeyService {
|
|
27
28
|
constructor() {
|
|
28
29
|
this.log = logger_1.default.scope('ApiKey');
|
|
@@ -38,8 +39,13 @@ let ApiKeyService = class ApiKeyService {
|
|
|
38
39
|
generateRaw() {
|
|
39
40
|
return crypto_1.default.randomBytes(32).toString('hex');
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The live key for `raw`. A use of it is recorded (`lastUsedAt`);
|
|
44
|
+
* `touch: false` only looks, for a check that isn't a use (a socket's
|
|
45
|
+
* re-check). Likewise verifyPair.
|
|
46
|
+
*/
|
|
47
|
+
verify(raw_1) {
|
|
48
|
+
return __awaiter(this, arguments, void 0, function* (raw, { touch = true } = {}) {
|
|
43
49
|
if (!raw)
|
|
44
50
|
return null;
|
|
45
51
|
const row = yield prisma_1.prisma.apiKey.findUnique({ where: { keyHash: this.hash(raw) } });
|
|
@@ -47,6 +53,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
47
53
|
return null;
|
|
48
54
|
if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
|
|
49
55
|
return null;
|
|
56
|
+
if (!touch)
|
|
57
|
+
return row;
|
|
50
58
|
prisma_1.prisma.apiKey
|
|
51
59
|
.update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
|
|
52
60
|
.catch(() => undefined);
|
|
@@ -77,8 +85,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
77
85
|
return { id: row.id, raw };
|
|
78
86
|
});
|
|
79
87
|
}
|
|
80
|
-
verifyPair(
|
|
81
|
-
return __awaiter(this,
|
|
88
|
+
verifyPair(accessKey_1, token_1) {
|
|
89
|
+
return __awaiter(this, arguments, void 0, function* (accessKey, token, { touch = true } = {}) {
|
|
82
90
|
if (!accessKey || !token)
|
|
83
91
|
return null;
|
|
84
92
|
const user = yield prisma_1.prisma.user.findUnique({ where: { accessKey } });
|
|
@@ -91,6 +99,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
91
99
|
return null;
|
|
92
100
|
if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
|
|
93
101
|
return null;
|
|
102
|
+
if (!touch)
|
|
103
|
+
return row;
|
|
94
104
|
prisma_1.prisma.apiKey
|
|
95
105
|
.update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
|
|
96
106
|
.catch(() => undefined);
|
|
@@ -99,7 +109,8 @@ let ApiKeyService = class ApiKeyService {
|
|
|
99
109
|
}
|
|
100
110
|
revoke(id) {
|
|
101
111
|
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
-
yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
|
|
112
|
+
const row = yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
|
|
113
|
+
yield (0, identityChanges_1.identityChanged)(row.userId);
|
|
103
114
|
});
|
|
104
115
|
}
|
|
105
116
|
list() {
|
|
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
|
|
|
23
23
|
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
24
24
|
const config_1 = require("../config");
|
|
25
25
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
|
+
const passwordResetLink_1 = require("./passwordResetLink");
|
|
26
27
|
let EmailService = class EmailService {
|
|
27
28
|
constructor() {
|
|
28
29
|
this.log = logger_1.default.scope('Email');
|
|
@@ -55,6 +56,34 @@ let EmailService = class EmailService {
|
|
|
55
56
|
'over the account for the link lifetime. Configure XENON_SMTP_URL, or unset this and ' +
|
|
56
57
|
'issue links from the dashboard Users page.');
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Startup notice about XENON_PUBLIC_URL, the address links Xenon hands out
|
|
61
|
+
* are built from (never the request's Host, which the asker chooses):
|
|
62
|
+
*
|
|
63
|
+
* - set but refused (not an http(s) server address): whatever the mail
|
|
64
|
+
* setup, since it also decides each device's dashboard_link. The value is
|
|
65
|
+
* not logged: a user name and password in it is one reason to refuse it.
|
|
66
|
+
* - unset while Xenon could send reset links: the sign-in page's "forgot
|
|
67
|
+
* password" then sends people to an administrator, and an administrator's
|
|
68
|
+
* reset link is handed back to them rather than emailed.
|
|
69
|
+
*/
|
|
70
|
+
warnAboutPublicUrl() {
|
|
71
|
+
var _a;
|
|
72
|
+
if (((_a = config_1.config.publicUrl) === null || _a === void 0 ? void 0 : _a.trim()) && (0, passwordResetLink_1.resetLinkBase)() === null) {
|
|
73
|
+
this.warnLog('XENON_PUBLIC_URL is set but is not an http(s) server address such as ' +
|
|
74
|
+
'https://xenon.example.com or http://lab-mac:4723 (a trailing /xenon is fine; any ' +
|
|
75
|
+
'other path, a query, a fragment or a user name is not), so Xenon ignores it: it ' +
|
|
76
|
+
'emails no password-reset links, and device dashboard links are paths on this server.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!this.canDeliver() || (0, passwordResetLink_1.resetLinkBase)() !== null)
|
|
80
|
+
return;
|
|
81
|
+
this.warnLog("XENON_PUBLIC_URL is not set to this server's address, so Xenon sends no password-reset " +
|
|
82
|
+
'links: "Forgot password" asks people to contact an administrator, and the Users ' +
|
|
83
|
+
'page hands an administrator the link instead of emailing it. Set XENON_PUBLIC_URL to ' +
|
|
84
|
+
'the http(s) address people reach this server at, such as https://xenon.example.com ' +
|
|
85
|
+
'or http://lab-mac:4723 (a trailing /xenon is fine; any other path is not).');
|
|
86
|
+
}
|
|
58
87
|
send(mail) {
|
|
59
88
|
return __awaiter(this, void 0, void 0, function* () {
|
|
60
89
|
const smtpUrl = config_1.config.smtpUrl;
|
|
@@ -240,7 +240,11 @@ let InterceptorService = class InterceptorService {
|
|
|
240
240
|
throw new Error(`Interceptor not active for session ${sessionId}`);
|
|
241
241
|
return state;
|
|
242
242
|
}
|
|
243
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* `udid` is the session's phone. The dashboard events reach only admins who
|
|
245
|
+
* see it: a captured request's headers and bodies can carry the app's
|
|
246
|
+
* sign-in tokens, and REST's /interceptor routes are admin-only too.
|
|
247
|
+
*/
|
|
244
248
|
emit(evt, udid) {
|
|
245
249
|
for (const l of this.listeners) {
|
|
246
250
|
try {
|
|
@@ -255,7 +259,7 @@ let InterceptorService = class InterceptorService {
|
|
|
255
259
|
broadcast(evt, udid) {
|
|
256
260
|
try {
|
|
257
261
|
const socket = typedi_1.Container.get(SocketServer_1.SocketServer);
|
|
258
|
-
const scope = { udid };
|
|
262
|
+
const scope = { udid, adminOnly: true };
|
|
259
263
|
if (evt.type === 'request') {
|
|
260
264
|
void socket.emitToDashboardForDevices(SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, evt.payload, scope);
|
|
261
265
|
}
|
|
@@ -26,6 +26,8 @@ const event_manager_1 = require("../dashboard/event-manager");
|
|
|
26
26
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
27
27
|
const device_service_1 = require("../data-service/device-service");
|
|
28
28
|
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
29
|
+
const TracingService_1 = require("./TracingService");
|
|
30
|
+
const sessionMemory_1 = require("../sessions/sessionMemory");
|
|
29
31
|
let OrphanSweeper = class OrphanSweeper {
|
|
30
32
|
constructor() {
|
|
31
33
|
this.log = logger_1.default.scope('OrphanSweeper');
|
|
@@ -74,6 +76,12 @@ let OrphanSweeper = class OrphanSweeper {
|
|
|
74
76
|
// back before the phone is released; for a previous process's it is
|
|
75
77
|
// the ledger's, at boot (PhoneNetworkRestore).
|
|
76
78
|
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
|
|
79
|
+
(0, sessionMemory_1.forgetSessionMemory)(s.id);
|
|
80
|
+
// A previous process's session has no span here; this one's ends.
|
|
81
|
+
typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(s.id, {
|
|
82
|
+
failed: true,
|
|
83
|
+
reason: 'Session heartbeat timeout',
|
|
84
|
+
});
|
|
77
85
|
// This session's claim only, not every row with its udid: the phone
|
|
78
86
|
// may be another session's by now (deviceClaims.ts).
|
|
79
87
|
yield (0, device_service_1.releaseSessionDevices)(s.id);
|
|
@@ -51,6 +51,10 @@ function whereTuple(strategy, selector) {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
/** Just the selector a change is about, for the socket server's scoped emit. */
|
|
55
|
+
function selectorOf(tuple) {
|
|
56
|
+
return { strategy: tuple.strategy, selector: tuple.selector };
|
|
57
|
+
}
|
|
54
58
|
/** The SelectorEvent row for one change. */
|
|
55
59
|
function selectorEventData(tuple, action, userId, reason) {
|
|
56
60
|
return {
|
|
@@ -140,7 +144,7 @@ let SelectorStateService = class SelectorStateService {
|
|
|
140
144
|
return upserted;
|
|
141
145
|
}));
|
|
142
146
|
scopedLog.info(`markFixed: ${ctx.strategy}=${ctx.selector} → pending (api_key=${ctx.apiKeyId})`);
|
|
143
|
-
this.socket.
|
|
147
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_FIXED, serialize(row), selectorOf(ctx));
|
|
144
148
|
return row;
|
|
145
149
|
});
|
|
146
150
|
}
|
|
@@ -174,7 +178,7 @@ let SelectorStateService = class SelectorStateService {
|
|
|
174
178
|
return upserted;
|
|
175
179
|
}));
|
|
176
180
|
scopedLog.info(`mute: ${ctx.strategy}=${ctx.selector} → muted (api_key=${ctx.apiKeyId})`);
|
|
177
|
-
this.socket.
|
|
181
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_MUTED, serialize(row), selectorOf(ctx));
|
|
178
182
|
return row;
|
|
179
183
|
});
|
|
180
184
|
}
|
|
@@ -231,13 +235,13 @@ let SelectorStateService = class SelectorStateService {
|
|
|
231
235
|
scopedLog.info(`unmute: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
|
|
232
236
|
// Emit the post-state. When the row was deleted, surface the prior tuple
|
|
233
237
|
// so the dashboard can drop it from the muted list.
|
|
234
|
-
this.socket.
|
|
238
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, result
|
|
235
239
|
? serialize(result)
|
|
236
240
|
: {
|
|
237
241
|
original_strategy: ctx.strategy,
|
|
238
242
|
original_selector: ctx.selector,
|
|
239
243
|
status: 'deleted',
|
|
240
|
-
});
|
|
244
|
+
}, selectorOf(ctx));
|
|
241
245
|
return result;
|
|
242
246
|
});
|
|
243
247
|
}
|
|
@@ -279,13 +283,13 @@ let SelectorStateService = class SelectorStateService {
|
|
|
279
283
|
return row;
|
|
280
284
|
}));
|
|
281
285
|
scopedLog.info(`cancelVerification: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
|
|
282
|
-
this.socket.
|
|
286
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, result
|
|
283
287
|
? serialize(result)
|
|
284
288
|
: {
|
|
285
289
|
original_strategy: ctx.strategy,
|
|
286
290
|
original_selector: ctx.selector,
|
|
287
291
|
status: 'deleted',
|
|
288
|
-
});
|
|
292
|
+
}, selectorOf(ctx));
|
|
289
293
|
return result;
|
|
290
294
|
});
|
|
291
295
|
}
|
|
@@ -329,7 +333,7 @@ let SelectorStateService = class SelectorStateService {
|
|
|
329
333
|
return updated;
|
|
330
334
|
}));
|
|
331
335
|
scopedLog.warn(`onHealRecorded: ${ctx.strategy}=${ctx.selector} regressed from ${existing.status} (session=${ctx.sessionId})`);
|
|
332
|
-
this.socket.
|
|
336
|
+
void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, serialize(row), selectorOf(ctx));
|
|
333
337
|
return row;
|
|
334
338
|
});
|
|
335
339
|
}
|