@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
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const deviceTeamGuard_1 = require("../../src/middleware/deviceTeamGuard");
20
20
  const controlDevice_1 = require("../../src/middleware/controlDevice");
21
21
  const deviceVisibility_1 = require("../../src/services/device-access/deviceVisibility");
@@ -93,7 +93,7 @@ function appWith(caller, findDevice = (udid) => __awaiter(this, void 0, void 0,
93
93
  return { app, seen };
94
94
  }
95
95
  function call(app, method, path) {
96
- return (0, supertest_1.default)(app)[method](path);
96
+ return (0, loopbackRequest_1.default)(app)[method](path);
97
97
  }
98
98
  /** What a caller can observe of an answer, with the requested udid masked out. */
99
99
  function observed(res, udid) {
@@ -153,7 +153,7 @@ describe('deviceTeamGuard', () => {
153
153
  }
154
154
  it('hands a hidden phone on as a udid no device has, keeping the original URL', () => __awaiter(void 0, void 0, void 0, function* () {
155
155
  const { app, seen } = appWith(ALICE);
156
- yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
156
+ yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
157
157
  (0, chai_1.expect)(seen).to.deep.equal([controlDevice_1.HIDDEN_DEVICE_UDID]);
158
158
  (0, chai_1.expect)(DEVICES[controlDevice_1.HIDDEN_DEVICE_UDID]).to.equal(undefined);
159
159
  }));
@@ -171,7 +171,7 @@ describe('deviceTeamGuard', () => {
171
171
  res.end();
172
172
  });
173
173
  app.use('/control', router);
174
- yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
174
+ yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot?x=1`);
175
175
  (0, chai_1.expect)(originalUrl).to.equal(`/control/${PHONE_B}/screenshot?x=1`);
176
176
  }));
177
177
  describe('restoreHiddenDeviceUrl, mounted after the router at the parent', () => {
@@ -196,7 +196,7 @@ describe('deviceTeamGuard', () => {
196
196
  return { app, seenByRoute };
197
197
  }
198
198
  it('gives a later layer the real req.url after the router falls through', () => __awaiter(void 0, void 0, void 0, function* () {
199
- const res = yield (0, supertest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_B}/bogus?x=1`);
199
+ const res = yield (0, loopbackRequest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_B}/bogus?x=1`);
200
200
  (0, chai_1.expect)(res.body).to.deep.equal({
201
201
  url: `/control/${PHONE_B}/bogus?x=1`,
202
202
  path: `/control/${PHONE_B}/bogus`,
@@ -204,26 +204,26 @@ describe('deviceTeamGuard', () => {
204
204
  }));
205
205
  it('answers a hidden phone and an unknown udid alike, even where a layer echoes req.url', () => __awaiter(void 0, void 0, void 0, function* () {
206
206
  const { app } = parentApp(ALICE);
207
- const hidden = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/bogus`);
208
- const unknown = yield (0, supertest_1.default)(app).get(`/control/${UNKNOWN}/bogus`);
207
+ const hidden = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/bogus`);
208
+ const unknown = yield (0, loopbackRequest_1.default)(app).get(`/control/${UNKNOWN}/bogus`);
209
209
  (0, chai_1.expect)(observed(hidden, PHONE_B)).to.deep.equal(observed(unknown, UNKNOWN));
210
210
  (0, chai_1.expect)(hidden.text).to.not.include(controlDevice_1.HIDDEN_DEVICE_UDID);
211
211
  }));
212
212
  it('still hands a routed request the placeholder, not the real udid', () => __awaiter(void 0, void 0, void 0, function* () {
213
213
  const { app, seenByRoute } = parentApp(ALICE);
214
- const res = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
214
+ const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
215
215
  (0, chai_1.expect)(res.text).to.equal('Device not found');
216
216
  (0, chai_1.expect)(seenByRoute).to.deep.equal([controlDevice_1.HIDDEN_DEVICE_UDID]);
217
217
  }));
218
218
  it('leaves a request it did not rewrite alone', () => __awaiter(void 0, void 0, void 0, function* () {
219
- const res = yield (0, supertest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_A}/bogus`);
219
+ const res = yield (0, loopbackRequest_1.default)(parentApp(ALICE).app).get(`/control/${PHONE_A}/bogus`);
220
220
  (0, chai_1.expect)(res.body.url).to.equal(`/control/${PHONE_A}/bogus`);
