@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
@@ -66,6 +66,7 @@ exports.unblockCandidateDevices = unblockCandidateDevices;
66
66
  exports.releaseBlockedDevices = releaseBlockedDevices;
67
67
  exports.setupCronReleaseBlockedDevices = setupCronReleaseBlockedDevices;
68
68
  exports.setupCronUpdateDeviceList = setupCronUpdateDeviceList;
69
+ exports.unregisterNodeFromHub = unregisterNodeFromHub;
69
70
  exports.setupCronLocalDiscovery = setupCronLocalDiscovery;
70
71
  exports.cleanPendingSessions = cleanPendingSessions;
71
72
  exports.setupCronCleanPendingSessions = setupCronCleanPendingSessions;
@@ -82,6 +83,8 @@ const async_wait_until_1 = __importDefault(require("async-wait-until"));
82
83
  const typedi_1 = require("typedi");
83
84
  const device_managers_1 = require("./device-managers");
84
85
  const device_service_1 = require("./data-service/device-service");
86
+ const deviceClaims_1 = require("./data-service/deviceClaims");
87
+ const PluginContext_1 = require("./PluginContext");
85
88
  const logger_1 = __importDefault(require("./logger"));
86
89
  const Platform_1 = __importDefault(require("./enums/Platform"));
87
90
  const lodash_1 = __importDefault(require("lodash"));
@@ -91,6 +94,7 @@ const AndroidDeviceManager_1 = __importDefault(require("./device-managers/Androi
91
94
  const IOSDeviceManager_1 = __importDefault(require("./device-managers/IOSDeviceManager"));
92
95
  const IOSDiscoveryService_1 = require("./device-managers/ios/IOSDiscoveryService");
93
96
  const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
97
+ const localDeviceHosts_1 = require("./device-managers/localDeviceHosts");
94
98
  const config_1 = require("./config");
95
99
  const device_store_1 = require("./data-service/device-store");
96
100
  const leaseSessionCaps_1 = require("./services/lease/leaseSessionCaps");
@@ -204,8 +208,9 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
204
208
  const availableCandidate = matchingDevices.find((d) => !(0, device_service_1.isDeviceReserved)(d));
205
209
  if (availableCandidate) {
206
210
  // Principal Intelligence: Multi-node consistent locking
207
- // Attempt to atomically claim this specific device
208
- device = yield store.findAndLockDevice(Object.assign(Object.assign({}, filters), { udid: [availableCandidate.udid] }));
211
+ // Attempt to atomically claim this specific device: busy, with a
212
+ // pending claim for the session being created (deviceClaims.ts).
213
+ device = yield store.findAndLockDevice(Object.assign(Object.assign({}, filters), { udid: [availableCandidate.udid] }), { claim: true });
209
214
  return device !== null;
210
215
  }
211
216
  logger_1.default.info(`Waiting for free device. Filter: ${JSON.stringify(filters)}}`);
@@ -245,7 +250,7 @@ function allocateDeviceForSession(capability_1, deviceTimeOutMs_1, deviceQueryIn
245
250
  const isReady = yield manager.readyForSession(lockedDevice);
246
251
  if (!isReady) {
247
252
  logger_1.default.error(`❌ [Allocation] Device ${lockedDevice.udid} failed pre-session health check. Unblocking.`);
248
- yield (0, device_service_1.unblockDevice)(lockedDevice.udid, lockedDevice.host);
253
+ yield (0, device_service_1.releasePendingClaim)(lockedDevice);
249
254
  throw new Error(`Device ${lockedDevice.udid} is unhealthy and could not be autonomously recovered.`);
250
255
  }
251
256
  }
@@ -468,17 +473,20 @@ function getBusyDevicesCount() {
468
473
  }).length;
469
474
  });
470
475
  }
