@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,356 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const events_1 = require("events");
50
+ const chai_1 = require("chai");
51
+ const sinon_1 = __importDefault(require("sinon"));
52
+ const typedi_1 = require("typedi");
53
+ const prisma_1 = require("../../src/prisma");
54
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
55
+ const device_store_1 = require("../../src/data-service/device-store");
56
+ const device_service_1 = require("../../src/data-service/device-service");
57
+ const device_utils_1 = require("../../src/device-utils");
58
+ const helpers = __importStar(require("../../src/helpers"));
59
+ const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
60
+ const AndroidDeviceManager_1 = __importDefault(require("../../src/device-managers/AndroidDeviceManager"));
61
+ const IOSDiscoveryService_1 = require("../../src/device-managers/ios/IOSDiscoveryService");
62
+ const iOSTracker_1 = require("../../src/device-managers/iOSTracker");
63
+ const device_managers_1 = require("../../src/device-managers");
64
+ const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
65
+ const PluginContext_1 = require("../../src/PluginContext");
66
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
67
+ const SocketServer_1 = require("../../src/services/SocketServer");
68
+ const NotificationService_1 = require("../../src/services/NotificationService");
69
+ const container_registration_1 = require("../helpers/container-registration");
70
+ const fake_device_table_1 = require("../helpers/fake-device-table");
71
+ /**
72
+ * A hub on :4724 and a node on :4725 on one machine share an IP, so their
73
+ * phones' hosts differ only by port. Seen on 2026-09-28: the node registered
74
+ * its phone with the hub, and the hub's own local sync deleted it a few
75
+ * seconds later as "no longer discovered on this host", because it decided
76
+ * which rows were its own by IP.
77
+ */
78
+ const IP = '192.168.0.104';
79
+ const HUB = `http://${IP}:4724`;
80
+ const NODE = `http://${IP}:4725`;
81
+ const hubHosts = (over = {}) => (0, localDeviceHosts_1.localDeviceHosts)(Object.assign({ bindHostOrIp: IP }, over), 4724);
82
+ const nodeHosts = (over = {}) => (0, localDeviceHosts_1.localDeviceHosts)(Object.assign({ bindHostOrIp: IP }, over), 4725);
83
+ const phone = (udid, host, over = {}) => (Object.assign({ udid,
84
+ host, name: udid, platform: 'android', deviceType: 'real', realDevice: true, state: 'device', sdk: '14' }, over));
85
+ const context = (args, port) => {
86
+ const ctx = new PluginContext_1.PluginContext();
87
+ ctx.setContext(Object.assign({}, IPluginArgs_1.DefaultPluginArgs, args), port, 'test-node', '');
88
+ return ctx;
89
+ };
90
+ describe('localDeviceHosts', () => {
91
+ it("is this server's origin when nothing else files a phone elsewhere", () => {
92
+ const local = hubHosts();
93
+ (0, chai_1.expect)(local.origin).to.equal(HUB);
94
+ (0, chai_1.expect)([...local.hosts]).to.deep.equal([HUB]);
95
+ });
96
+ it('adds both remoteMachineProxyIP forms: with the port (Android) and as given (iOS)', () => {
97
+ (0, chai_1.expect)([...hubHosts({ remoteMachineProxyIP: '203.0.113.7' }).hosts]).to.have.members([
98
+ HUB,
99
+ 'http://203.0.113.7:4724',
100
+ '203.0.113.7',
101
+ ]);
102
+ });
103
+ it('adds each adbRemote server, with adb’s default port when none is given', () => {
104
+ (0, chai_1.expect)((0, localDeviceHosts_1.parseAdbRemote)('10.0.0.9')).to.deep.equal({ adbHost: '10.0.0.9', adbPort: 5037 });
105
+ (0, chai_1.expect)([...hubHosts({ adbRemote: ['10.0.0.9', '10.0.0.8:5038'] }).hosts]).to.have.members([
106
+ HUB,
107
+ 'http://10.0.0.9:5037',
108
+ 'http://10.0.0.8:5038',
109
+ ]);
110
+ });
111
+ it('matches a whole host only, never an IP, a prefix or a path', () => {
112
+ const local = hubHosts();
113
+ (0, chai_1.expect)((0, localDeviceHosts_1.isLocalDeviceHost)(local, HUB)).to.equal(true);
114
+ for (const other of [
115
+ NODE,
116
+ IP,
117
+ `${HUB}/wd/hub`,
118
+ `http://${IP}/wd/hub`,
119
+ 'http://192.168.0.10:4724',
120
+ ]) {
121
+ (0, chai_1.expect)((0, localDeviceHosts_1.isLocalDeviceHost)(local, other), other).to.equal(false);
122
+ }
123
+ (0, chai_1.expect)((0, localDeviceHosts_1.isLocalDeviceHost)(local, undefined)).to.equal(false);
124
+ });
125
+ });
126
+ describe('A hub and a node on one machine keep to their own phones', () => {
127
+ let table;
128
+ let discover;
129
+ let restoreContainer;
130
+ let savedStore;
131
+ const seed = (rows) => {
132
+ table = (0, fake_device_table_1.fakeDeviceTable)(rows);
133
+ for (const m of Object.keys(table.delegate)) {
134
+ sinon_1.default.stub(prisma_1.prisma.device, m).callsFake(table.delegate[m]);
135
+ }
136
+ };
137
+ beforeEach(() => {
138
+ discover = () => __awaiter(void 0, void 0, void 0, function* () { return []; });
139
+ restoreContainer = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager, SocketServer_1.SocketServer, NotificationService_1.NotificationService, iOSTracker_1.IosTracker);
140
+ typedi_1.Container.set({
141
+ id: device_managers_1.XenonManager,
142
+ value: { getDevices: (known) => discover(known) },
143
+ });
144
+ typedi_1.Container.set({
145
+ id: SocketServer_1.SocketServer,
146
+ value: {
147
+ emitToDashboard: () => undefined,
148
+ emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
149
+ hasScopedDashboard: () => false,
150
+ },
151
+ });
152
+ typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }) } });
153
+ sinon_1.default.stub(prisma_1.prisma.lease, 'findMany').resolves([]);
154
+ savedStore = device_store_1.DeviceStoreFactory._deviceStore;
155
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
156
+ });
157
+ afterEach(() => {
158
+ device_store_1.DeviceStoreFactory._deviceStore = savedStore;
159
+ sinon_1.default.restore();
160
+ restoreContainer();
161
+ });
162
+ describe("the hub's local sync", () => {
163
+ it("removes the hub's own stale phone and keeps the node's", () => __awaiter(void 0, void 0, void 0, function* () {
164
+ seed([phone('hub-phone', HUB), phone('node-phone', NODE)]);
165
+ yield (0, device_utils_1.updateDeviceList)(hubHosts());
166
+ (0, chai_1.expect)(table.row('hub-phone')).to.equal(undefined);
167
+ (0, chai_1.expect)(table.row('node-phone')).to.include({ host: NODE });
168
+ }));
169
+ it("keeps phones on the hub's IP whose host has a path and another port or none", () => __awaiter(void 0, void 0, void 0, function* () {
170
+ seed([
171
+ phone('hub-phone', HUB),
172
+ phone('behind-proxy', `http://${IP}/wd/hub`),
173
+ phone('node-wd-hub', `${NODE}/wd/hub`),
174
+ ]);
175
+ yield (0, device_utils_1.updateDeviceList)(hubHosts());
176
+ (0, chai_1.expect)(table.row('hub-phone')).to.equal(undefined);
177
+ (0, chai_1.expect)(table.row('behind-proxy')).to.include({ host: `http://${IP}/wd/hub` });
178
+ (0, chai_1.expect)(table.row('node-wd-hub')).to.include({ host: `${NODE}/wd/hub` });
179
+ }));
180
+ it('prunes its own phones filed under remoteMachineProxyIP, in both forms', () => __awaiter(void 0, void 0, void 0, function* () {
181
+ seed([
182
+ phone('android-proxied', 'http://203.0.113.7:4724'),
183
+ phone('iphone-proxied', '203.0.113.7', { platform: 'ios' }),
184
+ phone('node-phone', NODE),
185
+ ]);
186
+ yield (0, device_utils_1.updateDeviceList)(hubHosts({ remoteMachineProxyIP: '203.0.113.7' }));
187
+ (0, chai_1.expect)(table.row('android-proxied')).to.equal(undefined);
188
+ (0, chai_1.expect)(table.row('iphone-proxied')).to.equal(undefined);
189
+ (0, chai_1.expect)(table.row('node-phone')).to.include({ host: NODE });
190
+ }));
191
+ it('treats a remoteMachineProxyIP with a path as one whole host', () => __awaiter(void 0, void 0, void 0, function* () {
192
+ seed([
193
+ phone('hub-iphone', 'https://lab.example.com/hub', { platform: 'ios' }),
194
+ phone('node-iphone', 'https://lab.example.com/hub-node', { platform: 'ios' }),
195
+ ]);
196
+ yield (0, device_utils_1.updateDeviceList)(hubHosts({ remoteMachineProxyIP: 'https://lab.example.com/hub' }));
197
+ (0, chai_1.expect)(table.row('hub-iphone')).to.equal(undefined);
198
+ (0, chai_1.expect)(table.row('node-iphone')).to.include({ host: 'https://lab.example.com/hub-node' });
199
+ }));
200
+ it('prunes a phone on one of its adbRemote servers that is gone', () => __awaiter(void 0, void 0, void 0, function* () {
201
+ seed([phone('remote-adb', 'http://10.0.0.9:5037'), phone('node-phone', NODE)]);
202
+ yield (0, device_utils_1.updateDeviceList)(hubHosts({ adbRemote: ['10.0.0.9'] }));
203
+ (0, chai_1.expect)(table.row('remote-adb')).to.equal(undefined);
204
+ (0, chai_1.expect)(table.row('node-phone')).to.include({ host: NODE });
205
+ }));
206
+ });
207
+ describe("the node's own sync", () => {
208
+ it("prunes the node's stale phone, tells the hub, and leaves the hub's row alone", () => __awaiter(void 0, void 0, void 0, function* () {
209
+ // One table for both, as when the two share a database.
210
+ seed([phone('node-live', NODE), phone('node-gone', NODE), phone('hub-phone', HUB)]);
211
+ discover = (known) => __awaiter(void 0, void 0, void 0, function* () { return known.filter((d) => d.udid === 'node-live').map((d) => (Object.assign({}, d))); });
212
+ sinon_1.default.stub(helpers, 'isXenonRunning').resolves(true);
213
+ const post = sinon_1.default.stub(NodeDevices_1.default.prototype, 'postDevicesToHub').resolves();
214
+ yield (0, device_utils_1.updateDeviceList)(nodeHosts(), HUB);
215
+ (0, chai_1.expect)(table.row('node-gone')).to.equal(undefined);
216
+ (0, chai_1.expect)(table.row('node-live')).to.include({ host: NODE });
217
+ (0, chai_1.expect)(table.row('hub-phone')).to.include({ host: HUB });
218
+ const removed = post.getCalls().filter((c) => c.args[1] === 'remove');
219
+ (0, chai_1.expect)(removed).to.have.length(1);
220
+ (0, chai_1.expect)(removed[0].args[0].map((d) => d.udid)).to.deep.equal(['node-gone']);
221
+ }));
222
+ });
223
+ describe("the hub's stale-host check", () => {
224
+ it('host-checks a node on the same IP, and drops its phones when it is down', () => __awaiter(void 0, void 0, void 0, function* () {
225
+ seed([phone('hub-phone', HUB), phone('node-phone', NODE)]);
226
+ const alive = sinon_1.default.stub(helpers, 'isXenonRunning').resolves(false);
227
+ yield (0, device_utils_1.removeStaleDevices)(hubHosts());
228
+ (0, chai_1.expect)(alive.calledWith(NODE)).to.equal(true);
229
+ (0, chai_1.expect)(alive.calledWith(HUB)).to.equal(false);
230
+ (0, chai_1.expect)(table.row('node-phone')).to.equal(undefined);
231
+ (0, chai_1.expect)(table.row('hub-phone')).to.include({ host: HUB });
232
+ }));
233
+ it("keeps a live node's phones", () => __awaiter(void 0, void 0, void 0, function* () {
234
+ seed([phone('hub-phone', HUB), phone('node-phone', NODE)]);
235
+ const alive = sinon_1.default.stub(helpers, 'isXenonRunning').resolves(true);
236
+ yield (0, device_utils_1.removeStaleDevices)(hubHosts());
237
+ (0, chai_1.expect)(alive.calledWith(NODE)).to.equal(true);
238
+ (0, chai_1.expect)(table.row('node-phone')).to.include({ host: NODE });
239
+ (0, chai_1.expect)(table.row('hub-phone')).to.include({ host: HUB });
240
+ }));
241
+ it("does not take a node whose IP starts with the hub's for its own", () => __awaiter(void 0, void 0, void 0, function* () {
242
+ seed([phone('hub-phone', 'http://192.168.0.10:4724'), phone('node-phone', NODE)]);
243
+ sinon_1.default.stub(helpers, 'isXenonRunning').resolves(false);
244
+ yield (0, device_utils_1.removeStaleDevices)((0, localDeviceHosts_1.localDeviceHosts)({ bindHostOrIp: '192.168.0.10' }, 4724));
245
+ (0, chai_1.expect)(table.row('node-phone')).to.equal(undefined);
246
+ (0, chai_1.expect)(table.row('hub-phone')).to.include({ host: 'http://192.168.0.10:4724' });
247
+ }));
248
+ it('never host-checks its own phones, in any host form', () => __awaiter(void 0, void 0, void 0, function* () {
249
+ seed([
250
+ phone('origin', HUB),
251
+ phone('android-proxied', 'http://203.0.113.7:4724'),
252
+ phone('iphone-proxied', '203.0.113.7', { platform: 'ios' }),
253
+ phone('remote-adb', 'http://10.0.0.9:5037'),
254
+ ]);
255
+ const alive = sinon_1.default.stub(helpers, 'isXenonRunning').resolves(false);
256
+ yield (0, device_utils_1.removeStaleDevices)(hubHosts({ remoteMachineProxyIP: '203.0.113.7', adbRemote: ['10.0.0.9'] }));
257
+ (0, chai_1.expect)(alive.called).to.equal(false);
258
+ for (const udid of ['origin', 'android-proxied', 'iphone-proxied', 'remote-adb']) {
259
+ (0, chai_1.expect)(table.row(udid), udid).to.not.equal(undefined);
260
+ }
261
+ }));
262
+ });
263
+ describe('a node leaving the hub', () => {
264
+ // The hub's /register?type=unregister handler.
265
+ const hubReceives = () => sinon_1.default
266
+ .stub(NodeDevices_1.default.prototype, 'unRegisterNode')
267
+ .callsFake((host, nodeId) => __awaiter(void 0, void 0, void 0, function* () {
268
+ yield (0, device_service_1.removeNodeDevices)([{ host, nodeId }], (d) => d.host === HUB);
269
+ }));
270
+ it("removes only the node's phones from the hub", () => __awaiter(void 0, void 0, void 0, function* () {
271
+ seed([phone('hub-phone', HUB), phone('node-phone', NODE)]);
272
+ const sent = hubReceives();
273
+ yield (0, device_utils_1.unregisterNodeFromHub)(HUB, nodeHosts());
274
+ (0, chai_1.expect)(sent.getCalls().map((c) => c.args[0])).to.deep.equal([NODE]);
275
+ (0, chai_1.expect)(table.row('node-phone')).to.equal(undefined);
276
+ (0, chai_1.expect)(table.row('hub-phone')).to.include({ host: HUB });
277
+ }));
278
+ it('sends only URLs, which the hub matches exactly, never a bare proxy address', () => __awaiter(void 0, void 0, void 0, function* () {
279
+ seed([phone('hub-phone', HUB), phone('other-node', 'http://203.0.113.7:4726')]);
280
+ const sent = hubReceives();
281
+ yield (0, device_utils_1.unregisterNodeFromHub)(HUB, nodeHosts({ remoteMachineProxyIP: '203.0.113.7' }));
282
+ (0, chai_1.expect)(sent.getCalls().map((c) => c.args[0])).to.have.members([
283
+ NODE,
284
+ 'http://203.0.113.7:4725',
285
+ ]);
286
+ (0, chai_1.expect)(table.row('other-node')).to.include({ host: 'http://203.0.113.7:4726' });
287
+ (0, chai_1.expect)(table.row('hub-phone')).to.include({ host: HUB });
288
+ }));
289
+ });
290
+ describe("the hub's Android discovery", () => {
291
+ const localAdb = { adbPort: 5037 };
292
+ let deviceInfo;
293
+ // resetTestContainer() (test/helpers/test-container.ts) stubs these two on
294
+ // the prototype for the rest of the process, so in a full run discovery
295
+ // never reaches them. Put the real ones on this instance.
296
+ const real = (fn) => { var _a; return (_a = fn.wrappedMethod) !== null && _a !== void 0 ? _a : fn; };
297
+ const hubAndroid = (adb = localAdb) => {
298
+ const manager = new AndroidDeviceManager_1.default(context({ bindHostOrIp: IP }, 4724));
299
+ const proto = AndroidDeviceManager_1.default.prototype;
300
+ Object.assign(manager, {
301
+ getDevices: real(proto.getDevices),
302
+ fetchAndroidDevices: real(proto.fetchAndroidDevices),
303
+ });
304
+ sinon_1.default.stub(manager, 'requireSdkRoot').resolves();
305
+ sinon_1.default
306
+ .stub(manager, 'getConnectedDevices')
307
+ .resolves(new Map([[adb, [{ udid: 'shared', state: 'device' }]]]));
308
+ deviceInfo = sinon_1.default
309
+ .stub(manager, 'deviceInfo')
310
+ .callsFake((device) => __awaiter(void 0, void 0, void 0, function* () { return phone(device.udid, HUB); }));
311
+ return manager;
312
+ };
313
+ it('builds its own row for a phone the node on the same IP already listed', () => __awaiter(void 0, void 0, void 0, function* () {
314
+ const devices = yield hubAndroid().getDevices({ androidDeviceType: 'both' }, [
315
+ phone('shared', NODE),
316
+ ]);
317
+ (0, chai_1.expect)(deviceInfo.calledOnce).to.equal(true);
318
+ (0, chai_1.expect)(devices.map((d) => d.host)).to.deep.equal([HUB]);
319
+ }));
320
+ it('reuses its own row without asking the phone again', () => __awaiter(void 0, void 0, void 0, function* () {
321
+ const devices = yield hubAndroid().getDevices({ androidDeviceType: 'both' }, [
322
+ phone('shared', HUB),
323
+ ]);
324
+ (0, chai_1.expect)(deviceInfo.called).to.equal(false);
325
+ (0, chai_1.expect)(devices.map((d) => d.host)).to.deep.equal([HUB]);
326
+ }));
327
+ it('reuses its row for a phone on an adbRemote server, filed under that server', () => __awaiter(void 0, void 0, void 0, function* () {
328
+ const remote = 'http://10.0.0.9:5037';
329
+ const devices = yield hubAndroid({ adbHost: '10.0.0.9', adbPort: 5037 }).getDevices({ androidDeviceType: 'both' }, [phone('shared', remote)]);
330
+ (0, chai_1.expect)(deviceInfo.called).to.equal(false);
331
+ (0, chai_1.expect)(devices.map((d) => d.host)).to.deep.equal([remote]);
332
+ }));
333
+ });
334
+ describe('a phone unplugged from one server', () => {
335
+ it("Android: removes that server's row only, not another's with the same udid", () => __awaiter(void 0, void 0, void 0, function* () {
336
+ // emulator-5554 is on every machine running an emulator; the node's
337
+ // IP starts with the hub's.
338
+ const hub = 'http://192.168.0.10:4724';
339
+ seed([phone('emulator-5554', hub), phone('emulator-5554', NODE)]);
340
+ const args = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, { bindHostOrIp: '192.168.0.10' });
341
+ const manager = new AndroidDeviceManager_1.default(context(args, 4724));
342
+ yield manager.onDeviceRemoved({ id: 'emulator-5554', type: 'offline' }, args);
343
+ const left = (yield prisma_1.prisma.device.findMany());
344
+ (0, chai_1.expect)(left.map((d) => d.host)).to.deep.equal([NODE]);
345
+ }));
346
+ it("iOS: removes that server's row only", () => __awaiter(void 0, void 0, void 0, function* () {
347
+ seed([phone('iphone', HUB, { platform: 'ios' }), phone('iphone', NODE, { platform: 'ios' })]);
348
+ const usbmux = new events_1.EventEmitter();
349
+ typedi_1.Container.set({ id: iOSTracker_1.IosTracker, value: { getListener: () => usbmux } });
350
+ new IOSDiscoveryService_1.IOSDiscoveryService(context({ bindHostOrIp: IP }, 4724)).trackIOSDevices();
351
+ yield usbmux.listeners('detached')[0]('iphone');
352
+ const left = (yield prisma_1.prisma.device.findMany());
353
+ (0, chai_1.expect)(left.map((d) => d.host)).to.deep.equal([NODE]);
354
+ }));
355
+ });
356
+ });
@@ -0,0 +1,165 @@
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 express_1 = __importDefault(require("express"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const typedi_1 = require("typedi");
21
+ const grid_1 = __importDefault(require("../../src/app/routers/grid"));
22
+ const PluginContext_1 = require("../../src/PluginContext");
23
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
24
+ const device_store_1 = require("../../src/data-service/device-store");
25
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
26
+ const SocketServer_1 = require("../../src/services/SocketServer");
27
+ const NotificationService_1 = require("../../src/services/NotificationService");
28
+ const container_registration_1 = require("../helpers/container-registration");
29
+ const scratch_database_1 = require("../helpers/scratch-database");
30
+ /**
31
+ * A node asks the hub to forget its phones with `POST /xenon/api/register`:
32
+ * `type=remove` with the phones (one unplugged, or pruned as stale), and
33
+ * `type=unregister&host=` once per host when it shuts down. The hub matched
34
+ * the host of either as a substring whenever it wasn't a URL, and a remove
35
+ * with no host matched the udid alone, so a node (an older one sends its bare
36
+ * IP) could delete the hub's own phones, or another node's. Only the asking
37
+ * node's phones go: by its nodeId when it names one, else by exact host, and
38
+ * never one of the hub's own.
39
+ */
40
+ describe('a node’s request to forget its phones, on the hub', () => {
41
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
42
+ const HUB_NODE_ID = 'hub-node-id';
43
+ const HUB = 'http://10.0.0.1:4724';
44
+ const NODE = 'http://10.0.0.1:4725';
45
+ const OTHER_NODE = 'http://10.0.0.1:4726';
46
+ let app;
47
+ let context;
48
+ let saved;
49
+ let restore;
50
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
51
+ restore = (0, container_registration_1.saveRegistrations)(SocketServer_1.SocketServer, NotificationService_1.NotificationService);
52
+ typedi_1.Container.set({
53
+ id: SocketServer_1.SocketServer,
54
+ value: {
55
+ emitToDashboard: () => undefined,
56
+ emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
57
+ hasScopedDashboard: () => false,
58
+ },
59
+ });
60
+ typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }) } });
61
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
62
+ saved = { context: Object.assign({}, context), store: device_store_1.DeviceStoreFactory._deviceStore };
63
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
64
+ const pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '10.0.0.1' });
65
+ context.setContext(pluginArgs, 4724, HUB_NODE_ID, '');
66
+ app = (0, express_1.default)();
67
+ app.use(express_1.default.json());
68
+ // The node's per-node key: role ADMIN, scope devices.
69
+ app.use((req, _res, next) => {
70
+ req.auth = { userId: 'node-key-owner', role: 'ADMIN', scopes: 'devices' };
71
+ next();
72
+ });
73
+ const router = express_1.default.Router();
74
+ grid_1.default.register(router, pluginArgs);
75
+ app.use('/xenon/api', router);
76
+ yield scratch.db.device.deleteMany({});
77
+ const rows = [
78
+ ['emulator-5554', HUB, HUB_NODE_ID],
79
+ ['hub-iphone', HUB, HUB_NODE_ID],
80
+ // The hub's own iPhone, filed under a bare remoteMachineProxyIP.
81
+ ['hub-bare-iphone', '10.0.0.1', HUB_NODE_ID],
82
+ ['emulator-5554', NODE, 'node-1'],
83
+ ['node-phone', NODE, 'node-1'],
84
+ ['node-iphone', '10.0.0.1', 'node-1'],
85
+ ['emulator-5554', OTHER_NODE, 'node-2'],
86
+ ['other-node-phone', OTHER_NODE, 'node-2'],
87
+ ];
88
+ for (const [udid, host, nodeId] of rows) {
89
+ yield scratch.db.device.create({
90
+ data: { udid, host, nodeId, platform: 'android', name: udid },
91
+ });
92
+ }
93
+ }));
94
+ afterEach(() => {
95
+ Object.assign(context, saved.context);
96
+ device_store_1.DeviceStoreFactory._deviceStore = saved.store;
97
+ sinon_1.default.restore();
98
+ restore();
99
+ });
100
+ const left = () => __awaiter(void 0, void 0, void 0, function* () {
101
+ return (yield scratch.db.device.findMany({ select: { udid: true, host: true } }))
102
+ .map((d) => `${d.udid}@${d.host}`)
103
+ .sort();
104
+ });
105
+ const ALL = [
106
+ `emulator-5554@${HUB}`,
107
+ `emulator-5554@${NODE}`,
108
+ `emulator-5554@${OTHER_NODE}`,
109
+ 'hub-bare-iphone@10.0.0.1',
110
+ `hub-iphone@${HUB}`,
111
+ 'node-iphone@10.0.0.1',
112
+ `node-phone@${NODE}`,
113
+ `other-node-phone@${OTHER_NODE}`,
114
+ ];
115
+ const without = (...gone) => ALL.filter((row) => !gone.includes(row));
116
+ const register = (type, body, query = {}) => (0, loopbackRequest_1.default)(app)
117
+ .post('/xenon/api/register')
118
+ .query(Object.assign({ type }, query))
119
+ .send(body)
120
+ .expect(200);
121
+ describe('unregister (a node shutting down)', () => {
122
+ it('by the node’s exact host takes its phones there, and nobody else’s', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ yield register('unregister', [], { host: NODE });
124
+ (0, chai_1.expect)(yield left()).to.deep.equal(without(`emulator-5554@${NODE}`, `node-phone@${NODE}`));
125
+ }));
126
+ it('by a bare IP (an older node) takes only what is filed under exactly it, never the hub’s', () => __awaiter(void 0, void 0, void 0, function* () {
127
+ yield register('unregister', [], { host: '10.0.0.1' });
128
+ (0, chai_1.expect)(yield left()).to.deep.equal(without('node-iphone@10.0.0.1'));
129
+ }));
130
+ it('naming the hub’s own host takes none of the hub’s phones', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ yield register('unregister', [], { host: HUB });
132
+ (0, chai_1.expect)(yield left()).to.deep.equal(ALL);
133
+ }));
134
+ it('with the node’s id takes every phone it reported, whatever the host', () => __awaiter(void 0, void 0, void 0, function* () {
135
+ yield register('unregister', [], { host: NODE, nodeId: 'node-1' });
136
+ (0, chai_1.expect)(yield left()).to.deep.equal(without(`emulator-5554@${NODE}`, `node-phone@${NODE}`, 'node-iphone@10.0.0.1'));
137
+ }));
138
+ it('with the hub’s own id takes nothing', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ yield register('unregister', [], { host: HUB, nodeId: HUB_NODE_ID });
140
+ (0, chai_1.expect)(yield left()).to.deep.equal(ALL);
141
+ }));
142
+ });
143
+ describe('remove (a node’s phone unplugged or stale)', () => {
144
+ it('takes that phone at the node’s exact host only', () => __awaiter(void 0, void 0, void 0, function* () {
145
+ yield register('remove', [{ udid: 'emulator-5554', host: NODE }]);
146
+ (0, chai_1.expect)(yield left()).to.deep.equal(without(`emulator-5554@${NODE}`));
147
+ }));
148
+ it('with a bare IP (an older node) takes nothing', () => __awaiter(void 0, void 0, void 0, function* () {
149
+ yield register('remove', [{ udid: 'emulator-5554', host: '10.0.0.1' }]);
150
+ (0, chai_1.expect)(yield left()).to.deep.equal(ALL);
151
+ }));
152
+ it('with no host takes nothing, never every row with that udid', () => __awaiter(void 0, void 0, void 0, function* () {
153
+ yield register('remove', [{ udid: 'emulator-5554' }]);
154
+ (0, chai_1.expect)(yield left()).to.deep.equal(ALL);
155
+ }));
156
+ it('naming one of the hub’s own phones takes nothing', () => __awaiter(void 0, void 0, void 0, function* () {
157
+ yield register('remove', [{ udid: 'emulator-5554', host: HUB }]);
158
+ (0, chai_1.expect)(yield left()).to.deep.equal(ALL);
159
+ }));
160
+ it('with the node’s id takes its phone, even filed under a bare host', () => __awaiter(void 0, void 0, void 0, function* () {
161
+ yield register('remove', [{ udid: 'node-iphone', host: '10.0.0.1', nodeId: 'node-1' }]);
162
+ (0, chai_1.expect)(yield left()).to.deep.equal(without('node-iphone@10.0.0.1'));
163
+ }));
164
+ });
165
+ });