221
221
  }));
222
222
  });
223
223
  it('gives a member in no team the shared pool only', () => __awaiter(void 0, void 0, void 0, function* () {
224
224
  const { app } = appWith(NO_TEAM);
225
- (0, chai_1.expect)((yield (0, supertest_1.default)(app).get(`/control/${SHARED}/screenshot`)).status).to.equal(200);
226
- const a = yield (0, supertest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
225
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).get(`/control/${SHARED}/screenshot`)).status).to.equal(200);
226
+ const a = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
227
227
  (0, chai_1.expect)(a.status).to.equal(404);
228
228
  (0, chai_1.expect)(a.text).to.equal('Device not found');
229
229
  }));
@@ -231,16 +231,16 @@ describe('deviceTeamGuard', () => {
231
231
  // plain-text default. A hidden phone gets the same body, from the handler.
232
232
  for (const action of ['display', 'appium-session']) {
233
233
  it(`answers ${action} with that handler's own JSON 404 body`, () => __awaiter(void 0, void 0, void 0, function* () {
234
- const res = yield (0, supertest_1.default)(appWith(ALICE).app).get(`/control/${PHONE_B}/${action}`);
234
+ const res = yield (0, loopbackRequest_1.default)(appWith(ALICE).app).get(`/control/${PHONE_B}/${action}`);
235
235
  (0, chai_1.expect)(res.status).to.equal(404);
236
236
  (0, chai_1.expect)(res.body).to.deep.equal({ status: 'error', message: 'Device not found' });
237
237
  }));
238
238
  }
239
239
  it('guards an action in any letter case', () => __awaiter(void 0, void 0, void 0, function* () {
240
240
  const { app, seen } = appWith(ALICE);
241
- const start = yield (0, supertest_1.default)(app).post(`/control/${PHONE_B}/Stream/Start`);
241
+ const start = yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_B}/Stream/Start`);
242
242
  (0, chai_1.expect)(start.status).to.equal(404);
243
- const display = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/Display`);
243
+ const display = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/Display`);
244
244
  (0, chai_1.expect)(display.status).to.equal(404);
245
245
  (0, chai_1.expect)(display.body).to.deep.equal({ status: 'error', message: 'Device not found' });
246
246
  (0, chai_1.expect)(seen).to.not.include(PHONE_B);
@@ -272,7 +272,7 @@ describe('deviceTeamGuard', () => {
272
272
  // drive a phone the device list hides from them.
273
273
  it('ignores an admin scope on a caller whose teams are scoped', () => __awaiter(void 0, void 0, void 0, function* () {
274
274
  const scoped = { role: 'MEMBER', scopes: 'admin,devices', teamIds: [TEAM_A] };
275
- const res = yield (0, supertest_1.default)(appWith(scoped).app).post(`/control/${PHONE_B}/tap`);
275
+ const res = yield (0, loopbackRequest_1.default)(appWith(scoped).app).post(`/control/${PHONE_B}/tap`);
276
276
  (0, chai_1.expect)(res.status).to.equal(404);
277
277
  }));
278
278
  it('does not look the device up for an admin', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -282,13 +282,13 @@ describe('deviceTeamGuard', () => {
282
282
  lookups += 1;
283
283
  return (_a = DEVICES[udid]) !== null && _a !== void 0 ? _a : null;
284
284
  }));
285
- const res = yield (0, supertest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
285
+ const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_B}/screenshot`);
286
286
  (0, chai_1.expect)(res.status).to.equal(200);
287
287
  (0, chai_1.expect)(lookups).to.equal(0);
288
288
  }));