471
- function updateDeviceList(host, hubArgument, tlsRejectUnauthorized) {
476
+ function updateDeviceList(local, hubArgument, tlsRejectUnauthorized) {
472
477
  return __awaiter(this, void 0, void 0, function* () {
473
478
  const allExistingDevices = yield (0, device_service_1.getAllDevices)();
474
479
  const devices = yield getDeviceManager().getDevices(allExistingDevices);
475
480
  // first thing first. Update device list in local list
476
- yield (0, device_service_1.syncDiscoveredDevices)(devices, allExistingDevices, host);
477
- // Prune any devices that are in our local DB for this host but NO LONGER discovered.
478
- // This automatically cleans up disconnected Android devices safely, or filtered
479
- // iOS simulators (e.g. when booted-simulators is turned on and a simulator shuts down)
481
+ yield (0, device_service_1.syncDiscoveredDevices)(devices, allExistingDevices, local.origin);
482
+ // Prune this server's own phones that discovery no longer reports: an
483
+ // unplugged Android phone, or a simulator filtered out (e.g. booted-simulators
484
+ // is on and it shut down). Only its own (isOwnDevice: by nodeId, or else by
485
+ // exact host): a hub keeps its nodes' phones in the same table, and a node
486
+ // on the same machine differs from the hub only by port.
487
+ const nodeId = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
480
488
  const discoveredUdids = new Set(devices.map((d) => d.udid));
481
- const staleLocalDevices = allExistingDevices.filter((d) => (d.host === host || d.host.includes(`//${host}:`) || d.host.includes(`//${host}/`)) && !discoveredUdids.has(d.udid));
489
+ const staleLocalDevices = allExistingDevices.filter((d) => (0, localDeviceHosts_1.isOwnDevice)(local, nodeId, d) && !discoveredUdids.has(d.udid));
482
490
  if (staleLocalDevices.length > 0) {
483
491
  logger_1.default.info(`Removing ${staleLocalDevices.length} stale devices/simulators no longer discovered on this host.`);
484
492
  yield (0, device_service_1.removeDevice)(staleLocalDevices.map((d) => ({ udid: d.udid, host: d.host })));
@@ -519,23 +527,25 @@ function refreshSimulatorState(pluginArgs, hostPort) {
519
527
  }), 10000);
520
528
  });
521
529
  }
