@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -45,7 +45,8 @@ 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
+ exports.parseVisualVerdict = parseVisualVerdict;
49
50
  const generative_ai_1 = require("@google/generative-ai");
50
51
  const openai_1 = __importDefault(require("openai"));
51
52
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
@@ -55,13 +56,128 @@ const fs = __importStar(require("fs"));
55
56
  const path = __importStar(require("path"));
56
57
  const config_1 = require("../config");
57
58
  const CircuitBreaker_1 = require("./CircuitBreaker");
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
+ }
141
+ /**
142
+ * A provider's answer to the assertion prompt, `{ "result": true|false,
143
+ * "reason": "..." }`, read leniently (code fences, text around the JSON,
144
+ * `message` for `reason`, "true"/"false" as strings) but never guessed: null
145
+ * when it holds no true/false `result`.
146
+ */
147
+ function parseVisualVerdict(answer) {
148
+ const text = answer.replace(/```(?:json)?/gi, '').trim();
149
+ const start = text.indexOf('{');
150
+ const end = text.lastIndexOf('}');
151
+ if (start === -1 || end <= start)
152
+ return null;
153
+ let data;
154
+ try {
155
+ data = JSON.parse(text.slice(start, end + 1));
156
+ }
157
+ catch (_a) {
158
+ return null;
159
+ }
160
+ const raw = data === null || data === void 0 ? void 0 : data.result;
161
+ const result = raw === true || raw === 'true' ? true : raw === false || raw === 'false' ? false : null;
162
+ if (result === null)
163
+ return null;
164
+ const reason = typeof data.reason === 'string'
165
+ ? data.reason
166
+ : typeof data.message === 'string'
167
+ ? data.message
168
+ : '';
169
+ return {
170
+ result,
171
+ message: reason.trim() || (result ? 'The condition holds.' : 'The condition does not hold.'),
172
+ };
173
+ }
58
174
  class GeminiProvider {
59
175
  constructor(apiKey, modelName) {
60
176
  this.genAI = new generative_ai_1.GoogleGenerativeAI(apiKey.trim());
61
177
  this.modelName =
62
178
  modelName && modelName.trim() !== '' ? modelName.trim() : 'gemini-3-flash-preview';
63
179
  }
64
- analyze(prompt, screenshotBase64) {
180
+ analyze(prompt, screenshotBase64, signal) {
65
181
  return __awaiter(this, void 0, void 0, function* () {
66
182
  const parts = [prompt];
67
183
  if (screenshotBase64) {
@@ -80,17 +196,17 @@ class GeminiProvider {
80
196
  for (const version of versions) {
81
197
  try {
82
198
  const model = this.genAI.getGenerativeModel({ model: this.modelName }, { apiVersion: version });
83
- const result = yield model.generateContent(parts);
199
+ const result = yield model.generateContent(parts, signal ? { signal } : undefined);
84
200
  const response = yield result.response;
85
201
  return response.text();
86
202
  }
87
203
  catch (err) {
88
204
  lastError = err;
89
- // If we get a 429, the connection IS WORKING, just rate limited.
90
- if (err.message.includes('429') || err.message.includes('Quota exceeded')) {
91
- logger_1.default.info(`[Gemini] Connection verified (Rate Limited) via ${version} endpoint.`);
92
- return 'CONNECTION_OK_RATE_LIMITED';
93
- }
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);
94
210
  if (err.message.includes('404') ||
95
211
  err.message.includes('not found') ||
96
212
  err.message.includes('unsupported')) {
@@ -109,7 +225,7 @@ class OpenAIProvider {
109
225
  this.client = new openai_1.default({ apiKey, baseURL });
110
226
  this.model = model;
111
227
  }
112
- analyze(prompt, screenshotBase64) {
228
+ analyze(prompt, screenshotBase64, signal) {
113
229
  return __awaiter(this, void 0, void 0, function* () {
114
230
  const messages = [
115
231
  {
@@ -127,7 +243,7 @@ class OpenAIProvider {
127
243
  model: this.model,
128
244
  messages,
129
245
  max_tokens: 500,
130
- });
246
+ }, { signal });
131
247
  return response.choices[0].message.content || '';
132
248
  });
133
249
  }
@@ -137,7 +253,7 @@ class AnthropicProvider {
137
253
  this.client = new sdk_1.default({ apiKey });
138
254
  this.model = model;
139
255
  }
140
- analyze(prompt, screenshotBase64) {
256
+ analyze(prompt, screenshotBase64, signal) {
141
257
  return __awaiter(this, void 0, void 0, function* () {
142
258
  const content = [{ type: 'text', text: prompt }];
143
259
  if (screenshotBase64) {
@@ -154,7 +270,7 @@ class AnthropicProvider {
154
270
  model: this.model,
155
271
  max_tokens: 500,
156
272
  messages: [{ role: 'user', content }],
157
- });
273
+ }, { signal });
158
274
  return response.content[0].text || '';
159
275
  });
160
276
  }
@@ -170,10 +286,11 @@ class OllamaProvider {
170
286
  checkAvailability() {
171
287
  return __awaiter(this, void 0, void 0, function* () {
172
288
  const now = Date.now();
173
- // Use cached status if checked recently
174
- if (this.isAvailable !== null &&
175
- now - this.lastAvailabilityCheck < this.AVAILABILITY_CHECK_INTERVAL) {
176
- return this.isAvailable;
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;
177
294
  }
178
295
  try {
179
296
  // Quick health check - Ollama has a /api/tags endpoint
@@ -189,7 +306,7 @@ class OllamaProvider {
189
306
  }
190
307
  });
191
308
  }
192
- analyze(prompt, screenshotBase64) {
309
+ analyze(prompt, screenshotBase64, signal) {
193
310
  return __awaiter(this, void 0, void 0, function* () {
194
311
  var _a;
195
312
  // Check if Ollama is available before attempting
@@ -203,7 +320,7 @@ class OllamaProvider {
203
320
  prompt: prompt,
204
321
  images: screenshotBase64 ? [screenshotBase64] : [],
205
322
  stream: false,
206
- }, { timeout: 30000 });
323
+ }, { timeout: 30000, signal });
207
324
  return response.data.response || '';
208
325
  }
209
326
  catch (err) {
@@ -221,6 +338,8 @@ class AIService {
221
338
  constructor() {
222
339
  this.provider = null;
223
340
  this.isMock = false;
341
+ /** The settings `provider` was set up from (initializeProvider). */
342
+ this.setUpFrom = null;
224
343
  this.initializeProvider();
225
344
  }
226
345
  // Key the breaker by provider + resolved model name so a misbehaving model
@@ -240,16 +359,60 @@ class AIService {
240
359
  return `ai:${provider}:${model || 'default'}`;
241
360
  }
242
361
  // Single choke point for every LLM call so circuit-breaker state is shared
243
- // across analyzeFailure / visualFind / healLocator.
244
- callProvider(prompt, screenshotBase64) {
245
- return __awaiter(this, void 0, void 0, function* () {
246
- return CircuitBreaker_1.CIRCUIT_BREAKERS.execute(this.breakerKey(), () => this.provider.analyze(prompt, screenshotBase64));
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
+ }
247
385
  });
248
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
+ */
249
393
  initializeProvider() {
250
394
  const providerType = config_1.config.aiProvider;
251
395
  const genericModel = config_1.config.aiModel;
252
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;
253
416
  logger_1.default.info(`[AIService] Initializing with provider: ${providerType}`);
254
417
  if (process.env.GEMINI_API_KEY === 'mock') {
255
418
  this.isMock = true;
@@ -285,7 +448,15 @@ class AIService {
285
448
  logger_1.default.error(`[AIService] Initialization error: ${err.message}`);
286
449
  }
287
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
+ */
288
458
  isEnabled() {
459
+ this.initializeProvider();
289
460
  return this.provider !== null || this.isMock;
290
461
  }
291
462
  analyzeFailure(context) {
@@ -305,7 +476,9 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
305
476
  try {
306
477
  const prompt = this.constructPrompt(context);
307
478
  const screenshotBase64 = this.getScreenshotBase64(context.screenshotPath);
308
- 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
+ });
309
482
  logger_1.default.info(`[AIService] Analysis complete for ${context.sessionId}`);
310
483
  return text;
311
484
  }
@@ -313,6 +486,12 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
313
486
  if (err instanceof CircuitBreaker_1.CircuitOpenError) {
314
487
  logger_1.default.debug(`[AIService] Analysis skipped: ${err.message}`);
315
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
+ }
316
495
  else {
317
496
  logger_1.default.error(`[AIService] Analysis failed: ${err.message}`);
318
497
  }
@@ -365,6 +544,71 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
365
544
  }
366
545
  });
367
546
  }
547
+ /**
548
+ * Asks the provider whether `condition` holds on the screenshot
549
+ * (`xenon: assertVisualState`). Answers only with the provider's own
550
+ * verdict: with no provider, a failed call, a rate limit or an answer that
551
+ * isn't true or false it throws, saying the condition was not checked. A
552
+ * `false` there would pass a test asserting that something is absent.
553
+ */
554
+ assertVisual(screenshotBase64, condition) {
555
+ return __awaiter(this, void 0, void 0, function* () {
556
+ var _a;
557
+ this.initializeProvider();
558
+ const notChecked = 'so the condition was not checked';
559
+ if (!this.provider) {
560
+ throw new Error(`No AI provider is configured, ${notChecked}. Set XENON_AI_PROVIDER and the provider's key.`);
561
+ }
562
+ const prompt = `
563
+ You are checking a screenshot of a mobile app for an automated test.
564
+
565
+ Condition: ${JSON.stringify(condition)}
566
+
567
+ Is the condition true of what the screenshot shows? Judge only what is visible on the screen.
568
+ Answer with JSON only, exactly: {"result": true or false, "reason": "one sentence on what you see"}
569
+ `.trim();
570
+ let answer;
571
+ try {
572
+ answer = yield this.callProvider(prompt, screenshotBase64);
573
+ }
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
+ }
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}`);
582
+ }
583
+ const verdict = parseVisualVerdict(answer);
584
+ if (!verdict) {
585
+ const said = answer.replace(/\s+/g, ' ').trim().slice(0, 200);
586
+ throw new Error(`Xenon could not read the AI provider's answer as true or false, ${notChecked}. ` +
587
+ `It answered: ${JSON.stringify(said)}`);
588
+ }
589
+ return verdict;
590
+ });
591
+ }
592
+ /**
593
+ * A short description of the screen in the screenshot, for
594
+ * `xenon: analyzeScreen` and device control's Omni-Scan. Throws when there
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.
598
+ */
599
+ describeScreen(screenshotBase64) {
600
+ return __awaiter(this, void 0, void 0, function* () {
601
+ this.initializeProvider();
602
+ if (!this.provider)
603
+ throw new Error('No AI provider is configured');
604
+ const prompt = `
605
+ You are looking at a screenshot of a mobile app, for a software tester.
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.
607
+ Describe only what is visible. Answer in plain text.
608
+ `.trim();
609
+ return this.callProvider(prompt, screenshotBase64);
610
+ });
611
+ }
368
612
  /**
369
613
  * Heals a broken locator using deep LLM reasoning (Tier 5)
370
614
  */
