@xenon-device-management/xenon 2.0.0 → 2.2.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 (289) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +3 -4
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
  5. package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
  69. package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
  70. package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +11 -104
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/routers/nodePhoneControl.js +301 -0
  84. package/lib/src/app/ws/h264StreamWs.js +43 -12
  85. package/lib/src/app/ws/logcatWs.js +48 -14
  86. package/lib/src/app/ws/nodeSocketRelay.js +167 -0
  87. package/lib/src/app/ws/upgradeRouter.js +215 -0
  88. package/lib/src/dashboard/event-manager.js +7 -6
  89. package/lib/src/data-service/device-service.js +152 -49
  90. package/lib/src/data-service/device-store.js +92 -10
  91. package/lib/src/data-service/deviceClaims.js +116 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  93. package/lib/src/data-service/prisma-store.js +102 -31
  94. package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
  95. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  96. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  97. package/lib/src/device-managers/NodeDevices.js +7 -2
  98. package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
  99. package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
  100. package/lib/src/device-managers/android/previewHold.js +2 -2
  101. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  102. package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
  103. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  104. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  105. package/lib/src/device-utils.js +78 -24
  106. package/lib/src/gateway/createHandoff.js +73 -0
  107. package/lib/src/gateway/defaultGateway.js +80 -0
  108. package/lib/src/gateway/forwardToNode.js +147 -0
  109. package/lib/src/gateway/hubSessionToken.js +356 -0
  110. package/lib/src/gateway/internalCall.js +90 -0
  111. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  112. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  113. package/lib/src/gateway/sessionCreate.js +136 -0
  114. package/lib/src/gateway/sessionGateway.js +201 -0
  115. package/lib/src/gateway/sessionLocator.js +100 -0
  116. package/lib/src/generated/client/edge.js +7 -4
  117. package/lib/src/generated/client/index-browser.js +4 -1
  118. package/lib/src/generated/client/index.d.ts +112 -1
  119. package/lib/src/generated/client/index.js +7 -4
  120. package/lib/src/generated/client/package.json +1 -1
  121. package/lib/src/generated/client/schema.prisma +9 -0
  122. package/lib/src/generated/client/wasm.js +4 -1
  123. package/lib/src/helpers/index.js +5 -1
  124. package/lib/src/logger.js +5 -0
  125. package/lib/src/middleware/authMiddleware.js +80 -0
  126. package/lib/src/middleware/commandAuth.js +31 -17
  127. package/lib/src/middleware/csrfMiddleware.js +3 -2
  128. package/lib/src/middleware/deviceAccessGuard.js +4 -1
  129. package/lib/src/middleware/sessionListingFilter.js +153 -0
  130. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  131. package/lib/src/plugin.js +27 -1
  132. package/lib/src/prisma.js +2 -1
  133. package/lib/src/services/DeviceReconciler.js +5 -2
  134. package/lib/src/services/OrphanSweeper.js +12 -19
  135. package/lib/src/services/PortAllocator.js +75 -16
  136. package/lib/src/services/ServerManager.js +63 -30
  137. package/lib/src/services/SessionLifecycleService.js +335 -76
  138. package/lib/src/services/ShutdownCoordinator.js +7 -1
  139. package/lib/src/services/SocketServer.js +11 -2
  140. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  141. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  142. package/lib/src/services/device-access/ticketActorAccess.js +3 -1
  143. package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
  144. package/lib/src/services/lease/LeaseService.js +19 -5
  145. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  146. package/lib/src/services/session/sessionCredentials.js +7 -59
  147. package/lib/src/sessions/CloudSession.js +12 -0
  148. package/lib/src/sessions/LocalSession.js +52 -26
  149. package/lib/src/sessions/RemoteSession.js +116 -9
  150. package/lib/src/sessions/SessionManager.js +49 -6
  151. package/lib/src/sessions/appiumUmbrella.js +32 -0
  152. package/lib/test/helpers/appium-umbrella.js +84 -0
  153. package/lib/test/helpers/fake-device-table.js +97 -0
  154. package/lib/test/helpers/loki-stores.js +41 -0
  155. package/lib/test/helpers/loopbackRequest.js +77 -0
  156. package/lib/test/helpers/own-node-id.js +29 -0
  157. package/lib/test/helpers/scratch-database.js +100 -0
  158. package/lib/test/helpers/scratch-port-leases.js +119 -0
  159. package/lib/test/helpers/test-container.js +35 -27
  160. package/lib/test/integration/androidDevices.spec.js +5 -4
  161. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  162. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  163. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  164. package/lib/test/setup/scratch-default-database.js +32 -0
  165. package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
  166. package/lib/test/unit/DeviceModel.spec.js +9 -9
  167. package/lib/test/unit/HealingCascade.test.js +5 -1
  168. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  169. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  170. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  171. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  172. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  173. package/lib/test/unit/android-h264-service.spec.js +24 -0
  174. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  175. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  176. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  177. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  178. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  179. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  180. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  181. package/lib/test/unit/authMiddleware.test.js +3 -0
  182. package/lib/test/unit/bug-report/route.spec.js +9 -9
  183. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  184. package/lib/test/unit/clipboard-route.spec.js +22 -8
  185. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  186. package/lib/test/unit/command-auth.spec.js +31 -31
  187. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  188. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  189. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  190. package/lib/test/unit/create-error-message.spec.js +43 -0
  191. package/lib/test/unit/create-handoff.spec.js +86 -0
  192. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  193. package/lib/test/unit/device-access-guard.spec.js +66 -28
  194. package/lib/test/unit/device-service.spec.js +4 -0
  195. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  196. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  197. package/lib/test/unit/device-team-guard.spec.js +20 -20
  198. package/lib/test/unit/device-utils.spec.js +8 -0
  199. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  200. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  201. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  202. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  203. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  204. package/lib/test/unit/helpers.spec.js +27 -0
  205. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  206. package/lib/test/unit/hub-create-once.spec.js +190 -0
  207. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  208. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  209. package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
  210. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  211. package/lib/test/unit/hub-node-race.spec.js +426 -0
  212. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  213. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  214. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  215. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  216. package/lib/test/unit/hub-session-token.spec.js +265 -0
  217. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  218. package/lib/test/unit/install-repository-app-team.spec.js +6 -2
  219. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  220. package/lib/test/unit/internal-call.spec.js +181 -0
  221. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  222. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  223. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  224. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  225. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  226. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  227. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  228. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  229. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
  230. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  231. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  232. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  233. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  234. package/lib/test/unit/logcat-ws.spec.js +12 -1
  235. package/lib/test/unit/loopback-request.spec.js +81 -0
  236. package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
  237. package/lib/test/unit/node-create-caps.spec.js +54 -0
  238. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  239. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  240. package/lib/test/unit/node-phone-control.spec.js +131 -0
  241. package/lib/test/unit/node-socket-relay.spec.js +357 -0
  242. package/lib/test/unit/omni-vision.spec.js +7 -3
  243. package/lib/test/unit/plugin.spec.js +45 -4
  244. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  245. package/lib/test/unit/profile-router.test.js +11 -11
  246. package/lib/test/unit/queue-requester.spec.js +5 -0
  247. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  248. package/lib/test/unit/queue-visibility.spec.js +1 -1
  249. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  250. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  251. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  252. package/lib/test/unit/recording-store.spec.js +9 -3
  253. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  254. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  255. package/lib/test/unit/remote-session-create.spec.js +130 -0
  256. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  257. package/lib/test/unit/reset-link.test.js +14 -14
  258. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  259. package/lib/test/unit/session-assets.spec.js +9 -9
  260. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  261. package/lib/test/unit/session-create-layer.spec.js +255 -0
  262. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  263. package/lib/test/unit/session-credentials.spec.js +0 -47
  264. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  265. package/lib/test/unit/session-locator.spec.js +250 -0
  266. package/lib/test/unit/session-node-calls.spec.js +174 -0
  267. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  268. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  269. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  270. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  271. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  272. package/lib/test/unit/stream-leave-route.spec.js +19 -7
  273. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  274. package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
  275. package/lib/test/unit/stream-stop-recording.spec.js +17 -5
  276. package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
  277. package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
  278. package/lib/test/unit/upgrade-router.spec.js +446 -0
  279. package/lib/test/unit/users-router.test.js +11 -8
  280. package/lib/test/unit/verify-credential.spec.js +4 -1
  281. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  282. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  283. package/lib/tsconfig.tsbuildinfo +1 -1
  284. package/package.json +3 -4
  285. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  286. package/prisma/schema.prisma +9 -0
  287. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  288. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  289. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -17,6 +17,8 @@ exports.parseH264WsPath = parseH264WsPath;
