@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
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.LiveSessionOwners = void 0;
10
+ const typedi_1 = require("typedi");
11
+ /** Far more live sessions than one server runs; bounds a leak if an end is missed. */
12
+ const MAX_LIVE_SESSIONS = 1000;
13
+ /**
14
+ * The owners of the sessions this server runs, while they run.
15
+ *
16
+ * SessionOwnerResolver reads a session's owner from its Session row. A node
17
+ * writes none for a session the hub created (the hub keeps that record, in
18
+ * its own database), so the node's own ownership checks (the /control guard,
19
+ * the logcat WebSocket, the session listing) found no owner and, failing
20
+ * closed, refused the phone to everyone but admins, its owner included. The
21
+ * same held for any server with the dashboard off.
22
+ *
23
+ * finalizeSession records the owner of each session this server drives (on a
24
+ * node, the one the hub's create token named), and the session's end forgets
25
+ * it: deleteSession, an unexpected shutdown, or the server's own shutdown.
26
+ * SessionOwnerResolver asks here before the database.
27
+ */
28
+ let LiveSessionOwners = class LiveSessionOwners {
29
+ constructor() {
30
+ this.owners = new Map();
31
+ }
32
+ record(sessionId, userId) {
33
+ if (!sessionId || !userId)
34
+ return;
35
+ this.owners.delete(sessionId);
36
+ if (this.owners.size >= MAX_LIVE_SESSIONS) {
37
+ const oldest = this.owners.keys().next().value;
38
+ this.owners.delete(oldest);
39
+ }
40
+ this.owners.set(sessionId, userId);
41
+ }
42
+ forget(sessionId) {
43
+ if (sessionId)
44
+ this.owners.delete(sessionId);
45
+ }
46
+ ownerOf(sessionId) {
47
+ return sessionId ? this.owners.get(sessionId) : undefined;
48
+ }
49
+ };
50
+ exports.LiveSessionOwners = LiveSessionOwners;
51
+ exports.LiveSessionOwners = LiveSessionOwners = __decorate([
52
+ (0, typedi_1.Service)()
53
+ ], LiveSessionOwners);
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.SessionOwnerResolver = void 0;
22
22
  const typedi_1 = require("typedi");
23
23
  const prisma_1 = require("../../prisma");
24
+ const LiveSessionOwners_1 = require("./LiveSessionOwners");
24
25
  /** Bound so a long-lived server cannot accumulate entries without limit. */
25
26
  const MAX_CACHE_ENTRIES = 500;
26
27
  /**
@@ -35,12 +36,18 @@ const MAX_CACHE_ENTRIES = 500;
35
36
  * given session id, so a resolved owner cannot go stale — but a *negative*
36
37
  * result can simply mean the Session row has not been written yet, and caching
37
38
  * that would deny the owner their own device for the life of the process.
39
+ *
40
+ * A live session this server drives is looked up in memory first
41
+ * (LiveSessionOwners): a node writes no row for a session the hub created, and
42
+ * a server with the dashboard off writes none at all.
38
43
  */
39
44
  let SessionOwnerResolver = class SessionOwnerResolver {
40
45
  constructor(db = prisma_1.prisma) {
41
46
  this.db = db;
42
47
  this.ownerCache = new Map();
43
48
  this.nameCache = new Map();
49
+ /** The owners of this server's live sessions. A field, not a constructor parameter: TypeDI. */
50
+ this.liveOwners = () => typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners);
44
51
  }
