@xenon-device-management/xenon 2.14.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -49,12 +49,19 @@ let AICommandService = class AICommandService {
49
49
  return __awaiter(this, void 0, void 0, function* () {
50
50
  this.logger.info(`Testing AI locator: ${locator.strategy}=${locator.selector}`);
51
51
  const omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
52
+ // A session's own route: its test may act on what it found.
53
+ const keep = (found) => {
54
+ if (driver === null || driver === void 0 ? void 0 : driver.sessionId)
55
+ for (const el of found)
56
+ omniService.remember(driver.sessionId, el);
57
+ return found;
58
+ };
52
59
  if (locator.strategy === '-custom:ai-text') {
53
- return yield omniService.findByText(driver, locator.selector);
60
+ return keep(yield omniService.findByText(driver, locator.selector));
54
61
  }
55
62
  else if (locator.strategy === '-custom:ai-icon') {
56
63
  const result = yield omniService.findByIcon(driver, locator.selector);
57
- return result ? [result] : [];
64
+ return keep(result ? [result] : []);
58
65
  }
59
66
  throw new Error(`Unsupported AI strategy: ${locator.strategy}`);
60
67
  });
@@ -45,7 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.AI_SERVICE = exports.AIService = void 0;
48
+ exports.AI_SERVICE = exports.AIService = exports.FAILURE_ANALYSIS_TIMEOUT_MS = exports.AI_CALL_TIMEOUT_MS = exports.AITimeoutError = exports.AIProviderPausedError = exports.AIRateLimitedError = void 0;
49
49
  exports.parseVisualVerdict = parseVisualVerdict;
50
50
  const generative_ai_1 = require("@google/generative-ai");
51
51
  const openai_1 = __importDefault(require("openai"));
@@ -56,8 +56,88 @@ const fs = __importStar(require("fs"));
56
56
  const path = __importStar(require("path"));
57
57
  const config_1 = require("../config");
58
58
  const CircuitBreaker_1 = require("./CircuitBreaker");
59
- /** What GeminiProvider answers, instead of throwing, when the provider is rate-limited (429). */
60
- const RATE_LIMITED = 'CONNECTION_OK_RATE_LIMITED';
59
+ /**
60
+ * A provider refused a call for its rate limit or quota (HTTP 429), whichever
61
+ * provider it is. A failed call like any other: its caller gets no answer and
62
+ * the circuit breaker counts it (`status`). Through 2.14 Gemini's provider
63
+ * answered a 429 with the text 'CONNECTION_OK_RATE_LIMITED', which was saved
64
+ * as a failed session's AI analysis and counted by the breaker as a success.
65
+ */
66
+ class AIRateLimitedError extends Error {
67
+ constructor(detail) {
68
+ super('The AI provider is rate-limited or out of quota');
69
+ this.name = 'AIRateLimitedError';
70
+ this.status = 429;
71
+ this.detail = detail;
72
+ }
73
+ }
74
+ exports.AIRateLimitedError = AIRateLimitedError;
75
+ /** A provider's 429, as its SDK (`status`) or axios (`response.status`) reports it. */
76
+ function isRateLimit(err) {
77
+ var _a;
78
+ return err instanceof AIRateLimitedError || (err === null || err === void 0 ? void 0 : err.status) === 429 || ((_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.status) === 429;
79
+ }
80
+ /**
81
+ * The circuit breaker didn't send the call: the provider's recent calls
82
+ * failed. Still a CircuitOpenError, in words for testers: the Omni-Scan, Test
83
+ * locator and visual assertion answers show it, and "Circuit open for
84
+ * 'ai:gemini:<model>'; retry in 43120ms" isn't.
85
+ */
86
+ class AIProviderPausedError extends CircuitBreaker_1.CircuitOpenError {
87
+ constructor(from) {
88
+ super(from.key, from.retryAfterMs);
89
+ this.seconds = Math.max(1, Math.ceil(from.retryAfterMs / 1000));
90
+ this.message = `The AI provider has been failing, so Xenon will call it again in ${this.seconds} s`;
91
+ }
92
+ }
93
+ exports.AIProviderPausedError = AIProviderPausedError;
94
+ /** A call that outlasted its time limit; its request was cancelled. The circuit breaker counts it (`code`). */
95
+ class AITimeoutError extends Error {
96
+ constructor(ms) {
97
+ super(`The AI provider didn't answer within ${Math.round(ms / 1000)} s`);
98
+ this.name = 'AITimeoutError';
99
+ this.code = 'ETIMEDOUT';
100
+ }
101
+ }
102
+ exports.AITimeoutError = AITimeoutError;
103
+ /**
104
+ * How long an AI call may take, retries included, unless it says otherwise
105
+ * (failure analysis). Most are made while a test command runs: the LLM and
106
+ * visual healing tiers inside a failing findElement (both: 2 of these), the
107
+ * visual assertion and the screen description inside an execute script, an
108
+ * ai-icon find. The OpenAI and Anthropic SDKs otherwise wait up to 10 minutes
109
+ * a try, three tries, and Gemini has no limit, which held the command past
110
+ * the client's own timeout. Ollama's generate call already had 30 s.
111
+ */
112
+ exports.AI_CALL_TIMEOUT_MS = 30000;
113
+ /**
114
+ * How long a failed session's AI analysis may take, retries included. The
115
+ * OpenAI and Anthropic SDKs otherwise wait up to 10 minutes a try, three
116
+ * tries. The analysis runs after the session has ended (onSessionStopped
117
+ * doesn't wait for it), so this bounds the work left running, not a client's
118
+ * wait: an answer with a screenshot usually takes seconds.
119
+ */
120
+ exports.FAILURE_ANALYSIS_TIMEOUT_MS = 120000;
121
+ /** `call`, given a signal that cancels it after `ms`, when it fails with AITimeoutError. */
122
+ function withTimeLimit(ms, call) {
123
+ return __awaiter(this, void 0, void 0, function* () {
124
+ const controller = new AbortController();
125
+ let timer;
126
+ const expired = new Promise((_, reject) => {
127
+ timer = setTimeout(() => {
128
+ // The time-out first, so it, not the cancelled request's error, is the answer.
129
+ reject(new AITimeoutError(ms));
130
+ controller.abort();
131
+ }, ms);
132
+ });
133
+ try {
134
+ return yield Promise.race([call(controller.signal), expired]);
135
+ }
136
+ finally {
137
+ clearTimeout(timer);
138
+ }
139
+ });
140
+ }
61
141
  /**
62
142
  * A provider's answer to the assertion prompt, `{ "result": true|false,
63
143
  * "reason": "..." }`, read leniently (code fences, text around the JSON,
@@ -97,7 +177,7 @@ class GeminiProvider {
97
177
  this.modelName =
98
178
  modelName && modelName.trim() !== '' ? modelName.trim() : 'gemini-3-flash-preview';
99
179
  }
100
- analyze(prompt, screenshotBase64) {
180
+ analyze(prompt, screenshotBase64, signal) {
101
181
  return __awaiter(this, void 0, void 0, function* () {
102
182
  const parts = [prompt];
103
183
  if (screenshotBase64) {
@@ -116,17 +196,17 @@ class GeminiProvider {
116
196
  for (const version of versions) {
117
197
  try {
118
198
  const model = this.genAI.getGenerativeModel({ model: this.modelName }, { apiVersion: version });
119
- const result = yield model.generateContent(parts);
199
+ const result = yield model.generateContent(parts, signal ? { signal } : undefined);
120
200
  const response = yield result.response;
121
201
  return response.text();
122
202
  }
123
203
  catch (err) {
124
204
  lastError = err;
125
- // If we get a 429, the connection IS WORKING, just rate limited.
126
- if (err.message.includes('429') || err.message.includes('Quota exceeded')) {
127
- logger_1.default.info(`[Gemini] Connection verified (Rate Limited) via ${version} endpoint.`);
128
- return 'CONNECTION_OK_RATE_LIMITED';
129
- }
205
+ // The other endpoint has the same quota. By status only: the message
206
+ // holds the URL and the provider's text, where "429" can be a token
207
+ // count or part of the model's name.
208
+ if (err.status === 429)
209
+ throw new AIRateLimitedError(err.message);
130
210
  if (err.message.includes('404') ||
131
211
  err.message.includes('not found') ||
132
212
  err.message.includes('unsupported')) {
@@ -145,7 +225,7 @@ class OpenAIProvider {
145
225
  this.client = new openai_1.default({ apiKey, baseURL });
146
226
  this.model = model;
147
227
  }
148
- analyze(prompt, screenshotBase64) {
228
+ analyze(prompt, screenshotBase64, signal) {
149
229
  return __awaiter(this, void 0, void 0, function* () {
150
230
  const messages = [
151
231
  {
@@ -163,7 +243,7 @@ class OpenAIProvider {
163
243
  model: this.model,
164
244
  messages,
165
245
  max_tokens: 500,
166
- });
246
+ }, { signal });
167
247
  return response.choices[0].message.content || '';
168
248
  });
169
249
  }
@@ -173,7 +253,7 @@ class AnthropicProvider {
173
253
  this.client = new sdk_1.default({ apiKey });
174
254
  this.model = model;
175
255
  }
176
- analyze(prompt, screenshotBase64) {
256
+ analyze(prompt, screenshotBase64, signal) {
177
257
  return __awaiter(this, void 0, void 0, function* () {
178
258
  const content = [{ type: 'text', text: prompt }];
179
259
  if (screenshotBase64) {
@@ -190,7 +270,7 @@ class AnthropicProvider {
190
270
  model: this.model,
191
271
  max_tokens: 500,
192
272
  messages: [{ role: 'user', content }],
193
- });
273
+ }, { signal });
194
274
  return response.content[0].text || '';
195
275
  });
196
276
  }
@@ -206,10 +286,11 @@ class OllamaProvider {
206
286
  checkAvailability() {
207
287
  return __awaiter(this, void 0, void 0, function* () {
208
288
  const now = Date.now();
209
- // Use cached status if checked recently
210
- if (this.isAvailable !== null &&
211
- now - this.lastAvailabilityCheck < this.AVAILABILITY_CHECK_INTERVAL) {
212
- 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;
213
294
  }
214
295
  try {
215
296
  // Quick health check - Ollama has a /api/tags endpoint
@@ -225,7 +306,7 @@ class OllamaProvider {
225
306
  }
226
307
  });
227
308
  }
228
- analyze(prompt, screenshotBase64) {
309
+ analyze(prompt, screenshotBase64, signal) {
229
310
  return __awaiter(this, void 0, void 0, function* () {
230
311
  var _a;
231
312
  // Check if Ollama is available before attempting
@@ -239,7 +320,7 @@ class OllamaProvider {
239
320
  prompt: prompt,
240
321
  images: screenshotBase64 ? [screenshotBase64] : [],
241
322
  stream: false,
242
- }, { timeout: 30000 });
323
+ }, { timeout: 30000, signal });
243
324
  return response.data.response || '';
244
325
  }
245
326
  catch (err) {
@@ -257,6 +338,8 @@ class AIService {
257
338
  constructor() {
258
339
  this.provider = null;
259
340
  this.isMock = false;
341
+ /** The settings `provider` was set up from (initializeProvider). */
342
+ this.setUpFrom = null;
260
343
  this.initializeProvider();
261
344
  }
262
345
  // Key the breaker by provider + resolved model name so a misbehaving model
@@ -276,16 +359,60 @@ class AIService {
276
359
  return `ai:${provider}:${model || 'default'}`;
277
360
  }
278
361
  // Single choke point for every LLM call so circuit-breaker state is shared
279
- // across analyzeFailure / visualFind / healLocator.
280
- callProvider(prompt, screenshotBase64) {
281
- return __awaiter(this, void 0, void 0, function* () {
282
- 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
+ }
283
385
  });
284
386
  }
