@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,131 @@
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 express_1 = __importDefault(require("express"));
18
+ const typedi_1 = require("typedi");
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const loopbackServer_1 = require("../helpers/loopbackServer");
21
+ const PluginContext_1 = require("../../src/PluginContext");
22
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
23
+ const nodePhoneControl_1 = require("../../src/app/routers/nodePhoneControl");
24
+ /**
25
+ * The gate in front of /control's handlers, on its own: what it does when the
26
+ * phone's node is slow or its row can't be read. The actions themselves are
27
+ * in hub-node-control-forward.spec.ts.
28
+ */
29
+ describe('nodePhoneControl', () => {
30
+ const loopback = (0, loopbackServer_1.loopbackServers)();
31
+ let context;
32
+ let saved;
33
+ let findDevice;
34
+ let held;
35
+ beforeEach(() => {
36
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
37
+ saved = Object.assign({}, context);
38
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), 1, 'hub-1', '');
39
+ held = [];
40
+ });
41
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
42
+ Object.assign(context, saved);
43
+ yield loopback.closeAll();
44
+ }));
45
+ const app = (timeoutMs = 60000) => {
46
+ const a = (0, express_1.default)();
47
+ a.use(express_1.default.json());
48
+ a.use('/xenon/api/control', (0, nodePhoneControl_1.nodePhoneControl)({ findDevice: (udid) => findDevice(udid), timeoutMs }), (_req, res) => res.status(299).json({ handledHere: true }));
49
+ return a;
50
+ };
51
+ it('a node that doesn’t answer in time: 504, naming it', () => __awaiter(void 0, void 0, void 0, function* () {
52
+ const node = (0, express_1.default)();
53
+ node.use((req) => held.push(req)); // never answers
54
+ const origin = `http://127.0.0.1:${(yield loopback.serve(node)).address().port}`;
55
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: origin, nodeId: 'node-1' }); });
56
+ const res = yield (0, loopbackRequest_1.default)(app(150)).get('/xenon/api/control/p/screenshot');
57
+ (0, chai_1.expect)(res.status).to.equal(504);
58
+ (0, chai_1.expect)(res.body.error).to.equal('node_timeout');
59
+ (0, chai_1.expect)(res.body.message).to.include(origin);
60
+ (0, chai_1.expect)(held).to.have.length(1);
61
+ }));
62
+ it('a device row that can’t be read: 503, and no handler runs it here', () => __awaiter(void 0, void 0, void 0, function* () {
63
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () {
64
+ throw new Error('database is locked');
65
+ });
66
+ const res = yield (0, loopbackRequest_1.default)(app()).post('/xenon/api/control/p/tap').send({ x: 1, y: 2 });
67
+ (0, chai_1.expect)(res.status).to.equal(503);
68
+ (0, chai_1.expect)(res.body.error).to.equal('device_ownership_unavailable');
69
+ }));
70
+ it('this server’s own phone, and an unknown one, go to the handlers', () => __awaiter(void 0, void 0, void 0, function* () {
71
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: 'http://10.0.0.9:4725', nodeId: 'hub-1' }); });
72
+ yield (0, loopbackRequest_1.default)(app()).post('/xenon/api/control/p/tap').send({}).expect(299);
73
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () { return null; });
74
+ yield (0, loopbackRequest_1.default)(app()).post('/xenon/api/control/p/tap').send({}).expect(299);
75
+ }));
76
+ it('a stream outlives the timeout once the node has answered', () => __awaiter(void 0, void 0, void 0, function* () {
77
+ const node = (0, express_1.default)();
78
+ node.get('/xenon/api/control/p/stream', (_req, res) => {
79
+ // Text, so the test's client doesn't try to parse it; an MJPEG
80
+ // answer is relayed the same way.
81
+ res.writeHead(200, { 'content-type': 'text/plain' });
82
+ res.write('frame one;');
83
+ setTimeout(() => res.end('frame two;'), 400);
84
+ });
85
+ const origin = `http://127.0.0.1:${(yield loopback.serve(node)).address().port}`;
86
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: origin, nodeId: 'node-1' }); });
87
+ const res = yield (0, loopbackRequest_1.default)(app(150)).get('/xenon/api/control/p/stream');
88
+ (0, chai_1.expect)(res.status).to.equal(200);
89
+ (0, chai_1.expect)(res.text).to.equal('frame one;frame two;');
90
+ }));
91
+ it('sends the node none of the caller’s ticket, and the rest of the query', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const node = (0, express_1.default)();
93
+ const seen = [];
94
+ node.use((req, res) => {
95
+ seen.push(req.originalUrl);
96
+ res.json({ status: 'stopped' });
97
+ });
98
+ const origin = `http://127.0.0.1:${(yield loopback.serve(node)).address().port}`;
99
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: origin, nodeId: 'node-1' }); });
100
+ yield (0, loopbackRequest_1.default)(app()).get('/xenon/api/control/p/stream/status?ticket=spent&t=1&r=2').expect(200);
101
+ (0, chai_1.expect)(seen).to.deep.equal(['/xenon/api/control/p/stream/status?t=1&r=2']);
102
+ }));
103
+ it('marks the phone busy for its node once the node has started a preview, and only then', () => __awaiter(void 0, void 0, void 0, function* () {
104
+ let answer = 200;
105
+ const node = (0, express_1.default)();
106
+ node.use((_req, res) => res.status(answer).json({}));
107
+ const origin = `http://127.0.0.1:${(yield loopback.serve(node)).address().port}`;
108
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: origin, nodeId: 'node-1' }); });
109
+ const marked = [];
110
+ const a = (0, express_1.default)();
111
+ a.use(express_1.default.json());
112
+ a.use('/xenon/api/control', (0, nodePhoneControl_1.nodePhoneControl)({
113
+ findDevice: (udid) => findDevice(udid),
114
+ markNodeBusy: (udid, host) => __awaiter(void 0, void 0, void 0, function* () {
115
+ marked.push(`${udid}@${host}`);
116
+ }),
117
+ }));
118
+ yield (0, loopbackRequest_1.default)(a).post('/xenon/api/control/p/stream/start').send({}).expect(200);
119
+ answer = 409;
120
+ yield (0, loopbackRequest_1.default)(a).post('/xenon/api/control/p/stream/start').send({}).expect(409);
121
+ answer = 200;
122
+ yield (0, loopbackRequest_1.default)(a).get('/xenon/api/control/p/stream/status').expect(200);
123
+ (0, chai_1.expect)(marked).to.deep.equal([`p@${origin}`]);
124
+ }));
125
+ it('refuses a host it must never call', () => __awaiter(void 0, void 0, void 0, function* () {
126
+ findDevice = () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: 'http://169.254.169.254', nodeId: 'node-1' }); });
127
+ const res = yield (0, loopbackRequest_1.default)(app()).get('/xenon/api/control/p/screenshot');
128
+ (0, chai_1.expect)(res.status).to.equal(400);
129
+ (0, chai_1.expect)(res.body).to.deep.equal({ error: 'Unsafe device host' });
130
+ }));
131
+ });
@@ -0,0 +1,357 @@
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 http_1 = __importDefault(require("http"));
18
+ const events_1 = require("events");
19
+ const express_1 = __importDefault(require("express"));
20
+ const typedi_1 = require("typedi");
21
+ const ws_1 = require("ws");
22
+ const loopbackServer_1 = require("../helpers/loopbackServer");
23
+ const PluginContext_1 = require("../../src/PluginContext");
24
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
25
+ const nodeSocketRelay_1 = require("../../src/app/ws/nodeSocketRelay");
26
+ const h264StreamWs_1 = require("../../src/app/ws/h264StreamWs");
27
+ const logcatWs_1 = require("../../src/app/ws/logcatWs");
28
+ /**
29
+ * The H.264 and logcat sockets for a node's phone, on a hub: the hub checks
30
+ * its own ticket, opens the same socket on the node with a ticket of the
31
+ * node's own, and relays it. Frames and close codes pass unchanged, and a
32
+ * viewer that falls behind holds the node back rather than growing the
33
+ * hub's memory, so the node's own slow-viewer rule applies (dropped H.264
34
+ * frames, a visible "lines dropped" record in logcat).
35
+ */
36
+ const opened = (ws) => new Promise((resolve, reject) => {
37
+ ws.once('open', () => resolve());
38
+ ws.once('error', reject);
39
+ });
40
+ const closed = (ws) => new Promise((resolve) => ws.once('close', (code, reason) => resolve({ code, reason: reason.toString() })));
41
+ const nextMessage = (ws) => new Promise((resolve) => ws.once('message', (data, isBinary) => resolve({ data: data, isBinary })));
42
+ /** An upstream socket as openNodeSocket hands one over: open and paused. */
43
+ const nodeSocketAt = (url) => __awaiter(void 0, void 0, void 0, function* () {
44
+ const up = new ws_1.WebSocket(url);
45
+ yield new Promise((resolve, reject) => {
46
+ up.once('open', () => {
47
+ up.pause();
48
+ resolve();
49
+ });
50
+ up.once('error', reject);
51
+ });
52
+ return up;
53
+ });
54
+ describe('nodeSocketRelay', () => {
55
+ const loopback = (0, loopbackServer_1.loopbackServers)();
56
+ const sockets = [];
57
+ let context;
58
+ let saved;
59
+ beforeEach(() => {
60
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
61
+ saved = Object.assign({}, context);
62
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), 1, 'hub-1', '');
63
+ });
64
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
65
+ Object.assign(context, saved);
66
+ for (const wss of sockets.splice(0)) {
67
+ for (const client of wss.clients)
68
+ client.terminate();
69
+ wss.close();
70
+ }
71
+ yield loopback.closeAll();
72
+ }));
73
+ /** A node: a ticket route and a socket on the same server. */
74
+ function fakeNode() {
75
+ return __awaiter(this, arguments, void 0, function* (opts = {}) {
76
+ const seen = [];
77
+ const app = (0, express_1.default)();
78
+ app.post('/xenon/api/control/:udid/stream/ticket', (req, res) => {
79
+ var _a;
80
+ seen.push({ url: req.originalUrl, hubToken: req.header('x-xenon-hub-token') });
81
+ res.status((_a = opts.ticketStatus) !== null && _a !== void 0 ? _a : 200).json({ ticket: 'node-ticket', expiresIn: 60 });
82
+ });
83
+ const server = yield loopback.serve(app);
84
+ const wss = new ws_1.WebSocketServer({ server });
85
+ sockets.push(wss);
86
+ wss.on('connection', (ws, req) => {
87
+ var _a;
88
+ seen.push({ url: req.url });
89
+ (_a = opts.onSocket) === null || _a === void 0 ? void 0 : _a.call(opts, ws, req.url);
90
+ });
91
+ const origin = `http://127.0.0.1:${server.address().port}`;
92
+ return { origin, seen, wss };
93
+ });
94
+ }
95
+ /** A hub socket whose connections are relayed to `upstream()`. */
96
+ function relayingHub(upstream, highWaterBytes) {
97
+ return __awaiter(this, void 0, void 0, function* () {
98
+ const server = yield loopback.serve((0, express_1.default)());
99
+ const wss = new ws_1.WebSocketServer({ server });
100
+ sockets.push(wss);
101
+ wss.on('connection', (client) => __awaiter(this, void 0, void 0, function* () {
102
+ (0, nodeSocketRelay_1.relaySocket)(client, yield upstream(), highWaterBytes);
103
+ }));
104
+ return `ws://127.0.0.1:${server.address().port}`;
105
+ });
106
+ }
107
+ describe('relaySocket', () => {
108
+ it('passes messages both ways, binary and text as they were', () => __awaiter(void 0, void 0, void 0, function* () {
109
+ let atNode;
110
+ const node = yield fakeNode({ onSocket: (ws) => (atNode = ws) });
111
+ const hub = yield relayingHub(() => nodeSocketAt(`${node.origin.replace('http', 'ws')}/x`));
112
+ const viewer = new ws_1.WebSocket(hub);
113
+ yield opened(viewer);
114
+ yield new Promise((r) => setTimeout(r, 50));
115
+ const binary = nextMessage(viewer);
116
+ atNode.send(Buffer.from([1, 2, 3]));
117
+ (0, chai_1.expect)(yield binary).to.deep.equal({ data: Buffer.from([1, 2, 3]), isBinary: true });
118
+ const text = nextMessage(viewer);
119
+ atNode.send('{"message":"a line"}');
120
+ const got = yield text;
121
+ (0, chai_1.expect)(got.isBinary).to.equal(false);
122
+ (0, chai_1.expect)(got.data.toString()).to.equal('{"message":"a line"}');
123
+ const fromViewer = nextMessage(atNode);
124
+ viewer.send('hello');
125
+ (0, chai_1.expect)((yield fromViewer).data.toString()).to.equal('hello');
126
+ viewer.close();
127
+ }));
128
+ it('closes the viewer with the node’s own code and reason', () => __awaiter(void 0, void 0, void 0, function* () {
129
+ for (const [code, reason] of [
130
+ [1012, 'stream ended'],
131
+ [1008, 'device held by another user'],
132
+ [1011, 'stream failed'],
133
+ ]) {
134
+ const node = yield fakeNode({
135
+ onSocket: (ws) => setTimeout(() => ws.close(code, reason), 30),
136
+ });
137
+ const hub = yield relayingHub(() => nodeSocketAt(`${node.origin.replace('http', 'ws')}/x`));
138
+ const viewer = new ws_1.WebSocket(hub);
139
+ (0, chai_1.expect)(yield closed(viewer)).to.deep.equal({ code, reason });
140
+ }
141
+ }));
142
+ it('a node connection that drops is 1011 to the viewer, who retries', () => __awaiter(void 0, void 0, void 0, function* () {
143
+ const node = yield fakeNode({ onSocket: (ws) => setTimeout(() => ws.terminate(), 30) });
144
+ const hub = yield relayingHub(() => nodeSocketAt(`${node.origin.replace('http', 'ws')}/x`));
145
+ const viewer = new ws_1.WebSocket(hub);
146
+ (0, chai_1.expect)((yield closed(viewer)).code).to.equal(1011);
147
+ }));
148
+ it('the viewer leaving closes the node’s socket', () => __awaiter(void 0, void 0, void 0, function* () {
149
+ let atNode;
150
+ const node = yield fakeNode({ onSocket: (ws) => (atNode = ws) });
151
+ const hub = yield relayingHub(() => nodeSocketAt(`${node.origin.replace('http', 'ws')}/x`));
152
+ const viewer = new ws_1.WebSocket(hub);
153
+ yield opened(viewer);
154
+ yield new Promise((r) => setTimeout(r, 50));
155
+ const nodeClosed = closed(atNode);
156
+ viewer.close();
157
+ yield nodeClosed;
158
+ }));
159
+ it('stops reading from the node while the viewer is behind, and resumes once it catches up', () => {
160
+ class FakeSocket extends events_1.EventEmitter {
161
+ constructor() {
162
+ super(...arguments);
163
+ this.readyState = ws_1.WebSocket.OPEN;
164
+ this.paused = false;
165
+ this.pending = [];
166
+ }
167
+ send(_data, _opts, cb) {
168
+ this.pending.push(cb);
169
+ }
170
+ pause() {
171
+ this.paused = true;
172
+ }
173
+ resume() {
174
+ this.paused = false;
175
+ }
176
+ close() { }
177
+ terminate() { }
178
+ }
179
+ const viewer = new FakeSocket();
180
+ const node = new FakeSocket();
181
+ (0, nodeSocketRelay_1.relaySocket)(viewer, node, 100);
182
+ node.emit('message', Buffer.alloc(60), true);
183
+ (0, chai_1.expect)(node.paused).to.equal(false);
184
+ node.emit('message', Buffer.alloc(60), true);
185
+ (0, chai_1.expect)(node.paused, '120 bytes unwritten').to.equal(true);
186
+ const written = () => viewer.pending.shift()();
187
+ written();
188
+ (0, chai_1.expect)(node.paused, '60 bytes unwritten').to.equal(true);
189
+ written();
190
+ (0, chai_1.expect)(node.paused, 'all written').to.equal(false);
191
+ });
192
+ });
193
+ describe('openNodeSocket', () => {
194
+ const actor = { actorId: 'alice', isAdmin: false };
195
+ it('is null for this server’s own phone, and asks nothing of anyone', () => __awaiter(void 0, void 0, void 0, function* () {
196
+ const node = yield fakeNode();
197
+ const socket = yield (0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor, path: 'stream/h264' }, { findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: node.origin, nodeId: 'hub-1' }); }) });
198
+ (0, chai_1.expect)(socket).to.equal(null);
199
+ (0, chai_1.expect)(node.seen).to.deep.equal([]);
200
+ }));
201
+ it('opens the node’s socket with a ticket of the node’s own, asked for with the hub’s token', () => __awaiter(void 0, void 0, void 0, function* () {
202
+ const node = yield fakeNode();
203
+ const socket = yield (0, nodeSocketRelay_1.openNodeSocket)({
204
+ udid: 'a b',
205
+ actor,
206
+ path: 'logcat',
207
+ query: new URLSearchParams({ levels: 'error', process: 'App' }),
208
+ }, {
209
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'a b', host: node.origin, nodeId: 'node-1' }); }),
210
+ controlToken: () => __awaiter(void 0, void 0, void 0, function* () { return 'signed-for-alice'; }),
211
+ });
212
+ (0, chai_1.expect)(socket).to.be.instanceOf(ws_1.WebSocket);
213
+ socket.close();
214
+ (0, chai_1.expect)(node.seen).to.deep.equal([
215
+ { url: '/xenon/api/control/a%20b/stream/ticket', hubToken: 'signed-for-alice' },
216
+ { url: '/xenon/api/control/a%20b/logcat?levels=error&process=App&ticket=node-ticket' },
217
+ ]);
218
+ }));
219
+ it('holds what the node sends first until something reads it', () => __awaiter(void 0, void 0, void 0, function* () {
220
+ // H.264's config packet and logcat's replay go out the moment the
221
+ // node's socket opens; lost, the preview never decodes.
222
+ const node = yield fakeNode({ onSocket: (ws) => ws.send('first frame') });
223
+ const socket = yield (0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor, path: 'stream/h264' }, {
224
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: node.origin, nodeId: 'node-1' }); }),
225
+ controlToken: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
226
+ });
227
+ const up = socket;
228
+ yield new Promise((r) => setTimeout(r, 100));
229
+ const first = nextMessage(up);
230
+ up.resume();
231
+ (0, chai_1.expect)((yield first).data.toString()).to.equal('first frame');
232
+ up.close();
233
+ }));
234
+ it('a node that refuses the ticket is a refusal, one that can’t be reached is not', () => __awaiter(void 0, void 0, void 0, function* () {
235
+ const refusing = yield fakeNode({ ticketStatus: 409 });
236
+ const find = (host) => () => __awaiter(void 0, void 0, void 0, function* () {
237
+ return ({
238
+ udid: 'p',
239
+ host,
240
+ nodeId: 'node-1',
241
+ });
242
+ });
243
+ let error;
244
+ yield (0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor, path: 'stream/h264' }, { findDevice: find(refusing.origin), controlToken: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) }).catch((e) => (error = e));
245
+ (0, chai_1.expect)(error).to.be.instanceOf(nodeSocketRelay_1.NodeSocketRefused);
246
+ error = undefined;
247
+ yield (0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor, path: 'stream/h264' }, { findDevice: find('http://127.0.0.1:1'), controlToken: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) }).catch((e) => (error = e));
248
+ (0, chai_1.expect)(error).to.be.instanceOf(Error);
249
+ (0, chai_1.expect)(error).to.not.be.instanceOf(nodeSocketRelay_1.NodeSocketRefused);
250
+ }));
251
+ it('a cloud provider’s phone is refused without a request', () => __awaiter(void 0, void 0, void 0, function* () {
252
+ const node = yield fakeNode();
253
+ let error;
254
+ yield (0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor, path: 'stream/h264' }, {
255
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: 'p', host: node.origin, cloud: '{"cloudName":"x"}' }); }),
256
+ }).catch((e) => (error = e));
257
+ (0, chai_1.expect)(error).to.be.instanceOf(nodeSocketRelay_1.NodeSocketRefused);
258
+ (0, chai_1.expect)(node.seen).to.deep.equal([]);
259
+ }));
260
+ });
261
+ describe('the hub’s own sockets', () => {
262
+ function hubWith(attach) {
263
+ return __awaiter(this, void 0, void 0, function* () {
264
+ const server = http_1.default.createServer();
265
+ attach(server);
266
+ yield new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
267
+ loopbackServersCleanup.push(server);
268
+ return `ws://127.0.0.1:${server.address().port}`;
269
+ });
270
+ }
271
+ const loopbackServersCleanup = [];
272
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
273
+ var _a;
274
+ for (const s of loopbackServersCleanup.splice(0)) {
275
+ (_a = s.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(s);
276
+ yield new Promise((r) => s.close(r));
277
+ }
278
+ }));
279
+ it('H.264: a node’s phone is relayed from its node, with the ticket’s user', () => __awaiter(void 0, void 0, void 0, function* () {
280
+ const node = yield fakeNode({ onSocket: (ws) => ws.send(Buffer.from([0, 9, 9])) });
281
+ const asked = [];
282
+ const hub = yield hubWith((server) => (0, h264StreamWs_1.attachH264Ws)(server, {
283
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'alice', isAdmin: true }); }),
284
+ startStream: () => __awaiter(void 0, void 0, void 0, function* () {
285
+ throw new Error('not this server’s phone');
286
+ }),
287
+ nodeSocket: (udid, who) => __awaiter(void 0, void 0, void 0, function* () {
288
+ asked.push({ udid, who });
289
+ return nodeSocketAt(`${node.origin.replace('http', 'ws')}/h264`);
290
+ }),
291
+ }));
292
+ const viewer = new ws_1.WebSocket(`${hub}/xenon/api/control/p/stream/h264?ticket=t`);
293
+ (0, chai_1.expect)((yield nextMessage(viewer)).data).to.deep.equal(Buffer.from([0, 9, 9]));
294
+ (0, chai_1.expect)(asked).to.deep.equal([{ udid: 'p', who: { actorId: 'alice', isAdmin: true } }]);
295
+ viewer.close();
296
+ }));
297
+ it('H.264: a refusal from the node is 1008, anything else 1011', () => __awaiter(void 0, void 0, void 0, function* () {
298
+ for (const [thrown, code] of [
299
+ [new nodeSocketRelay_1.NodeSocketRefused('the node refused the ticket'), 1008],
300
+ [new Error('ECONNREFUSED'), 1011],
301
+ ]) {
302
+ const hub = yield hubWith((server) => (0, h264StreamWs_1.attachH264Ws)(server, {
303
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'alice' }); }),
304
+ startStream: () => __awaiter(void 0, void 0, void 0, function* () {
305
+ throw new Error('not this server’s phone');
306
+ }),
307
+ nodeSocket: () => __awaiter(void 0, void 0, void 0, function* () {
308
+ throw thrown;
309
+ }),
310
+ }));
311
+ const viewer = new ws_1.WebSocket(`${hub}/xenon/api/control/p/stream/h264?ticket=t`);
312
+ (0, chai_1.expect)((yield closed(viewer)).code).to.equal(code);
313
+ }
314
+ }));
315
+ it('logcat: relayed after this server’s own ownership check, with the filter', () => __awaiter(void 0, void 0, void 0, function* () {
316
+ const node = yield fakeNode({ onSocket: (ws) => ws.send('{"message":"from the node"}') });
317
+ const asked = [];
318
+ let authorized = 0;
319
+ const hub = yield hubWith((server) => (0, logcatWs_1.attachLogcatWs)(server, {
320
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'alice' }); }),
321
+ authorize: () => __awaiter(void 0, void 0, void 0, function* () {
322
+ authorized++;
323
+ return true;
324
+ }),
325
+ startStream: () => __awaiter(void 0, void 0, void 0, function* () {
326
+ throw new Error('not this server’s phone');
327
+ }),
328
+ nodeSocket: (udid, who, filter) => __awaiter(void 0, void 0, void 0, function* () {
329
+ asked.push({ udid, filter });
330
+ return nodeSocketAt(`${node.origin.replace('http', 'ws')}/logcat`);
331
+ }),
332
+ }));
333
+ const viewer = new ws_1.WebSocket(`${hub}/xenon/api/control/p/logcat?ticket=t&levels=error&process=App`);
334
+ (0, chai_1.expect)((yield nextMessage(viewer)).data.toString()).to.equal('{"message":"from the node"}');
335
+ (0, chai_1.expect)(authorized).to.equal(1);
336
+ (0, chai_1.expect)(asked).to.deep.equal([{ udid: 'p', filter: { levels: ['error'], process: 'App' } }]);
337
+ viewer.close();
338
+ }));
339
+ it('logcat: a phone this server may not show isn’t asked of the node', () => __awaiter(void 0, void 0, void 0, function* () {
340
+ let asked = 0;
341
+ const hub = yield hubWith((server) => (0, logcatWs_1.attachLogcatWs)(server, {
342
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'bob' }); }),
343
+ authorize: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
344
+ startStream: () => __awaiter(void 0, void 0, void 0, function* () {
345
+ throw new Error('unused');
346
+ }),
347
+ nodeSocket: () => __awaiter(void 0, void 0, void 0, function* () {
348
+ asked++;
349
+ return null;
350
+ }),
351
+ }));
352
+ const viewer = new ws_1.WebSocket(`${hub}/xenon/api/control/p/logcat?ticket=t`);
353
+ (0, chai_1.expect)((yield closed(viewer)).code).to.equal(1008);
354
+ (0, chai_1.expect)(asked).to.equal(0);
355
+ }));
356
+ });
357
+ });
@@ -19,13 +19,16 @@ const typedi_1 = require("typedi");
19
19
  const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
