@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
@@ -90,6 +90,11 @@ const typedi_2 = require("typedi");
90
90
  * they had worked. Within the 15 minutes a hub gives a node's install.
91
91
  */
92
92
  exports.ANDROID_INSTALL_TIMEOUT_MS = 10 * 60000;
93
+ /** How long after adb, or its device tracking, failed to start it is tried again. */
94
+ const ADB_RETRY_MS = 60000;
95
+ function reasonOf(e) {
96
+ return e instanceof Error ? e.message : String(e);
97
+ }
93
98
  /**
94
99
  * Whether a server set to `androidDeviceType` serves this phone: real phones,
95
100
  * emulators ('simulated'), or both, and with `bootedEmulators` only the
@@ -110,6 +115,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
110
115
  this.context = context;
111
116
  this.log = logger_1.default.scope('AndroidManager');
112
117
  this.adbAvailable = true;
118
+ // When adb last failed to start, discovery answers no phones until this
119
+ // time, then tries again.
120
+ this.adbRetryAt = 0;
121
+ // When adb's device tracking last failed to start, when to try it again.
122
+ this.trackerRetryAt = 0;
113
123
  this.abortControl = new Map();
114
124
  this.tracker = undefined;
115
125
  this.remoteTrackers = [];
@@ -161,8 +171,11 @@ let AndroidDeviceManager = class AndroidDeviceManager {
161
171
  return __awaiter(this, void 0, void 0, function* () {
162
172
  var _a;
163
173
  if (!this.adbAvailable) {
164
- logger_1.default.info('adb is not available. So, returning empty list');
165
- return [];
174
+ if (Date.now() < this.adbRetryAt) {
175
+ logger_1.default.debug('adb is not available. So, returning empty list');
176
+ return [];
177
+ }
178
+ this.adbAvailable = true;
166
179
  }
167
180
  let devices = [];
168
181
  try {
@@ -401,32 +414,57 @@ let AndroidDeviceManager = class AndroidDeviceManager {
401
414
  }
402
415
  getAdb() {
403
416
  return __awaiter(this, void 0, void 0, function* () {
404
- try {
405
- if (!this.adb) {
406
- try {
407
- this.adb = yield appium_adb_1.ADB.createADB({});
408
- }
409
- catch (e) {
410
- this.adbAvailable = false;
411
- this.log.error('Could not find ADB');
412
- }
413
- if (process.env.NODE_ENV !== 'test') {
414
- const client = adbkit_1.default.createClient();
415
- this.tracker = yield client.trackDevices();
416
- if (this.tracker && this.adb) {
417
- const originalADBTracking = this.createLocalAdbTracker(this.tracker, this.adb);
418
- yield originalADBTracking();
419
- }
420
- }
417
+ // One start at a time: calls that come while adb or its tracker is
418
+ // starting wait for that start. Two discoveries at once each started a
419
+ // tracker, and each tracker handled every plug event again.
420
+ if (!this.adbStarting) {
421
+ this.adbStarting = this.startAdb().finally(() => {
422
+ this.adbStarting = undefined;
423
+ });
424
+ }
425
+ yield this.adbStarting;
426
+ return { adbInstance: this.adb, adbTracker: this.tracker };
427
+ });
428
+ }
429
+ startAdb() {
430
+ return __awaiter(this, void 0, void 0, function* () {
431
+ if (!this.adb) {
432
+ try {
433
+ this.adb = yield appium_adb_1.ADB.createADB({});
434
+ }
435
+ catch (e) {
436
+ this.adbUnavailable('Could not start adb', e);
421
437
  }
422
438
  }
423
- catch (e) {
424
- logger_1.default.error(`Failed to initialize ADB: ${e}`);
425
- this.adbAvailable = false;
439
+ // Plug events. Without them discovery still lists phones each time it
440
+ // runs, so a failure here hides none; it is tried again a minute later.
441
+ if (this.adb &&
442
+ !this.tracker &&
443
+ process.env.NODE_ENV !== 'test' &&
444
+ Date.now() >= this.trackerRetryAt) {
445
+ try {
446
+ const tracker = yield adbkit_1.default.createClient().trackDevices();
447
+ yield this.createLocalAdbTracker(tracker, this.adb)();
448
+ this.tracker = tracker;
449
+ }
450
+ catch (e) {
451
+ this.trackerRetryAt = Date.now() + ADB_RETRY_MS;
452
+ this.log.error(`Could not follow Android plug events through adb: ${reasonOf(e)}. ` +
453
+ `Trying again in ${ADB_RETRY_MS / 1000} s.`);
454
+ }
426
455
  }
427
- return { adbInstance: this.adb, adbTracker: this.tracker };
428
456
  });
429
457
  }
458
+ /**
459
+ * Discovery answers no phones for a minute, then tries again: adb may work
460
+ * by then (an SDK installed, a stuck adb server restarted). Through 2.15 it
461
+ * never tried again before a restart, and didn't say why.
462
+ */
463
+ adbUnavailable(what, e) {
464
+ this.adbAvailable = false;
465
+ this.adbRetryAt = Date.now() + ADB_RETRY_MS;
466
+ this.log.error(`${what}: ${reasonOf(e)}. Trying again in ${ADB_RETRY_MS / 1000} s.`);
467
+ }
430
468
  getAdbForDevice(udid) {
431
469
  return __awaiter(this, void 0, void 0, function* () {
432
470
  const { adbInstance } = yield this.getAdb();
@@ -266,6 +266,16 @@ let IOSDeviceManager = class IOSDeviceManager {
266
266
  var _a;
267
267
  if (device.cloud)
268
268
  return true;
269
+ // A simulator's WebDriverAgent is the XCUITest driver's: it builds,
270
+ // installs and launches it on the session's wdaLocalPort (iOSCapabilities)
271
+ // once Appium creates the session, after this check, and boots a shut-down
272
+ // simulator itself. Xenon runs WDA only on an iPhone (go-ios runwda), so
273
+ // asking for it here refused every simulator whose WDA wasn't already up,
274
+ // and recoverHealth's reboot never starts one. Through 2.15 it did.
275
+ if (!device.realDevice) {
276
+ this.log.info(`[SessionStart] ${device.udid} is a simulator: the driver starts its WebDriverAgent`);
277
+ return true;
278
+ }
269
279
  this.log.info(`🚀 [SessionStart] Verifying readiness for ${device.udid}...`);
270
280
  const streamService = typedi_1.Container.get(IOSStreamService_1.default);
271
281
  const streamStatus = streamService.getStreamStatus(device.udid);
@@ -93,27 +93,34 @@ let AndroidH264StreamService = class AndroidH264StreamService {
93
93
  startWatchdog() {
94
94
  // unref()ed below: the server's listener keeps the process alive; this only
95
95
  // needs to fire while it runs.
96
- setInterval(() => {
97
- const now = Date.now();
98
- for (const [udid, s] of this.sessions.entries()) {
99
- if (s.status !== 'running')
100
- continue;
101
- if (s.mux.clientCount > 0) {
102
- s.emptyAt = undefined;
103
- }
104
- else if (s.emptyAt === undefined) {
105
- s.emptyAt = now;
106
- }
107
- else if (now - s.emptyAt > IDLE_TIMEOUT_MS) {
108
- logger_1.default.info(`[${udid}] Stopping idle H.264 stream (no viewers for ${IDLE_TIMEOUT_MS}ms)`);
109
- // The stream went, but the live-preview hold stayed: with H.264
110
- // preview, a tab closed without releasing kept the device busy for
111
- // good. stop() itself must not release (a recording starting calls
112
- // it); an idle stop does, if nothing else uses the device.
113
- void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
114
- }
96
+ setInterval(() => this.sweep(Date.now()), 60000).unref();
97
+ }
98
+ /** One look at every capture: ends those nobody has watched for long enough. */
99
+ sweep(now) {
100
+ for (const [udid, s] of this.sessions.entries()) {
101
+ if (s.status !== 'running')
102
+ continue;
103
+ if (s.mux.clientCount > 0) {
104
+ s.emptyAt = undefined;
105
+ }
106
+ else if (s.endWhenUnwatched && !this.startPromises.has(udid)) {
107
+ // A page asked for MJPEG while this capture was starting for a viewer
108
+ // who then never came (endWhenUnwatched leaves a start alone).
109
+ logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
110
+ void this.stop(udid);
115
111
  }
116
- }, 60000).unref();
112
+ else if (s.emptyAt === undefined) {
113
+ s.emptyAt = now;
114
+ }
115
+ else if (now - s.emptyAt > IDLE_TIMEOUT_MS) {
116
+ logger_1.default.info(`[${udid}] Stopping idle H.264 stream (no viewers for ${IDLE_TIMEOUT_MS}ms)`);
117
+ // The stream went, but the live-preview hold stayed: with H.264
118
+ // preview, a tab closed without releasing kept the device busy for
119
+ // good. stop() itself must not release (a recording starting calls
120
+ // it); an idle stop does, if nothing else uses the device.
121
+ void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
122
+ }
123
+ }
117
124
  }
118
125
  getMultiplexer(udid) {
119
126
  var _a;
@@ -131,6 +138,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
131
138
  var _a, _b;
132
139
  const mux = new H264Multiplexer_1.H264Multiplexer();
133
140
  const session = { status: 'running', mux };
141
+ mux.onEmpty(() => this.endIfUnwatched(udid, session));
134
142
  this.sessions.set(udid, session);
135
143
  try {
136
144
  let resolveConfig = () => undefined;
@@ -178,6 +186,44 @@ let AndroidH264StreamService = class AndroidH264StreamService {
178
186
  }
179
187
  });
180
188
  }
189
+ /**
190
+ * A page that showed this phone's H.264 preview now shows MJPEG (its player
191
+ * failed, or it cannot play H.264), and the MJPEG capture is starting for
192
+ * it. One capture per phone where possible, but never at the expense of a
193
+ * viewer still playing H.264: another tile, tab or admin may be. So the
194
+ * capture ends now if nobody watches it, else the moment its last viewer
195
+ * leaves, never after the idle wait. A viewer who joins meanwhile is served
196
+ * and ends it in turn. A capture still starting is left to the viewer it is
197
+ * starting for, and the watchdog ends it if that viewer never comes.
198
+ */
199
+ endWhenUnwatched(udid) {
200
+ return __awaiter(this, void 0, void 0, function* () {
201
+ const session = this.sessions.get(udid);
202
+ if (!session || session.status !== 'running')
203
+ return;
204
+ session.endWhenUnwatched = true;
205
+ const viewers = session.mux.clientCount;
206
+ if (viewers > 0) {
207
+ logger_1.default.info(`[${udid}] H.264 stream kept beside MJPEG for its ${viewers} viewer(s); ` +
208
+ 'it ends when the last one leaves');
209
+ return;
210
+ }
211
+ if (this.startPromises.has(udid)) {
212
+ logger_1.default.info(`[${udid}] H.264 stream still starting: kept for the viewer it is starting for`);
213
+ return;
214
+ }
215
+ logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
216
+ yield this.stop(udid);
217
+ });
218
+ }
219
+ /** The last viewer left `session`: end it if a page asked (endWhenUnwatched). */
220
+ endIfUnwatched(udid, session) {
221
+ // A capture replaced since (a stop and a new start) is not this one.
222
+ if (this.sessions.get(udid) !== session || !session.endWhenUnwatched)
223
+ return;
224
+ logger_1.default.info(`[${udid}] Stopping the H.264 stream: its last viewer left, the phone is shown as MJPEG`);
225
+ void this.stop(udid);
226
+ }
181
227
  stop(udid) {
182
228
  return __awaiter(this, void 0, void 0, function* () {
183
229
  var _a;
@@ -192,6 +238,9 @@ let AndroidH264StreamService = class AndroidH264StreamService {
192
238
  /* best-effort */
193
239
  }
194
240
  this.sessions.delete(udid);
241
+ // Viewers still on it (a recording starting, a stream/stop) are told, so
242
+ // their players fall back to MJPEG instead of freezing on the last frame.
243
+ session.mux.close();
195
244
  logger_1.default.info(`[${udid}] H.264 stream terminated.`);
196
245
  });
197
246
  }
@@ -9,6 +9,7 @@ class H264Multiplexer {
9
9
  constructor() {
10
10
  this.clients = new Set();
11
11
  this.gop = []; // current GOP: [keyframe, ...deltas-since]
12
+ this.closed = false;
12
13
  }
13
14
  setConfig(p) {
14
15
  this.config = p;
@@ -16,9 +17,21 @@ class H264Multiplexer {
16
17
  get clientCount() {
17
18
  return this.clients.size;
18
19
  }
19
- /** Register a client sink. Returns a remover. */
20
- addClient(send) {
21
- const c = { send, started: false };
20
+ /** Called each time the last client leaves. */
21
+ onEmpty(listener) {
22
+ this.emptyListener = listener;
23
+ }
24
+ /**
25
+ * Register a client sink, and how to end it when the stream does (see
26
+ * `close`). Returns a remover (safe to call twice).
27
+ */
28
+ addClient(send, end) {
29
+ const c = { send, started: false, end };
30
+ if (this.closed) {
31
+ // Joined a stream already stopped: end it now rather than starve it.
32
+ end === null || end === void 0 ? void 0 : end();
33
+ return () => undefined;
34
+ }
22
35
  this.clients.add(c);
23
36
  if (this.config)
24
37
  send(this.config);
@@ -29,9 +42,30 @@ class H264Multiplexer {
29
42
  c.started = true;
30
43
  }
31
44
  return () => {
32
- this.clients.delete(c);
45
+ var _a;
46
+ if (this.clients.delete(c) && this.clients.size === 0)
47
+ (_a = this.emptyListener) === null || _a === void 0 ? void 0 : _a.call(this);
33
48
  };
34
49
  }
50
+ /**
51
+ * The capture stopped: end every client (its socket closes, so its player
52
+ * falls back to MJPEG instead of freezing on its last frame). Not an
53
+ * `onEmpty`: nobody left, the stream did.
54
+ */
55
+ close() {
56
+ var _a;
57
+ this.closed = true;
58
+ const ending = [...this.clients];
59
+ this.clients.clear();
60
+ for (const c of ending) {
61
+ try {
62
+ (_a = c.end) === null || _a === void 0 ? void 0 : _a.call(c);
63
+ }
64
+ catch (_b) {
65
+ /* one client's end must not keep the others open */
66
+ }
67
+ }
68
+ }
35
69
  /** Feed one upstream packet; fans out per the join semantics above. */
36
70
  push(p) {
37
71
  if (p.type === 'config') {
@@ -87,6 +87,7 @@ const device_managers_1 = require("./device-managers");
87
87
  const device_service_1 = require("./data-service/device-service");
88
88
  const deviceClaims_1 = require("./data-service/deviceClaims");
89
89
  const PluginContext_1 = require("./PluginContext");
90
+ const TracingService_1 = require("./services/TracingService");
90
91
  const logger_1 = __importDefault(require("./logger"));
91
92
  const Platform_1 = __importDefault(require("./enums/Platform"));
92
93
  const lodash_1 = __importDefault(require("lodash"));
@@ -250,7 +251,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
250
251
  if (device !== null)
251
252
  return true;
252
253
  }
253
- logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}}`);
254
+ logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}`);
254
255
  return false;
255
256
  }));
256
257
  }), { timeout, intervalBetweenAttempts });
@@ -269,7 +270,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
269
270
  else if (possibleDevice && (0, device_service_1.isDeviceReserved)(possibleDevice)) {
270
271
  failureReason = `Device is reserved by ${possibleDevice.reservedBy}.`;
271
272
  }
272
- throw new Error(`${failureReason}. Device request: ${JSON.stringify(filterCopy)}`);
273
+ throw new Error(`${failureReason} Device request: ${JSON.stringify(filterCopy)}`);
273
274
  }