289
289
  it('passes an unknown device on untouched, for the handler to answer', () => __awaiter(void 0, void 0, void 0, function* () {
290
290
  const { app, seen } = appWith(ALICE);
291
- const res = yield (0, supertest_1.default)(app).post(`/control/${UNKNOWN}/tap`);
291
+ const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${UNKNOWN}/tap`);
292
292
  (0, chai_1.expect)(res.status).to.equal(404);
293
293
  (0, chai_1.expect)(seen).to.deep.equal([UNKNOWN]);
294
294
  }));
@@ -296,7 +296,7 @@ describe('deviceTeamGuard', () => {
296
296
  const { app, seen } = appWith(ALICE, () => __awaiter(void 0, void 0, void 0, function* () {
297
297
  throw new Error('store hiccup');
298
298
  }));
299
- const res = yield (0, supertest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
299
+ const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
300
300
  (0, chai_1.expect)(res.status).to.equal(503);
301
301
  (0, chai_1.expect)(res.body).to.deep.equal({
302
302
  success: false,
@@ -310,13 +310,13 @@ describe('deviceTeamGuard', () => {
310
310
  // An async middleware that throws never answers under Express 4, so keep
311
311
  // a short timeout: a regression should fail fast, not after 60s.
312
312
  this.timeout(5000);
313
- const res = yield (0, supertest_1.default)(appWith(ALICE).app).get('/control/100%/screenshot');
313
+ const res = yield (0, loopbackRequest_1.default)(appWith(ALICE).app).get('/control/100%/screenshot');
314
314
  (0, chai_1.expect)(res.status).to.equal(400);
315
315
  (0, chai_1.expect)(res.body).to.deep.equal({ success: false, error: 'invalid_udid' });
316
316
  });
317
317
  });
318
318
  it('401s when there is no caller', () => __awaiter(void 0, void 0, void 0, function* () {
319
- const res = yield (0, supertest_1.default)(appWith(null).app).get(`/control/${PHONE_A}/screenshot`);
319
+ const res = yield (0, loopbackRequest_1.default)(appWith(null).app).get(`/control/${PHONE_A}/screenshot`);
320
320
  (0, chai_1.expect)(res.status).to.equal(401);
321
321
  }));
322
322
  });
@@ -57,11 +57,18 @@ const device_service_1 = require("../../src/data-service/device-service");
57
57
  const device_utils_1 = require("../../src/device-utils");
58
58
  const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
59
59
  const test_container_1 = require("../helpers/test-container");
60
+ const loki_stores_1 = require("../helpers/loki-stores");
61
+ const scratch_database_1 = require("../helpers/scratch-database");
60
62
  chai_1.default.should();
61
63
  chai_1.default.use(sinon_chai_1.default);
62
64
  const expect = chai_1.default.expect;
63
65
  const sandbox = sinon_1.default.createSandbox();
64
66
  describe('Device Utils', () => {
67
+ // The in-memory device store, also when this file runs on its own, and a
68
+ // database of its own for what allocation reads through Prisma. It passed
69
+ // only against the developer's ~/.cache/xenon/xenon.db, which has the tables.
70
+ (0, loki_stores_1.useLokiStores)();
71
+ (0, scratch_database_1.useScratchDatabase)();
65
72
  const hub1Device = {
66
73
  systemPort: 56205,
67
74
  sdk: '10',
@@ -162,6 +169,7 @@ describe('Device Utils', () => {
162
169
  yield (0, test_container_1.resetTestContainer)();
163
170
  yield (0, test_container_1.setupTestContainer)();
164
171
  }));
172
+ after(() => (0, test_container_1.resetTestContainer)());
165
173
  it('Allocate devices for session with host filter', () => __awaiter(void 0, void 0, void 0, function* () {
166
174
  (yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
167
175
  const deviceManager = (0, test_container_1.createTestXenonManager)(Object.assign({}, pluginArgs, { maxSessions: 3, platform: 'android' }));
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const XenonCapabilityManager_1 = require("../../src/XenonCapabilityManager");
5
+ // The team a session asks to be placed in. Documented as xe:options.team;
6
+ // xenon:options is read as its alias, through the same precedence helper as
7
+ // every other Xenon option. The flat Xenon-prefixed caps older clients send
8
+ // (xenon:team, xe:teamId, ...) still work, below the options. df: is never
9
+ // read.
10
+ const always = (alwaysMatch) => ({ alwaysMatch, firstMatch: [{}] });
11
+ describe('extractTeamCap', () => {
12
+ it('reads xe:options.team, the documented form', () => {
13
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xe:options': { team: 'team_a' } }))).to.equal('team_a');
14
+ });
15
+ it('reads xe:options.teamId', () => {
16
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xe:options': { teamId: 'team_a' } }))).to.equal('team_a');
17
+ });
18
+ it('still reads the xenon:options alias alone', () => {
19
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xenon:options': { team: 'team_a' } }))).to.equal('team_a');
20
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xenon:options': { teamId: 'team_a' } }))).to.equal('team_a');
21
+ });
22
+ it('lets xe:options win over xenon:options', () => {
23
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xenon:options': { team: 'team_old' }, 'xe:options': { team: 'team_new' } }))).to.equal('team_new');
24
+ // Across alwaysMatch and firstMatch, as allocation merges them.
25
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)({
26
+ alwaysMatch: { 'xenon:options': { teamId: 'team_old' } },
27
+ firstMatch: [{ 'xe:options': { teamId: 'team_new' } }],
28
+ })).to.equal('team_new');
29
+ });
30
+ it('lets an options field win over a flat cap', () => {
31
+ for (const flat of ['xenon:team', 'xenon:teamId', 'xe:team', 'xe:teamId', 'appium:team']) {
32
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ [flat]: 'team_flat', 'xe:options': { team: 'team_opt' } })), flat).to.equal('team_opt');
33
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ [flat]: 'team_flat', 'xenon:options': { teamId: 'team_opt' } })), flat).to.equal('team_opt');
34
+ }
35
+ });
36
+ it('keeps every flat form working when no option is set', () => {
37
+ for (const prefix of ['xenon:', 'xe:', 'appium:', '']) {
38
+ for (const name of ['teamId', 'team_id', 'team']) {
39
+ const key = `${prefix}${name}`;
40
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ [key]: 'team_flat' })), key).to.equal('team_flat');
41
+ }
42
+ }
43
+ });
44
+ it('falls back to a flat cap when the option is empty or not a string', () => {
45
+ for (const team of ['', 7, null, { id: 'team_x' }]) {
46
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({ 'xe:options': { team }, 'xenon:team': 'team_flat' })), JSON.stringify(team)).to.equal('team_flat');
47
+ }
48
+ });
49
+ it('never reads df:', () => {
50
+ for (const caps of [
51
+ { 'df:options': { team: 'team_df' } },
52
+ { 'df:options': { teamId: 'team_df' } },
53
+ { 'xenon:df:options': { team: 'team_df' } },
54
+ { 'appium:df:options': { team: 'team_df' } },
55
+ { 'df:team': 'team_df' },
56
+ { 'df:teamId': 'team_df' },
57
+ ]) {
58
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always(caps)), JSON.stringify(caps)).to.equal(undefined);
59
+ }
60
+ });
61
+ it('is undefined when nothing asks for a team', () => {
62
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)(always({}))).to.equal(undefined);
63
+ (0, chai_1.expect)((0, XenonCapabilityManager_1.extractTeamCap)({})).to.equal(undefined);
64
+ });
65
+ });
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const grid_1 = __importDefault(require("../../src/app/routers/grid"));
20
20
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
21
21
  /**
@@ -44,12 +44,12 @@ describe('grid operator reads are admin-only', () => {
44
44
  }
45
45
  for (const url of ['/logs/requests', '/node/status', '/node/10.0.0.2/status']) {
46
46
  it(`a member gets 403 from GET ${url}`, () => __awaiter(void 0, void 0, void 0, function* () {
47
- const r = yield (0, supertest_1.default)(app('MEMBER')).get(url);
47
+ const r = yield (0, loopbackRequest_1.default)(app('MEMBER')).get(url);
48
48
  (0, chai_1.expect)(r.status).to.equal(403);
49
49
  }));
50
50
  }
51
51
  it('an admin still reads the request log', () => __awaiter(void 0, void 0, void 0, function* () {
52
- const r = yield (0, supertest_1.default)(app('ADMIN')).get('/logs/requests');
52
+ const r = yield (0, loopbackRequest_1.default)(app('ADMIN')).get('/logs/requests');
53
53
  (0, chai_1.expect)(r.status).to.equal(200);
54
54
  }));
55
55
  });
@@ -9,12 +9,14 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ require("reflect-metadata");
12
13
  const chai_1 = require("chai");
13
14
  const typedi_1 = require("typedi");
14
15
  const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
15
16
  const HealEtalonService_1 = require("../../src/services/healing/HealEtalonService");
16
17
  const HealedLocatorGenerator_1 = require("../../src/services/healing/HealedLocatorGenerator");
17
18
  const types_1 = require("../../src/services/healing/types");
19
+ const container_registration_1 = require("../helpers/container-registration");
18
20
  // §2.7 healing-tier capability gate: xe:options.healingTiers restricts which
19
21
  // self-healing providers may be dispatched. Tier index mapping used by the gate
20
22
  // (array-position based, NOT the internal HealingTier enum): 1=Resilio,
@@ -104,8 +106,9 @@ describe('HealingOrchestrator.attemptHealing allowedTiers dispatch gate', () =>
104
106
  let orchestrator;
105
107
  let mockEtalonService;
106
108
  const dispatched = [];
109
+ let restoreContainer;
107
110
  beforeEach(() => {
108
- typedi_1.Container.reset();
111
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealEtalonService_1.HealEtalonService, HealedLocatorGenerator_1.HealedLocatorGenerator);
109
112
  dispatched.length = 0;
110
113
  mockEtalonService = {
111
114
  getSignature: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
@@ -123,6 +126,7 @@ describe('HealingOrchestrator.attemptHealing allowedTiers dispatch gate', () =>
123
126
  });
124
127
  });
125
128
  });
129
+ afterEach(() => restoreContainer());
126
130
  const mockDriver = {
127
131
  getPageSource: () => __awaiter(void 0, void 0, void 0, function* () { return '<xml/>'; }),
128
132
  getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return 'fake-screenshot'; }),
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const HealthMonitorService_1 = require("../../src/device-managers/HealthMonitorService");
20
+ const device_managers_1 = require("../../src/device-managers");
21
+ const device_store_1 = require("../../src/data-service/device-store");
22
+ const PluginContext_1 = require("../../src/PluginContext");
23
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
24
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
25
+ const container_registration_1 = require("../helpers/container-registration");
26
+ /**
27
+ * A hub keeps its nodes' phones in its own Device table. Its health monitor
28
+ * ran adb (or go-ios) health checks on every row, so it checked node phones it
29
+ * cannot reach from its own machine: each came back unhealthy, the hub wrote
30
+ * that over the node's report, started a "recovery" with its own adb, and
31
+ * reclaimed a busy node phone whose session it didn't hold in memory. Only
32
+ * this server's own phones are checked (isOwnDevice: by nodeId, else by exact
33
+ * host).
34
+ */
35
+ // Named like the real manager: the monitor picks one by constructor.name.
36
+ class AndroidDeviceManager {
37
+ constructor() {
38
+ this.checked = [];
39
+ this.checkHealth = (device) => __awaiter(this, void 0, void 0, function* () {
40
+ this.checked.push(device.udid);
41
+ return { healthStatus: 'Healthy' };
42
+ });
43
+ }
44
+ }
45
+ describe('HealthMonitorService checks only this server’s own phones', () => {
46
+ const HUB_NODE_ID = 'hub-node-id';
47
+ const HUB_HOST = 'http://10.0.0.1:4799';
48
+ let manager;
49
+ let updateDevice;
50
+ let context;
51
+ let saved;
52
+ let restore;
53
+ const devices = [
54
+ // Found by this hub's own discovery.
55
+ { udid: 'hub-phone', host: HUB_HOST, nodeId: HUB_NODE_ID },
56
+ // A node's, reported over /register with the node's id.
57
+ { udid: 'node-phone', host: 'http://10.0.0.2:4725', nodeId: 'node-peer' },
58
+ // A node's on the same machine, differing only by port.
59
+ { udid: 'same-machine-node-phone', host: 'http://10.0.0.1:4725', nodeId: 'node-2' },
60
+ // A node's busy phone, its session not in this hub's memory.
61
+ {
62
+ udid: 'busy-node-phone',
63
+ host: 'http://10.0.0.2:4725',
64
+ nodeId: 'node-peer',
65
+ busy: true,
66
+ session_id: 'node-session',
67
+ },
68
+ // A row from before nodeId, known by its host alone.
69
+ { udid: 'legacy-hub-phone', host: HUB_HOST, nodeId: null },
70
+ { udid: 'legacy-node-phone', host: 'http://10.0.0.3:4725', nodeId: null },
71
+ ].map((d) => (Object.assign({ platform: 'android', busy: false, cloud: null }, d)));
72
+ beforeEach(() => {
73
+ sinon_1.default.stub(process, 'kill');
74
+ manager = new AndroidDeviceManager();
75
+ restore = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager);
76
+ typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(void 0, void 0, void 0, function* () { return [manager]; }) });
77
+ updateDevice = sinon_1.default.spy(() => __awaiter(void 0, void 0, void 0, function* () { return undefined; }));
78
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
79
+ getAllDevices: () => __awaiter(void 0, void 0, void 0, function* () { return devices; }),
80
+ updateDevice,
81
+ });
82
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'isValidSession').returns(false);
83
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
84
+ saved = Object.assign({}, context);
85
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '10.0.0.1' }), 4799, HUB_NODE_ID, '');
86
+ });
87
+ afterEach(() => {
88
+ Object.assign(context, saved);
89
+ sinon_1.default.restore();
90
+ restore();
91
+ });
92
+ const run = () => new HealthMonitorService_1.HealthMonitorService({}).checkAllDevices();
93
+ it('checks the hub’s own phones and none of its nodes’', () => __awaiter(void 0, void 0, void 0, function* () {
94
+ yield run();
95
+ (0, chai_1.expect)(manager.checked.sort()).to.deep.equal(['hub-phone', 'legacy-hub-phone']);
96
+ }));
97
+ it('never writes to a node’s phone, nor reclaims a busy one', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ yield run();
99
+ const written = updateDevice.getCalls().map((c) => c.args[0]);
100
+ (0, chai_1.expect)(written.sort()).to.deep.equal(['hub-phone', 'legacy-hub-phone']);
101
+ }));
102
+ });
@@ -22,6 +22,7 @@ const device_store_1 = require("../../src/data-service/device-store");
22
22
  const SessionManager_1 = require("../../src/sessions/SessionManager");
23
23
  const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
24
24
  const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
25
+ const PluginContext_1 = require("../../src/PluginContext");
25
26
  // Named like the real managers: the monitor picks one by constructor.name.
26
27
  class AndroidDeviceManager {
27
28
  constructor() {
@@ -33,12 +34,15 @@ class IOSDeviceManager {
33
34
  this.checkHealth = () => __awaiter(this, void 0, void 0, function* () { return ({ healthStatus: 'Healthy' }); });
34
35
  }
35
36
  }
37
+ const THIS_SERVER = 'this-server-node-id';
36
38
  // A device busy with an Appium session this process no longer knows about,
37
39
  // which is the case the monitor reclaims, unless a stream is still running.
38
40
  function lostSessionDevice(platform) {
39
41
  return {
40
42
  udid: `${platform}-1`,
41
43
  host: 'http://127.0.0.1:4723',
44
+ // This server's own phone: the monitor checks no other server's.
45
+ nodeId: THIS_SERVER,
42
46
  platform,
43
47
  busy: true,
44
48
  session_id: 'appium-session-not-in-memory',
@@ -51,7 +55,10 @@ describe('HealthMonitorService — stream lookups', () => {
51
55
  let androidStatus;
52
56
  let iosStatus;
53
57
  const reclaimed = () => updateDevice.getCalls().some((c) => { var _a, _b; return ((_a = c.args[2]) === null || _a === void 0 ? void 0 : _a.busy) === false && !((_b = c.args[2]) === null || _b === void 0 ? void 0 : _b.session_id); });
58
+ let nodeIdBefore;
54
59
  beforeEach(() => {
60
+ nodeIdBefore = typedi_1.Container.get(PluginContext_1.PluginContext).nodeId;
61
+ typedi_1.Container.get(PluginContext_1.PluginContext).nodeId = THIS_SERVER;
55
62
  sinon_1.default.stub(process, 'kill');
56
63
  updateDevice = sinon_1.default.spy(() => __awaiter(void 0, void 0, void 0, function* () { return undefined; }));
57
64
  sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
@@ -78,7 +85,10 @@ describe('HealthMonitorService — stream lookups', () => {
78
85
  return real(token);
79
86
  });
80
87
  });
81
- afterEach(() => sinon_1.default.restore());
88
+ afterEach(() => {
89
+ sinon_1.default.restore();
90
+ typedi_1.Container.get(PluginContext_1.PluginContext).nodeId = nodeIdBefore;
91
+ });
82
92
  const run = () => new HealthMonitorService_1.HealthMonitorService({}).checkAllDevices();
83
93
  it('leaves an Android device with a running stream alone', () => __awaiter(void 0, void 0, void 0, function* () {
84
94
  device = lostSessionDevice('android');
@@ -46,6 +46,33 @@ describe('redactSecrets', () => {
46
46
  const redacted = (0, helpers_1.redactSecrets)(arr);
47
47
  (0, chai_1.expect)(redacted[0]).to.equal('[Circular]');
48
48
  });
49
+ // Half of an xe:options credential pair: without its token it proves
50
+ // nothing, but it names the key a leaked token would complete.
51
+ it('should redact access keys in every spelling', () => {
52
+ const KEY = 'xen_AbC123dEf456';
53
+ const obj = {
54
+ accessKey: KEY,
55
+ access_key: KEY,
56
+ ACCESS_KEY: KEY,
57
+ 'x-xenon-access-key': KEY,
58
+ 'xe:options': { accessKey: KEY, leaseId: 'lse_1' },
59
+ };
60
+ const redacted = (0, helpers_1.redactSecrets)(obj);
61
+ (0, chai_1.expect)(redacted).to.deep.equal({
62
+ accessKey: '***REDACTED***',
63
+ access_key: '***REDACTED***',
64
+ ACCESS_KEY: '***REDACTED***',
65
+ 'x-xenon-access-key': '***REDACTED***',
66
+ 'xe:options': { accessKey: '***REDACTED***', leaseId: 'lse_1' },
67
+ });
68
+ });
69
+ // GET /xenon/api/profile/access-key and /auth/me hand the caller their own
70
+ // key. Logging the same object must not take it out of the response.
71
+ it('should leave the object it was given untouched', () => {
72
+ const body = { accessKey: 'xen_AbC123dEf456', nested: { token: 't' } };
73
+ (0, helpers_1.redactSecrets)(body);
74
+ (0, chai_1.expect)(body).to.deep.equal({ accessKey: 'xen_AbC123dEf456', nested: { token: 't' } });
75
+ });
49
76
  it('should redact falsy sensitive values', () => {
50
77
  const obj = {
51
78
  password: '',