522
- function setupCronCheckStaleDevices(intervalMs, currentHost, tlsRejectUnauthorized) {
530
+ function setupCronCheckStaleDevices(intervalMs, local, tlsRejectUnauthorized) {
523
531
  return __awaiter(this, void 0, void 0, function* () {
524
532
  setInterval(() => __awaiter(this, void 0, void 0, function* () {
525
- yield removeStaleDevices(currentHost, tlsRejectUnauthorized);
533
+ yield removeStaleDevices(local, tlsRejectUnauthorized);
526
534
  }), intervalMs);
527
535
  });
528
536
  }
529
537
  /**
530
538
  * Remove devices where the host is not alive nor defined.
531
- * @param currentHost current host ip address
539
+ * @param local the hosts this server files its own phones under
532
540
  */
533
- function removeStaleDevices(currentHost, tlsRejectUnauthorized) {
541
+ function removeStaleDevices(local, tlsRejectUnauthorized) {
534
542
  return __awaiter(this, void 0, void 0, function* () {
535
543
  const allDevices = yield (0, device_service_1.getAllDevices)();
544
+ const nodeId = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
536
545
  const nodeDevices = allDevices.filter((device) => {
537
- // devices that's not from this node ip address
538
- return device.host !== undefined && !device.host.includes(currentHost);
546
+ // Phones another server drives: by nodeId, or else by exact host (a node
547
+ // on this machine shares the IP, and an IP can be a prefix of another's).
548
+ return device.host !== undefined && !(0, localDeviceHosts_1.isOwnDevice)(local, nodeId, device);
539
549
  });
540
550
  const devicesWithNoHost = nodeDevices.filter((device) => {
541
551
  return device.host === undefined;
@@ -558,8 +568,14 @@ function removeStaleDevices(currentHost, tlsRejectUnauthorized) {
558
568
  const allAliveHosts = [...aliveHosts, ...aliveCloudHosts]
559
569
  .filter((item) => item.status === 'fulfilled' && item.value.alive)
560
570
  .map((item) => item.value.host);
571
+ // A node's phones carry its nodeId, and live as long as the node does:
572
+ // while any host of that node answers, its phone filed under a host that
573
+ // can't be probed (an iPhone under a bare remoteMachineProxyIP) stays.
574
+ const liveNodeIds = new Set(nodeDevices
575
+ .filter((device) => device.nodeId && allAliveHosts.includes(device.host))
576
+ .map((device) => device.nodeId));
561
577
  // stale devices are devices that's not alive
562
- const staleDevices = nodeDevices.filter((device) => !allAliveHosts.includes(device.host));
578
+ const staleDevices = nodeDevices.filter((device) => !allAliveHosts.includes(device.host) && !(device.nodeId && liveNodeIds.has(device.nodeId)));
563
579
  yield (0, device_service_1.removeDevice)(staleDevices.map((device) => ({ udid: device.udid, host: device.host })));
564
580
  if (staleDevices.length > 0) {
565
581
  logger_1.default.debug(`Removing device with udid(s): ${staleDevices
@@ -586,14 +602,26 @@ function unblockCandidateDevices() {
586
602
  }
587
603
  function releaseBlockedDevices(newCommandTimeout) {
588
604
  return __awaiter(this, void 0, void 0, function* () {
605
+ var _a;
589
606
  const busyDevices = yield unblockCandidateDevices();
590
607
  logger_1.default.debug(`Found ${busyDevices.length} device candidates to be released`);
591
- busyDevices.forEach(function (device) {
608
+ for (const device of busyDevices) {
592
609
  // need to keep this to make typescript happy. good thing tho.
593
610
  if (device.lastCmdExecutedAt == undefined) {
594
- return;
611
+ continue;
595
612
  }
596
613
  const currentEpoch = new Date().getTime();
614
+ // A claim whose session is still being created is not idle: a first
615
+ // driver install can outlast the new-command timeout. It has its own
616
+ // timeout, for a create that never finished (deviceClaims.ts).
617
+ if ((0, deviceClaims_1.isPendingClaim)(device)) {
618
+ if ((0, deviceClaims_1.pendingClaimExpired)(device, currentEpoch)) {
619
+ logger_1.default.warn(`Releasing ${device.udid} at ${device.host}: its session was never created ` +
620
+ `(claimed ${Math.round((currentEpoch - device.claimedAt) / 1000)} seconds ago)`);
621
+ yield (0, device_service_1.releasePendingClaim)(device).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
622
+ }
623
+ continue;
624
+ }
597
625
  const timeoutSeconds = device.newCommandTimeout != undefined ? device.newCommandTimeout : newCommandTimeout;
598
626
  const timeSinceLastCmdExecuted = (currentEpoch - device.lastCmdExecutedAt) / 1000;
599
627
  if (timeSinceLastCmdExecuted > timeoutSeconds) {
@@ -612,9 +640,14 @@ function releaseBlockedDevices(newCommandTimeout) {
612
640
  });
613
641
  logger_1.default.warn(`🕒 Session ${sessionId} timed out on device ${device.udid}`);
614
642
  }
615
- (0, device_service_1.unblockDevice)(device.udid, device.host);
643
+ // Keyed on the session read here, so a release that loses the race to
644
+ // the session's own end (and the phone's next claim) frees nothing.
645
+ const claimant = (_a = device.claimSessionId) !== null && _a !== void 0 ? _a : device.session_id;
646
+ yield (claimant
647
+ ? (0, device_service_1.releaseSessionDevice)(device.udid, device.host, claimant)
648
+ : (0, device_service_1.unblockDevice)(device.udid, device.host)).catch((err) => logger_1.default.error(`Unable to release ${device.udid}: ${err}`));
616
649
  }
617
- });
650
+ }
618
651
  });
619
652
  }
620
653
  function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
@@ -628,29 +661,50 @@ function setupCronReleaseBlockedDevices(intervalMs, newCommandTimeoutSec) {
628
661
  }), intervalMs);
629
662
  });
630
663
  }
631
- function setupCronUpdateDeviceList(host, hubArgument, intervalMs, tlsRejectUnauthorized) {
664
+ function setupCronUpdateDeviceList(local, hubArgument, intervalMs, tlsRejectUnauthorized) {
632
665
  return __awaiter(this, void 0, void 0, function* () {
633
666
  if (cronTimerToUpdateDevices) {
634
667
  clearInterval(cronTimerToUpdateDevices);
635
668
  }
636
669
  logger_1.default.info(`This node will send device list update to the hub (${hubArgument}) every ${intervalMs} ms`);
637
670
  cronTimerToUpdateDevices = setInterval(() => __awaiter(this, void 0, void 0, function* () {
638
- yield updateDeviceList(host, hubArgument, tlsRejectUnauthorized);
671
+ yield updateDeviceList(local, hubArgument, tlsRejectUnauthorized);
639
672
  }), intervalMs);
640
673
  });
641
674
  }
675
+ /**
676
+ * Tells the hub this node is leaving, once for each host its phones are filed
677
+ * under, with the node's id: the hub drops the phones this node reported
678
+ * (by `nodeId`, else by exact host) and never another server's. Only URL
679
+ * hosts are sent: a hub before that rule matched anything that isn't a URL as
680
+ * a substring, which is how the node's bare IP also took the hub's own phones
681
+ * on the same machine.
682
+ */
683
+ function unregisterNodeFromHub(hubArgument, local, tlsRejectUnauthorized, nodeId) {
684
+ return __awaiter(this, void 0, void 0, function* () {
685
+ const hub = new NodeDevices_1.default(hubArgument, {
686
+ tlsRejectUnauthorized,
687
+ hubAccessKey: config_1.config.hubAccessKey,
688
+ hubToken: config_1.config.hubToken,
689
+ });
690
+ for (const host of local.hosts) {
691
+ if (/^https?:\/\//.test(host))
692
+ yield hub.unRegisterNode(host, nodeId);
693
+ }
694
+ });
695
+ }
642
696
  /**
643
697
  * Principal discovery: Periodically poll for local devices to prune stales/offlines.
644
698
  * Critical for standalone Hubs where setupCronUpdateDeviceList isn't called.
645
699
  */
646
- function setupCronLocalDiscovery(host, intervalMs) {
700
+ function setupCronLocalDiscovery(local, intervalMs) {
647
701
  return __awaiter(this, void 0, void 0, function* () {
648
702
  if (timer) {
649
703
  clearInterval(timer);
650
704
  }
651
705
  logger_1.default.info(`Local device discovery poll started every ${intervalMs} ms`);
652
706
  timer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
653
- yield updateDeviceList(host);
707
+ yield updateDeviceList(local);
654
708
  }), intervalMs);
655
709
  });
656
710
  }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CreateHandoff = void 0;
4
+ exports.runWithCreateHandoff = runWithCreateHandoff;
5
+ exports.currentCreateHandoff = currentCreateHandoff;
6
+ const async_hooks_1 = require("async_hooks");
7
+ /**
8
+ * The allocation the session gateway made for one `POST <basePath>/session`,
9
+ * on its way to the plugin.
10
+ *
11
+ * For a phone this server drives, the gateway allocates in front of Appium's
12
+ * route, then calls `next()` inside this request's async context. Appium's
13
+ * route, its umbrella driver and every plugin before Xenon's run inside that
14
+ * context, so XenonPlugin.createSession finds the allocation here and does not
15
+ * allocate a second phone (sessionCreate.ts).
16
+ *
17
+ * It is handed over at most once. Whatever happens first wins:
18
+ * - the plugin takes it, and from then on releasing the phone on failure is
19
+ * the plugin's job (SessionLifecycleService.completeLocalSession);
20
+ * - or the request ends without the plugin taking it (Appium or another
21
+ * plugin refused the create first, or the client went away), and the
22
+ * gateway abandons it, which gives the phone back. A plugin that arrives
23
+ * after that finds it closed.
24
+ *
25
+ * Both happen on the event loop, so there is no interleaving between them.
26
+ * Once taken or given back it keeps no reference to the allocation: things
27
+ * the driver starts during the create (sockets, timers) keep this context,
28
+ * and with it this object, for as long as they live.
29
+ */
30
+ class CreateHandoff {
31
+ constructor(allocation, giveBack) {
32
+ this.allocation = allocation;
33
+ this.giveBack = giveBack;
34
+ this.state = 'waiting';
35
+ }
36
+ /** The allocation, once. Throws when it was taken already or given back. */
37
+ take() {
38
+ if (this.state === 'taken') {
39
+ throw new Error("This request's device allocation was already used by a createSession.");
40
+ }
41
+ if (this.state === 'closed') {
42
+ throw new Error('The request was closed before its session was created; its device was given back.');
43
+ }
44
+ this.state = 'taken';
45
+ const allocation = this.allocation;
46
+ this.allocation = undefined;
47
+ return allocation;
48
+ }
49
+ /** Give the allocation back if nobody took it. Safe to call more than once. */
50
+ abandon() {
51
+ if (this.state !== 'waiting')
52
+ return;
53
+ this.state = 'closed';
54
+ const allocation = this.allocation;
55
+ this.allocation = undefined;
56
+ try {
57
+ this.giveBack(allocation);
58
+ }
59
+ catch (_a) {
60
+ /* the giver logs its own failures; a throw here would reach the socket */
61
+ }
62
+ }
63
+ }
64
+ exports.CreateHandoff = CreateHandoff;
65
+ const handoffs = new async_hooks_1.AsyncLocalStorage();
66
+ /** Run `fn` (the rest of the request) with this handoff as its context. */
67
+ function runWithCreateHandoff(handoff, fn) {
68
+ return handoffs.run(handoff, fn);
69
+ }
70
+ /** The handoff of the create request this code runs for, if the gateway made one. */
71
+ function currentCreateHandoff() {
72
+ return handoffs.getStore();
73
+ }
@@ -0,0 +1,80 @@
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.hubGatewayOptions = hubGatewayOptions;
7
+ exports.nodeGatewayOptions = nodeGatewayOptions;
8
+ exports.gatewayOptionsFor = gatewayOptionsFor;
9
+ const typedi_1 = require("typedi");
10
+ const base_driver_1 = require("@appium/base-driver");
11
+ const logger_1 = __importDefault(require("../logger"));
12
+ const prisma_1 = require("../prisma");
13
+ const appiumBasePath_1 = require("../app/appiumBasePath");
14
+ const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
15
+ const SessionManager_1 = require("../sessions/SessionManager");
16
+ const SessionLifecycleService_1 = require("../services/SessionLifecycleService");
17
+ const NetworkConditioningService_1 = require("../services/NetworkConditioningService");
18
+ const event_manager_1 = require("../dashboard/event-manager");
19
+ const device_service_1 = require("../data-service/device-service");
20
+ const hubSessionToken_1 = require("./hubSessionToken");
21
+ const nodeWebDriverUrl_1 = require("./nodeWebDriverUrl");
22
+ const sessionLocator_1 = require("./sessionLocator");
23
+ const sessionGateway_1 = require("./sessionGateway");
24
+ /**
25
+ * The session gateway as a Xenon server runs it: the production wiring of
26
+ * sessionGateway.ts, sessionCreate.ts, sessionLocator.ts and hubSessionToken.ts.
27
+ */
28
+ const latencyOf = (sessionId) => typedi_1.Container.get(NetworkConditioningService_1.NetworkConditioningService).getLatency(sessionId);
29
+ /** `POST <basePath>/session` in the gateway, with the server's SessionLifecycleService. */
30
+ const createWith = (hubGrants) => ({
31
+ lifecycle: () => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService),
32
+ hubGrants,
33
+ logger: logger_1.default.scope('SessionGateway'),
34
+ });
35
+ const dashboardHooks = {
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),
38
+ };
39
+ /** A hub: sessions other servers run are forwarded to them. */
40
+ function hubGatewayOptions(config) {
41
+ const basePath = (0, appiumBasePath_1.normalizeBasePath)(config.basePath);
42
+ const isLocalDevice = (device) => !device.cloud && (device.nodeId === config.nodeId() || config.isLocalHost(device.host));
43
+ const locator = new sessionLocator_1.SessionLocator(Object.assign(Object.assign({ getSession: (sessionId) => SessionManager_1.SESSION_MANAGER.getSession(sessionId) }, (0, sessionLocator_1.sessionRowsFrom)(prisma_1.prisma)), { isLocalDevice, webDriverUrlOf: (device) => (0, nodeWebDriverUrl_1.nodeWebDriverUrl)(device, basePath) }));
44
+ const tokens = () => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer);
45
+ const routing = (0, sessionGateway_1.createHubRouting)({
46
+ basePath,
47
+ locate: (sessionId) => locator.locate(sessionId),
48
+ forget: (sessionId) => locator.forget(sessionId),
49
+ tokenFor: (sessionId) => tokens().tokenFor(sessionId),
50
+ forgetToken: (sessionId) => tokens().forget(sessionId),
51
+ deleteSession: (next, sessionId) => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(next, sessionId),
52
+ touch: (sessionId) => (0, device_service_1.updateCmdExecutedTime)(sessionId),
53
+ commandName: (path, method) => (0, base_driver_1.routeToCommandName)(path, method, basePath),
54
+ get dashboard() {
55
+ return config.dashboard() ? dashboardHooks : undefined;
56
+ },
57
+ logger: logger_1.default.scope('SessionGateway'),
58
+ });
59
+ return { routing, latencyOf, create: createWith() };
60
+ }
61
+ /**
62
+ * A node: the hub's session token is accepted in place of the client's
63
+ * credentials, and its create token is a create's credential.
64
+ */
65
+ function nodeGatewayOptions(hubUrl) {
66
+ const hub = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
67
+ return { hubTokens: hub, latencyOf, create: createWith(hub) };
68
+ }
69
+ /** The gateway for this server: a node when it has a hub, a hub otherwise. */
70
+ function gatewayOptionsFor(pluginArgs, cliArgs, nodeId) {
71
+ if (pluginArgs.hub !== undefined)
72
+ return nodeGatewayOptions(pluginArgs.hub);
73
+ const localHosts = (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, cliArgs.port);
74
+ return hubGatewayOptions({
75
+ basePath: cliArgs.basePath,
76
+ isLocalHost: (host) => (0, localDeviceHosts_1.isLocalDeviceHost)(localHosts, host),
77
+ nodeId,
78
+ dashboard: () => !!pluginArgs.enableDashboard,
79
+ });
80
+ }
@@ -0,0 +1,147 @@
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.NODE_UNREACHABLE_BODY = exports.NODE_UNREACHABLE_STATUS = void 0;
7
+ exports.forwardedRequestHeaders = forwardedRequestHeaders;
8
+ exports.sendToNode = sendToNode;
9
+ exports.relayAnswer = relayAnswer;
10
+ exports.readAnswer = readAnswer;
11
+ exports.sendAnswer = sendAnswer;
12
+ const http_1 = __importDefault(require("http"));
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
+ const internalCall_1 = require("./internalCall");
17
+ const hubSessionToken_1 = require("./hubSessionToken");
18
+ /**
19
+ * Sending a session command on to the node that runs the session, and handing
20
+ * the node's answer back to the client unchanged.
21
+ */
22
+ // Hop-by-hop headers describe one connection, not the message. `host` and
23
+ // `content-length` are recomputed for the hop to the node.
24
+ const HOP_BY_HOP = new Set([
25
+ 'connection',
26
+ 'keep-alive',
27
+ 'proxy-authenticate',
28
+ 'proxy-authorization',
29
+ 'proxy-connection',
30
+ 'te',
31
+ 'trailer',
32
+ 'transfer-encoding',
33
+ 'upgrade',
34
+ 'host',
35
+ 'content-length',
36
+ ]);
37
+ // The client's credentials were checked here and are not the node's business:
38
+ // the node trusts the hub's session token instead. Xenon's own markers are
39
+ // never taken from a client.
40
+ const NEVER_FORWARDED = new Set([
41
+ 'authorization',
42
+ 'cookie',
43
+ 'x-xenon-access-key',
44
+ 'x-xenon-token',
45
+ internalCall_1.INTERNAL_CALL_HEADER,
46
+ hubSessionToken_1.HUB_TOKEN_HEADER,
47
+ ]);
48
+ function forwardedRequestHeaders(incoming) {
49
+ const out = {};
50
+ for (const [name, value] of Object.entries(incoming)) {
51
+ const lower = name.toLowerCase();
52
+ if (value === undefined || HOP_BY_HOP.has(lower) || NEVER_FORWARDED.has(lower))
53
+ continue;
54
+ out[lower] = value;
55
+ }
56
+ // The answer is relayed byte for byte and read for the dashboard's command
57
+ // log, so ask for it uncompressed.
58
+ out['accept-encoding'] = 'identity';
59
+ return out;
60
+ }
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
+ /**
69
+ * Send one request to a node and resolve with its response stream. No retry:
70
+ * a WebDriver command is not safe to repeat. Credentials in the URL (a cloud
71
+ * provider's) become basic auth.
72
+ */
73
+ function sendToNode(request) {
74
+ const url = new URL(request.url);
75
+ const lib = url.protocol === 'https:' ? https_1.default : http_1.default;
76
+ const headers = Object.assign({}, request.headers);
77
+ if (request.body)
78
+ headers['content-length'] = request.body.length;
79
+ return new Promise((resolve, reject) => {
80
+ const outgoing = lib.request(url, { method: request.method, headers, agent: proxyAgentFor(url), signal: request.signal }, resolve);
81
+ outgoing.on('error', reject);
82
+ outgoing.end(request.body);
83
+ });
84
+ }
85
+ function copyResponseHeaders(upstream, res) {
86
+ for (const [name, value] of Object.entries(upstream.headers)) {
87
+ if (value === undefined || HOP_BY_HOP.has(name.toLowerCase()))
88
+ continue;
89
+ res.setHeader(name, value);
90
+ }
91
+ if (upstream.headers['content-length'] !== undefined) {
92
+ res.setHeader('content-length', upstream.headers['content-length']);
93
+ }
94
+ }
95
+ /**
96
+ * Stream the node's answer to the client: its status, its headers (less the
97
+ * hop-by-hop ones) and its body, unchanged. With `capture`, also resolve with
98
+ * the body as text, for the dashboard's command log.
99
+ */
100
+ function relayAnswer(upstream, res, capture) {
101
+ var _a;
102
+ res.status((_a = upstream.statusCode) !== null && _a !== void 0 ? _a : 502);
103
+ copyResponseHeaders(upstream, res);
104
+ const chunks = capture ? [] : undefined;
105
+ return new Promise((resolve, reject) => {
106
+ if (chunks)
107
+ upstream.on('data', (chunk) => chunks.push(chunk));
108
+ upstream.on('end', () => resolve(chunks ? Buffer.concat(chunks).toString('utf8') : undefined));
109
+ upstream.on('error', reject);
110
+ upstream.pipe(res);
111
+ });
112
+ }
113
+ /** Read a whole answer, for a caller that must act before replying (DELETE). */
114
+ function readAnswer(upstream) {
115
+ return new Promise((resolve, reject) => {
116
+ const chunks = [];
117
+ upstream.on('data', (chunk) => chunks.push(chunk));
118
+ upstream.on('end', () => {
119
+ var _a;
120
+ return resolve({
121
+ status: (_a = upstream.statusCode) !== null && _a !== void 0 ? _a : 502,
122
+ headers: upstream.headers,
123
+ body: Buffer.concat(chunks),
124
+ });
125
+ });
126
+ upstream.on('error', reject);
127
+ });
128
+ }
129
+ function sendAnswer(res, answer) {
130
+ res.status(answer.status);
131
+ for (const [name, value] of Object.entries(answer.headers)) {
132
+ if (value === undefined || HOP_BY_HOP.has(name.toLowerCase()))
133
+ continue;
134
+ res.setHeader(name, value);
135
+ }
136
+ res.setHeader('content-length', answer.body.length);
137
+ res.end(answer.body);
138
+ }
139
+ /** WebDriver's `unknown error`, for a node that could not be reached. */
140
+ exports.NODE_UNREACHABLE_STATUS = 500;
141
+ exports.NODE_UNREACHABLE_BODY = {
142
+ value: {
143
+ error: 'unknown error',
144
+ message: 'Xenon could not reach the node running this session.',
145
+ stacktrace: '',
146
+ },
147
+ };