274
275
  // We have a locked device here
275
276
  if (device !== null) {
@@ -696,8 +697,16 @@ function releaseBlockedDevices(newCommandTimeout) {
696
697
  if (timeSinceLastCmdExecuted > timeoutSeconds) {
697
698
  // unblock regardless of whether the device has session or not
698
699
  logger_1.default.info(`Unblocking device ${device.udid} at host ${device.host} because it has been idle for ${timeSinceLastCmdExecuted} seconds`);
700
+ const idleSession = (_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id;
699
701
  // Before anything below can release the phone (onSessionStopped too).
700
- yield restoreIdleSessionNetwork((_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id);
702
+ yield restoreIdleSessionNetwork(idleSession);
703
+ yield forgetIdleSessionMemory(idleSession);
704
+ if (idleSession) {
705
+ typedi_1.Container.get(TracingService_1.TracingService).endSessionSpan(idleSession, {
706
+ failed: true,
707
+ reason: 'Session timed out due to inactivity',
708
+ });
709
+ }
701
710
  // Principal Protection: If this device has an active dashboard session, stop it properly
702
711
  if (device.session_id) {
703
712
  const sessionId = device.session_id;
@@ -739,6 +748,23 @@ function restoreIdleSessionNetwork(sessionId) {
739
748
  }
740
749
  });
741
750
  }
751
+ /**
752
+ * The idle session's virtual elements and autowait settings
753
+ * (sessionMemory.ts). Imported lazily, as above. Never throws.
754
+ */
755
+ function forgetIdleSessionMemory(sessionId) {
756
+ return __awaiter(this, void 0, void 0, function* () {
757
+ if (!sessionId)
758
+ return;
759
+ try {
760
+ const { forgetSessionMemory } = yield Promise.resolve().then(() => __importStar(require('./sessions/sessionMemory')));
761
+ forgetSessionMemory(sessionId);
762
+ }
763
+ catch (err) {
764
+ logger_1.default.warn(`Could not forget idle session ${sessionId}'s elements: ${err}`);
765
+ }
766
+ });
767
+ }
742
768
  function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
743
769
  return __awaiter(this, void 0, void 0, function* () {
744
770
  if (cronTimerToReleaseBlockedDevices) {
@@ -34,7 +34,7 @@ const createWith = (hubGrants) => ({
34
34
  });
35
35
  const dashboardHooks = {
36
36
  before: (sessionId, command, req, res) => event_manager_1.DASHBORD_EVENT_MANAGER.beforeSessionCommand(sessionId, command, req, res),
37
- after: (sessionId, command, req, res, body) => event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, command, null, req, res, body),
37
+ after: (sessionId, command, req, res, body, heal) => event_manager_1.DASHBORD_EVENT_MANAGER.afterSessionCommand(sessionId, command, null, req, res, body, heal),
38
38
  };
39
39
  /** A hub: sessions other servers run are forwarded to them. */
40
40
  function hubGatewayOptions(config) {
@@ -11,10 +11,10 @@ exports.readAnswer = readAnswer;
11
11
  exports.sendAnswer = sendAnswer;
12
12
  const http_1 = __importDefault(require("http"));
13
13
  const https_1 = __importDefault(require("https"));
14
- const https_proxy_agent_1 = require("https-proxy-agent");
15
- const http_proxy_agent_1 = require("http-proxy-agent");
16
14
  const internalCall_1 = require("./internalCall");
17
15
  const hubSessionToken_1 = require("./hubSessionToken");
16
+ const healReport_1 = require("./healReport");
17
+ const outboundProxy_1 = require("../helpers/outboundProxy");
18
18
  /**
19
19
  * Sending a session command on to the node that runs the session, and handing
20
20
  * the node's answer back to the client unchanged.
@@ -44,6 +44,7 @@ const NEVER_FORWARDED = new Set([
44
44
  'x-xenon-token',
45
45
  internalCall_1.INTERNAL_CALL_HEADER,
46
46
  hubSessionToken_1.HUB_TOKEN_HEADER,
47
+ healReport_1.HEAL_REPORT_HEADER,
47
48
  ]);
48
49
  function forwardedRequestHeaders(incoming) {
49
50
  const out = {};
@@ -58,17 +59,11 @@ function forwardedRequestHeaders(incoming) {
58
59
  out['accept-encoding'] = 'identity';
59
60
  return out;
60
61
  }
61
- /** HTTP(S)_PROXY from the environment, as the old command proxy honoured it. */
62
- function proxyAgentFor(url) {
63
- const proxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
64
- if (!proxy)
65
- return undefined;
66
- return url.protocol === 'https:' ? new https_proxy_agent_1.HttpsProxyAgent(proxy) : new http_proxy_agent_1.HttpProxyAgent(proxy);
67
- }
68
62
  /**
69
63
  * Send one request to a node and resolve with its response stream. No retry:
70
64
  * a WebDriver command is not safe to repeat. Credentials in the URL (a cloud
71
- * provider's) become basic auth.
65
+ * provider's) become basic auth. Through the environment's proxy for the URL,
66
+ * by axios's rule (helpers/outboundProxy.ts).
72
67
  */
73
68
  function sendToNode(request) {
74
69
  const url = new URL(request.url);
@@ -78,7 +73,7 @@ function sendToNode(request) {
78
73
  if (Buffer.isBuffer(body))
79
74
  headers['content-length'] = body.length;
80
75
  return new Promise((resolve, reject) => {
81
- const outgoing = lib.request(url, { method: request.method, headers, agent: proxyAgentFor(url), signal: request.signal }, resolve);
76
+ const outgoing = lib.request(url, { method: request.method, headers, agent: (0, outboundProxy_1.proxyAgentFor)(url), signal: request.signal }, resolve);
82
77
  outgoing.on('error', reject);
83
78
  if (body && !Buffer.isBuffer(body)) {
84
79
  body.on('error', (err) => outgoing.destroy(err));
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HEAL_REPORT_HEADER = void 0;
4
+ exports.runReportingHeals = runReportingHeals;
5
+ exports.reportHeal = reportHeal;
6
+ exports.takeHealReport = takeHealReport;
7
+ const async_hooks_1 = require("async_hooks");
8
+ /**
9
+ * A heal on a node's phone, on its way back to the hub.
10
+ *
11
+ * A session the hub created runs its commands on the node, so the node's
12
+ * CommandInterceptor heals its finds. The node keeps no record of such a
13
+ * session; the hub does. So the node puts the heal on its answer to the
14
+ * forwarded command, as the `x-xenon-heal` header, and the hub takes it off
15
+ * before relaying the answer and records it with the command, as it records a
16
+ * local session's heal.
17
+ *
18
+ * The node's session gateway runs a command that came from a hub inside
19
+ * `runReportingHeals`, so the interceptor, deep inside Appium's route, finds
20
+ * the answer to put it on. Anywhere else `reportHeal` does nothing, and the
21
+ * server records the heal itself.
22
+ */
23
+ exports.HEAL_REPORT_HEADER = 'x-xenon-heal';
24
+ /**
25
+ * A header longer than this isn't sent: the hub's HTTP parser refuses an
26
+ * answer whose headers pass 16 KB, and a refused answer would fail the
27
+ * command. A heal of selectors that long goes unrecorded instead.
28
+ */
29
+ const MAX_HEADER_LENGTH = 8 * 1024;
30
+ const answers = new async_hooks_1.AsyncLocalStorage();
31
+ /** Run the rest of a command that came from a hub with its answer at hand. */
32
+ function runReportingHeals(res, fn) {
33
+ const answer = { res };
34
+ const forget = () => {
35
+ answer.res = null;
36
+ };
37
+ res.once('finish', forget);
38
+ res.once('close', forget);
39
+ return answers.run(answer, fn);
40
+ }
41
+ /**
42
+ * Put `heal` on the answer to the hub's command this code runs for.
43
+ * `not-from-hub` when the command didn't come from a hub (or the answer has
44
+ * gone): the caller records the heal itself.
45
+ */
46
+ function reportHeal(heal) {
47
+ var _a;
48
+ const res = (_a = answers.getStore()) === null || _a === void 0 ? void 0 : _a.res;
49
+ if (!res || res.headersSent)
50
+ return 'not-from-hub';
51
+ const encoded = Buffer.from(JSON.stringify(heal), 'utf8').toString('base64url');
52
+ if (encoded.length > MAX_HEADER_LENGTH)
53
+ return 'too-long';
54
+ res.setHeader(exports.HEAL_REPORT_HEADER, encoded);
55
+ return 'reported';
56
+ }
57
+ /**
58
+ * The heal a node put on its answer, taken off the headers so it isn't
59
+ * relayed to the client. Undefined when there is none, or it isn't one.
60
+ */
61
+ function takeHealReport(headers) {
62
+ const raw = headers[exports.HEAL_REPORT_HEADER];
63
+ delete headers[exports.HEAL_REPORT_HEADER];
64
+ if (typeof raw !== 'string' || !raw)
65
+ return undefined;
66
+ try {
67
+ const heal = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8'));
68
+ if (!heal ||
69
+ typeof heal.originalSelector !== 'string' ||
70
+ typeof heal.healedSelector !== 'string' ||
71
+ typeof heal.confidence !== 'number') {
72
+ return undefined;
73
+ }
74
+ const optional = (v) => (typeof v === 'string' ? v : undefined);
75
+ return {
76
+ originalSelector: heal.originalSelector,
77
+ originalStrategy: optional(heal.originalStrategy),
78
+ healedSelector: heal.healedSelector,
79
+ healedStrategy: optional(heal.healedStrategy),
80
+ confidence: heal.confidence,
81
+ tier: typeof heal.tier === 'number' ? heal.tier : undefined,
82
+ };
83
+ }
84
+ catch (_a) {
85
+ return undefined;
86
+ }
87
+ }
@@ -59,6 +59,7 @@ const jose = __importStar(require("jose"));
59
59
  const logger_1 = __importDefault(require("../logger"));
60
60
  const JwtKeyService_1 = require("../services/token/JwtKeyService");
61
61
  const singleUseLedger_1 = require("../services/token/singleUseLedger");
62
+ const outboundProxy_1 = require("../helpers/outboundProxy");
62
63
  /**
63
64
  * The credential a hub's calls to a node carry.
64
65
  *
@@ -221,6 +222,22 @@ exports.HubSessionTokenIssuer = HubSessionTokenIssuer = __decorate([
221
222
  function hubJwksUrl(hub) {
222
223
  return new URL('/xenon/api/auth/jwks.json', hub);
223
224
  }
225
+ /**
226
+ * A key lookup that fails after `ms`. jose's own timeout starts once its
227
+ * request has a socket, which a proxy agent hands it only after the proxy
228
+ * answers, so a proxy that never answers held every token check.
229
+ */
230
+ function withinTime(getKey, ms) {
231
+ return (header, token) => {
232
+ let timer;
233
+ return Promise.race([
234
+ Promise.resolve(getKey(header, token)),
235
+ new Promise((_, reject) => {
236
+ timer = setTimeout(() => reject(new Error(`the hub's keys did not arrive in ${ms} ms`)), ms);
237
+ }),
238
+ ]).finally(() => clearTimeout(timer));
239
+ };
240
+ }
224
241
  /** The node could not check a hub token (the hub's keys could not be fetched). */
225
242
  class HubTokenUnavailableError extends Error {
226
243
  }
@@ -251,15 +268,21 @@ function isVerdict(err) {
251
268
  * a token names a key it does not have (a hub that rotated its key).
252
269
  */
253
270
  class HubSessionTokenVerifier {
254
- constructor(hubUrl, keys) {
271
+ constructor(hubUrl, keys, opts = {}) {
272
+ var _a;
255
273
  /** The create tokens already taken, by id, until each one expires. */
256
274
  this.usedCreates = new singleUseLedger_1.SingleUseLedger(exports.HUB_CREATE_TTL_SECONDS);
275
+ const jwksUrl = hubJwksUrl(hubUrl);
276
+ const timeoutMs = (_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : 5000;
257
277
  this.keys =
258
- keys !== null && keys !== void 0 ? keys : jose.createRemoteJWKSet(hubJwksUrl(hubUrl), {
259
- timeoutDuration: 5000,
278
+ keys !== null && keys !== void 0 ? keys : withinTime(jose.createRemoteJWKSet(jwksUrl, {
279
+ timeoutDuration: timeoutMs,
260
280
  cooldownDuration: 30000,
261
281
  cacheMaxAge: 10 * 60000,
262
- });
282
+ // The proxy the node's axios calls to its hub take; jose's own
283
+ // fetch takes none.
284
+ agent: (0, outboundProxy_1.proxyAgentFor)(jwksUrl),
285
+ }), timeoutMs);
263
286
  }
264
287
  verify(token, sessionId) {
265
288
  return __awaiter(this, void 0, void 0, function* () {
@@ -30,6 +30,7 @@ const logger_1 = __importDefault(require("../logger"));
30
30
  const helpers_1 = require("../helpers");
31
31
  const appiumBasePath_1 = require("../app/appiumBasePath");
32
32
  const PluginContext_1 = require("../PluginContext");
33
+ const outboundProxy_1 = require("../helpers/outboundProxy");
33
34
  /**
34
35
  * Where a node's WebDriver API lives.
35
36
  *
@@ -95,11 +96,8 @@ let NodeBasePathResolver = class NodeBasePathResolver {
95
96
  let advertised;
96
97
  try {
97
98
  const rejectUnauthorized = (_a = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs) === null || _a === void 0 ? void 0 : _a.tlsRejectUnauthorized;
98
- const response = yield axios_1.default.get(`${origin}${exports.WEBDRIVER_INFO_PATH}`, {
99
- timeout: exports.LOOKUP_TIMEOUT_MS,
100
- validateStatus: (status) => status === 200,
101
- httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }),
102
- });
99
+ const url = `${origin}${exports.WEBDRIVER_INFO_PATH}`;
100
+ const response = yield axios_1.default.get(url, Object.assign({ timeout: exports.LOOKUP_TIMEOUT_MS, validateStatus: (status) => status === 200, httpsAgent: new https_1.default.Agent({ rejectUnauthorized: rejectUnauthorized !== false }) }, (0, outboundProxy_1.axiosProxyConfig)(url, { rejectUnauthorized: rejectUnauthorized !== false })));
103
101
  const basePath = (_b = response.data) === null || _b === void 0 ? void 0 : _b.basePath;
104
102
  if (typeof basePath === 'string')
105
103
  advertised = (0, appiumBasePath_1.normalizeBasePath)(basePath);
@@ -15,6 +15,7 @@ exports.createHubRouting = createHubRouting;
15
15
  const commandAuth_1 = require("../middleware/commandAuth");
16
16
  const internalCall_1 = require("./internalCall");
17
17
  const hubSessionToken_1 = require("./hubSessionToken");
18
+ const healReport_1 = require("./healReport");
18
19
  const forwardToNode_1 = require("./forwardToNode");
19
20
  /** Where a session runs could not be determined (a database error). */
20
21
  exports.ROUTING_UNAVAILABLE_STATUS = 503;
@@ -62,7 +63,14 @@ function createSessionGatewayLayer(deps) {
62
63
  const latency = (_b = (_a = deps.latencyOf) === null || _a === void 0 ? void 0 : _a.call(deps, sessionId)) !== null && _b !== void 0 ? _b : 0;
63
64
  if (latency > 0)
64
65
  yield new Promise((resolve) => setTimeout(resolve, latency));
65
- next();
66
+ // On a node, a heal of the hub's command goes back to the hub on the answer
67
+ // (healReport.ts). The header proves nothing elsewhere: a hub or a
68
+ // standalone server checks no hub token, so a client could send one there
69
+ // to keep its heals out of the record.
70
+ if (fromHub && deps.hubTokens)
71
+ (0, healReport_1.runReportingHeals)(res, next);
72
+ else
73
+ next();
66
74
  });
67
75
  }
68
76
  return function xenonSessionGateway(req, res, next) {
@@ -176,8 +184,10 @@ function createHubRouting(deps) {
176
184
  abort.abort();
177
185
  });
178
186
  let captured;
187
+ let heal;
179
188
  try {
180
189
  const upstream = yield (0, forwardToNode_1.sendToNode)(yield nodeRequest(req, sessionId, location, abort.signal));
190
+ heal = (0, healReport_1.takeHealReport)(upstream.headers);
181
191
  captured = yield (0, forwardToNode_1.relayAnswer)(upstream, res, !!hooks);
182
192
  }
183
193
  catch (error) {
@@ -192,7 +202,7 @@ function createHubRouting(deps) {
192
202
  }
193
203
  if (hooks && captured !== undefined) {
194
204
  yield hooks
195
- .after(sessionId, command, req, res, captured)
205
+ .after(sessionId, command, req, res, captured, heal)
196
206
  .catch((error) => logger.warn(`Dashboard log failed for session ${sessionId}: ${(0, commandAuth_1.summarize)(error)}`));
197
207
  }
198
208
  });