@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
@@ -16,7 +16,7 @@ const chai_1 = require("chai");
16
16
  const path_1 = __importDefault(require("path"));
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const commandAuth_1 = require("../../src/middleware/commandAuth");
21
21
  const commandCaller_1 = require("../../src/middleware/commandCaller");
22
22
  const loopbackServer_1 = require("../helpers/loopbackServer");
@@ -199,14 +199,14 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
199
199
  describe('who may drive a session', () => {
200
200
  it("lets the owner's key through to the route", () => __awaiter(void 0, void 0, void 0, function* () {
201
201
  const { app, ran } = yield lab();
202
- const res = yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
202
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
203
203
  (0, chai_1.expect)(res.status).to.equal(200);
204
204
  (0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
205
205
  (0, chai_1.expect)(warnings).to.deep.equal([]);
206
206
  }));
207
207
  it("lets the owner's Bearer token through", () => __awaiter(void 0, void 0, void 0, function* () {
208
208
  const { app, ran } = yield lab();
209
- yield (0, supertest_1.default)(app)
209
+ yield (0, loopbackRequest_1.default)(app)
210
210
  .get('/wd/hub/session/s-alice/url')
211
211
  .set(bearer('alice-jwt'))
212
212
  .expect(200);
@@ -214,13 +214,13 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
214
214
  }));
215
215
  it('refuses another user with the unknown-session answer, before the route runs', () => __awaiter(void 0, void 0, void 0, function* () {
216
216
  const { app, ran } = yield lab();
217
- const res = yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token'));
217
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token'));
218
218
  yield expectRefused(res);
219
219
  (0, chai_1.expect)(ran).to.deep.equal([]);
220
220
  }));
221
221
  it('logs the refusal with the session and the caller, never the credential', () => __awaiter(void 0, void 0, void 0, function* () {
222
222
  const { app } = yield lab();
223
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token'));
223
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token'));
224
224
  (0, chai_1.expect)(warnings).to.have.length(1);
225
225
  (0, chai_1.expect)(warnings[0]).to.include('s-alice');
226
226
  (0, chai_1.expect)(warnings[0]).to.include('bob');
@@ -229,7 +229,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
229
229
  }));
230
230
  it('refuses a request with no credentials, without looking anything up', () => __awaiter(void 0, void 0, void 0, function* () {
231
231
  const { app, ran } = yield lab();
232
- yield expectRefused(yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url'));
232
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url'));
233
233
  (0, chai_1.expect)(ran).to.deep.equal([]);
234
234
  (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called).to.equal(false);
235
235
  (0, chai_1.expect)(ownerOf.called).to.equal(false);
@@ -238,7 +238,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
238
238
  }));
239
239
  it('refuses credentials that do not verify, without looking up the owner', () => __awaiter(void 0, void 0, void 0, function* () {
240
240
  const { app } = yield lab();
241
- const res = yield (0, supertest_1.default)(app)
241
+ const res = yield (0, loopbackRequest_1.default)(app)
242
242
  .get('/wd/hub/session/s-alice/url')
243
243
  .set({ 'x-xenon-access-key': 'ak-alice', 'x-xenon-token': 'guessed' });
244
244
  yield expectRefused(res);
@@ -248,34 +248,34 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
248
248
  }));
249
249
  it('lets an admin-scoped key drive a session it does not own', () => __awaiter(void 0, void 0, void 0, function* () {
250
250
  const { app, ran } = yield lab();
251
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('carol-admin')).expect(200);
251
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('carol-admin')).expect(200);
252
252
  (0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
253
253
  }));
254
254
  it('lets a SUPER_ADMIN drive a session they do not own', () => __awaiter(void 0, void 0, void 0, function* () {
255
255
  const { app, ran } = yield lab();
256
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(bearer('root-jwt')).expect(200);
256
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(bearer('root-jwt')).expect(200);
257
257
  (0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
258
258
  }));
259
259
  it("refuses an ADMIN's ordinary key on someone else's session", () => __awaiter(void 0, void 0, void 0, function* () {
260
260
  const { app, ran } = yield lab();
261
- yield expectRefused(yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('carol-ordinary')));
261
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('carol-ordinary')));
262
262
  (0, chai_1.expect)(ran).to.deep.equal([]);
263
263
  }));
264
264
  it('refuses everyone but override admins on a session nobody owns', () => __awaiter(void 0, void 0, void 0, function* () {
265
265
  const { app, ran } = yield lab();
266
- yield expectRefused(yield (0, supertest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('alice-token')));
267
- yield expectRefused(yield (0, supertest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('carol-ordinary')));
266
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('alice-token')));
267
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('carol-ordinary')));
268
268
  (0, chai_1.expect)(ran).to.deep.equal([]);