45
52
  ownerOf(sessionId) {
46
53
  return __awaiter(this, void 0, void 0, function* () {
@@ -50,6 +57,11 @@ let SessionOwnerResolver = class SessionOwnerResolver {
50
57
  const cached = this.ownerCache.get(sessionId);
51
58
  if (cached)
52
59
  return cached;
60
+ const live = this.liveOwners().ownerOf(sessionId);
61
+ if (live) {
62
+ this.remember(this.ownerCache, sessionId, live);
63
+ return live;
64
+ }
53
65
  const session = yield this.db.session.findUnique({
54
66
  where: { id: sessionId },
55
67
  select: { api_key_id: true, user_id: true },
@@ -70,6 +82,68 @@ let SessionOwnerResolver = class SessionOwnerResolver {
70
82
  return owner;
71
83
  });
72
84
  }
85
+ /**
86
+ * The owner of each of several sessions, by exactly ownerOf's rule, in one
87
+ * query: one `session.findMany` for the ids not already cached, plus one
88
+ * `apiKey.findMany` only if some of those rows predate `user_id`. Used to
89
+ * filter Appium's session listing, where an ownerOf per listed session would
90
+ * be a query per session.
91
+ *
92
+ * Every requested id is in the result, null when no owner is on record.
93
+ * Shares ownerOf's cache, with the same rule: only resolved owners are kept.
94
+ * A failed query rejects; nothing is guessed.
95
+ */
96
+ ownersOf(sessionIds) {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ var _a, _b, _c;
99
+ const ids = [...new Set(sessionIds)];
100
+ const resolved = new Map();
101
+ const missing = [];
102
+ for (const id of ids) {
103
+ const cached = id ? ((_a = this.ownerCache.get(id)) !== null && _a !== void 0 ? _a : this.liveOwners().ownerOf(id)) : undefined;
104
+ if (cached) {
105
+ resolved.set(id, cached);
106
+ this.remember(this.ownerCache, id, cached);
107
+ }
108
+ else if (id)
109
+ missing.push(id);
110
+ else
111
+ resolved.set(id, null);
112
+ }
113
+ if (missing.length > 0) {
114
+ const rows = yield this.db.session.findMany({
115
+ where: { id: { in: missing } },
116
+ select: { id: true, api_key_id: true, user_id: true },
117
+ });
118
+ const byId = new Map(rows.map((row) => [row.id, row]));
119
+ // Rows written before user_id existed resolve through the key, as in ownerOf.
120
+ const legacyKeyIds = [
121
+ ...new Set(rows
122
+ .filter((row) => !row.user_id && row.api_key_id)
123
+ .map((row) => row.api_key_id)),
124
+ ];
125
+ const keyOwner = new Map();
126
+ if (legacyKeyIds.length > 0) {
127
+ const keys = yield this.db.apiKey.findMany({
128
+ where: { id: { in: legacyKeyIds } },
129
+ select: { id: true, userId: true },
130
+ });
131
+ for (const key of keys)
132
+ if (key.userId)
133
+ keyOwner.set(key.id, key.userId);
134
+ }
135
+ for (const id of missing) {
136
+ const row = byId.get(id);
137
+ const owner = (_b = row === null || row === void 0 ? void 0 : row.user_id) !== null && _b !== void 0 ? _b : ((row === null || row === void 0 ? void 0 : row.api_key_id) ? ((_c = keyOwner.get(row.api_key_id)) !== null && _c !== void 0 ? _c : null) : null);
138
+ resolved.set(id, owner);
139
+ if (owner)
140
+ this.remember(this.ownerCache, id, owner);
141
+ }
142
+ }
143
+ // In the order asked, so a caller filtering a list keeps its order.
144
+ return new Map(ids.map((id) => { var _a; return [id, (_a = resolved.get(id)) !== null && _a !== void 0 ? _a : null]; }));
145
+ });
146
+ }
73
147
  displayName(userId) {
74
148
  return __awaiter(this, void 0, void 0, function* () {
75
149
  var _a, _b;
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.makeTicketActorAuthorizer = makeTicketActorAuthorizer;
13
13
  const manualLock_1 = require("../recording/manualLock");
14
14
  const deviceAccessPolicy_1 = require("./deviceAccessPolicy");
15
+ const deviceClaims_1 = require("../../data-service/deviceClaims");
15
16
  /**
16
17
  * Build the ownership check for a caller authenticated by a stream ticket.
17
18
  *
@@ -54,7 +55,8 @@ function makeTicketActorAuthorizer(deps) {
54
55
  : null;
55
56
  return (0, deviceAccessPolicy_1.evaluateDeviceAccess)({
56
57
  udid,
57
- busy: !!device.busy,
58
+ // heldHere, as deviceAccessGuard reads it.
59
+ busy: (0, deviceClaims_1.heldHere)(device),
58
60
  sessionId: device.session_id,
59
61
  sessionOwnerUserId,
60
62
  // The ticket's actorId is a User id — see StreamTicketService.
@@ -22,10 +22,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.LeaseOrphanSweeper = void 0;
25
+ exports.leaseReapReason = leaseReapReason;
25
26
  const typedi_1 = require("typedi");
26
27
  const prisma_1 = require("../../prisma");
27
28
  const device_store_1 = require("../../data-service/device-store");
28
29
  const logger_1 = __importDefault(require("../../logger"));
30
+ /**
31
+ * Why an active lease is over, or null while it isn't: its client missed
32
+ * three heartbeats, or it passed expiresAt. The second is the rule
33
+ * LeaseService already applies (heartbeat and extend refuse such a lease,
34
+ * authorizeSessionUse won't resolve it); without it here, a client that kept
35
+ * heartbeating held the phone for up to 3 x heartbeatSeconds past the end.
36
+ */
37
+ function leaseReapReason(lease, now) {
38
+ var _a;
39
+ // The end time first: past it, heartbeats are refused, so a lease that
40
+ // expired has missed its heartbeats too by the next sweep.
41
+ if (lease.expiresAt < now)
42
+ return `expired at ${new Date(lease.expiresAt).toISOString()}`;
43
+ const thresholdMs = ((_a = lease.heartbeatSeconds) !== null && _a !== void 0 ? _a : 30) * 1000 * 3;
44
+ if (lease.lastHeartbeatAt + thresholdMs < now)
45
+ return 'missed heartbeats';
46
+ return null;
47
+ }
29
48
  let LeaseOrphanSweeper = class LeaseOrphanSweeper {
30
49
  constructor(db = prisma_1.prisma, store = device_store_1.DeviceStoreFactory.getStore()) {
31
50
  this.db = db;
@@ -34,21 +53,30 @@ let LeaseOrphanSweeper = class LeaseOrphanSweeper {
34
53
  }
35
54
  sweep() {
36
55
  return __awaiter(this, void 0, void 0, function* () {
37
- var _a;
38
56
  const now = Date.now();
39
57
  const candidates = yield this.db.lease.findMany({
40
58
  where: { status: 'active' },
41
- select: { id: true, deviceUdid: true, deviceHost: true, heartbeatSeconds: true, lastHeartbeatAt: true },
59
+ select: {
60
+ id: true,
61
+ deviceUdid: true,
62
+ deviceHost: true,
63
+ heartbeatSeconds: true,
64
+ lastHeartbeatAt: true,
65
+ expiresAt: true,
66
+ },
42
67
  });
43
68
  for (const lease of candidates) {
44
- const thresholdMs = ((_a = lease.heartbeatSeconds) !== null && _a !== void 0 ? _a : 30) * 1000 * 3;
45
- if (lease.lastHeartbeatAt + thresholdMs >= now)
69
+ const reason = leaseReapReason(lease, now);
70
+ if (!reason)
46
71
  continue;
47
72
  try {
48
73
  yield this.db.lease.update({ where: { id: lease.id }, data: { status: 'expired' } });
49
74
  yield this.db.portLease.deleteMany({ where: { leaseId: lease.id } });
50
- yield this.store.updateDevice(lease.deviceUdid, lease.deviceHost, { busy: false });
51
- this.logger.info(`reaped lease ${lease.id} (missed heartbeats); device ${lease.deviceUdid}@${lease.deviceHost} unblocked`);
75
+ // Only the lease's lock: a session that outlived its lease keeps the
76
+ // phone, and its own release frees it.
77
+ const freed = yield this.store.releaseLeaseLock(lease.deviceUdid, lease.deviceHost);
78
+ const device = `${lease.deviceUdid}@${lease.deviceHost}`;
79
+ this.logger.info(`reaped lease ${lease.id} (${reason}); device ${device} ${freed ? 'unblocked' : 'still held (a session or a hold), left busy'}`);
52
80
  }
53
81
  catch (err) {
54
82
  this.logger.warn(`failed to reap lease ${lease.id}: ${err.message}`);
@@ -279,13 +279,26 @@ let LeaseService = class LeaseService {
279
279
  return lease;
280
280
  });
281
281
  }
282
- heartbeat(leaseId, token) {
282
+ /**
283
+ * The lease, as loadActiveLease, if it also hasn't passed its expiresAt.
284
+ * The sweeper marks leases by missed heartbeats, not by expiresAt, so a
285
+ * lapsed lease can still read 'active'. It is gone all the same: heartbeat
286
+ * must not keep it and extend must not revive it. Release still takes it,
287
+ * so its holder can hand the device back early.
288
+ */
289
+ loadLiveLease(leaseId, token) {
283
290
  return __awaiter(this, void 0, void 0, function* () {
284
291
  const lease = yield this.loadActiveLease(leaseId, token);
285
- const now = Date.now();
286
- if (lease.expiresAt < now) {
292
+ if (lease.expiresAt < Date.now()) {
287
293
  throw new LeaseGone(`lease ${leaseId} expired at ${lease.expiresAt}`);
288
294
  }
295
+ return lease;
296
+ });
297
+ }
298
+ heartbeat(leaseId, token) {
299
+ return __awaiter(this, void 0, void 0, function* () {
300
+ const lease = yield this.loadLiveLease(leaseId, token);
301
+ const now = Date.now();
289
302
  // Guarded by status='active' so a concurrent sweeper expiration is observed.
290
303
  const result = yield this.db.lease.updateMany({
291
304
  where: { id: leaseId, status: 'active' },
@@ -299,7 +312,7 @@ let LeaseService = class LeaseService {
299
312
  }
300
313
  extend(leaseId, token, additionalMs) {
301
314
  return __awaiter(this, void 0, void 0, function* () {
302
- const lease = yield this.loadActiveLease(leaseId, token);
315
+ const lease = yield this.loadLiveLease(leaseId, token);
303
316
  const now = Date.now();
304
317
  const createdAtMs = typeof lease.createdAt === 'number' ? lease.createdAt : new Date(lease.createdAt).getTime();
305
318
  const ceiling = createdAtMs + MAX_LEASE_MS;
@@ -325,7 +338,8 @@ let LeaseService = class LeaseService {
325
338
  // idempotent and safe — port rows may already be gone, device may already
326
339
  // be unlocked. We treat that as a successful release.
327
340
  yield this.db.portLease.deleteMany({ where: { leaseId } });
328
- yield this.store.updateDevice(lease.deviceUdid, lease.deviceHost, { busy: false });
341
+ // Only the lease's lock: a session still on the phone keeps it busy.
342
+ yield this.store.releaseLeaseLock(lease.deviceUdid, lease.deviceHost);
329
343
  void result;
330
344
  });
331
345
  }
@@ -0,0 +1,55 @@
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.capsForNode = capsForNode;
7
+ const lodash_1 = __importDefault(require("lodash"));
8
+ const sessionCredentials_1 = require("./sessionCredentials");
9
+ const xenonOptions_1 = require("./xenonOptions");
10
+ /**
11
+ * The capabilities a hub sends the node whose phone it allocated.
12
+ *
13
+ * Each instance has its own database, and the hub has already done what
14
+ * needs its database: it checked the credentials, applied the team rule and
15
+ * resolved the lease to a phone. The node gets a request that stands on its
16
+ * own, whatever the node's auth settings: that one phone, and nothing the
17
+ * node can't act on.
18
+ *
19
+ * - No credentials. createSession took them out of `caps` first thing; they
20
+ * are removed here again so no copy that leaves the hub can carry one. The
21
+ * node gets the hub's create token instead (hubSessionToken.ts).
22
+ * - No lease id. The lease lives in the hub's database; a node that looked it
23
+ * up in its own would refuse the session.
24
+ * - The phone, pinned: `appium:udid` in alwaysMatch, and no `appium:udid` or
25
+ * `appium:udids` anywhere else. A lease-bound allocation does not write the
26
+ * udid into the caps, and a `udids` list would let the node pick another
27
+ * phone than the one the hub holds.
28
+ *
29
+ * `caps` is not modified: the hub keeps its own copy for its records.
30
+ */
31
+ const NOT_FOR_THE_NODE = ['leaseId', ...sessionCredentials_1.SECRET_OPTION_FIELDS];
32
+ const DEVICE_PICKERS = ['appium:udid', 'appium:udids'];
33
+ function capsForNode(caps, udid) {
34
+ const alwaysMatch = (0, xenonOptions_1.isOptionsObject)(caps === null || caps === void 0 ? void 0 : caps.alwaysMatch)
35
+ ? lodash_1.default.cloneDeep(caps.alwaysMatch)
36
+ : {};
37
+ const firstMatch = Array.isArray(caps === null || caps === void 0 ? void 0 : caps.firstMatch) && caps.firstMatch.length > 0
38
+ ? caps.firstMatch.map((entry) => ((0, xenonOptions_1.isOptionsObject)(entry) ? lodash_1.default.cloneDeep(entry) : entry))
39
+ : [{}];
40
+ for (const bucket of [alwaysMatch, ...firstMatch]) {
41
+ if (!(0, xenonOptions_1.isOptionsObject)(bucket))
42
+ continue;
43
+ for (const key of DEVICE_PICKERS)
44
+ delete bucket[key];
45
+ for (const namespace of xenonOptions_1.OPTION_NAMESPACES) {
46
+ const options = bucket[namespace];
47
+ if (!(0, xenonOptions_1.isOptionsObject)(options))
48
+ continue;
49
+ for (const field of NOT_FOR_THE_NODE)
50
+ delete options[field];
51
+ }
52
+ }
53
+ alwaysMatch['appium:udid'] = udid;
54
+ return Object.assign(Object.assign({}, caps), { alwaysMatch, firstMatch });
55
+ }
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SECRET_OPTION_FIELDS = void 0;
4
4
  exports.takeSessionCredentials = takeSessionCredentials;
5
- exports.capsWithCredentials = capsWithCredentials;
6
5
  const XenonCapabilityManager_1 = require("../../XenonCapabilityManager");
7
6
  const xenonOptions_1 = require("./xenonOptions");
8
7
  /**
@@ -15,7 +14,8 @@ const xenonOptions_1 = require("./xenonOptions");
15
14
  * them out of the capabilities, in place, before anything else reads them:
16
15
  * the pending-session row, device allocation, the driver, the Session row,
17
16
  * the dashboard and the logs. Every other option, the lease id included,
18
- * stays: a lease id is not a secret.
17
+ * stays: a lease id is not a secret. They never leave the server they were
18
+ * sent to.
19
19
  */
20
20
  exports.SECRET_OPTION_FIELDS = ['accessKey', 'token', 'sessionToken', 'leaseToken'];
21
21
  function buckets(caps) {
@@ -27,8 +27,11 @@ function buckets(caps) {
27
27
  * Read the session's credentials, then remove every secret field from both
28
28
  * namespaces in alwaysMatch and every firstMatch entry, in place: Appium hands
29
29
  * this same object to the driver. The namespaces themselves are kept, emptied
30
- * if need be, so a peer node's copy can put the credentials back where they
31
- * came from (see capsWithCredentials).
30
+ * if need be, with the options that are not secrets.
31
+ *
32
+ * Nothing puts the credentials back. A hub forwarding a create to a node
33
+ * sends the hub's own create token instead (gateway/hubSessionToken.ts): each
34
+ * instance has its own database, so the node could not check them anyway.
32
35
  */
33
36
  function takeSessionCredentials(caps) {
34
37
  var _a;
@@ -48,58 +51,3 @@ function takeSessionCredentials(caps) {
48
51
  }
49
52
  return credentials;
50
53
  }
51
- /** The secret fields to put back, from what was read. */
52
- function secretFields(credentials) {
53
- const fields = {};
54
- if (credentials.pair) {
55
- fields.accessKey = credentials.pair.accessKey;
56
- fields.token = credentials.pair.token;
57
- }
58
- if (credentials.sessionToken)
59
- fields.sessionToken = credentials.sessionToken;
60
- if (credentials.leaseToken)
61
- fields.leaseToken = credentials.leaseToken;
62
- return fields;
63
- }
64
- /**
65
- * Where the winning namespace lives in the merged view: the bucket (the one
66
- * that holds it, alwaysMatch first) and the namespace itself.
67
- */
68
- function home(caps) {
69
- const always = caps === null || caps === void 0 ? void 0 : caps.alwaysMatch;
70
- const first = Array.isArray(caps === null || caps === void 0 ? void 0 : caps.firstMatch) ? caps.firstMatch[0] : undefined;
71
- for (const namespace of xenonOptions_1.OPTION_NAMESPACES) {
72
- for (const bucket of [always, first]) {
73
- if (!(0, xenonOptions_1.isOptionsObject)(bucket) || !Object.prototype.hasOwnProperty.call(bucket, namespace)) {
74
- continue;
75
- }
76
- // alwaysMatch holding the key hides firstMatch's, as in the merged view.
77
- if ((0, xenonOptions_1.isOptionsObject)(bucket[namespace]))
78
- return { bucket, namespace };
79
- break;
80
- }
81
- }
82
- return undefined;
83
- }
84
- /**
85
- * A copy of `caps` with the credentials put back, for a peer Xenon node. The
86
- * node re-runs createSession: it authenticates and attributes the session,
87
- * enforces XENON_REQUIRE_SESSION_TOKEN and checks the lease itself, so it
88
- * needs exactly what this hub read. It takes them out again before its own
89
- * driver sees them. They go into the namespace that wins, so the node reads
90
- * the same values. `caps` is not modified. Never hand this to a cloud
91
- * provider: it is not a Xenon node.
92
- */
93
- function capsWithCredentials(caps, credentials) {
94
- var _a;
95
- const fields = secretFields(credentials);
96
- const target = home(caps);
97
- if (Object.keys(fields).length === 0 || !target)
98
- return caps;
99
- const { bucket, namespace } = target;
100
- const withCredentials = Object.assign(Object.assign({}, bucket), { [namespace]: Object.assign(Object.assign({}, bucket[namespace]), fields) });
101
- if (bucket === caps.alwaysMatch)
102
- return Object.assign(Object.assign({}, caps), { alwaysMatch: withCredentials });
103
- const [, ...rest] = (_a = caps.firstMatch) !== null && _a !== void 0 ? _a : [];
104
- return Object.assign(Object.assign({}, caps), { firstMatch: [withCredentials, ...rest] });
105
- }
@@ -19,6 +19,18 @@ class CloudSession extends RemoteSession_1.RemoteSession {
19
19
  getType() {
20
20
  return SessionType_1.default.CLOUD;
21
21
  }
22
+ /** A cloud provider is not a Xenon node: it never gets the hub's session token. */
23
+ callOptions() {
24
+ return __awaiter(this, void 0, void 0, function* () {
25
+ return {};
26
+ });
27
+ }
28
+ /** A cloud provider has no Xenon session-status route: the WebDriver probe it is. */
29
+ askNodeForSession() {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ return null;
32
+ });
33
+ }
22
34
  getScreenShot() {
23
35
  return __awaiter(this, void 0, void 0, function* () {
24
36
  return '';
@@ -49,6 +49,7 @@ exports.LocalSession = void 0;
49
49
  const AndroidDeviceManager_1 = __importDefault(require("../device-managers/AndroidDeviceManager"));
50
50
  const logger_1 = __importDefault(require("../logger"));
51
51
  const SessionType_1 = __importDefault(require("../enums/SessionType"));
52
+ const XenonSession_1 = require("./XenonSession");
52
53
  const RemoteSession_1 = require("./RemoteSession");
53
54
  const typedi_1 = require("typedi");
54
55
  const device_managers_1 = require("../device-managers");
@@ -56,27 +57,57 @@ const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
56
57
  const VideoPipelineService_1 = require("../services/VideoPipelineService");
57
58
  const AndroidStreamService_1 = __importDefault(require("../device-managers/android/AndroidStreamService"));
58
59
  const IOSStreamService_1 = __importDefault(require("../device-managers/ios/IOSStreamService"));
59
- function constructBasePath(path) {
60
- if (!path || path == '') {
61
- return '/wd-internal';
62
- }
63
- if (!path.startsWith('/')) {
64
- path = `/${path}`;
65
- }
66
- if (path.endsWith('/')) {
67
- path = path.substr(0, path.length - 2);
68
- }
69
- return `${path}/wd-internal`;
70
- }
60
+ const internalCall_1 = require("../gateway/internalCall");
71
61
  class LocalSession extends RemoteSession_1.RemoteSession {
72
62
  constructor(options) {
73
63
  const { address, port, basePath } = options.driver.opts || options.driver;
74
- super(Object.assign(Object.assign({}, options), { baseUrl: `http://${address}:${port}${constructBasePath(basePath)}` }));
64
+ // The inherited HTTP calls loop back to this server's own
65
+ // `<basePath>/wd-internal`, which the session gateway accepts only with
66
+ // this process's secret (gateway/internalCall.ts).
67
+ super(Object.assign(Object.assign({}, options), { baseUrl: (0, internalCall_1.internalCallBaseUrl)(address, port, basePath) }));
75
68
  this.driver = options.driver;
76
- // Store the proper Appium base URL for video recording commands.
77
- // Principal Intelligence: Map 0.0.0.0 to 127.0.0.1 for internal loopack safety.
78
- const safeAddress = address === '0.0.0.0' ? '127.0.0.1' : address;
79
- this.appiumBaseUrl = `http://${safeAddress}:${port}${basePath || ''}`;
69
+ }
70
+ /**
71
+ * A loopback call carries the per-process secret, and never goes through an
72
+ * HTTP proxy from the environment, which would receive the secret.
73
+ */
74
+ callOptions() {
75
+ return __awaiter(this, void 0, void 0, function* () {
76
+ return { headers: (0, internalCall_1.internalCallHeaders)(), proxy: false };
77
+ });
78
+ }
79
+ /**
80
+ * Whether Appium still has this session, asked of the in-process driver.
81
+ *
82
+ * Not an HTTP probe: any command sent to the session, `timeouts` included,
83
+ * restarts the driver's new-command timeout and Xenon's own idle clock, so a
84
+ * heartbeat every 30 s would keep a session its client abandoned (and its
85
+ * phone) alive for ever. The umbrella drops a session from its map when it
86
+ * is deleted, times out, or its driver shuts down unexpectedly.
87
+ */
88
+ checkHealth() {
89
+ return __awaiter(this, void 0, void 0, function* () {
90
+ var _a;
91
+ if (!this.sessionId) {
92
+ return {
93
+ isHealthy: false,
94
+ errorType: XenonSession_1.HealthErrorType.NONE,
95
+ message: 'No session ID assigned',
96
+ };
97
+ }
98
+ const umbrella = this.driver;
99
+ const exists = typeof (umbrella === null || umbrella === void 0 ? void 0 : umbrella.sessionExists) === 'function'
100
+ ? !!umbrella.sessionExists(this.sessionId)
101
+ : !!((_a = umbrella === null || umbrella === void 0 ? void 0 : umbrella.sessions) === null || _a === void 0 ? void 0 : _a[this.sessionId]);
102
+ if (exists)
103
+ return { isHealthy: true, errorType: XenonSession_1.HealthErrorType.NONE };
104
+ return {
105
+ isHealthy: false,
106
+ errorType: XenonSession_1.HealthErrorType.SESSION_NOT_FOUND,
107
+ message: 'Appium no longer has this session',
108
+ statusCode: 404,
109
+ };
110
+ });
80
111
  }
81
112
  /**
82
113
  * A local session's driver is in this process, so ask it directly instead of
@@ -280,7 +311,7 @@ class LocalSession extends RemoteSession_1.RemoteSession {
280
311
  isVideoRecordingInProgress() {
281
312
  return typedi_1.Container.get(VideoPipelineService_1.VideoPipelineService).isRecording(this.sessionId);
282
313
  }
283
- // Override to use proper Appium URL for video commands
314
+ // The in-process pipeline or driver first; the loopback call last.
284
315
  stopVideoRecording(driver) {
285
316
  const _super = Object.create(null, {
286
317
  stopVideoRecording: { get: () => super.stopVideoRecording }
@@ -313,14 +344,9 @@ class LocalSession extends RemoteSession_1.RemoteSession {
313
344
  catch (err) {
314
345
  logger_1.default.warn(`[LocalSession] Direct stopRecordingScreen failed: ${err.message}. Falling back to HTTP.`);
315
346
  }
316
- const originalBaseUrl = this.baseUrl;
317
- this.baseUrl = this.appiumBaseUrl;
318
- try {
319
- return yield _super.stopVideoRecording.call(this);
320
- }
321
- finally {
322
- this.baseUrl = originalBaseUrl;
323
- }
347
+ // Over HTTP to this server's own /wd-internal, with the secret: the public
348
+ // session path would need the owner's credentials under per-command auth.
349
+ return yield _super.stopVideoRecording.call(this);
324
350
  });
325
351
  }
326
352
  getLiveVideoUrl() {