387
+ /**
388
+ * Sets the provider up from the server's AI settings, which the AI engine
389
+ * page changes while the server runs (`POST /config`). Again only when they
390
+ * changed, so a provider's client, and Ollama's availability check, carry
391
+ * over from one call to the next.
392
+ */
285
393
  initializeProvider() {
286
394
  const providerType = config_1.config.aiProvider;
287
395
  const genericModel = config_1.config.aiModel;
288
396
  const baseUrl = config_1.config.aiBaseUrl;
397
+ const settings = JSON.stringify([
398
+ providerType,
399
+ genericModel,
400
+ baseUrl,
401
+ config_1.config.geminiApiKey,
402
+ config_1.config.openaiApiKey,
403
+ config_1.config.anthropicApiKey,
404
+ config_1.config.geminiModel,
405
+ config_1.config.openaiModel,
406
+ config_1.config.anthropicModel,
407
+ config_1.config.ollamaModel,
408
+ process.env.GEMINI_API_KEY === 'mock',
409
+ ]);
410
+ if (settings === this.setUpFrom)
411
+ return;
412
+ this.setUpFrom = settings;
413
+ // A provider chosen without a key is no provider, never the previous one.
414
+ this.provider = null;
415
+ this.isMock = false;
289
416
  logger_1.default.info(`[AIService] Initializing with provider: ${providerType}`);
290
417
  if (process.env.GEMINI_API_KEY === 'mock') {
291
418
  this.isMock = true;
@@ -321,7 +448,15 @@ class AIService {
321
448
  logger_1.default.error(`[AIService] Initialization error: ${err.message}`);
322
449
  }
323
450
  }
451
+ /**
452
+ * Whether a provider is set up for the settings in force now. Failure
453
+ * analysis and the LLM and visual healing tiers ask this first; it used to
454
+ * answer for the provider set up last, so choosing a configured provider on
455
+ * the AI engine page never turned them on, and choosing one without a key
456
+ * left the previous provider answering.
457
+ */
324
458
  isEnabled() {
459
+ this.initializeProvider();
325
460
  return this.provider !== null || this.isMock;
326
461
  }
327
462
  analyzeFailure(context) {
@@ -341,7 +476,9 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
341
476
  try {
342
477
  const prompt = this.constructPrompt(context);
343
478
  const screenshotBase64 = this.getScreenshotBase64(context.screenshotPath);
344
- const text = yield this.callProvider(prompt, screenshotBase64 || undefined);
479
+ const text = yield this.callProvider(prompt, screenshotBase64 || undefined, {
480
+ timeoutMs: exports.FAILURE_ANALYSIS_TIMEOUT_MS,
481
+ });
345
482
  logger_1.default.info(`[AIService] Analysis complete for ${context.sessionId}`);
346
483
  return text;
347
484
  }
@@ -349,6 +486,12 @@ Fix: Add a pre-emptive check for the location permission dialog or use the \`aut
349
486
  if (err instanceof CircuitBreaker_1.CircuitOpenError) {
350
487
  logger_1.default.debug(`[AIService] Analysis skipped: ${err.message}`);
351
488
  }
489
+ else if (err instanceof AIRateLimitedError) {
490
+ logger_1.default.warn(`[AIService] No analysis for ${context.sessionId}: ${err.message}: ${err.detail}`);
491
+ }
492
+ else if (err instanceof AITimeoutError) {
493
+ logger_1.default.warn(`[AIService] No analysis for ${context.sessionId}: ${err.message}`);
494
+ }
352
495
  else {
353
496
  logger_1.default.error(`[AIService] Analysis failed: ${err.message}`);
354
497
  }
@@ -429,11 +572,14 @@ Answer with JSON only, exactly: {"result": true or false, "reason": "one sentenc
429
572
  answer = yield this.callProvider(prompt, screenshotBase64);
430
573
  }
431
574
  catch (err) {
575
+ if (err instanceof AIRateLimitedError || err instanceof AITimeoutError) {
576
+ throw new Error(`${err.message}, ${notChecked}. Try again later.`);
577
+ }
578
+ if (err instanceof AIProviderPausedError) {
579
+ throw new Error(`The AI provider has been failing, ${notChecked}. Xenon will call it again in ${err.seconds} s.`);
580
+ }
432
581
  throw new Error(`The AI provider failed, ${notChecked}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
433
582
  }
434
- if (answer === RATE_LIMITED) {
435
- throw new Error(`The AI provider is rate-limited, ${notChecked}. Try again later.`);
436
- }
437
583
  const verdict = parseVisualVerdict(answer);
438
584
  if (!verdict) {
439
585
  const said = answer.replace(/\s+/g, ' ').trim().slice(0, 200);
@@ -446,7 +592,9 @@ Answer with JSON only, exactly: {"result": true or false, "reason": "one sentenc
446
592
  /**
447
593
  * A short description of the screen in the screenshot, for
448
594
  * `xenon: analyzeScreen` and device control's Omni-Scan. Throws when there
449
- * is no provider or the call fails, so the caller can say why there is none.
595
+ * is no provider or the call fails (AIRateLimitedError for a rate limit,
596
+ * AITimeoutError after AI_CALL_TIMEOUT_MS), so the caller can say why there
597
+ * is none.
450
598
  */
451
599
  describeScreen(screenshotBase64) {
452
600
  return __awaiter(this, void 0, void 0, function* () {
@@ -458,10 +606,7 @@ You are looking at a screenshot of a mobile app, for a software tester.
458
606
  Describe the screen in a few short sentences: which screen it is, its main elements and their state (for example filled in, empty, selected or disabled), and anything unexpected, such as an error message, a system dialog, a crash or a loading indicator.
459
607
  Describe only what is visible. Answer in plain text.
460
608
  `.trim();
461
- const answer = yield this.callProvider(prompt, screenshotBase64);
462
- if (answer === RATE_LIMITED)
463
- throw new Error('The AI provider is rate-limited');
464
- return answer;
609
+ return this.callProvider(prompt, screenshotBase64);
465
610
  });
466
611
  }
467
612
  /**
@@ -512,9 +657,10 @@ Describe only what is visible. Answer in plain text.
512
657
  }
513
658
  testConnection(testConfig) {
514
659
  return __awaiter(this, void 0, void 0, function* () {
660
+ var _a;
661
+ const providerType = testConfig.aiProvider || config_1.config.aiProvider;
515
662
  try {
516
663
  let testProvider = null;
517
- const providerType = testConfig.aiProvider || config_1.config.aiProvider;
518
664
  const model = testConfig.aiModel || config_1.config.aiModel;
519
665
  const baseUrl = testConfig.aiBaseUrl || config_1.config.aiBaseUrl;
520
666
  logger_1.default.info(`[AIService] Testing connection for provider: ${providerType}`);
@@ -547,17 +693,21 @@ Describe only what is visible. Answer in plain text.
547
693
  }
548
694
  if (!testProvider)
549
695
  throw new Error('Failed to initialize provider for testing');
550
- // Send a minimal ping command
551
- const responseText = yield testProvider.analyze('Hello. Response: OK');
552
- if (responseText === 'CONNECTION_OK_RATE_LIMITED') {
553
- return {
554
- success: true,
555
- message: `Successfully connected to ${providerType}! (Note: You are currently out of quota/rate-limited, but the setup is correct)`,
556
- };
557
- }
696
+ // Send a minimal ping command. Not through the circuit breaker (a test
697
+ // shouldn't pause the provider for everyone), but with the time limit.
698
+ const pinged = testProvider;
699
+ yield withTimeLimit(exports.AI_CALL_TIMEOUT_MS, (signal) => pinged.analyze('Hello. Response: OK', undefined, signal));
558
700
  return { success: true, message: `Successfully connected to ${providerType}!` };
559
701
  }
560
702
  catch (err) {
703
+ // Not a success: every AI call fails the same way until it has quota again.
704
+ if (isRateLimit(err)) {
705
+ logger_1.default.warn(`[AIService] Connection test: ${providerType} is rate-limited: ${(_a = err.detail) !== null && _a !== void 0 ? _a : err.message}`);
706
+ return {
707
+ success: false,
708
+ message: `${providerType} answered, but it is rate-limited or out of quota. AI features won't work with it until it has quota again.`,
709
+ };
710
+ }
561
711
  logger_1.default.error(`[AIService] Connection test failed: ${err.message}`);
562
712
  return { success: false, message: `Connection failed: ${err.message}` };
563
713
  }
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
23
23
  const crypto_1 = __importDefault(require("crypto"));
24
24
  const prisma_1 = require("../prisma");
25
25
  const logger_1 = __importDefault(require("../logger"));
26
+ const identityChanges_1 = require("./identity/identityChanges");
26
27
  let ApiKeyService = class ApiKeyService {
27
28
  constructor() {
28
29
  this.log = logger_1.default.scope('ApiKey');
@@ -38,8 +39,13 @@ let ApiKeyService = class ApiKeyService {
38
39
  generateRaw() {
39
40
  return crypto_1.default.randomBytes(32).toString('hex');
40
41
  }
41
- verify(raw) {
42
- return __awaiter(this, void 0, void 0, function* () {
42
+ /**
43
+ * The live key for `raw`. A use of it is recorded (`lastUsedAt`);
44
+ * `touch: false` only looks, for a check that isn't a use (a socket's
45
+ * re-check). Likewise verifyPair.
46
+ */
47
+ verify(raw_1) {
48
+ return __awaiter(this, arguments, void 0, function* (raw, { touch = true } = {}) {
43
49
  if (!raw)
44
50
  return null;
45
51
  const row = yield prisma_1.prisma.apiKey.findUnique({ where: { keyHash: this.hash(raw) } });
@@ -47,6 +53,8 @@ let ApiKeyService = class ApiKeyService {
47
53
  return null;
48
54
  if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
49
55
  return null;
56
+ if (!touch)
57
+ return row;
50
58
  prisma_1.prisma.apiKey
51
59
  .update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
52
60
  .catch(() => undefined);
@@ -77,8 +85,8 @@ let ApiKeyService = class ApiKeyService {
77
85
  return { id: row.id, raw };
78
86
  });
79
87
  }
80
- verifyPair(accessKey, token) {
81
- return __awaiter(this, void 0, void 0, function* () {
88
+ verifyPair(accessKey_1, token_1) {
89
+ return __awaiter(this, arguments, void 0, function* (accessKey, token, { touch = true } = {}) {
82
90
  if (!accessKey || !token)
83
91
  return null;
84
92
  const user = yield prisma_1.prisma.user.findUnique({ where: { accessKey } });
@@ -91,6 +99,8 @@ let ApiKeyService = class ApiKeyService {
91
99
  return null;
92
100
  if (row.expiresAt && row.expiresAt.getTime() <= Date.now())
93
101
  return null;
102
+ if (!touch)
103
+ return row;
94
104
  prisma_1.prisma.apiKey
95
105
  .update({ where: { id: row.id }, data: { lastUsedAt: new Date() } })
96
106
  .catch(() => undefined);
@@ -99,7 +109,8 @@ let ApiKeyService = class ApiKeyService {
99
109
  }
100
110
  revoke(id) {
101
111
  return __awaiter(this, void 0, void 0, function* () {
102
- yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
112
+ const row = yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
113
+ yield (0, identityChanges_1.identityChanged)(row.userId);
103
114
  });
104
115
  }
105
116
  list() {
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
23
23
  const nodemailer_1 = __importDefault(require("nodemailer"));
24
24
  const config_1 = require("../config");
25
25
  const logger_1 = __importDefault(require("../logger"));
26
+ const passwordResetLink_1 = require("./passwordResetLink");
26
27
  let EmailService = class EmailService {
27
28
  constructor() {
28
29
  this.log = logger_1.default.scope('Email');
@@ -55,6 +56,34 @@ let EmailService = class EmailService {
55
56
  'over the account for the link lifetime. Configure XENON_SMTP_URL, or unset this and ' +
56
57
  'issue links from the dashboard Users page.');
57
58
  }
59
+ /**
60
+ * Startup notice about XENON_PUBLIC_URL, the address links Xenon hands out
61
+ * are built from (never the request's Host, which the asker chooses):
62
+ *
63
+ * - set but refused (not an http(s) server address): whatever the mail
64
+ * setup, since it also decides each device's dashboard_link. The value is
65
+ * not logged: a user name and password in it is one reason to refuse it.
66
+ * - unset while Xenon could send reset links: the sign-in page's "forgot
67
+ * password" then sends people to an administrator, and an administrator's
68
+ * reset link is handed back to them rather than emailed.
69
+ */
70
+ warnAboutPublicUrl() {
71
+ var _a;
72
+ if (((_a = config_1.config.publicUrl) === null || _a === void 0 ? void 0 : _a.trim()) && (0, passwordResetLink_1.resetLinkBase)() === null) {
73
+ this.warnLog('XENON_PUBLIC_URL is set but is not an http(s) server address such as ' +
74
+ 'https://xenon.example.com or http://lab-mac:4723 (a trailing /xenon is fine; any ' +
75
+ 'other path, a query, a fragment or a user name is not), so Xenon ignores it: it ' +
76
+ 'emails no password-reset links, and device dashboard links are paths on this server.');
77
+ return;
78
+ }
79
+ if (!this.canDeliver() || (0, passwordResetLink_1.resetLinkBase)() !== null)
80
+ return;
81
+ this.warnLog("XENON_PUBLIC_URL is not set to this server's address, so Xenon sends no password-reset " +
82
+ 'links: "Forgot password" asks people to contact an administrator, and the Users ' +
83
+ 'page hands an administrator the link instead of emailing it. Set XENON_PUBLIC_URL to ' +
84
+ 'the http(s) address people reach this server at, such as https://xenon.example.com ' +
85
+ 'or http://lab-mac:4723 (a trailing /xenon is fine; any other path is not).');
86
+ }
58
87
  send(mail) {
59
88
  return __awaiter(this, void 0, void 0, function* () {
60
89
  const smtpUrl = config_1.config.smtpUrl;
@@ -240,7 +240,11 @@ let InterceptorService = class InterceptorService {
240
240
  throw new Error(`Interceptor not active for session ${sessionId}`);
241
241
  return state;
242
242
  }
243
- /** `udid` is the session's phone: the dashboard events reach only the teams that see it. */
243
+ /**
244
+ * `udid` is the session's phone. The dashboard events reach only admins who
245
+ * see it: a captured request's headers and bodies can carry the app's
246
+ * sign-in tokens, and REST's /interceptor routes are admin-only too.
247
+ */
244
248
  emit(evt, udid) {
245
249
  for (const l of this.listeners) {
246
250
  try {
@@ -255,7 +259,7 @@ let InterceptorService = class InterceptorService {
255
259
  broadcast(evt, udid) {
256
260
  try {
257
261
  const socket = typedi_1.Container.get(SocketServer_1.SocketServer);
258
- const scope = { udid };
262
+ const scope = { udid, adminOnly: true };
259
263
  if (evt.type === 'request') {
260
264
  void socket.emitToDashboardForDevices(SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, evt.payload, scope);
261
265
  }
@@ -26,6 +26,8 @@ const event_manager_1 = require("../dashboard/event-manager");
26
26
  const SessionStatus_1 = require("../types/SessionStatus");
27
27
  const device_service_1 = require("../data-service/device-service");
28
28
  const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
29
+ const TracingService_1 = require("./TracingService");
30
+ const sessionMemory_1 = require("../sessions/sessionMemory");
29
31
  let OrphanSweeper = class OrphanSweeper {
30
32
  constructor() {
31
33
  this.log = logger_1.default.scope('OrphanSweeper');
@@ -74,6 +76,12 @@ let OrphanSweeper = class OrphanSweeper {
74
76
  // back before the phone is released; for a previous process's it is
75
77
  // the ledger's, at boot (PhoneNetworkRestore).
76
78
  yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
79
+ (0, sessionMemory_1.forgetSessionMemory)(s.id);
80
+ // A previous process's session has no span here; this one's ends.
81
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(s.id, {
82
+ failed: true,
83
+ reason: 'Session heartbeat timeout',
84
+ });
77
85
  // This session's claim only, not every row with its udid: the phone
78
86
  // may be another session's by now (deviceClaims.ts).
79
87
  yield (0, device_service_1.releaseSessionDevices)(s.id);
@@ -51,6 +51,10 @@ function whereTuple(strategy, selector) {
51
51
  },
52
52
  };
53
53
  }
54
+ /** Just the selector a change is about, for the socket server's scoped emit. */
55
+ function selectorOf(tuple) {
56
+ return { strategy: tuple.strategy, selector: tuple.selector };
57
+ }
54
58
  /** The SelectorEvent row for one change. */
55
59
  function selectorEventData(tuple, action, userId, reason) {
56
60
  return {
@@ -140,7 +144,7 @@ let SelectorStateService = class SelectorStateService {
140
144
  return upserted;
141
145
  }));
142
146
  scopedLog.info(`markFixed: ${ctx.strategy}=${ctx.selector} → pending (api_key=${ctx.apiKeyId})`);
143
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_FIXED, serialize(row));
147
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_FIXED, serialize(row), selectorOf(ctx));
144
148
  return row;
145
149
  });
146
150
  }
@@ -174,7 +178,7 @@ let SelectorStateService = class SelectorStateService {
174
178
  return upserted;
175
179
  }));
176
180
  scopedLog.info(`mute: ${ctx.strategy}=${ctx.selector} → muted (api_key=${ctx.apiKeyId})`);
177
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_MUTED, serialize(row));
181
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_MUTED, serialize(row), selectorOf(ctx));
178
182
  return row;
179
183
  });
180
184
  }
@@ -231,13 +235,13 @@ let SelectorStateService = class SelectorStateService {
231
235
  scopedLog.info(`unmute: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
232
236
  // Emit the post-state. When the row was deleted, surface the prior tuple
233
237
  // so the dashboard can drop it from the muted list.
234
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, result
238
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_UNMUTED, result
235
239
  ? serialize(result)
236
240
  : {
237
241
  original_strategy: ctx.strategy,
238
242
  original_selector: ctx.selector,
239
243
  status: 'deleted',
240
- });
244
+ }, selectorOf(ctx));
241
245
  return result;
242
246
  });
243
247
  }
@@ -279,13 +283,13 @@ let SelectorStateService = class SelectorStateService {
279
283
  return row;
280
284
  }));
281
285
  scopedLog.info(`cancelVerification: ${ctx.strategy}=${ctx.selector} → ${result ? 'active' : 'deleted'} (api_key=${ctx.apiKeyId})`);
282
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, result
286
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_CANCELLED, result
283
287
  ? serialize(result)
284
288
  : {
285
289
  original_strategy: ctx.strategy,
286
290
  original_selector: ctx.selector,
287
291
  status: 'deleted',
288
- });
292
+ }, selectorOf(ctx));
289
293
  return result;
290
294
  });
291
295
  }
@@ -329,7 +333,7 @@ let SelectorStateService = class SelectorStateService {
329
333
  return updated;
330
334
  }));
331
335
  scopedLog.warn(`onHealRecorded: ${ctx.strategy}=${ctx.selector} regressed from ${existing.status} (session=${ctx.sessionId})`);
332
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, serialize(row));
336
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_REGRESSED, serialize(row), selectorOf(ctx));
333
337
  return row;
334
338
  });
335
339
  }