269
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('carol-admin')).expect(200);
270
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-anon/url').set(bearer('root-jwt')).expect(200);
269
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('carol-admin')).expect(200);
270
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(bearer('root-jwt')).expect(200);
271
271
  (0, chai_1.expect)(ran).to.deep.equal(['getUrl', 'getUrl']);
272
272
  }));
273
273
  it('answers a session the caller may not use exactly as one that does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
274
274
  const { app } = yield lab();
275
- const refused = yield (0, supertest_1.default)(app)
275
+ const refused = yield (0, loopbackRequest_1.default)(app)
276
276
  .get('/wd/hub/session/s-alice/url')
277
277
  .set(as('bob-token'));
278
- const missing = yield (0, supertest_1.default)(app)
278
+ const missing = yield (0, loopbackRequest_1.default)(app)
279
279
  .get('/wd/hub/session/no-such-session/url')
280
280
  .set(as('bob-token'));
281
281
  (0, chai_1.expect)(refused.status).to.equal(missing.status);
@@ -293,38 +293,38 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
293
293
  for (const [method, url, name] of commands) {
294
294
  it(`guards ${method.toUpperCase()} ${url.replace('/wd/hub', '')}`, () => __awaiter(void 0, void 0, void 0, function* () {
295
295
  const { app, ran } = yield lab();
296
- yield expectRefused(yield (0, supertest_1.default)(app)[method](url).set(as('bob-token')).send({}));
296
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app)[method](url).set(as('bob-token')).send({}));
297
297
  (0, chai_1.expect)(ran).to.deep.equal([]);
298
- yield (0, supertest_1.default)(app)[method](url).set(as('alice-token')).send({}).expect(200);
298
+ yield (0, loopbackRequest_1.default)(app)[method](url).set(as('alice-token')).send({}).expect(200);
299
299
  (0, chai_1.expect)(ran).to.deep.equal([name]);
300
300
  }));
301
301
  }
302
302
  it('guards a path in another letter case, which the routes also match', () => __awaiter(void 0, void 0, void 0, function* () {
303
303
  const { app, ran } = yield lab();
304
- yield expectRefused(yield (0, supertest_1.default)(app).get('/WD/HUB/SESSION/s-alice/url').set(as('bob-token')));
304
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/WD/HUB/SESSION/s-alice/url').set(as('bob-token')));
305
305
  (0, chai_1.expect)(ran).to.deep.equal([]);
306
306
  }));
307
307
  });