17
17
  exports.attachH264Ws = attachH264Ws;
18
18
  const ws_1 = require("ws");
19
19
  const logger_1 = __importDefault(require("../../logger"));
20
+ const upgradeRouter_1 = require("./upgradeRouter");
21
+ const nodeSocketRelay_1 = require("./nodeSocketRelay");
20
22
  /**
21
23
  * Authenticated H.264 video WebSocket.
22
24
  *
@@ -44,19 +46,24 @@ function parseH264WsPath(url) {
44
46
  }
45
47
  /**
46
48
  * Attach the h264 WebSocket upgrade handler to the plugin's HTTP server. Only
47
- * claims `/xenon/api/control/:udid/stream/h264` — other upgrades (e.g.
48
- * socket.io) are left for their own listeners. Every connection must redeem a
49
- * valid stream ticket before any video flows.
49
+ * claims `/xenon/api/control/:udid/stream/h264` (with a ticket), as a route of
50
+ * the server's upgrade router, so no other handler sees these sockets and this
51
+ * one sees no others (upgradeRouter.ts). Every connection must redeem a valid
52
+ * stream ticket before any video flows.
50
53
  */
51
54
  function attachH264Ws(server, deps) {
52
55
  var _a;
53
56
  const wss = new ws_1.WebSocketServer({ noServer: true });
54
57
  const maxBuffered = (_a = deps.maxBufferedBytes) !== null && _a !== void 0 ? _a : 4 * 1024 * 1024;
55
- server.on('upgrade', (req, socket, head) => {
56
- const parsed = req.url ? parseH264WsPath(req.url) : null;
57
- if (!parsed)
58
- return; // not our path — let other upgrade listeners handle it
58
+ (0, upgradeRouter_1.upgradeRouterFor)(server).add({
59
+ name: 'H.264 live preview',
60
+ matches: (req) => !!req.url && parseH264WsPath(req.url) !== null,
61
+ handle: upgrade,
62
+ });
63
+ function upgrade(req, socket, head) {
64
+ const parsed = parseH264WsPath(req.url);
59
65
  wss.handleUpgrade(req, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
66
+ var _a, _b, _c;
60
67
  // Register cleanup BEFORE the awaits below. redeem + startStream can take
61
68
  // seconds; if the client disconnects during that window we must still
62
69
  // remove it from the multiplexer — otherwise clientCount stays inflated
@@ -69,20 +76,44 @@ function attachH264Ws(server, deps) {
69
76
  };
70
77
  ws.on('close', onClose);
71
78
  ws.on('error', onClose);
79
+ let actor;
72
80
  try {
73
- yield deps.redeem(parsed.ticket, parsed.udid);
81
+ actor = yield deps.redeem(parsed.ticket, parsed.udid);
74
82
  }
75
- catch (_a) {
83
+ catch (_d) {
76
84
  try {
77
85
  ws.close(1008, 'unauthorized');
78
86
  }
79
- catch (_b) {
87
+ catch (_e) {
80
88
  /* noop */
81
89
  }
82
90
  return;
83
91
  }
84
92
  if (closed)
85
93
  return; // disconnected during redeem
94
+ // Another server's phone: its node streams it, relayed from here.
95
+ let upstream = null;
96
+ try {
97
+ upstream = (_b = (yield ((_a = deps.nodeSocket) === null || _a === void 0 ? void 0 : _a.call(deps, parsed.udid, actor)))) !== null && _b !== void 0 ? _b : null;
98
+ }
99
+ catch (e) {
100
+ const refused = e instanceof nodeSocketRelay_1.NodeSocketRefused;
101
+ logger_1.default.warn(`[${parsed.udid}] H.264 WS from its node failed: ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`);
102
+ try {
103
+ ws.close(refused ? 1008 : 1011, refused ? 'refused by the node' : 'node unreachable');
104
+ }
105
+ catch (_f) {
106
+ /* noop */
107
+ }
108
+ return;
109
+ }
110
+ if (upstream) {
111
+ if (closed)
112
+ return void upstream.terminate();
113
+ (0, nodeSocketRelay_1.relaySocket)(ws, upstream, maxBuffered);
114
+ logger_1.default.info(`[${parsed.udid}] H.264 WS client connected, relayed from its node`);
115
+ return;
116
+ }
86
117
  let mux;
87
118
  try {
88
119
  mux = yield deps.startStream(parsed.udid);
@@ -92,7 +123,7 @@ function attachH264Ws(server, deps) {
92
123
  try {
93
124
  ws.close(1011, 'stream failed');
94
125
  }
95
- catch (_c) {
126
+ catch (_g) {
96
127
  /* noop */
97
128
  }
98
129
  return;
@@ -111,5 +142,5 @@ function attachH264Ws(server, deps) {
111
142
  });
112
143
  logger_1.default.info(`[${parsed.udid}] H.264 WS client connected`);
113
144
  }));
114
- });
145
+ }
115
146
  }
@@ -16,6 +16,8 @@ exports.parseLogcatWsPath = parseLogcatWsPath;
16
16
  exports.attachLogcatWs = attachLogcatWs;
17
17
  const ws_1 = require("ws");
18
18
  const logger_1 = __importDefault(require("../../logger"));
19
+ const upgradeRouter_1 = require("./upgradeRouter");
20
+ const nodeSocketRelay_1 = require("./nodeSocketRelay");
19
21
  const ostraceParse_1 = require("../../services/logcat/ostraceParse");
20
22
  const LOGCAT_PATH_RE = /^\/xenon\/api\/control\/([^/]+)\/logcat$/;
21
23
  /** Parse the logcat stream upgrade path; returns null for any non-matching URL. */
@@ -42,8 +44,9 @@ function parseLogcatWsPath(url) {
42
44
  }
43
45
  /**
44
46
  * Attach the logcat WebSocket upgrade handler to the plugin's HTTP server.
45
- * Only claims `/xenon/api/control/:udid/logcat` — other upgrades (socket.io,
46
- * the h264 stream) are left for their own listeners.
47
+ * Only claims `/xenon/api/control/:udid/logcat` (with a ticket), as a route of
48
+ * the server's upgrade router, so no other handler sees these sockets and this
49
+ * one sees no others (upgradeRouter.ts).
47
50
  *
48
51
  * Authentication mirrors `attachH264Ws`: a single-use, udid-bound stream
49
52
  * ticket. Authorization does not — device logs routinely carry auth tokens,
@@ -58,12 +61,15 @@ function attachLogcatWs(server, deps) {
58
61
  var _a;
59
62
  const wss = new ws_1.WebSocketServer({ noServer: true });
60
63
  const maxBuffered = (_a = deps.maxBufferedBytes) !== null && _a !== void 0 ? _a : 4 * 1024 * 1024;
61
- server.on('upgrade', (req, socket, head) => {
62
- const parsed = req.url ? parseLogcatWsPath(req.url) : null;
63
- if (!parsed)
64
- return; // not our path — let other upgrade listeners handle it
64
+ (0, upgradeRouter_1.upgradeRouterFor)(server).add({
65
+ name: 'logcat',
66
+ matches: (req) => !!req.url && parseLogcatWsPath(req.url) !== null,
67
+ handle: upgrade,
68
+ });
69
+ function upgrade(req, socket, head) {
70
+ const parsed = parseLogcatWsPath(req.url);
65
71
  wss.handleUpgrade(req, socket, head, (ws) => __awaiter(this, void 0, void 0, function* () {
66
- var _a, _b;
72
+ var _a, _b, _c, _d, _e;
67
73
  // Register cleanup BEFORE the awaits below. redeem + authorize +
68
74
  // startStream can together take seconds; a client that disconnects in
69
75
  // that window must still be removed from the multiplexer once it's
@@ -81,11 +87,11 @@ function attachLogcatWs(server, deps) {
81
87
  try {
82
88
  actor = yield deps.redeem(parsed.ticket, parsed.udid);
83
89
  }
84
- catch (_c) {
90
+ catch (_f) {
85
91
  try {
86
92
  ws.close(1008, 'unauthorized');
87
93
  }
88
- catch (_d) {
94
+ catch (_g) {
89
95
  /* noop */
90
96
  }
91
97
  return;
@@ -105,7 +111,7 @@ function attachLogcatWs(server, deps) {
105
111
  try {
106
112
  ws.close(1011, 'ownership unavailable');
107
113
  }
108
- catch (_e) {
114
+ catch (_h) {
109
115
  /* noop */
110
116
  }
111
117
  return;
@@ -130,13 +136,41 @@ function attachLogcatWs(server, deps) {
130
136
  try {
131
137
  ws.close(1008, unresolvable ? 'device not found' : 'device held by another user');
132
138
  }
133
- catch (_f) {
139
+ catch (_j) {
134
140
  /* noop */
135
141
  }
136
142
  return;
137
143
  }
138
144
  if (closed)
139
145
  return; // disconnected during authorize
146
+ // Another server's phone: its node streams the log, relayed from here.
147
+ // The node keeps the replay, the drop markers and the close codes.
148
+ let upstream = null;
149
+ try {
150
+ upstream =
151
+ (_c = (yield ((_b = deps.nodeSocket) === null || _b === void 0 ? void 0 : _b.call(deps, parsed.udid, actor, {
152
+ levels: parsed.levels,
153
+ process: parsed.process,
154
+ })))) !== null && _c !== void 0 ? _c : null;
155
+ }
156
+ catch (e) {
157
+ const refused = e instanceof nodeSocketRelay_1.NodeSocketRefused;
158
+ logger_1.default.warn(`[${parsed.udid}] logcat WS from its node failed: ${(_d = e === null || e === void 0 ? void 0 : e.message) !== null && _d !== void 0 ? _d : e}`);
159
+ try {
160
+ ws.close(refused ? 1008 : 1011, refused ? 'refused by the node' : 'node unreachable');
161
+ }
162
+ catch (_k) {
163
+ /* noop */
164
+ }
165
+ return;
166
+ }
167
+ if (upstream) {
168
+ if (closed)
169
+ return void upstream.terminate();
170
+ (0, nodeSocketRelay_1.relaySocket)(ws, upstream, maxBuffered);
171
+ logger_1.default.info(`[${parsed.udid}] logcat WS client connected, relayed from its node`);
172
+ return;
173
+ }
140
174
  let mux;
141
175
  let effectiveLevels;
142
176
  try {
@@ -148,11 +182,11 @@ function attachLogcatWs(server, deps) {
148
182
  effectiveLevels = started.levels;
149
183
  }
150
184
  catch (e) {
151
- logger_1.default.warn(`[${parsed.udid}] logcat WS start failed: ${(_b = e === null || e === void 0 ? void 0 : e.message) !== null && _b !== void 0 ? _b : e}`);
185
+ logger_1.default.warn(`[${parsed.udid}] logcat WS start failed: ${(_e = e === null || e === void 0 ? void 0 : e.message) !== null && _e !== void 0 ? _e : e}`);
152
186
  try {
153
187
  ws.close(1011, 'stream failed');
154
188
  }
155
- catch (_g) {
189
+ catch (_l) {
156
190
  /* noop */
157
191
  }
158
192
  return;
@@ -282,5 +316,5 @@ function attachLogcatWs(server, deps) {
282
316
  };
283
317
  logger_1.default.info(`[${parsed.udid}] logcat WS client connected`);
284
318
  }));
285
- });
319
+ }
286
320
  }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.RELAY_HIGH_WATER_BYTES = exports.NODE_SOCKET_TIMEOUT_MS = exports.NodeSocketRefused = void 0;