@@ -413,9 +657,10 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
413
657
  }
414
658
  testConnection(testConfig) {
415
659
  return __awaiter(this, void 0, void 0, function* () {
660
+ var _a;
661
+ const providerType = testConfig.aiProvider || config_1.config.aiProvider;
416
662
  try {
417
663
  let testProvider = null;
418
- const providerType = testConfig.aiProvider || config_1.config.aiProvider;
419
664
  const model = testConfig.aiModel || config_1.config.aiModel;
420
665
  const baseUrl = testConfig.aiBaseUrl || config_1.config.aiBaseUrl;
421
666
  logger_1.default.info(`[AIService] Testing connection for provider: ${providerType}`);
@@ -448,17 +693,21 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
448
693
  }
449
694
  if (!testProvider)
450
695
  throw new Error('Failed to initialize provider for testing');
451
- // Send a minimal ping command
452
- const responseText = yield testProvider.analyze('Hello. Response: OK');
453
- if (responseText === 'CONNECTION_OK_RATE_LIMITED') {
454
- return {
455
- success: true,
456
- message: `Successfully connected to ${providerType}! (Note: You are currently out of quota/rate-limited, but the setup is correct)`,
457
- };
458
- }
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));
459
700
  return { success: true, message: `Successfully connected to ${providerType}!` };
460
701
  }
