@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
@@ -32,10 +32,17 @@ 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;
42
+ /** The selector a row is about, as the socket server's scoped emit names it. */
43
+ function keyOfRow(row) {
44
+ return { strategy: row.original_strategy, selector: row.original_selector };
45
+ }
39
46
  /**
40
47
  * Periodically scans Pending SelectorState rows and promotes them to Resolved
41
48
  * once enough clean CI builds have been observed (no heals on the selector
@@ -127,12 +134,20 @@ let SelectorVerificationJob = class SelectorVerificationJob {
127
134
  // that ran under a CI build (build_id IS NOT NULL); ad-hoc local runs
128
135
  // intentionally don't move verification forward.
129
136
  //
137
+ // A find that found the selector is one that didn't fail (`is_error`,
138
+ // the dashboard's record of an error answer) and didn't answer an empty
139
+ // list (`{"value":[]`, findElements finding nothing). The command log
140
+ // keeps the answer as JSON.stringify wrote it, or as the node sent it.
141
+ //
130
142
  // Identifier names (PascalCase tables, camelCase createdAt) match the
131
143
  // actual SQLite schema generated by Prisma — see
132
144
  // `sqlite3 prisma/dev.db ".schema SessionLog"`.
133
145
  const heals = yield this.prisma.$queryRaw `
134
146
  SELECT s."build_id" AS build_id,
135
- MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed
147
+ MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed,
148
+ MAX(CASE WHEN NOT sl."is_healed" AND NOT sl."is_error"
149
+ AND sl."response" NOT LIKE '{"value":[]%'
150
+ THEN 1 ELSE 0 END) AS found
136
151
  FROM "SessionLog" sl
137
152
  INNER JOIN "Session" s ON sl."session_id" = s."id"
138
153
  WHERE sl."original_strategy" = ${row.original_strategy}
@@ -142,7 +157,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
142
157
  AND s."build_id" IS NOT NULL
143
158
  GROUP BY s."build_id"
144
159
  `;
145
- const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0).length;
160
+ const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0 && Number(r.found) === 1).length;
146
161
  if (cleanBuildCount >= CLEAN_BUILDS_TO_RESOLVE) {
147
162
  yield this.promoteToResolved(row);
148
163
  }
@@ -169,7 +184,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
169
184
  return promoted;
170
185
  }));
171
186
  log.info(`[${row.id}] promoted to resolved (${row.original_strategy}=${row.original_selector})`);
172
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_RESOLVED, this.serialize(updated));
187
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_RESOLVED, this.serialize(updated), keyOfRow(row));
173
188
  });
174
189
  }
175
190
  updateProgress(row, cleanBuildCount) {
@@ -178,7 +193,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
178
193
  where: { id: row.id },
179
194
  data: { clean_builds_count: cleanBuildCount, last_event_at: new Date() },
180
195
  });
181
- this.socket.emitToDashboard(SocketEvents_1.SocketEvents.SELECTOR_PROGRESS, this.serialize(updated));
196
+ void this.socket.emitToDashboardForSelector(SocketEvents_1.SocketEvents.SELECTOR_PROGRESS, this.serialize(updated), keyOfRow(row));
182
197
  });
183
198
  }
184
199
  serialize(row) {
@@ -79,6 +79,7 @@ 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
81
  const SelfHealingSwitch_1 = require("./settings/SelfHealingSwitch");
82
+ const aiEngineSettings_1 = require("./settings/aiEngineSettings");
82
83
  const device_store_1 = require("../data-service/device-store");
83
84
  const device_utils_1 = require("../device-utils");
84
85
  const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
@@ -140,6 +141,9 @@ let ServerManager = ServerManager_1 = class ServerManager {
140
141
  // The Settings page's AI self-healing toggle, which the command interceptor
141
142
  // reads from memory at every command: load it before a command can arrive.
142
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();
143
147
  this.registerRoutes(expressApp, httpServer, cliArgs, pluginArgs);
144
148
  yield this.bootEmulators(pluginArgs);
145
149
  this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
@@ -397,6 +401,7 @@ let ServerManager = ServerManager_1 = class ServerManager {
397
401
  const { bootstrapIdentity } = yield Promise.resolve().then(() => __importStar(require('./identity/bootstrap')));
398
402
  yield bootstrapIdentity();
399
403
  typedi_1.Container.get(EmailService_1.EmailService).warnIfLogFallbackEnabled();
404
+ typedi_1.Container.get(EmailService_1.EmailService).warnAboutPublicUrl();
400
405
  const { startUserSessionCleanupCron } = yield Promise.resolve().then(() => __importStar(require('./identity/sessionCleanupCron')));
401
406
  startUserSessionCleanupCron();
402
407
  // Hub-issued JWT signing key (REST/MCP tokens, stream tickets) — key
@@ -445,7 +450,12 @@ let ServerManager = ServerManager_1 = class ServerManager {
445
450
  });
446
451
  }
447
452
  registerRoutes(expressApp, httpServer, cliArgs, pluginArgs) {
448
- 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
+ }));
449
459
  // The session gateway goes in front of Appium's routes: Xenon's own
450
460
  // /wd-internal calls (with the per-process secret), per-command auth
451
461
  // (XENON_REQUIRE_COMMAND_AUTH), then, on a hub, forwarding of the sessions
@@ -56,8 +56,6 @@ const typedi_1 = require("typedi");
56
56
  const uuid_1 = require("uuid");
57
57
  const lodash_1 = __importDefault(require("lodash"));
58
58
  const axios_1 = __importDefault(require("axios"));
59
- const https_proxy_agent_1 = require("https-proxy-agent");
60
- const http_proxy_agent_1 = require("http-proxy-agent");
61
59
  const logger_1 = __importDefault(require("../logger"));
62
60
  const helpers_1 = require("../helpers");
63
61
  const InternalHttpClient_1 = require("../InternalHttpClient");
@@ -69,7 +67,6 @@ const device_utils_1 = require("../device-utils");
69
67
  const TracingService_1 = require("./TracingService");
70
68
  const PortAllocator_1 = require("./PortAllocator");
71
69
  const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
72
- const JwtKeyService_1 = require("./token/JwtKeyService");
73
70
  const sessionIdentity_1 = require("./session/sessionIdentity");
74
71
  const leaseSessionCaps_1 = require("./lease/leaseSessionCaps");
75
72
  const sessionCredentials_1 = require("./session/sessionCredentials");
@@ -80,6 +77,7 @@ const callerTeamIds_1 = require("./device-access/callerTeamIds");
80
77
  const queueVisibility_1 = require("./device-access/queueVisibility");
81
78
  const appVisibility_1 = require("./device-access/appVisibility");
82
79
  const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
80
+ const sessionMemory_1 = require("../sessions/sessionMemory");
83
81
  const SessionMetricsService_1 = require("./metrics/SessionMetricsService");
84
82
  const appCapability_1 = require("./session/appCapability");
85
83
  const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
@@ -100,6 +98,21 @@ const async_lock_1 = __importDefault(require("async-lock"));
100
98
  const base_driver_1 = require("@appium/base-driver");
101
99
  const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
102
100
  const interceptorOptions_1 = require("./interceptor/interceptorOptions");
101
+ /**
102
+ * A session token admits a session only with the `sessions` scope, as a key
103
+ * pair does. Tokens minted before this check carry no `scopes` claim, and
104
+ * POST /auth/token gave one to any credential, a read-only one included:
105
+ * they are refused too.
106
+ */
107
+ function sessionTokenGrantsSessions(scopes) {
108
+ if (typeof scopes !== 'string')
109
+ return false;
110
+ const owned = scopes.split(',').map((s) => s.trim());
111
+ return owned.includes('sessions') || owned.includes('admin');
112
+ }
113
+ const SESSION_TOKEN_LACKS_SCOPE = 'session rejected: xe:options.sessionToken carries no `sessions` scope; tokens minted by ' +
114
+ 'Xenon 2.14 or earlier carry none — mint a new one with POST /xenon/api/auth/token ' +
115
+ '({"audience":"xenon-mcp"}) using a credential that has the `sessions` scope.';
103
116
  /**
104
117
  * How long the hub waits for a node (or cloud provider) to create a session.
105
118
  * A create is sent once (InternalRequestConfig `retry: false`): the node may
@@ -263,6 +276,7 @@ let SessionLifecycleService = class SessionLifecycleService {
263
276
  apiKeyId: authResult.apiKeyId,
264
277
  userId: authResult.userId,
265
278
  appDownload,
279
+ allocatedAt: Date.now(),
266
280
  };
267
281
  // The grant opens one phone, on this node. A token taken to another node
268
282
  // (an emulator's udid repeats across machines), or naming a phone this
@@ -391,7 +405,7 @@ let SessionLifecycleService = class SessionLifecycleService {
391
405
  this.logger.debug(`📱 Removing pending session with capability_id: ${allocation.pendingSessionId}`);
392
406
  yield (0, pending_sessions_service_1.removePendingSession)(allocation.pendingSessionId);
393
407
  if (this.isCreateSessionResponseInternal(session)) {
394
- yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId);
408
+ yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId, allocation.allocatedAt);
395
409
  }
396
410
  else {
397
411
  yield this.handleSessionFailure(session, device, remote);
@@ -505,9 +519,11 @@ let SessionLifecycleService = class SessionLifecycleService {
505
519
  // took out of `xe:options` (or its `xenon:options` alias): an access-key +
506
520
  // token pair, or a session token. Today this is best-effort: if none is
507
521
  // supplied we log a warning and let the request through to avoid breaking
508
- // pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on. A
509
- // supplied key that is invalid / revoked / has insufficient scope is
510
- // rejected. Respects the global `authDisabled` flag.
522
+ // pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on.
523
+ // Credentials that don't verify — a wrong, revoked or expired key or token,
524
+ // or one whose user is deleted or not ACTIVE — count as none. Credentials
525
+ // that verify without the `sessions` scope are rejected. Respects the
526
+ // global `authDisabled` flag.
511
527
  //
512
528
  // Returns the caller's { apiKeyId, callerTeamIds } so allocation can filter
513
529
  // devices, and app resolution uploaded apps, by team. `apiKeyId` is null
@@ -549,23 +565,47 @@ let SessionLifecycleService = class SessionLifecycleService {
549
565
  }
550
566
  const { ApiKeyService } = yield Promise.resolve().then(() => __importStar(require('./ApiKeyService')));
551
567
  const svc = typedi_1.Container.get(ApiKeyService);
568
+ const { verifyKeyPairCredential, verifySessionTokenCredential, isWrongSessionToken } = yield Promise.resolve().then(() => __importStar(require('../middleware/verifyCredential')));
552
569
  // xe:options.{accessKey, token} — the only key-pair credential shape.
570
+ // Checked by REST's rule (verifyKeyPairCredential): a live key whose owner
571
+ // exists and is ACTIVE. The key alone (verifyPair) let a user set to
572
+ // Inactive, or deleted, keep creating sessions as themselves. The owner
573
+ // found here is the one lookup the session's scope and lease use.
553
574
  const pair = credentials.pair;
554
- const row = pair ? yield svc.verifyPair(pair.accessKey, pair.token) : null;
575
+ const verifiedPair = pair ? yield verifyKeyPairCredential(pair.accessKey, pair.token) : null;
576
+ // The session token, verified once for both the gate and attribution, by
577
+ // REST's rule too: a live signature and an ACTIVE subject, looked up now.
578
+ let tokenCheck;
579
+ const verifyToken = (t) => {
580
+ if (!tokenCheck)
581
+ tokenCheck = verifySessionTokenCredential(t);
582
+ return tokenCheck;
583
+ };
584
+ // Both readers below treat any failure as a token that doesn't check out:
585
+ // the gate refuses it as "invalid or expired", attribution ignores it. A
586
+ // check that could not run (the database or the signing key unavailable)
587
+ // is not that: it refuses the create here, as a key whose owner can't be
588
+ // looked up does, rather than run the session with no owner.
589
+ if (!verifiedPair && credentials.sessionToken) {
590
+ yield verifyToken(credentials.sessionToken).catch((err) => {
591
+ if (!isWrongSessionToken(err))
592
+ throw err;
593
+ });
594
+ }
555
595
  const { assertSessionTokenGate, sessionTokenGateEnabled } = yield Promise.resolve().then(() => __importStar(require('./sessionTokenGate')));
556
596
  try {
557
597
  yield assertSessionTokenGate({
558
598
  enabled: sessionTokenGateEnabled(),
559
- hasValidKeyPair: !!row,
599
+ hasValidKeyPair: !!verifiedPair,
560
600
  token: credentials.sessionToken,
561
- verify: (t) => typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(t, { audience: 'xenon-session' }),
601
+ verify: verifyToken,
562
602
  });
563
603
  }
564
604
  catch (err) {
565
605
  this.logger.error(`❌ ${err.message}`);
566
606
  throw new base_driver_1.errors.InvalidArgumentError(err.message);
567
607
  }
568
- if (!row) {
608
+ if (!verifiedPair) {
569
609
  // No key pair. An xe:options.sessionToken still identifies the caller,
570
610
  // so read it for attribution even when the gate is off — otherwise the
571
611
  // ownership guard fails closed on a session whose owner we actually know.
@@ -573,23 +613,36 @@ let SessionLifecycleService = class SessionLifecycleService {
573
613
  // The token's teamId claim narrows it the way a key's team does; kept
574
614
  // from the one verify rather than verifying again.
575
615
  let tokenTeamId = null;
616
+ let tokenScopes;
617
+ let tokenOwner;
576
618
  const identity = yield (0, sessionIdentity_1.resolveSessionIdentity)({
577
619
  row: null,
578
620
  sessionToken: credentials.sessionToken,
579
621
  verify: (t) => __awaiter(this, void 0, void 0, function* () {
580
- const payload = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(t, {
581
- audience: 'xenon-session',
582
- });
622
+ const { payload, user } = yield verifyToken(t);
583
623
  tokenTeamId = typeof (payload === null || payload === void 0 ? void 0 : payload.teamId) === 'string' ? payload.teamId : null;
624
+ tokenScopes = payload === null || payload === void 0 ? void 0 : payload.scopes;
625
+ tokenOwner = user;
584
626
  return payload;
585
627
  }),
586
628
  });
629
+ // A token that names its owner may create sessions only with the
630
+ // `sessions` scope, as a key pair may: refused, like a key without it.
631
+ if (identity.userId && !sessionTokenGrantsSessions(tokenScopes)) {
632
+ this.logger.error('Rejecting session: xe:options.sessionToken lacks the `sessions` scope');
633
+ throw new base_driver_1.errors.InvalidArgumentError(SESSION_TOKEN_LACKS_SCOPE);
634
+ }
587
635
  if (!identity.userId) {
588
636
  this.logger.warn('Session created without valid credentials. Pass `xe:options.accessKey` + ' +
589
637
  '`xe:options.token`, or `xe:options.sessionToken`.');
590
638
  }
591
- const leaseAccess = this.leaseAccessFor(identity.userId
592
- ? { kind: 'session-token', userId: identity.userId, teamId: tokenTeamId }
639
+ const leaseAccess = this.leaseAccessFor(identity.userId && tokenOwner
640
+ ? {
641
+ kind: 'session-token',
642
+ scopes: String(tokenScopes),
643
+ user: tokenOwner,
644
+ teamId: tokenTeamId,
645
+ }
593
646
  : { kind: 'none' });
594
647
  // A token's user is scoped as REST scopes them. No credentials at all
595
648
  // stays unscoped (the back-compat path), with no lookup.
@@ -599,6 +652,7 @@ let SessionLifecycleService = class SessionLifecycleService {
599
652
  teamId: identity.userId ? tokenTeamId : null,
600
653
  } });
601
654
  }
655
+ const { row, user: owner } = verifiedPair;
602
656
  if (!svc.hasScope(row, ['sessions'])) {
603
657
  this.logger.error('Rejecting session: credentials lack the `sessions` scope');
604
658
  throw new base_driver_1.errors.InvalidArgumentError('credentials are invalid, revoked, or lack the `sessions` scope');
@@ -607,7 +661,7 @@ let SessionLifecycleService = class SessionLifecycleService {
607
661
  const leaseAccess = this.leaseAccessFor({
608
662
  kind: 'api-key',
609
663
  scopes: row.scopes,
610
- userId: row.userId,
664
+ user: owner,
611
665
  teamId: (_a = row.teamId) !== null && _a !== void 0 ? _a : null,
612
666
  });
613
667
  // The phones and apps this session may use: REST's rule
@@ -642,7 +696,8 @@ let SessionLifecycleService = class SessionLifecycleService {
642
696
  // How a session that names a lease is judged, looked up at most once and
643
697
  // only when asked. Who may override follows canOverrideLease; which phones
644
698
  // it can see follows REST's computeTeamIds. Both need the owner's live role,
645
- // as authMiddleware reads it for every REST request.
699
+ // as authMiddleware reads it for every REST request: the owner the
700
+ // credential check looked up a moment ago, ACTIVE then.
646
701
  leaseAccessFor(credential) {
647
702
  let looked;
648
703
  return () => {
@@ -659,24 +714,24 @@ let SessionLifecycleService = class SessionLifecycleService {
659
714
  if (credential.kind === 'none') {
660
715
  return { canOverride: (0, leaseOverride_1.canOverrideLease)({ kind: 'none' }), teamIds: undefined };
661
716
  }
717
+ const { user } = credential;
662
718
  try {
663
- const { UserService } = yield Promise.resolve().then(() => __importStar(require('./UserService')));
664
- const found = yield typedi_1.Container.get(UserService).findById(credential.userId);
665
- const user = found && found.status === 'ACTIVE' ? found : null;
666
- const canOverride = (0, leaseOverride_1.canOverrideLease)(credential.kind === 'api-key'
667
- ? { kind: 'api-key', scopes: credential.scopes, user }
668
- : { kind: 'session-token', user });
669
- const role = (user === null || user === void 0 ? void 0 : user.role) === 'SUPER_ADMIN' || (user === null || user === void 0 ? void 0 : user.role) === 'ADMIN' ? user.role : 'MEMBER';
719
+ const canOverride = (0, leaseOverride_1.canOverrideLease)({
720
+ kind: credential.kind,
721
+ scopes: credential.scopes,
722
+ user,
723
+ });
724
+ const role = user.role === 'SUPER_ADMIN' || user.role === 'ADMIN' ? user.role : 'MEMBER';
670
725
  const teamIds = yield (0, callerTeamIds_1.computeTeamIds)({
671
726
  role,
672
- userId: credential.userId,
727
+ userId: user.id,
673
728
  apiKeyTeamId: credential.teamId,
674
729
  });
675
730
  return { canOverride, teamIds };
676
731
  }
677
732
  catch (err) {
678
733
  // Fail closed: no override, and the shared pool only.
679
- this.logger.warn(`Could not look up how user ${credential.userId} may use a lease: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
734
+ this.logger.warn(`Could not look up how user ${user.id} may use a lease: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
680
735
  return { canOverride: false, teamIds: [] };
681
736
  }
682
737
  });
@@ -790,8 +845,8 @@ let SessionLifecycleService = class SessionLifecycleService {
790
845
  }
791
846
  }
792
847
  finalizeSession(session_1, device_1, caps_1, driver_1, isRemote_1) {
793
- return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null) {
794
- var _a;
848
+ return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null, allocatedAt) {
849
+ var _a, _b;
795
850
  const sessionId = session.value[0];
796
851
  const sessionResponse = session.value[1];
797
852
  const requestedCaps = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
@@ -808,6 +863,23 @@ let SessionLifecycleService = class SessionLifecycleService {
808
863
  'xenon.udid': device.udid,
809
864
  });
810
865
  }
866
+ try {
867
+ yield this.registerSession(sessionId, sessionResponse, device, xenonCapabilities, driver, isRemote, apiKeyId, userId, allocatedAt);
868
+ }
869
+ catch (err) {
870
+ // The create fails, so no ending of the session will come to end it.
871
+ tracingService.endSessionSpan(sessionId, {
872
+ failed: true,
873
+ reason: `Xenon could not finish creating the session: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`,
874
+ });
875
+ throw err;
876
+ }
877
+ });
878
+ }
879
+ /** The rest of finalizeSession, once the session's span has started. */
880
+ registerSession(sessionId, sessionResponse, device, xenonCapabilities, driver, isRemote, apiKeyId, userId, allocatedAt) {
881
+ return __awaiter(this, void 0, void 0, function* () {
882
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
811
883
  // The session takes over the pending claim its phone was allocated with.
812
884
  yield (0, device_service_1.claimDeviceForSession)(device, sessionId, {
813
885
  session_id: sessionId,
@@ -826,6 +898,7 @@ let SessionLifecycleService = class SessionLifecycleService {
826
898
  const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver, webDriverUrl);
827
899
  sessionInstance.apiKeyId = apiKeyId;
828
900
  sessionInstance.userId = userId;
901
+ sessionInstance.allocatedAt = allocatedAt;
829
902
  // A session this server drives: its owner is known while it runs, row or
830
903
  // not (a node writes none for the hub's sessions; LiveSessionOwners).
831
904
  if (sessionInstance instanceof LocalSession_1.LocalSession) {
@@ -1005,19 +1078,8 @@ let SessionLifecycleService = class SessionLifecycleService {
1005
1078
  timeout: exports.REMOTE_CREATE_TIMEOUT_MS,
1006
1079
  retry: false,
1007
1080
  };
1008
- if (context.pluginArgs.proxy) {
1009
- this.logger.info(`Added proxy to axios config: ${JSON.stringify(context.pluginArgs.proxy)}`);
1010
- const proxyUrl = typeof context.pluginArgs.proxy === 'string'
1011
- ? context.pluginArgs.proxy
1012
- : `http://${context.pluginArgs.proxy.host}:${context.pluginArgs.proxy.port}`;
1013
- config.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl, {
1014
- rejectUnauthorized: context.pluginArgs.tlsRejectUnauthorized,
1015
- });
1016
- config.httpAgent = new http_proxy_agent_1.HttpProxyAgent(proxyUrl, {
1017
- rejectUnauthorized: context.pluginArgs.tlsRejectUnauthorized,
1018
- });
1019
- config.proxy = false;
1020
- }
1081
+ // Its proxy is InternalHttpClient's, by Xenon's one rule: the `proxy`
1082
+ // option, else the environment's (helpers/outboundProxy.ts).
1021
1083
  const createdSession = yield this.invokeSessionRequest(config, context.pluginArgs.tlsRejectUnauthorized);
1022
1084
  if (createdSession instanceof Error) {
1023
1085
  return createdSession;
@@ -1148,6 +1210,13 @@ let SessionLifecycleService = class SessionLifecycleService {
1148
1210
  finally {
1149
1211
  if (sessionId) {
1150
1212
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
1213
+ (0, sessionMemory_1.forgetSessionMemory)(sessionId);
1214
+ // Whether or not SESSION_MANAGER holds the session (a local one with
1215
+ // the dashboard off), and once: a second delete finds no span.
1216
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(sessionId, {
1217
+ failed: status === SessionStatus_1.SessionStatus.FAILED,
1218
+ reason,
1219
+ });
1151
1220
  // Before the lock's "still in memory?" check, which a dashboard-off
1152
1221
  // node fails. On a node this ends the figures it holds for its hub;
1153
1222
  // on a hub it collects a node session's last ones. Idempotent.
@@ -1181,8 +1250,11 @@ let SessionLifecycleService = class SessionLifecycleService {
1181
1250
  logger_1.default.warn(`Port release failed: ${err.message}`);
1182
1251
  }
1183
1252
  }
1184
- // 1. iOS Profiling Archival
1185
- if (device && ((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'ios') {
1253
+ // 1. iOS Profiling Archival. Only on an iPhone, the one place
1254
+ // EventManager.onSessionStarted starts it. A simulator's driver refuses
1255
+ // the stop without relaxed security, and the refused stop counted as a
1256
+ // failed command of the session: every simulator session ended failed.
1257
+ if (device && ((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'ios' && device.realDevice === true) {
1186
1258
  this.logger.info(`[${sessionId}] Stopping iOS profiling for asset archival`);
1187
1259
  try {
1188
1260
  const traceBase64 = yield session.stopPerformanceRecording();
@@ -1263,6 +1335,8 @@ let SessionLifecycleService = class SessionLifecycleService {
1263
1335
  }
1264
1336
  SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
1265
1337
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
1338
+ (0, sessionMemory_1.forgetSessionMemory)(sessionId);
1339
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(sessionId, { failed: true, reason });
1266
1340
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
1267
1341
  }));
1268
1342
  });
@@ -15,6 +15,7 @@ const typedi_1 = require("typedi");
15
15
  const logger_1 = __importDefault(require("../logger"));
16
16
  const config_1 = require("../config");
17
17
  const SocketEvents_1 = require("../enums/SocketEvents");
18
+ const outboundProxy_1 = require("../helpers/outboundProxy");
18
19
  let SocketClient = class SocketClient {
19
20
  constructor() {
20
21
  this.socket = null;
@@ -46,6 +47,16 @@ let SocketClient = class SocketClient {
46
47
  reconnectionDelay: 1000,
47
48
  reconnectionDelayMax: 5000,
48
49
  auth: socketAuth,
50
+ // The proxy the node's other calls to its hub take (its phone reports,
51
+ // its JWKS fetch); it used to go straight to the hub whatever the
52
+ // environment said. Polling goes as plain requests, as axios sends
53
+ // them; only the WebSocket upgrade needs a CONNECT tunnel, and when the
54
+ // proxy refuses one (a stock Squid allows port 443 only) the
55
+ // connection stays on polling.
56
+ transportOptions: {
57
+ polling: { agent: (0, outboundProxy_1.proxyAgentFor)(normalizedHubUrl) },
58
+ websocket: { agent: (0, outboundProxy_1.socketProxyAgentFor)(normalizedHubUrl) },
59
+ },
49
60
  });
50
61
  this.socket.on('connect', () => {
51
62
  var _a, _b, _c;