@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
@@ -1,43 +1,10 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
2
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
23
7
  };
24
- var __importStar = (this && this.__importStar) || (function () {
25
- var ownKeys = function(o) {
26
- ownKeys = Object.getOwnPropertyNames || function (o) {
27
- var ar = [];
28
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
- return ar;
30
- };
31
- return ownKeys(o);
32
- };
33
- return function (mod) {
34
- if (mod && mod.__esModule) return mod;
35
- var result = {};
36
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
- __setModuleDefault(result, mod);
38
- return result;
39
- };
40
- })();
41
8
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
9
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
10
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -54,6 +21,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
54
21
  exports.NetworkConditioningService = void 0;
55
22
  const typedi_1 = require("typedi");
56
23
  const logger_1 = __importDefault(require("../logger"));
24
+ const adbForPhone_1 = require("./network/adbForPhone");
25
+ const AndroidRadios_1 = require("./network/AndroidRadios");
26
+ const PhoneNetworkLedger_1 = require("./network/PhoneNetworkLedger");
27
+ const phoneNetworkLock_1 = require("./network/phoneNetworkLock");
57
28
  const NETWORK_PROFILES = {
58
29
  Normal: null,
59
30
  '4G': { latencyMs: 20, downloadKbps: 15000, uploadKbps: 7500 },
@@ -62,26 +33,47 @@ const NETWORK_PROFILES = {
62
33
  Offline: { latencyMs: 0, downloadKbps: 0, uploadKbps: 0 },
63
34
  };
64
35
  /**
65
- * Service to manage network conditioning (Throttling/Latency)
36
+ * A session's network profile (`xe:network_profile`).
37
+ *
38
+ * Every profile's delay is added to each of the session's commands by the
39
+ * session gateway (getLatency). On an Android phone `Offline` also turns
40
+ * mobile data and Wi-Fi off, and `Normal` turns them on; 4G, 3G and Edge
41
+ * change nothing on the phone. An iPhone, real or simulated, gets the delay
42
+ * only: Xcode has no command to change a simulator's network (`xcrun simctl`
43
+ * has no `network` subcommand in Xcode 26), and a real iPhone's can't be
44
+ * changed from a Mac.
45
+ *
46
+ * What `Offline` changed is undone when the session ends, however it ends
47
+ * (PhoneNetworkRestore), and written down meanwhile (PhoneNetworkLedger) so a
48
+ * restart can undo it after a crash.
66
49
  */
67
50
  let NetworkConditioningService = class NetworkConditioningService {
68
51
  constructor() {
69
52
  this.log = logger_1.default.scope('NetworkConditioning');
70
53
  this.activeConditions = new Map();
54
+ this.radios = new AndroidRadios_1.AndroidRadios(adbForPhone_1.adbForPhone);
55
+ this.toldAboutIPhones = false;
71
56
  }
72
57
  /**
73
58
  * Applies a network profile to a session/device
74
59
  */
75
60
  applyProfile(sessionId, device, profile) {
76
61
  return __awaiter(this, void 0, void 0, function* () {
62
+ var _a;
77
63
  this.log.info(`Applying network profile '${profile}' to session ${sessionId} on device ${device.udid}`);
78
- this.activeConditions.set(sessionId, profile);
79
- // Platform specific optimizations
80
- if (device.platform === 'android') {
81
- yield this.applyAndroidConditioning(device.udid, profile);
64
+ const condition = { profile, device };
65
+ this.activeConditions.set(sessionId, condition);
66
+ if (((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android') {
67
+ if (profile === 'Offline')
68
+ yield this.takeOffline(sessionId, condition);
69
+ else if (profile === 'Normal')
70
+ yield this.turnOnline(device.udid);
71
+ // 4G, 3G and Edge: the delay only.
82
72
  }
83
- else if (device.deviceType === 'simulator') {
84
- yield this.applyIOSSimulatorConditioning(device.udid, profile);
73
+ else if (!this.toldAboutIPhones) {
74
+ this.toldAboutIPhones = true;
75
+ this.log.info('Network profiles on iPhones and iOS simulators only delay each command: the ' +
76
+ "device's own network isn't changed, so Offline leaves it online.");
85
77
  }
86
78
  });
87
79
  }
@@ -89,70 +81,75 @@ let NetworkConditioningService = class NetworkConditioningService {
89
81
  * Gets the active profile for a session
90
82
  */
91
83
  getProfile(sessionId) {
92
- return this.activeConditions.get(sessionId);
84
+ var _a;
85
+ return (_a = this.activeConditions.get(sessionId)) === null || _a === void 0 ? void 0 : _a.profile;
86
+ }
87
+ /** Whether the session has a profile on this server. */
88
+ has(sessionId) {
89
+ return this.activeConditions.has(sessionId);
90
+ }
91
+ /** The sessions with a profile on this server. */
92
+ sessionIds() {
93
+ return [...this.activeConditions.keys()];
93
94
  }
94
95
  /**
95
- * Resets network conditions for a session
96
+ * Ends the session's profile and undoes what it changed on the phone, once:
97
+ * the entry is taken before anything is awaited, so a second call finds
98
+ * nothing to do. A phone that can't be reached keeps its record in the
99
+ * ledger, and is put back at its next session or this server's next start.
96
100
  */
97
- reset(sessionId, device) {
101
+ reset(sessionId) {
98
102
  return __awaiter(this, void 0, void 0, function* () {
99
- if (this.activeConditions.has(sessionId)) {
100
- this.log.info(`Resetting network conditions for session ${sessionId}`);
101
- this.activeConditions.delete(sessionId);
102
- if (device.platform === 'android') {
103
- yield this.applyAndroidConditioning(device.udid, 'Normal');
103
+ const condition = this.activeConditions.get(sessionId);
104
+ if (!condition)
105
+ return;
106
+ this.activeConditions.delete(sessionId);
107
+ this.log.info(`Resetting network conditions for session ${sessionId}`);
108
+ const prior = condition.offlinePrior;
109
+ if (!prior)
110
+ return;
111
+ const udid = condition.device.udid;
112
+ yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
113
+ try {
114
+ yield this.radios.turnOn(udid, AndroidRadios_1.AndroidRadios.toRestore(prior));
115
+ yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).settle(sessionId, 'offline');
116
+ this.log.info(`📱 Android: network put back on ${udid} after session ${sessionId}`);
104
117
  }
105
- else if (device.deviceType === 'simulator') {
106
- yield this.applyIOSSimulatorConditioning(device.udid, 'Normal');
118
+ catch (e) {
119
+ this.log.warn(`⚠️ Could not put the network back on ${udid} after session ${sessionId}: ${e === null || e === void 0 ? void 0 : e.message}. ` +
120
+ "Xenon tries again at the phone's next session and when it restarts.");
107
121
  }
108
- }
122
+ }));
109
123
  });
110
124
  }
111
- applyAndroidConditioning(udid, profile) {
125
+ takeOffline(sessionId, condition) {
112
126
  return __awaiter(this, void 0, void 0, function* () {
127
+ const udid = condition.device.udid;
113
128
  try {
114
- const { exec } = yield Promise.resolve().then(() => __importStar(require('child_process')));
115
- const { promisify } = yield Promise.resolve().then(() => __importStar(require('util')));
116
- const execAsync = promisify(exec);
117
- if (profile === 'Offline') {
118
- this.log.info(`📱 Android: Disabling data for ${udid}`);
119
- yield execAsync(`adb -s ${udid} shell svc data disable`);
120
- yield execAsync(`adb -s ${udid} shell svc wifi disable`);
121
- }
122
- else if (profile === 'Normal') {
123
- this.log.info(`📱 Android: Enabling data for ${udid}`);
124
- yield execAsync(`adb -s ${udid} shell svc data enable`);
125
- yield execAsync(`adb -s ${udid} shell svc wifi enable`);
126
- }
127
- // Latency is handled by the proxy
129
+ yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
130
+ const prior = yield this.radios.read(udid);
131
+ // Written down before the change: a crash after it must still find it.
132
+ yield typedi_1.Container.get(PhoneNetworkLedger_1.PhoneNetworkLedger).note(sessionId, udid, { offline: prior });
133
+ condition.offlinePrior = prior;
134
+ this.log.info(`📱 Android: turning mobile data and Wi-Fi off on ${udid}`);
135
+ yield this.radios.turnOff(udid);
136
+ }));
128
137
  }
129
138
  catch (e) {
130
- this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e.message}`);
139
+ this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e === null || e === void 0 ? void 0 : e.message}`);
131
140
  }
132
141
  });
133
142
  }
134
- applyIOSSimulatorConditioning(udid, profile) {
143
+ turnOnline(udid) {
135
144
  return __awaiter(this, void 0, void 0, function* () {
136
145
  try {
137
- const { exec } = yield Promise.resolve().then(() => __importStar(require('child_process')));
138
- const { promisify } = yield Promise.resolve().then(() => __importStar(require('util')));
139
- const execAsync = promisify(exec);
140
- // simctl network <device> <status>
141
- // Profiles: wifi, 3g, edge, etc. (depends on OS version)
142
- let status = 'none';
143
- if (profile === '3G')
144
- status = '3g';
145
- if (profile === 'Edge')
146
- status = 'edge';
147
- if (profile === 'Offline')
148
- status = 'off';
149
- if (profile === 'Normal')
150
- status = 'none';
151
- this.log.info(`🍎 iOS Sim: Setting network to ${status} for ${udid}`);
152
- yield execAsync(`xcrun simctl network ${udid} status ${status}`);
146
+ yield (0, phoneNetworkLock_1.withPhoneNetworkLock)(udid, () => __awaiter(this, void 0, void 0, function* () {
147
+ this.log.info(`📱 Android: turning mobile data and Wi-Fi on on ${udid}`);
148
+ yield this.radios.turnOn(udid, { data: true, wifi: true });
149
+ }));
153
150
  }
154
151
  catch (e) {
155
- this.log.warn(`⚠️ Failed to apply iOS Simulator conditioning for ${udid}: ${e.message}`);
152
+ this.log.warn(`⚠️ Failed to apply Android conditioning for ${udid}: ${e === null || e === void 0 ? void 0 : e.message}`);
156
153
  }
157
154
  });
158
155
  }
@@ -161,10 +158,10 @@ let NetworkConditioningService = class NetworkConditioningService {
161
158
  */
162
159
  getLatency(sessionId) {
163
160
  var _a;
164
- const profile = this.activeConditions.get(sessionId);
165
- if (!profile)
161
+ const condition = this.activeConditions.get(sessionId);
162
+ if (!condition)
166
163
  return 0;
167
- return ((_a = NETWORK_PROFILES[profile]) === null || _a === void 0 ? void 0 : _a.latencyMs) || 0;
164
+ return ((_a = NETWORK_PROFILES[condition.profile]) === null || _a === void 0 ? void 0 : _a.latencyMs) || 0;
168
165
  }
169
166
  };
170
167
  exports.NetworkConditioningService = NetworkConditioningService;
@@ -26,9 +26,14 @@ const axios_1 = __importDefault(require("axios"));
26
26
  const logger_1 = __importDefault(require("../logger"));
27
27
  const prisma_service_1 = require("../data-service/prisma-service");
28
28
  const typedi_1 = require("typedi");
29
+ const webhookEvents_1 = require("./webhookEvents");
30
+ /** How many sessions' failure notices are remembered, so a session ending twice sends once. */
31
+ const NOTIFIED_SESSIONS_KEPT = 2000;
29
32
  let NotificationService = class NotificationService {
30
33
  constructor(prisma) {
31
34
  this.prisma = prisma;
35
+ /** Sessions whose failure was sent, oldest first (a Set keeps insertion order). */
36
+ this.notifiedSessions = new Set();
32
37
  }
33
38
  getConfigs() {
34
39
  return __awaiter(this, void 0, void 0, function* () {
@@ -72,31 +77,41 @@ let NotificationService = class NotificationService {
72
77
  });
73
78
  }
74
79
  /**
75
- * Sends a sample event to a webhook the way a real one would be sent, and
76
- * fails if the delivery does: "Send test" used to say it worked whatever
77
- * happened, and ignored the webhook's type.
80
+ * Tells the webhooks subscribed to `session_failed` that a session failed.
81
+ *
82
+ * Once per session however many times it ends: a driver crash and the
83
+ * client's own delete both end it, as do a heartbeat timeout and a delete.
84
+ * Remembered by id in memory, so a restart forgets it, which is harmless:
85
+ * a session does not end again after one.
86
+ */
87
+ notifySessionFailed(session) {
88
+ return __awaiter(this, void 0, void 0, function* () {
89
+ if (this.notifiedSessions.has(session.id))
90
+ return;
91
+ this.notifiedSessions.add(session.id);
92
+ if (this.notifiedSessions.size > NOTIFIED_SESSIONS_KEPT) {
93
+ const oldest = this.notifiedSessions.values().next().value;
94
+ this.notifiedSessions.delete(oldest);
95
+ }
96
+ yield this.dispatchEvent('session_failed', (0, webhookEvents_1.sessionFailedPayload)(session));
97
+ });
98
+ }
99
+ /**
100
+ * Sends a sample of `event` to a webhook the way a real one would be sent,
101
+ * and fails if the delivery does: "Send test" used to say it worked
102
+ * whatever happened, and ignored the webhook's type and template.
78
103
  */
79
104
  sendTest(url_1) {
80
- return __awaiter(this, arguments, void 0, function* (url, type = 'slack', payloadTemplate) {
81
- yield this.sendToWebhook({ url, type, payloadTemplate: payloadTemplate !== null && payloadTemplate !== void 0 ? payloadTemplate : null }, 'device_new', { udid: 'test-device-udid', name: 'Test Device', host: '127.0.0.1' });
105
+ return __awaiter(this, arguments, void 0, function* (url, type = 'slack', payloadTemplate, event = 'device_new') {
106
+ yield this.sendToWebhook({ url, type, payloadTemplate: payloadTemplate !== null && payloadTemplate !== void 0 ? payloadTemplate : null }, event, webhookEvents_1.WEBHOOK_EVENTS[event].sample());
82
107
  });
83
108
  }
84
109
  /** Delivers one event. Throws when the webhook refuses or can't be reached. */
85
110
  sendToWebhook(config, eventType, payload) {
86
111
  return __awaiter(this, void 0, void 0, function* () {
87
- // Principal Logic: Use custom template if defined
112
+ // A custom template, if there is one, replaces the built-in message.
88
113
  if (config.payloadTemplate) {
89
- const substitutedBody = this.substituteTemplate(config.payloadTemplate, Object.assign({ eventType }, payload));
90
- // JSON if the template renders to JSON, otherwise sent as text. Only
91
- // the parse may fall back: one catch around both used to post again,
92
- // as text, when the JSON delivery itself failed.
93
- let body;
94
- try {
95
- body = JSON.parse(substitutedBody);
96
- }
97
- catch (_a) {
98
- body = { text: substitutedBody };
99
- }
114
+ const body = (0, webhookEvents_1.renderTemplate)(config.payloadTemplate, Object.assign({ eventType }, payload));
100
115
  yield axios_1.default.post(config.url, body);
101
116
  return;
102
117
  }
@@ -109,17 +124,6 @@ let NotificationService = class NotificationService {
109
124
  }
110
125
  });
111
126
  }
112
- // Principal Logic: Recursive variable substitution with {{key}} support
113
- substituteTemplate(template, data) {
114
- return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
115
- const keys = key.trim().split('.');
116
- let value = data;
117
- for (const k of keys) {
118
- value = value ? value[k] : undefined;
119
- }
120
- return value !== undefined ? String(value) : match;
121
- });
122
- }
123
127
  sendSlackMessage(url, eventType, payload) {
124
128
  return __awaiter(this, void 0, void 0, function* () {
125
129
  var _a, _b, _c;
@@ -131,7 +135,10 @@ let NotificationService = class NotificationService {
131
135
  color = '#ff0000';
132
136
  break;
133
137
  case 'session_failed':
134
- text = `❌ *Session Failed*: ${payload.sessionId}\nReason: ${payload.failureReason}`;
138
+ text =
139
+ `❌ *Session Failed*: ${payload.sessionId}\n` +
140
+ `Reason: ${payload.failureReason || 'no reason recorded'}\n` +
141
+ `Device: ${payload.deviceName || payload.udid} (${payload.udid})`;
135
142
  color = '#ff0000';
136
143
  break;
137
144
  case 'device_new':
@@ -25,6 +25,8 @@ const logger_1 = __importDefault(require("../logger"));
25
25
  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
+ const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
29
+ const sessionMemory_1 = require("../sessions/sessionMemory");
28
30
  let OrphanSweeper = class OrphanSweeper {
29
31
  constructor() {
30
32
  this.log = logger_1.default.scope('OrphanSweeper');
@@ -69,6 +71,11 @@ let OrphanSweeper = class OrphanSweeper {
69
71
  endTime: new Date(),
70
72
  },
71
73
  });
74
+ // A session this process still drove has its phone's network put
75
+ // back before the phone is released; for a previous process's it is
76
+ // the ledger's, at boot (PhoneNetworkRestore).
77
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
78
+ (0, sessionMemory_1.forgetSessionMemory)(s.id);
72
79
  // This session's claim only, not every row with its udid: the phone
73
80
  // may be another session's by now (deviceClaims.ts).
74
81
  yield (0, device_service_1.releaseSessionDevices)(s.id);
@@ -55,6 +55,11 @@ let PortAllocator = class PortAllocator {
55
55
  configure(overrides) {
56
56
  this.ranges = Object.assign(Object.assign({}, DEFAULT_RANGES), overrides);
57
57
  }
58
+ /** The ports leased for `purpose`, first and last included. */
59
+ rangeOf(purpose) {
60
+ const [start, end] = this.ranges[purpose];
61
+ return [start, end];
62
+ }
58
63
  acquire(purpose_1, udid_1) {
59
64
  return __awaiter(this, arguments, void 0, function* (purpose, udid, opts = {}) {
60
65
  var _a;
@@ -32,8 +32,11 @@ const selectorKeys_1 = require("./selector-health/selectorKeys");
32
32
  const log = logger_1.default.scope('SelectorVerification');
33
33
  /**
34
34
  * Threshold of distinct clean build_ids required to promote a Pending
35
- * SelectorState row to Resolved. "Clean" means at least one findElement
36
- * for the (strategy, selector) tuple ran in the build with no heal.
35
+ * SelectorState row to Resolved. "Clean" means the selector was found without
36
+ * healing in the build: a findElement of the (strategy, selector) tuple that
37
+ * answered an element, or a findElements that answered at least one, and no
38
+ * heal of it. Through 2.14 a find that failed outright counted too, so a
39
+ * selector that was never found again was verified as fixed.
37
40
  */
38
41
  const CLEAN_BUILDS_TO_RESOLVE = 3;
39
42
  /**
@@ -127,12 +130,20 @@ let SelectorVerificationJob = class SelectorVerificationJob {
127
130
  // that ran under a CI build (build_id IS NOT NULL); ad-hoc local runs
128
131
  // intentionally don't move verification forward.
129
132
  //
133
+ // A find that found the selector is one that didn't fail (`is_error`,
134
+ // the dashboard's record of an error answer) and didn't answer an empty
135
+ // list (`{"value":[]`, findElements finding nothing). The command log
136
+ // keeps the answer as JSON.stringify wrote it, or as the node sent it.
137
+ //
130
138
  // Identifier names (PascalCase tables, camelCase createdAt) match the
131
139
  // actual SQLite schema generated by Prisma — see
132
140
  // `sqlite3 prisma/dev.db ".schema SessionLog"`.
133
141
  const heals = yield this.prisma.$queryRaw `
134
142
  SELECT s."build_id" AS build_id,
135
- MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed
143
+ MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed,
144
+ MAX(CASE WHEN NOT sl."is_healed" AND NOT sl."is_error"
145
+ AND sl."response" NOT LIKE '{"value":[]%'
146
+ THEN 1 ELSE 0 END) AS found
136
147
  FROM "SessionLog" sl
137
148
  INNER JOIN "Session" s ON sl."session_id" = s."id"
138
149
  WHERE sl."original_strategy" = ${row.original_strategy}
@@ -142,7 +153,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
142
153
  AND s."build_id" IS NOT NULL
143
154
  GROUP BY s."build_id"
144
155
  `;
145
- const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0).length;
156
+ const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0 && Number(r.found) === 1).length;
146
157
  if (cleanBuildCount >= CLEAN_BUILDS_TO_RESOLVE) {
147
158
  yield this.promoteToResolved(row);
148
159
  }
@@ -78,9 +78,13 @@ const package_json_1 = __importDefault(require("../../package.json"));
78
78
  const IPluginArgs_1 = require("../interfaces/IPluginArgs");
79
79
  const config_service_1 = require("../data-service/config-service");
80
80
  const PluginContext_1 = require("../PluginContext");
81
+ const SelfHealingSwitch_1 = require("./settings/SelfHealingSwitch");
82
+ const aiEngineSettings_1 = require("./settings/aiEngineSettings");
81
83
  const device_store_1 = require("../data-service/device-store");
82
84
  const device_utils_1 = require("../device-utils");
83
85
  const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
86
+ const deviceSettings_1 = require("../data-service/deviceSettings");
87
+ const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
84
88
  const app_1 = require("../app");
85
89
  const registerCommandAuth_1 = require("../app/registerCommandAuth");
86
90
  const defaultGateway_1 = require("../gateway/defaultGateway");
@@ -129,10 +133,17 @@ let ServerManager = ServerManager_1 = class ServerManager {
129
133
  this.logger.warn('Authentication is DISABLED — every /xenon/api caller is treated as a ' +
130
134
  'synthetic SUPER_ADMIN. Local development only.');
131
135
  }
136
+ this.applyRecordingOptions(pluginArgs);
132
137
  yield this.syncDatabaseAndAIConfig(pluginArgs);
133
138
  yield this.initializeCoreSubsystems(pluginArgs, cliArgs.port);
134
139
  // Before anything below starts go-ios: the reap would kill it too.
135
140
  yield this.reapLeftoverGoIos();
141
+ // The Settings page's AI self-healing toggle, which the command interceptor
142
+ // reads from memory at every command: load it before a command can arrive.
143
+ yield typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).load();
144
+ // The AI engine page's provider, model and base URL, over the startup
145
+ // options applied above (what a cleared value goes back to).
146
+ yield typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).load();
136
147
  this.registerRoutes(expressApp, httpServer, cliArgs, pluginArgs);
137
148
  yield this.bootEmulators(pluginArgs);
138
149
  this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
@@ -258,6 +269,9 @@ let ServerManager = ServerManager_1 = class ServerManager {
258
269
  yield (0, device_utils_1.updateDeviceList)(localHosts, pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
259
270
  // remove stale devices
260
271
  yield (0, device_utils_1.removeStaleDevices)(localHosts, pluginArgs.tlsRejectUnauthorized);
272
+ // In the background: put back the network a previous run left changed on
273
+ // this server's phones (an Offline profile, an interceptor proxy).
274
+ void this.cleanUpPhoneNetworks(localHosts, nodeId);
261
275
  this.logger.info(`🚀 Xenon will be served at http://${pluginArgs.bindHostOrIp}:${cliArgs.port}/xenon with id ${nodeId}`);
262
276
  this.logger.info('You can provide the following URLs to access Xenon:');
263
277
  for (const url of (0, networkAddresses_1.listReachableBaseUrls)(cliArgs.port)) {
@@ -266,6 +280,29 @@ let ServerManager = ServerManager_1 = class ServerManager {
266
280
  }
267
281
  });
268
282
  }
283
+ /**
284
+ * What a previous run of this server left on its phones' network: the
285
+ * changes its ledger holds, and on this server's own Android phones a proxy
286
+ * that points at a capture port of this machine where nothing answers.
287
+ * Each change is logged. A phone not connected now is put back at its next
288
+ * session here, or at the next start.
289
+ */
290
+ cleanUpPhoneNetworks(localHosts, nodeId) {
291
+ return __awaiter(this, void 0, void 0, function* () {
292
+ var _a;
293
+ try {
294
+ const devices = yield device_store_1.DeviceStoreFactory.getStore().getAllDevices();
295
+ const androids = devices
296
+ .filter((d) => { var _a; return ((_a = d.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android' && !d.cloud; })
297
+ .filter((d) => (0, localDeviceHosts_1.isOwnDevice)(localHosts, nodeId, d))
298
+ .map((d) => d.udid);
299
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).cleanUpAtBoot([...new Set(androids)]);
300
+ }
301
+ catch (err) {
302
+ this.logger.warn(`Could not check the phones' network settings: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
303
+ }
304
+ });
305
+ }
269
306
  resolvePluginArgs(cliArgs) {
270
307
  return __awaiter(this, void 0, void 0, function* () {
271
308
  const pluginConfigs = cliArgs.plugin;
@@ -290,6 +327,24 @@ let ServerManager = ServerManager_1 = class ServerManager {
290
327
  return pluginArgs;
291
328
  });
292
329
  }
330
+ /**
331
+ * `maxConcurrentRecordings` and `recordingsAssetsPath` are schema options,
332
+ * but only their environment variables ever reached `config`. Apply the
333
+ * options now (option, else the variable, else the default) before the
334
+ * artifact store is built from the path and the recordings router first
335
+ * reads the cap. See `recordingConfigFrom`.
336
+ */
337
+ applyRecordingOptions(pluginArgs) {
338
+ const envCap = process.env.XENON_MAX_CONCURRENT_RECORDINGS;
339
+ const applied = (0, config_1.recordingConfigFrom)(pluginArgs);
340
+ if (pluginArgs.maxConcurrentRecordings === undefined &&
341
+ (envCap === null || envCap === void 0 ? void 0 : envCap.trim()) &&
342
+ Number(envCap) !== applied.maxConcurrentRecordings) {
343
+ this.logger.warn(`Ignoring XENON_MAX_CONCURRENT_RECORDINGS=${JSON.stringify(envCap)}: ` +
344
+ `it must be a whole number of at least 1. The cap is ${applied.maxConcurrentRecordings}.`);
345
+ }
346
+ (0, config_1.updateConfig)(applied);
347
+ }
293
348
  syncDatabaseAndAIConfig(pluginArgs) {
294
349
  return __awaiter(this, void 0, void 0, function* () {
295
350
  const { updateConfig } = yield Promise.resolve().then(() => __importStar(require('../config')));
@@ -334,14 +389,19 @@ let ServerManager = ServerManager_1 = class ServerManager {
334
389
  }
335
390
  initializeCoreSubsystems(pluginArgs, port) {
336
391
  return __awaiter(this, void 0, void 0, function* () {
392
+ // Before anything opens the database: the URL must suit the database
393
+ // client this install was built with (SQLite for the published plugin).
394
+ const { assertSupportedDatabase } = yield Promise.resolve().then(() => __importStar(require('../scripts/database-check')));
395
+ assertSupportedDatabase();
337
396
  yield (0, device_utils_1.initializeStorage)();
338
397
  const { runMigrations } = yield Promise.resolve().then(() => __importStar(require('../scripts/run-migrations')));
339
398
  yield runMigrations();
340
399
  // A hub keeps its nodes' phones: the sessions on them outlive its restart.
341
- yield device_store_1.DeviceStoreFactory.getStore().clearStorage((0, localDeviceHosts_1.devicesClearedAtBoot)(pluginArgs, (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, port)));
400
+ yield (0, deviceSettings_1.resetDevicesAtBoot)(device_store_1.DeviceStoreFactory.getStore(), pluginArgs, (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, port));
342
401
  const { bootstrapIdentity } = yield Promise.resolve().then(() => __importStar(require('./identity/bootstrap')));
343
402
  yield bootstrapIdentity();
344
403
  typedi_1.Container.get(EmailService_1.EmailService).warnIfLogFallbackEnabled();
404
+ typedi_1.Container.get(EmailService_1.EmailService).warnAboutPublicUrl();
345
405
  const { startUserSessionCleanupCron } = yield Promise.resolve().then(() => __importStar(require('./identity/sessionCleanupCron')));
346
406
  startUserSessionCleanupCron();
347
407
  // Hub-issued JWT signing key (REST/MCP tokens, stream tickets) — key
@@ -390,7 +450,12 @@ let ServerManager = ServerManager_1 = class ServerManager {
390
450
  });
391
451
  }
392
452
  registerRoutes(expressApp, httpServer, cliArgs, pluginArgs) {
393
- expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs));
453
+ expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs, {
454
+ address: cliArgs.address,
455
+ port: cliArgs.port,
456
+ // Appium serves HTTPS with both of these (base-driver's server()).
457
+ tls: !!(cliArgs.sslCertificatePath && cliArgs.sslKeyPath),
458
+ }));
394
459
  // The session gateway goes in front of Appium's routes: Xenon's own
395
460
  // /wd-internal calls (with the per-process secret), per-command auth
396
461
  // (XENON_REQUIRE_COMMAND_AUTH), then, on a hub, forwarding of the sessions
@@ -417,16 +482,47 @@ let ServerManager = ServerManager_1 = class ServerManager {
417
482
  `${router.describeAppiumPath()}.`);
418
483
  }
419
484
  }
485
+ /**
486
+ * Boot the emulators `emulators` lists, with each one's launch options.
487
+ *
488
+ * An iOS-only server, or one told to use real Android devices only
489
+ * (`androidDeviceType: real`, which discovery would then ignore them for),
490
+ * boots none. Anything else boots them, `platform: both` included: it used
491
+ * to boot only when `platform` named Android, so with the default it did
492
+ * nothing. A boot that fails is logged and the rest go on: an emulator that
493
+ * won't start must not keep the server from coming up with the phones it
494
+ * has, and with `both` counted this step now runs on many more servers.
495
+ */
420
496
  bootEmulators(pluginArgs) {
421
497
  return __awaiter(this, void 0, void 0, function* () {
422
- if (pluginArgs.emulators &&
423
- pluginArgs.emulators.length > 0 &&
424
- pluginArgs.platform.toLowerCase().includes('android')) {
425
- this.logger.info('Emulators will be booted!!');
426
- const adb = yield appium_adb_1.ADB.createADB({});
427
- const array = pluginArgs.emulators || [];
428
- yield Promise.all(array.map((arr) => adb.launchAVD(arr.avdName, arr)));
498
+ const emulators = pluginArgs.emulators || [];
499
+ if (emulators.length === 0)
500
+ return;
501
+ const platform = String(pluginArgs.platform).toLowerCase();
502
+ if (platform === 'ios') {
503
+ this.logger.warn(`Not booting the ${emulators.length} configured emulator(s): platform is ios.`);
504
+ return;
505
+ }
506
+ if (pluginArgs.androidDeviceType === 'real') {
507
+ this.logger.warn(`Not booting the ${emulators.length} configured emulator(s): androidDeviceType is real.`);
508
+ return;
429
509
  }
510
+ this.logger.info(`Booting ${emulators.length} configured emulator(s)...`);
511
+ let adb;
512
+ try {
513
+ adb = yield appium_adb_1.ADB.createADB({});
514
+ }
515
+ catch (err) {
516
+ this.logger.warn(`Not booting the configured emulators: no usable adb (${err === null || err === void 0 ? void 0 : err.message}).`);
517
+ return;
518
+ }
519
+ const results = yield Promise.allSettled(emulators.map((emulator) => adb.launchAVD(emulator.avdName, emulator)));
520
+ results.forEach((result, index) => {
521
+ var _a, _b;
522
+ if (result.status === 'rejected') {
523
+ this.logger.warn(`Could not boot emulator ${emulators[index].avdName}: ${(_b = (_a = result.reason) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : result.reason}`);
524
+ }
525
+ });
430
526
  });
431
527
  }
432
528
  registerDependenciesInContainer(pluginArgs, cliArgs, nodeId) {