461
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
+ }
462
711
  logger_1.default.error(`[AIService] Connection test failed: ${err.message}`);
463
712
  return { success: false, message: `Connection failed: ${err.message}` };
464
713
  }
@@ -25,6 +25,7 @@ const logger_1 = __importDefault(require("../logger"));
25
25
  const fs_1 = __importDefault(require("fs"));
26
26
  const path_1 = __importDefault(require("path"));
27
27
  const config_1 = require("../config");
28
+ const labSettings_1 = require("./settings/labSettings");
28
29
  const recordingCleanup_1 = require("./recording/recordingCleanup");
29
30
  /** Subdirectory holding composite (multi-device) outputs, keyed by group id. */
30
31
  const GROUPS_DIR = '_groups';
@@ -34,6 +35,8 @@ let CleanupService = class CleanupService {
34
35
  }
35
36
  /**
36
37
  * Orchestrates build + session + recording cleanup based on retention policy.
38
+ * The build retention window, the build cap and the asset purge are the ones
39
+ * saved on the dashboard's Maintenance page, else `pluginArgs`'.
37
40
  *
38
41
  * Phases:
39
42
  * 1. Purge old builds (by age AND count cap) together with their sessions.
@@ -44,7 +47,9 @@ let CleanupService = class CleanupService {
44
47
  */