308
308
  describe('what it leaves alone', () => {
309
309
  it('lets createSession through; it has its own credential checks', () => __awaiter(void 0, void 0, void 0, function* () {
310
310
  const { app, ran } = yield lab();
311
- yield (0, supertest_1.default)(app).post('/wd/hub/session').send({ capabilities: {} }).expect(200);
311
+ yield (0, loopbackRequest_1.default)(app).post('/wd/hub/session').send({ capabilities: {} }).expect(200);
312
312
  (0, chai_1.expect)(ran).to.deep.equal(['createSession']);
313
313
  (0, chai_1.expect)(ownerOf.called).to.equal(false);
314
314
  }));
315
315
  it('lets requests that are not session commands through', () => __awaiter(void 0, void 0, void 0, function* () {
316
316
  const { app, ran } = yield lab();
317
- yield (0, supertest_1.default)(app).get('/wd/hub/status').expect(200);
318
- yield (0, supertest_1.default)(app).get('/wd/hub/appium/sessions').expect(200);
319
- yield (0, supertest_1.default)(app).get('/xenon/api/devices').expect(200);
317
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/status').expect(200);
318
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').expect(200);
319
+ yield (0, loopbackRequest_1.default)(app).get('/xenon/api/devices').expect(200);
320
320
  (0, chai_1.expect)(ran).to.deep.equal(['getStatus', 'getAppiumSessions', 'xenonApi']);
321
321
  (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called || ownerOf.called).to.equal(false);
322
322
  }));
323
323
  it('does nothing at all when the setting is off (the default)', () => __awaiter(void 0, void 0, void 0, function* () {
324
324
  enabled = false;
325
325
  const { app, ran } = yield lab();
326
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').expect(200);
327
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token')).expect(200);
326
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').expect(200);
327
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token')).expect(200);
328
328
  (0, chai_1.expect)(ran).to.deep.equal(['getUrl', 'getUrl']);
329
329
  (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called || ownerOf.called).to.equal(false);
330
330
  (0, chai_1.expect)(warnings).to.deep.equal([]);
@@ -332,7 +332,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
332
332
  it('does nothing when auth is disabled, even with the setting on', () => __awaiter(void 0, void 0, void 0, function* () {
333
333
  authDisabled = true;
334
334
  const { app, ran } = yield lab();
335
- yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').expect(200);
335
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').expect(200);
336
336
  (0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
337
337
  (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called || ownerOf.called).to.equal(false);
338
338
  }));
@@ -341,7 +341,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
341
341
  it('answers 503 when the owner lookup fails, never letting the command through', () => __awaiter(void 0, void 0, void 0, function* () {
342
342
  ownerOf.rejects(new Error('database is locked'));
343
343
  const { app, ran } = yield lab();
344
- const res = yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
344
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
345
345
  (0, chai_1.expect)(res.status).to.equal(503);
346
346
  (0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
347
347
  (0, chai_1.expect)(ran).to.deep.equal([]);
@@ -351,7 +351,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
351
351
  it('answers 503 when the credential check fails', () => __awaiter(void 0, void 0, void 0, function* () {
352
352
  verifyKeyPair.rejects(new Error('database is locked'));
353
353
  const { app, ran } = yield lab();
354
- const res = yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
354
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
355
355
  (0, chai_1.expect)(res.status).to.equal(503);
356
356
  (0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
357
357
  (0, chai_1.expect)(ran).to.deep.equal([]);
@@ -360,13 +360,13 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
360
360
  describe('base path', () => {
361
361
  it("normalises the base path the way Appium does ('wd/hub/' → '/wd/hub')", () => __awaiter(void 0, void 0, void 0, function* () {
362
362
  const { app, ran } = yield lab({ basePath: 'wd/hub/' });
363
- yield expectRefused(yield (0, supertest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token')));
363
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token')));
364
364
  (0, chai_1.expect)(ran).to.deep.equal([]);
365
365
  }));
366
366
  it("guards /session/:id under Appium 3's default empty base path", () => __awaiter(void 0, void 0, void 0, function* () {
367
367
  const { app, ran } = yield lab({ basePath: '', routePrefix: '' });
368
- yield expectRefused(yield (0, supertest_1.default)(app).get('/session/s-alice/url').set(as('bob-token')));
369
- yield (0, supertest_1.default)(app).post('/session').send({}).expect(200);
368
+ yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/session/s-alice/url').set(as('bob-token')));
369
+ yield (0, loopbackRequest_1.default)(app).post('/session').send({}).expect(200);
370
370
  (0, chai_1.expect)(ran).to.deep.equal(['createSession']);
371
371
  }));
372
372
  });
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const deviceTeamGuard_1 = require("../../src/middleware/deviceTeamGuard");
20
20
  const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
21
21
  /**
@@ -71,41 +71,41 @@ function appWith(caller) {
71
71
  describe('the /control guards share one device lookup', () => {
72
72
  it("costs a member's tap one lookup, not one per guard", () => __awaiter(void 0, void 0, void 0, function* () {
73
73
  const { app, lookups } = appWith(ALICE);
74
- const res = yield (0, supertest_1.default)(app).post(`/control/${PHONE_A}/tap`);
74
+ const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_A}/tap`);
75
75
  (0, chai_1.expect)(res.status, res.text).to.equal(200);
76
76
  (0, chai_1.expect)(lookups).to.deep.equal([PHONE_A]);
77
77
  }));
78
78
  it("costs a member's clipboard read, which both guards check, one lookup", () => __awaiter(void 0, void 0, void 0, function* () {
79
79
  const { app, lookups } = appWith(ALICE);
80
- const res = yield (0, supertest_1.default)(app).get(`/control/${PHONE_A}/clipboard`);
80
+ const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/clipboard`);
81
81
  (0, chai_1.expect)(res.status, res.text).to.equal(200);
82
82
  (0, chai_1.expect)(lookups).to.deep.equal([PHONE_A]);
83
83
  }));
84
84
  it("costs a member's screenshot one lookup", () => __awaiter(void 0, void 0, void 0, function* () {
85
85
  const { app, lookups } = appWith(ALICE);
86
- yield (0, supertest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
86
+ yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
87
87
  (0, chai_1.expect)(lookups).to.deep.equal([PHONE_A]);
88
88
  }));
89
89
  // The same count either way, so the time an answer takes can't tell a
90
90
  // hidden phone from an unknown udid either.
91
91
  it('costs a hidden phone and an unknown udid the same single lookup', () => __awaiter(void 0, void 0, void 0, function* () {
92
92
  const hidden = appWith(ALICE);
93
- yield (0, supertest_1.default)(hidden.app).post(`/control/${PHONE_B}/tap`);
93
+ yield (0, loopbackRequest_1.default)(hidden.app).post(`/control/${PHONE_B}/tap`);
94
94
  const unknown = appWith(ALICE);
95
- yield (0, supertest_1.default)(unknown.app).post(`/control/${UNKNOWN}/tap`);
95
+ yield (0, loopbackRequest_1.default)(unknown.app).post(`/control/${UNKNOWN}/tap`);
96
96
  (0, chai_1.expect)(hidden.lookups).to.deep.equal([PHONE_B]);
97
97
  (0, chai_1.expect)(unknown.lookups).to.deep.equal([UNKNOWN]);
98
98
  }));
99
99
  it("costs an admin's tap one lookup (the team guard skips admins)", () => __awaiter(void 0, void 0, void 0, function* () {
100
100
  const { app, lookups } = appWith(ADMIN);
101
- const res = yield (0, supertest_1.default)(app).post(`/control/${PHONE_B}/tap`);
101
+ const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_B}/tap`);
102
102
  (0, chai_1.expect)(res.status, res.text).to.equal(200);
103
103
  (0, chai_1.expect)(lookups).to.deep.equal([PHONE_B]);
104
104
  }));
105
105
  it('does not share a lookup between two requests', () => __awaiter(void 0, void 0, void 0, function* () {
106
106
  const { app, lookups } = appWith(ALICE);
107
- yield (0, supertest_1.default)(app).post(`/control/${PHONE_A}/tap`);
108
- yield (0, supertest_1.default)(app).post(`/control/${PHONE_A}/tap`);
107
+ yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_A}/tap`);
108
+ yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_A}/tap`);
109
109
  (0, chai_1.expect)(lookups).to.deep.equal([PHONE_A, PHONE_A]);
110
110
  }));
111
111
  });
@@ -0,0 +1,115 @@
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 control_1 = __importDefault(require("../../src/app/routers/control"));
22
+ const device_store_1 = require("../../src/data-service/device-store");
23
+ const device_managers_1 = require("../../src/device-managers");
24
+ const PluginContext_1 = require("../../src/PluginContext");
25
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
26
+ const authMiddleware_1 = require("../../src/middleware/authMiddleware");
27
+ const container_registration_1 = require("../helpers/container-registration");
28
+ const loopbackServer_1 = require("../helpers/loopbackServer");
29
+ /**
30
+ * Whether a /control action is forwarded to another server is decided by the
31
+ * phone's own row (its nodeId, else its exact host against this server's own
32
+ * hosts), never by the Host header the caller happened to use. It was a
33
+ * substring match on that header: a lab reached as `localhost` forwarded its
34
+ * own phone's tap back to itself with no login, and the tap failed. A
35
+ * forwarded tap is also sent once: InternalHttpClient's retry on a 5xx or a
36
+ * timeout could land the same tap twice on a slow node.
37
+ */
38
+ // The route finds the manager by constructor name.
39
+ class AndroidDeviceManager {
40
+ constructor() {
41
+ this.taps = [];
42
+ this.tap = (udid) => __awaiter(this, void 0, void 0, function* () {
43
+ this.taps.push(udid);
44
+ });
45
+ }
46
+ }
47
+ const OWN_HOST = 'http://192.168.0.104:4723';
48
+ function buildApp() {
49
+ const app = (0, express_1.default)();
50
+ app.use(express_1.default.json());
51
+ const api = express_1.default.Router();
52
+ api.use((req, _res, next) => {
53
+ req.auth = {
54
+ kind: 'user-session',
55
+ userId: 'usr_me',
56
+ role: 'MEMBER',
57
+ scopes: (0, authMiddleware_1.scopesForRole)('MEMBER'),
58
+ rateLimit: 100,
59
+ };
60
+ next();
61
+ });
62
+ control_1.default.register(api);
63
+ app.use('/xenon/api', api);
64
+ return app;
65
+ }
66
+ describe('/control forwards only another server’s phone', () => {
67
+ const loopback = (0, loopbackServer_1.loopbackServers)();
68
+ let manager;
69
+ let restore;
70
+ let context;
71
+ let savedContext;
72
+ let device;
73
+ beforeEach(() => {
74
+ restore = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager);
75
+ manager = new AndroidDeviceManager();
76
+ typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(void 0, void 0, void 0, function* () { return [manager]; }) });
77
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
78
+ savedContext = Object.assign({}, context);
79
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '192.168.0.104' }), 4723, 'lab-1', '');
80
+ sinon_1.default
81
+ .stub(device_store_1.DeviceStoreFactory, 'getStore')
82
+ .returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return (Object.assign({}, device)); }) });
83
+ });
84
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
85
+ Object.assign(context, savedContext);
86
+ sinon_1.default.restore();
87
+ restore();
88
+ yield loopback.closeAll();
89
+ }));
90
+ const tap = (udid, hostHeader) => (0, loopbackRequest_1.default)(buildApp())
91
+ .post(`/xenon/api/control/${udid}/tap`)
92
+ .set('Host', hostHeader)
93
+ .send({ x: 1, y: 2 });
94
+ it('taps this server’s own phone here, whatever name the caller used for the server', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ device = { udid: 'own-1', host: OWN_HOST, nodeId: 'lab-1', platform: 'android' };
96
+ const res = yield tap('own-1', 'localhost:4723');
97
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
98
+ (0, chai_1.expect)(manager.taps).to.deep.equal(['own-1']);
99
+ }));
100
+ it('forwards a node’s phone once, never retried, and relays its answer', () => __awaiter(void 0, void 0, void 0, function* () {
101
+ const node = (0, express_1.default)();
102
+ const hits = [];
103
+ node.use((req, res) => {
104
+ hits.push(req.originalUrl);
105
+ res.status(503).json({ error: 'node busy' });
106
+ });
107
+ const nodeOrigin = `http://127.0.0.1:${(yield loopback.serve(node)).address().port}`;
108
+ device = { udid: 'node-phone', host: nodeOrigin, nodeId: 'node-9', platform: 'android' };
109
+ const res = yield tap('node-phone', '192.168.0.104:4723');
110
+ (0, chai_1.expect)(res.status).to.equal(503);
111
+ (0, chai_1.expect)(res.body).to.deep.equal({ error: 'node busy' });
112
+ (0, chai_1.expect)(manager.taps).to.deep.equal([]);
113
+ (0, chai_1.expect)(hits).to.deep.equal(['/xenon/api/control/node-phone/tap']);
114
+ }));
115
+ });
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const typedi_1 = require("typedi");
21
21
  const control_1 = __importDefault(require("../../src/app/routers/control"));
22
22
  const device_store_1 = require("../../src/data-service/device-store");
@@ -71,7 +71,7 @@ describe('stream/ticket and inspector/snapshot answer a failed device lookup', (
71
71
  it('POST stream/ticket gets 503, not a hung request', function () {
72
72
  return __awaiter(this, void 0, void 0, function* () {
73
73
  this.timeout(5000);
74
- const res = yield (0, supertest_1.default)(buildApp())
74
+ const res = yield (0, loopbackRequest_1.default)(buildApp())
75
75
  .post('/xenon/api/control/DEV-1/stream/ticket')
76
76
  .timeout(2000);
77
77
  (0, chai_1.expect)(res.status).to.equal(503);
@@ -81,7 +81,7 @@ describe('stream/ticket and inspector/snapshot answer a failed device lookup', (
81
81
  it('GET inspector/snapshot gets 503, not a hung request', function () {
82
82
  return __awaiter(this, void 0, void 0, function* () {
83
83
  this.timeout(5000);
84
- const res = yield (0, supertest_1.default)(buildApp())
84
+ const res = yield (0, loopbackRequest_1.default)(buildApp())
85
85
  .get('/xenon/api/control/DEV-1/inspector/snapshot')
86
86
  .timeout(2000);
87
87
  (0, chai_1.expect)(res.status).to.equal(503);
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("reflect-metadata");
4
+ const chai_1 = require("chai");
5
+ const base_driver_1 = require("@appium/base-driver");
6
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
7
+ /**
8
+ * Appium's createSession answers a failure as `{ error: <Error> }`. The old
9
+ * message JSON.stringify'd that Error, which is `{}`, so a create Appium
10
+ * refused (no driver for the automationName, bad capabilities) reached the
11
+ * client as "Failed to create session on node <host>. Error: {}" with a 500,
12
+ * instead of Appium's own reason and status.
13
+ */
14
+ describe('SessionLifecycleService.throwProperError', () => {
15
+ const throwProperError = (session) => SessionLifecycleService_1.SessionLifecycleService.prototype.throwProperError.call({}, session, 'http://node:4723');
16
+ function thrown(session) {
17
+ try {
18
+ throwProperError(session);
19
+ }
20
+ catch (error) {
21
+ return error;
22
+ }
23
+ throw new Error('expected throwProperError to throw');
24
+ }
25
+ it("rethrows the Error in Appium's `{ error }` answer, keeping its type and reason", () => {
26
+ const cause = new base_driver_1.errors.SessionNotCreatedError("Could not find a driver for automationName 'NoSuchDriver'");
27
+ const error = thrown({ error: cause });
28
+ (0, chai_1.expect)(error).to.equal(cause);
29
+ (0, chai_1.expect)(error.message).to.include("automationName 'NoSuchDriver'");
30
+ });
31
+ it('keeps a string reason, naming the node', () => {
32
+ const error = thrown({ error: 'no device' });
33
+ (0, chai_1.expect)(error.message).to.equal('Failed to create session on node http://node:4723. Error: no device');
34
+ });
35
+ it('keeps a plain object reason as JSON', () => {
36
+ const error = thrown({ error: { code: 'X' } });
37
+ (0, chai_1.expect)(error.message).to.include('{"code":"X"}');
38
+ });
39
+ it('rethrows an Error it is given directly', () => {
40
+ const cause = new Error('boom');
41
+ (0, chai_1.expect)(thrown(cause)).to.equal(cause);
42
+ });
43
+ });
@@ -0,0 +1,86 @@
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
+ const chai_1 = require("chai");
16
+ const sinon_1 = __importDefault(require("sinon"));
17
+ const createHandoff_1 = require("../../src/gateway/createHandoff");
18
+ /**
19
+ * The allocation the session gateway made for one `POST /session`, on its way
20
+ * through Appium's route to the plugin's createSession. Taken once by the
21
+ * plugin, or given back once if the plugin never takes it.
22
+ */
23
+ describe('CreateHandoff', () => {
24
+ afterEach(() => sinon_1.default.restore());
25
+ it('hands its allocation over exactly once', () => {
26
+ const release = sinon_1.default.spy();
27
+ const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, release);
28
+ (0, chai_1.expect)(handoff.take()).to.deep.equal({ udid: 'u1' });
29
+ (0, chai_1.expect)(() => handoff.take()).to.throw(/already/);
30
+ handoff.abandon();
31
+ (0, chai_1.expect)(release.called, 'a taken allocation is the taker’s to release').to.equal(false);
32
+ });
33
+ it('gives an allocation nobody took back once, and cannot be taken after', () => {
34
+ const release = sinon_1.default.spy();
35
+ const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, release);
36
+ handoff.abandon();
37
+ handoff.abandon();
38
+ (0, chai_1.expect)(release.callCount).to.equal(1);
39
+ (0, chai_1.expect)(release.firstCall.args[0]).to.deep.equal({ udid: 'u1' });
40
+ (0, chai_1.expect)(() => handoff.take()).to.throw(/closed/);
41
+ });
42
+ it('keeps no reference to the allocation once it is taken or released', () => {
43
+ const taken = new createHandoff_1.CreateHandoff({ udid: 'u1' }, () => undefined);
44
+ taken.take();
45
+ (0, chai_1.expect)(taken.allocation).to.equal(undefined);
46
+ const released = new createHandoff_1.CreateHandoff({ udid: 'u2' }, () => undefined);
47
+ released.abandon();
48
+ (0, chai_1.expect)(released.allocation).to.equal(undefined);
49
+ });
50
+ it('a release that throws is contained', () => {
51
+ const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, () => {
52
+ throw new Error('store down');
53
+ });
54
+ (0, chai_1.expect)(() => handoff.abandon()).to.not.throw();
55
+ });
56
+ describe('the request context', () => {
57
+ it('is none outside a request', () => {
58
+ (0, chai_1.expect)((0, createHandoff_1.currentCreateHandoff)()).to.equal(undefined);
59
+ });
60
+ it('follows the request through awaits, timers and callbacks', () => __awaiter(void 0, void 0, void 0, function* () {
61
+ const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, () => undefined);
62
+ const seen = yield (0, createHandoff_1.runWithCreateHandoff)(handoff, () => __awaiter(void 0, void 0, void 0, function* () {
63
+ yield Promise.resolve();
64
+ yield new Promise((resolve) => setTimeout(resolve, 5));
65
+ return new Promise((resolve) => setImmediate(() => resolve((0, createHandoff_1.currentCreateHandoff)())));
66
+ }));
67
+ (0, chai_1.expect)(seen).to.equal(handoff);
68
+ }));
69
+ it('keeps two requests apart', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ const a = new createHandoff_1.CreateHandoff({ udid: 'a' }, () => undefined);
71
+ const b = new createHandoff_1.CreateHandoff({ udid: 'b' }, () => undefined);
72
+ const [seenA, seenB] = yield Promise.all([
73
+ (0, createHandoff_1.runWithCreateHandoff)(a, () => __awaiter(void 0, void 0, void 0, function* () {
74
+ yield new Promise((resolve) => setTimeout(resolve, 10));
75
+ return (0, createHandoff_1.currentCreateHandoff)();
76
+ })),
77
+ (0, createHandoff_1.runWithCreateHandoff)(b, () => __awaiter(void 0, void 0, void 0, function* () {
78
+ yield new Promise((resolve) => setTimeout(resolve, 1));
79
+ return (0, createHandoff_1.currentCreateHandoff)();
80
+ })),
81
+ ]);
82
+ (0, chai_1.expect)(seenA).to.equal(a);
83
+ (0, chai_1.expect)(seenB).to.equal(b);
84
+ }));
85
+ });
86
+ });
@@ -52,7 +52,7 @@ const fs_1 = __importDefault(require("fs"));
52
52
  const os_1 = __importDefault(require("os"));
53
53
  const path_1 = __importDefault(require("path"));
54
54
  const express_1 = __importDefault(require("express"));
55
- const supertest_1 = __importDefault(require("supertest"));
55
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
56
56
  const typedi_1 = require("typedi");
57
57
  const SocketServer_1 = require("../../src/services/SocketServer");
58
58
  const SocketEvents_1 = require("../../src/enums/SocketEvents");
@@ -370,6 +370,9 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
370
370
  entries: [{ name: 'a.txt', source: { kind: 'buffer', data: Buffer.from('hi') } }],
371
371
  cleanup: () => __awaiter(void 0, void 0, void 0, function* () { }),
372
372
  });
373
+ // The route looks the session up first. No row: it assembles as before.
374
+ // Unstubbed, that read the developer's database, and hung without it.
375
+ sinon_1.default.stub(prisma_1.prisma.session, 'findUnique').resolves(null);
373
376
  const app = (0, express_1.default)();
374
377
  app.use((req, _res, next) => {
375
378
  req.auth = {
@@ -382,7 +385,7 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
382
385
  next();
383
386
  });
384
387
  bug_report_1.default.register(app);
385
- yield (0, supertest_1.default)(app)
388
+ yield (0, loopbackRequest_1.default)(app)
386
389
  .post('/sessions/s5/bug-report?mode=full')
387
390
  .buffer(true)
388
391
  .parse((r, cb) => {
@@ -419,7 +422,7 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
419
422
  const router = express_1.default.Router();
420
423
  grid_1.default.register(router, {});
421
424
  app.use(router);
422
- const res = yield (0, supertest_1.default)(app).put('/device/phone-a/team').send({ teamId: 'team-c' });
425
+ const res = yield (0, loopbackRequest_1.default)(app).put('/device/phone-a/team').send({ teamId: 'team-c' });
423
426
  (0, chai_1.expect)(res.status).to.equal(200);
424
427
  (0, chai_1.expect)(yield resolver.resolve('phone-a')).to.deep.equal({ known: true, teamId: 'team-c' });
425
428
  (0, chai_1.expect)(lookups).to.deep.equal(['phone-a']);