20
20
  const tesseract_js_1 = __importDefault(require("tesseract.js"));
21
21
  const AIService_1 = require("../../src/services/AIService");
22
+ const container_registration_1 = require("../helpers/container-registration");
22
23
  describe('OmniVisionService Unit Tests', () => {
23
24
  let omniService;
24
25
  let mockDriver;
26
+ let restoreContainer;
25
27
  beforeEach(() => {
26
- // Reset container to ensure clean state
27
- typedi_1.Container.reset();
28
- omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
28
+ // A fresh service for each test, registered only until it ends.
29
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
30
+ omniService = new OmniVisionService_1.OmniVisionService();
31
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, omniService);
29
32
  mockDriver = {
30
33
  getScreenshot: sinon_1.default.stub().resolves('mock_screenshot_base64'),
31
34
  sessionId: 'test_session',
@@ -33,6 +36,7 @@ describe('OmniVisionService Unit Tests', () => {
33
36
  });
34
37
  afterEach(() => {
35
38
  sinon_1.default.restore();
39
+ restoreContainer();
36
40
  });
37
41
  const createMockWorker = () => ({
38
42
  setParameters: sinon_1.default.stub().resolves(),
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
36
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
37
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -12,14 +45,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
45
  require("reflect-metadata");
13
46
  const device_utils_1 = require("../../src/device-utils");
14
47
  const chai_1 = require("chai");
15
- const pluginArgs_1 = require("../../src/data-service/pluginArgs");
16
48
  const cliArgs_1 = require("../integration/cliArgs");
17
49
  const db_1 = require("../../src/data-service/db");
18
50
  const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
51
+ const loki_stores_1 = require("../helpers/loki-stores");
19
52
  const pluginArgs = IPluginArgs_1.DefaultPluginArgs;
20
53
  describe('Device filter tests', () => {
54
+ // The in-memory stores, also when this file runs on its own: without
55
+ // NODE_ENV=test the factory handed out the Prisma stores, which are the
56
+ // developer's ~/.cache/xenon/xenon.db.
57
+ (0, loki_stores_1.useLokiStores)();
21
58
  it('Get Device filters for real device', () => __awaiter(void 0, void 0, void 0, function* () {
22
- yield (0, pluginArgs_1.addCLIArgs)(cliArgs_1.serverCliArgs);
59
+ // Imported here, not at the top: the module keeps the store it gets when
60
+ // it loads, and only here is that the in-memory one on a file run alone.
61
+ const { addCLIArgs } = yield Promise.resolve().then(() => __importStar(require('../../src/data-service/pluginArgs')));
62
+ yield addCLIArgs(cliArgs_1.serverCliArgs);
23
63
  (yield db_1.XenonDatabase.CLIArgs)
24
64
  .chain()
25
65
  .find()
@@ -111,7 +151,8 @@ describe('Device filter tests', () => {
111
151
  });
112
152
  });
113
153
  });
114
- describe('Pending sessions', () => __awaiter(void 0, void 0, void 0, function* () {
154
+ describe('Pending sessions', () => {
155
+ (0, loki_stores_1.useLokiStores)();
115
156
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
116
157
  (yield db_1.XenonDatabase.PendingSessionsModel).removeDataOnly();
117
158
  }));
@@ -131,4 +172,4 @@ describe('Pending sessions', () => __awaiter(void 0, void 0, void 0, function* (
131
172
  const pendingSessions = (yield db_1.XenonDatabase.PendingSessionsModel).chain().data();
132
173
  (0, chai_1.expect)(pendingSessions.length).to.equal(1);
133
174
  }));
134
- }));
175
+ });