45
48
  runCleanup(pluginArgs) {
46
49
  return __awaiter(this, void 0, void 0, function* () {
47
- const { buildCleanupDays = 30, buildCleanupMaxCount = 100, deleteBuildAssets = true, } = pluginArgs;
50
+ // What the Maintenance page saved wins over the startup options, and is
51
+ // read here, at each run, so a change needs no restart. See labSettings.ts.
52
+ const { buildCleanupDays, buildCleanupMaxCount, deleteBuildAssets } = yield (0, labSettings_1.loadEffectiveSettings)(pluginArgs);
48
53
  this.log.info(`Starting cleanup: Retention = ${buildCleanupDays} days, Max Builds = ${buildCleanupMaxCount}, Purge Assets = ${deleteBuildAssets}`);
49
54
  const expirationDate = new Date();
50
55
  expirationDate.setDate(expirationDate.getDate() - buildCleanupDays);
@@ -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;
@@ -72,6 +72,10 @@ const SessionArchive_1 = require("./interceptor/SessionArchive");
72
72
  const PortAllocator_1 = require("./PortAllocator");
73
73
  const SocketServer_1 = require("./SocketServer");
74
74
  const SocketEvents_1 = require("../enums/SocketEvents");
75
+ const adbForPhone_1 = require("./network/adbForPhone");
76
+ const PhoneNetworkLedger_1 = require("./network/PhoneNetworkLedger");
77
+ const phoneNetworkLock_1 = require("./network/phoneNetworkLock");
78
+ const xenonProxy_1 = require("./network/xenonProxy");
75
79
  const DEFAULT_BUFFER_CAP = 1000;
76
80
  const BODY_SPILL_THRESHOLD = 1024 * 1024;
77
81
  let InterceptorService = class InterceptorService {
@@ -88,6 +92,10 @@ let InterceptorService = class InterceptorService {
88
92
  isActive(sessionId) {
89
93
  return this.states.has(sessionId);
90
94
  }
95
+ /** The sessions whose traffic is captured on this server. */
96
+ sessionIds() {
97
+ return [...this.states.keys()];
98
+ }
91
99
  start(sessionId, device, opts) {
92
100
  return __awaiter(this, void 0, void 0, function* () {
93
101
  var _a, _b, _c, _d;
@@ -143,7 +151,7 @@ let InterceptorService = class InterceptorService {
143
151
  catch (err) {
144
152
  this.logger.warn(`Cert install (${installMode}) failed for ${device.udid}: ${err.message}. HTTPS interception may not work.`);
145
153
  }
146
- yield adapter.setProxy(device.udid, host, port);
154
+ const previousProxy = yield this.pointPhoneAtProxy(sessionId, device.udid, host, port, routing.reverseEstablished);
147
155
  const state = {
148
156
  sessionId,
149
157
  device,
@@ -155,6 +163,7 @@ let InterceptorService = class InterceptorService {
155
163
  certInstalledFilename: certFilename,
156
164
  startedAt: Date.now(),
157
165
  reverseEstablished: routing.reverseEstablished,
166
+ previousProxy,
158
167
  };
159
168
  this.states.set(sessionId, state);
160
169
  this.emit({ type: 'session_started', sessionId, port, host }, device.udid);
@@ -167,12 +176,7 @@ let InterceptorService = class InterceptorService {
167
176
  if (!state)
168
177
  return;
169
178
  this.states.delete(sessionId);
170
- try {
171
- yield this.getAndroidAdapter().clearProxy(state.device.udid);
172
- }
173
- catch (err) {
174
- this.logger.warn(`Clear proxy failed for ${state.device.udid}: ${err.message}`);
175
- }
179
+ yield this.putPhoneProxyBack(state);
176
180
  if (state.reverseEstablished) {
177
181
  try {
178
182
  yield this.getAndroidAdapter().removeReverse(state.device.udid, state.port);
@@ -284,15 +288,68 @@ let InterceptorService = class InterceptorService {
284
288
  requests: state.buffer.list(),
285
289
  };
286
290
  }
291
+ /**
292
+ * Points the whole phone at the session's proxy, and returns the proxy the
293
+ * phone had (null: none) for stop() to put back. The change is written down
294
+ * before it is made, so a restart after a crash can undo it
295
+ * (PhoneNetworkLedger, PhoneNetworkRestore).
296
+ *
297
+ * A previous proxy that is a capture an earlier run left behind (this
298
+ * machine, a capture port, nothing answering) is not the phone's own: it
299
+ * counts as none, so the end of this session doesn't put a dead proxy back.
300
+ */
301
+ pointPhoneAtProxy(sessionId, udid, host, port, reverseEstablished) {
302
+ return __awaiter(this, void 0, void 0, function* () {
303
+ const adapter = this.getAndroidAdapter();
304
+ const setting = `${host}:${port}`;
305
+ return yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
306
+ var _a;
307
+ let previous = null;
308
+ try {
309
+ const read = yield adapter.readProxy(udid);
310
+ const leftover = read === setting ||
311
+ (yield (0, xenonProxy_1.isDeadCaptureProxy)(read, typedi_1.Container.get(PortAllocator_1.PortAllocator).rangeOf('proxy')));
312
+ if (leftover) {
313
+ this.logger.info(`[${sessionId}] ${udid} had a proxy left by an earlier capture (${read})`);
314
+ }
315
+ previous = (0, xenonProxy_1.isUnsetProxy)(read) || leftover ? null : read;
316
+ }
317
+ catch (err) {
318
+ this.logger.warn(`Could not read the proxy of ${udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
319
+ }
320
+ yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).note(sessionId, udid, {
321
+ proxy: Object.assign({ set: setting, previous }, (reverseEstablished ? { reversePort: port } : {})),
322
+ });
323
+ yield adapter.setProxy(udid, host, port);
324
+ return previous;
325
+ }));
326
+ });
327
+ }
328
+ /**
329
+ * Puts back the proxy the phone had before the session. A phone that can't
330
+ * be reached keeps the change written down, and is put back at its next
331
+ * session or at this server's next start.
332
+ */
333
+ putPhoneProxyBack(state) {
334
+ return __awaiter(this, void 0, void 0, function* () {
335
+ const udid = state.device.udid;
336
+ yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
337
+ var _a;
338
+ try {
339
+ yield this.getAndroidAdapter().restoreProxy(udid, state.previousProxy);
340
+ yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(state.sessionId, 'proxy');
341
+ }
342
+ catch (err) {
343
+ this.logger.warn(`Could not put back the proxy of ${udid} after session ${state.sessionId}: ` +
344
+ `${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}. Xenon tries again at the phone's next session and when it restarts.`);
345
+ }
346
+ }));
347
+ });
348
+ }
287
349
  getAndroidAdapter() {
288
350
  if (this.androidAdapter)
289
351
  return this.androidAdapter;
290
- const provider = (udid) => __awaiter(this, void 0, void 0, function* () {
291
- const { default: AndroidDeviceManager } = yield Promise.resolve().then(() => __importStar(require('../device-managers/AndroidDeviceManager')));
292
- const mgr = typedi_1.Container.get(AndroidDeviceManager);
293
- return yield mgr.getAdbForDevice(udid);
294
- });
295
- this.androidAdapter = new AndroidProxyAdapter_1.AndroidProxyAdapter(provider);
352
+ this.androidAdapter = new AndroidProxyAdapter_1.AndroidProxyAdapter(adbForPhone_1.adbForPhone);
296
353
  return this.androidAdapter;
297
354
  }
298
355
  // Establishes how the device will reach the host-side MITM proxy.