16
+ exports.openNodeSocket = openNodeSocket;
17
+ exports.relaySocket = relaySocket;
18
+ const typedi_1 = require("typedi");
19
+ const ws_1 = require("ws");
20
+ const logger_1 = __importDefault(require("../../logger"));
21
+ const hubSessionToken_1 = require("../../gateway/hubSessionToken");
22
+ const forwardToNode_1 = require("../../gateway/forwardToNode");
23
+ const controlDevice_1 = require("../../middleware/controlDevice");
24
+ const nodePhoneControl_1 = require("../routers/nodePhoneControl");
25
+ /**
26
+ * The live preview's H.264 socket and the logcat socket, for another server's
27
+ * phone. The viewer's ticket is this server's (stream/ticket is answered
28
+ * here, after the team check) and is redeemed here; the socket itself is the
29
+ * node's, opened with a ticket the node mints for the same user, and relayed.
30
+ * The node keeps doing what it does for its own viewers: the multiplexer,
31
+ * the replay, the slow-viewer drops and the idle release.
32
+ */
33
+ /** The node, or this server's own rule, refused: the viewer gets 1008. */
34
+ class NodeSocketRefused extends Error {
35
+ }
36
+ exports.NodeSocketRefused = NodeSocketRefused;
37
+ /** How long the node has to mint its ticket and open its socket. */
38
+ exports.NODE_SOCKET_TIMEOUT_MS = 15000;
39
+ /** Unwritten bytes to the viewer beyond which reading from the node pauses. */
40
+ exports.RELAY_HIGH_WATER_BYTES = 4 * 1024 * 1024;
41
+ /**
42
+ * The node's socket for another server's phone, open and paused until
43
+ * relaySocket reads it; null for this server's own phone, which the caller
44
+ * serves itself. Throws NodeSocketRefused when the phone is a cloud
45
+ * provider's or the node refuses the user, and any other error when the node
46
+ * can't be reached.
47
+ */
48
+ function openNodeSocket(request_1) {
49
+ return __awaiter(this, arguments, void 0, function* (request, deps = {}) {
50
+ var _a, _b, _c, _d;
51
+ const findDevice = (_a = deps.findDevice) !== null && _a !== void 0 ? _a : controlDevice_1.findControlDeviceInStore;
52
+ const controlToken = (_b = deps.controlToken) !== null && _b !== void 0 ? _b : ((grant) => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor(grant));
53
+ const timeoutMs = (_c = deps.timeoutMs) !== null && _c !== void 0 ? _c : exports.NODE_SOCKET_TIMEOUT_MS;
54
+ const { udid, actor, path } = request;
55
+ const device = yield findDevice(udid);
56
+ if (!device || !(0, nodePhoneControl_1.isOtherServersPhone)(device))
57
+ return null;
58
+ if (device.cloud)
59
+ throw new NodeSocketRefused("a cloud provider's phone");
60
+ const origin = (0, nodePhoneControl_1.safeNodeOrigin)(device.host);
61
+ if (!origin)
62
+ throw new NodeSocketRefused('unsafe device host');
63
+ const token = yield controlToken({
64
+ userId: actor.actorId,
65
+ isAdmin: !!actor.isAdmin,
66
+ udid,
67
+ host: device.host,
68
+ });
69
+ const base = `/xenon/api/control/${encodeURIComponent(udid)}`;
70
+ const answer = yield (0, forwardToNode_1.readAnswer)(yield (0, forwardToNode_1.sendToNode)({
71
+ url: `${origin}${base}/stream/ticket`,
72
+ method: 'POST',
73
+ headers: Object.assign({ accept: 'application/json', 'content-type': 'application/json' }, (token ? { [hubSessionToken_1.HUB_TOKEN_HEADER]: token } : {})),
74
+ body: Buffer.from('{}'),
75
+ signal: AbortSignal.timeout(timeoutMs),
76
+ }));
77
+ if (answer.status >= 400 && answer.status < 500) {
78
+ throw new NodeSocketRefused(`the node answered ${answer.status} for a ticket`);
79
+ }
80
+ if (answer.status >= 300)
81
+ throw new Error(`the node answered ${answer.status} for a ticket`);
82
+ const ticket = (_d = JSON.parse(answer.body.toString('utf8'))) === null || _d === void 0 ? void 0 : _d.ticket;
83
+ if (typeof ticket !== 'string')
84
+ throw new Error('the node sent no ticket');
85
+ const query = new URLSearchParams(request.query);
86
+ query.set('ticket', ticket);
87
+ const url = `${origin.replace(/^http/, 'ws')}${base}/${path}?${query}`;
88
+ const socket = new ws_1.WebSocket(url, { handshakeTimeout: timeoutMs, perMessageDeflate: false });
89
+ yield new Promise((resolve, reject) => {
90
+ socket.once('open', () => {
91
+ // Paused until something reads it: the node sends H.264's config
92
+ // packet and logcat's replay as soon as it opens, and a message with
93
+ // no listener yet is lost. relaySocket resumes it.
94
+ socket.pause();
95
+ resolve();
96
+ });
97
+ socket.once('error', reject);
98
+ });
99
+ logger_1.default.info(`[${udid}] ${path} socket opened on node ${origin}`);
100
+ return socket;
101
+ });
102
+ }
103
+ /** A close code a server may send. 1005, 1006 and 1015 only describe a close. */
104
+ function isSendable(code) {
105
+ return ((code >= 1000 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ||
106
+ (code >= 3000 && code <= 4999));
107
+ }
108
+ function closeWith(ws, code, reason) {
109
+ if (ws.readyState === ws_1.WebSocket.CLOSING || ws.readyState === ws_1.WebSocket.CLOSED)
110
+ return;
111
+ try {
112
+ if (code === 1005)
113
+ ws.close();
114
+ // The node's connection dropped with no close: the viewer retries.
115
+ else if (!isSendable(code))
116
+ ws.close(1011, 'node connection lost');
117
+ else
118
+ ws.close(code, reason.toString());
119
+ }
120
+ catch (_a) {
121
+ ws.terminate();
122
+ }
123
+ }
124
+ /**
125
+ * Relay `viewer` to and from `node` until either closes. Messages pass
126
+ * unchanged, binary or text as they came, and the node's close code and
127
+ * reason reach the viewer. While more than `highWaterBytes` sent to the
128
+ * viewer are still unwritten, reading from the node pauses: the node then
129
+ * sees a slow viewer and applies its own rule, rather than this server
130
+ * buffering without bound.
131
+ */
132
+ function relaySocket(viewer, node, highWaterBytes = exports.RELAY_HIGH_WATER_BYTES) {
133
+ let unwritten = 0;
134
+ let paused = false;
135
+ node.on('message', (data, isBinary) => {
136
+ if (viewer.readyState !== ws_1.WebSocket.OPEN)
137
+ return;
138
+ const size = data.length;
139
+ unwritten += size;
140
+ viewer.send(data, { binary: isBinary }, () => {
141
+ unwritten -= size;
142
+ if (paused && unwritten <= highWaterBytes / 2) {
143
+ paused = false;
144
+ node.resume();
145
+ }
146
+ });
147
+ if (!paused && unwritten > highWaterBytes) {
148
+ paused = true;
149
+ node.pause();
150
+ }
151
+ });
152
+ viewer.on('message', (data, isBinary) => {
153
+ if (node.readyState === ws_1.WebSocket.OPEN)
154
+ node.send(data, { binary: isBinary });
155
+ });
156
+ node.on('close', (code, reason) => closeWith(viewer, code, reason));
157
+ viewer.on('close', () => {
158
+ if (node.readyState === ws_1.WebSocket.OPEN || node.readyState === ws_1.WebSocket.CONNECTING) {
159
+ node.close(1000);
160
+ }
161
+ });
162
+ // A 'close' follows each; nothing more to do here.
163
+ node.on('error', () => undefined);
164
+ viewer.on('error', () => undefined);
165
+ // openNodeSocket hands the node's socket over paused; read it now.
166
+ node.resume();
167
+ }
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UpgradeRouter = void 0;
7
+ exports.appiumPatternMatcher = appiumPatternMatcher;
8
+ exports.dispatchToAppiumHandlers = dispatchToAppiumHandlers;
9
+ exports.appiumUpgradeListener = appiumUpgradeListener;
10
+ exports.upgradeRouterFor = upgradeRouterFor;
11
+ const logger_1 = __importDefault(require("../../logger"));
12
+ const appiumBasePath_1 = require("../appiumBasePath");
13
+ function escapeRegExp(text) {
14
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+ }
16
+ /**
17
+ * The path-to-regexp v8 syntax Appium's patterns use: literal segments and
18
+ * `:name` parameters. Anything else (groups, wildcards, escapes) is not
19
+ * compiled, and such a pattern matches nothing here; see appiumPatternMatcher.
20
+ */
21
+ const UNSUPPORTED_PATTERN_SYNTAX = /[{}()[\]*+?!\\"]/;
22
+ /** Splits a pattern into literal text (even indexes) and `:name` parameters (odd). */
23
+ const PARAM = /(:[A-Za-z_$][\w$]*)/;
24
+ const compiled = new Map();
25
+ const warnedPatterns = new Set();
26
+ /**
27
+ * base-driver matches an upgrade's pathname against each registered pattern
28
+ * with path-to-regexp's `match(pattern)` (v8, defaults): case-insensitive, an
29
+ * optional trailing slash, the whole pathname, each parameter one or more
30
+ * characters other than `/`, and each matched parameter decoded with
31
+ * decodeURIComponent, which throws on a malformed escape. This is that, for
32
+ * the patterns Appium and drivers register. It returns null for a pattern it
33
+ * does not speak.
34
+ */
35
+ function appiumPatternMatcher(pattern) {
36
+ const cached = compiled.get(pattern);
37
+ if (cached !== undefined)
38
+ return cached;
39
+ let matcher = null;
40
+ if (!UNSUPPORTED_PATTERN_SYNTAX.test(pattern)) {
41
+ const source = pattern
42
+ .split(PARAM)
43
+ .map((part, i) => (i % 2 === 1 ? '([^/]+)' : escapeRegExp(part)))
44
+ .join('');
45
+ const regex = new RegExp(`^${source}(?:/)?$`, 'i');
46
+ matcher = (pathname) => {
47
+ const m = regex.exec(pathname);
48
+ if (!m)
49
+ return false;
50
+ // path-to-regexp decodes every matched parameter, and a malformed
51
+ // escape throws out of Appium's dispatch. Do the same, so this behaves
52
+ // as Appium's own listener does.
53
+ for (const value of m.slice(1))
54
+ decodeURIComponent(value);
55
+ return true;
56
+ };
57
+ }
58
+ compiled.set(pattern, matcher);
59
+ return matcher;
60
+ }
61
+ /**
62
+ * base-driver's `tryHandleWebSocketUpgrade`, over the server's public
63
+ * `webSocketsMapping`: hand a WebSocket upgrade to the first registered
64
+ * handler whose pattern matches its pathname. False when none does.
65
+ */
66
+ function dispatchToAppiumHandlers(req, socket, head, mapping, logger) {
67
+ var _a, _b;
68
+ const upgrade = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.upgrade;
69
+ if (typeof upgrade !== 'string' || upgrade.toLowerCase() !== 'websocket')
70
+ return false;
71
+ const pathname = (0, appiumBasePath_1.appiumPathname)((_b = req.url) !== null && _b !== void 0 ? _b : '');
72
+ for (const [pattern, wsServer] of Object.entries(mapping !== null && mapping !== void 0 ? mapping : {})) {
73
+ const matches = appiumPatternMatcher(pattern);
74
+ if (!matches) {
75
+ if (!warnedPatterns.has(pattern)) {
76
+ warnedPatterns.add(pattern);
77
+ logger.warn(`Appium WebSocket handler ${pattern} uses path syntax Xenon's copy of Appium's ` +
78
+ 'upgrade listener does not match; it is skipped on this Node (< 22.21).');
79
+ }
80
+ continue;
81
+ }
82
+ if (matches(pathname)) {
83
+ wsServer.handleUpgrade(req, socket, head, (ws) => wsServer.emit('connection', ws, req));
84
+ return true;
85
+ }
86
+ }
87
+ logger.info(`Did not match the websocket upgrade request at ${pathname} to any known route`);
88
+ return false;
89
+ }
90
+ /**
91
+ * The `upgrade` listener base-driver's configureHttp adds on Node >= 22.21:
92
+ * Appium's handlers take what they match, and every other socket is destroyed.
93
+ * `webSocketsMapping` is read per upgrade, since drivers add and remove
94
+ * handlers as sessions come and go.
95
+ */
96
+ function appiumUpgradeListener(server, logger) {
97
+ return function appiumUpgradeThroughXenon(req, socket, head) {
98
+ if (!dispatchToAppiumHandlers(req, socket, head, server.webSocketsMapping, logger)) {
99
+ socket.destroy();
100
+ }
101
+ };
102
+ }
103
+ /** The upgrade's path without the query, which carries tickets. */
104
+ function pathOf(req) {
105
+ var _a;
106
+ return String((_a = req.url) !== null && _a !== void 0 ? _a : '').split('?')[0];
107
+ }
108
+ class UpgradeRouter {
109
+ constructor(server, logger = logger_1.default.scope('UpgradeRouter')) {
110
+ this.server = server;
111
+ this.logger = logger;
112
+ this.routes = [];
113
+ // base-driver's own test (`hasShouldUpgradeCallback`): without the
114
+ // callback it took upgrades in Express and added no listener. A server
115
+ // with no listener at all (not Appium's) needs one too, or neither Node
116
+ // emits `upgrade` and Xenon's routes are never reached.
117
+ const appiumListens = typeof server.shouldUpgradeCallback !== 'undefined' && server.listenerCount('upgrade') > 0;
118
+ if (appiumListens) {
119
+ this.appiumPath = 'appium-listener';
120
+ }
121
+ else {
122
+ server.on('upgrade', appiumUpgradeListener(server, logger));
123
+ this.appiumPath = 'router-listener';
124
+ }
125
+ this.routeEmit();
126
+ }
127
+ /** Give every upgrade `route.matches` to `route.handle`, and to nothing else. */
128
+ add(route) {
129
+ this.routes.push(route);
130
+ }
131
+ /**
132
+ * Run `attach`, which attaches something that takes upgrades by adding an
133
+ * `upgrade` listener (socket.io). The listeners it added are taken off the
134
+ * server and become the route's handler, so they see only what `matches`.
135
+ */
136
+ adopt(name, matches, attach) {
137
+ const before = new Set(this.server.listeners('upgrade'));
138
+ const attached = attach();
139
+ const added = this.server.listeners('upgrade').filter((l) => !before.has(l));
140
+ for (const listener of added)
141
+ this.server.removeListener('upgrade', listener);
142
+ if (added.length > 0) {
143
+ const server = this.server;
144
+ this.add({
145
+ name,
146
+ matches,
147
+ handle: (req, socket, head) => {
148
+ for (const listener of added)
149
+ listener.call(server, req, socket, head);
150
+ },
151
+ });
152
+ }
153
+ return attached;
154
+ }
155
+ /** The route that owns an upgrade to `req`, or undefined when it is Appium's. */
156
+ routeFor(req) {
157
+ return this.routes.find((route) => route.matches(req));
158
+ }
159
+ /** Where the upgrades that are not Xenon's go, for the boot log. */
160
+ describeAppiumPath() {
161
+ return this.appiumPath === 'appium-listener'
162
+ ? "Appium's own upgrade listener"
163
+ : "Appium's WebSocket handlers, through Xenon's copy of Appium's upgrade listener " +
164
+ '(this Node has no http.Server shouldUpgradeCallback, so Appium took upgrades in ' +
165
+ 'Express, which no upgrade reaches once any upgrade listener exists)';
166
+ }
167
+ /** Close a socket whose upgrade could not be handed on, and say why. */
168
+ refuse(req, socket, what, error) {
169
+ var _a;
170
+ this.logger.error(`WebSocket upgrade ${pathOf(req)} ${what}: ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}. Closing the socket.`);
171
+ socket.destroy();
172
+ return true;
173
+ }
174
+ routeEmit() {
175
+ const server = this.server;
176
+ const emit = server.emit;
177
+ const routeFor = (req) => this.routeFor(req);
178
+ const refuse = (req, socket, what, error) => this.refuse(req, socket, what, error);
179
+ server.emit = function emitThroughUpgradeRouter(event, ...args) {
180
+ if (event !== 'upgrade')
181
+ return emit.call(this, event, ...args);
182
+ const [req, socket, head] = args;
183
+ let route;
184
+ try {
185
+ route = routeFor(req);
186
+ }
187
+ catch (error) {
188
+ return refuse(req, socket, 'could not be routed', error);
189
+ }
190
+ try {
191
+ if (!route)
192
+ return emit.call(this, event, ...args);
193
+ route.handle(req, socket, head);
194
+ return true;
195
+ }
196
+ catch (error) {
197
+ return refuse(req, socket, `failed in the ${route ? route.name : 'Appium'} handler`, error);
198
+ }
199
+ };
200
+ }
201
+ }
202
+ exports.UpgradeRouter = UpgradeRouter;
203
+ const routers = new WeakMap();
204
+ /**
205
+ * The server's router, installed on first use. ServerManager installs it in
206
+ * registerRoutes; attaching a WebSocket installs it on a server that has none.
207
+ */
208
+ function upgradeRouterFor(server) {
209
+ let router = routers.get(server);
210
+ if (!router) {
211
+ router = new UpgradeRouter(server);
212
+ routers.set(server, router);
213
+ }
214
+ return router;
215
+ }
@@ -225,9 +225,10 @@ let DashboardEventManager = class DashboardEventManager {
225
225
  // Principal Resource Management: Unblock device immediately
226
226
  const device = this.sessionToDevice.get(sessionId);
227
227
  if (device) {
228
- const { unblockDevice } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
228
+ const { releaseSessionDevice } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
229
229
  try {
230
- yield unblockDevice(device.udid, device.host);
230
+ // This session's claim only: the phone may have gone on to another.
231
+ yield releaseSessionDevice(device.udid, device.host, sessionId);
231
232
  logger_1.default.info(`🔓 [${sessionId}] Device ${device.udid} released.`);
232
233
  }
233
234
  catch (unblockErr) {
@@ -239,9 +240,9 @@ let DashboardEventManager = class DashboardEventManager {
239
240
  yield this.stopIdleStreamForDevice(device);
240
241
  }
241
242
  else {
242
- const { unblockDeviceMatchingFilter } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
243
+ const { releaseSessionDevices } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
243
244
  try {
244
- yield unblockDeviceMatchingFilter({ session_id: sessionId });
245
+ yield releaseSessionDevices(sessionId);
245
246
  logger_1.default.info(`🔓 [${sessionId}] Device released via session_id fallback.`);
246
247
  }
247
248
  catch (unblockErr) {
@@ -261,9 +262,9 @@ let DashboardEventManager = class DashboardEventManager {
261
262
  else {
262
263
  logger_1.default.warn(`⚠️ Session ${sessionId} not found in SESSION_MANAGER`);
263
264
  // Fallback: If session not in manager, attempt to unblock by session_id in store
264
- const { unblockDeviceMatchingFilter } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
265
+ const { releaseSessionDevices } = yield Promise.resolve().then(() => __importStar(require('../data-service/device-service')));
265
266
  try {
266
- yield unblockDeviceMatchingFilter({ session_id: sessionId });
267
+ yield releaseSessionDevices(sessionId);
267
268
  logger_1.default.info(`🔓 [${sessionId}] Orphaned device released via session_id fallback.`);
268
269
  }
269
270
  catch (unblockErr) {