@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 @@ 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 users_1 = require("../../src/app/routers/users");
22
22
  const auth_1 = require("../../src/app/routers/auth");
@@ -71,7 +71,7 @@ describe('password-reset links', () => {
71
71
  logged.push(args.map(String).join(' '));
72
72
  });
73
73
  }
74
- const r = yield (0, supertest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/m1/reset-link');
74
+ const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/m1/reset-link');
75
75
  (0, chai_1.expect)(r.status).to.equal(200);
76
76
  (0, chai_1.expect)(r.body.emailed).to.equal(false);
77
77
  (0, chai_1.expect)(r.body.link).to.match(new RegExp(`/xenon/reset-password#${RAW}$`));
@@ -87,7 +87,7 @@ describe('password-reset links', () => {
87
87
  .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
88
88
  .resolves({ raw: RAW, id: 't1' });
89
89
  const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
90
- const r = yield (0, supertest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/m1/reset-link');
90
+ const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/m1/reset-link');
91
91
  (0, chai_1.expect)(r.status).to.equal(200);
92
92
  (0, chai_1.expect)(r.body).to.include({ emailed: true });
93
93
  (0, chai_1.expect)(r.body).to.not.have.property('link');
@@ -99,20 +99,20 @@ describe('password-reset links', () => {
99
99
  .stub(prisma_1.prisma.user, 'findUnique')
100
100
  .resolves(Object.assign(Object.assign({}, member), { id: 's1', role: 'SUPER_ADMIN' }));
101
101
  const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
102
- const r = yield (0, supertest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/s1/reset-link');
102
+ const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/s1/reset-link');
103
103
  (0, chai_1.expect)(r.status).to.equal(403);
104
104
  (0, chai_1.expect)(create.called).to.equal(false);
105
105
  }));
106
106
  it('refuses an ADMIN issuing a link for another ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
107
107
  sinon_1.default.stub(prisma_1.prisma.user, 'findUnique').resolves(Object.assign(Object.assign({}, member), { id: 'a2', role: 'ADMIN' }));
108
108
  const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
109
- const r = yield (0, supertest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/a2/reset-link');
109
+ const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/a2/reset-link');
110
110
  (0, chai_1.expect)(r.status).to.equal(403);
111
111
  (0, chai_1.expect)(create.called).to.equal(false);
112
112
  }));
113
113
  it('refuses a link for yourself', () => __awaiter(void 0, void 0, void 0, function* () {
114
114
  const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
115
- const r = yield (0, supertest_1.default)(usersApp({ userId: 'me', role: 'SUPER_ADMIN' })).post('/users/me/reset-link');
115
+ const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'me', role: 'SUPER_ADMIN' })).post('/users/me/reset-link');
116
116
  (0, chai_1.expect)(r.status).to.equal(400);
117
117
  (0, chai_1.expect)(create.called).to.equal(false);
118
118
  }));
@@ -121,9 +121,9 @@ describe('password-reset links', () => {
121
121
  const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
122
122
  const app = usersApp({ userId: 'root', role: 'SUPER_ADMIN' });
123
123
  find.resolves(null);
124
- (0, chai_1.expect)((yield (0, supertest_1.default)(app).post('/users/nope/reset-link')).status).to.equal(404);
124
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/users/nope/reset-link')).status).to.equal(404);
125
125
  find.resolves(Object.assign(Object.assign({}, member), { status: 'DISABLED' }));
126
- (0, chai_1.expect)((yield (0, supertest_1.default)(app).post('/users/m1/reset-link')).status).to.equal(409);
126
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/users/m1/reset-link')).status).to.equal(409);
127
127
  (0, chai_1.expect)(create.called).to.equal(false);
128
128
  }));
129
129
  });
@@ -154,7 +154,7 @@ describe('password-reset links', () => {
154
154
  .resolves({ id: 'u1', email: 'u@x.local', name: 'U', status: 'ACTIVE' });
155
155
  const find = typedi_1.Container.get(UserService_1.UserService).findByEmail;
156
156
  const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
157
- const r = yield (0, supertest_1.default)(authApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
157
+ const r = yield (0, loopbackRequest_1.default)(authApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
158
158
  // The mint-or-skip decision runs in the same continuation as the user
159
159
  // lookup, so once the lookup has resolved and a macrotask has passed,
160
160
  // createToken has either been called or never will be.
@@ -172,7 +172,7 @@ describe('password-reset links', () => {
172
172
  .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
173
173
  .resolves({ raw: RAW, id: 't1' });
174
174
  const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
175
- yield (0, supertest_1.default)(authApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
175
+ yield (0, loopbackRequest_1.default)(authApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
176
176
  yield until(() => send.called);
177
177
  (0, chai_1.expect)(send.calledOnce).to.equal(true);
178
178
  (0, chai_1.expect)(send.firstCall.args[0].text).to.include(`/xenon/reset-password#${RAW}`);
@@ -191,10 +191,10 @@ describe('password-reset links', () => {
191
191
  const verify = sinon_1.default
192
192
  .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'verifyToken')
193
193
  .resolves({ id: 't1', userId: 'u1' });
194
- const ok = yield (0, supertest_1.default)(app).post('/auth/reset-password/check').send({ token: RAW });
194
+ const ok = yield (0, loopbackRequest_1.default)(app).post('/auth/reset-password/check').send({ token: RAW });
195
195
  (0, chai_1.expect)(ok.status).to.equal(200);
196
196
  (0, chai_1.expect)(verify.firstCall.args[0]).to.equal(RAW);
197
- const old = yield (0, supertest_1.default)(app).get(`/auth/reset-password/check/${RAW}`);
197
+ const old = yield (0, loopbackRequest_1.default)(app).get(`/auth/reset-password/check/${RAW}`);
198
198
  (0, chai_1.expect)(old.status).to.equal(404);
199
199
  }));
200
200
  });
@@ -210,7 +210,7 @@ describe('password-reset links', () => {
210
210
  sinon_1.default.stub(reset, 'consume').resolves();
211
211
  sinon_1.default.stub(typedi_1.Container.get(UserSessionService_1.UserSessionService), 'revokeAllForUser').resolves();
212
212
  const revoke = sinon_1.default.stub(reset, 'revokeAllForUser').resolves(2);
213
- const r = yield (0, supertest_1.default)(app)
213
+ const r = yield (0, loopbackRequest_1.default)(app)
214
214
  .post('/auth/reset-password')
215
215
  .send({ token: RAW, newPassword: 'a-new-password' });
216
216
  (0, chai_1.expect)(r.status).to.equal(204);
@@ -229,7 +229,7 @@ describe('password-reset links', () => {
229
229
  const revoke = sinon_1.default
230
230
  .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'revokeAllForUser')
231
231
  .resolves(1);
232
- const r = yield (0, supertest_1.default)(app)
232
+ const r = yield (0, loopbackRequest_1.default)(app)
233
233
  .post('/auth/change-password')
234
234
  .send({ oldPassword: 'old-password', newPassword: 'new-password' });
235
235
  (0, chai_1.expect)(r.status).to.equal(204);
@@ -51,8 +51,10 @@ const sinon_1 = __importDefault(require("sinon"));
51
51
  const fs_1 = __importDefault(require("fs"));
52
52
  const os_1 = __importDefault(require("os"));
53
53
  const path_1 = __importDefault(require("path"));
54
+ const jose = __importStar(require("jose"));
54
55
  const typedi_1 = require("typedi");
55
56
  const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
57
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
56
58
  const PluginContext_1 = require("../../src/PluginContext");
57
59
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
58
60
  const UserService_1 = require("../../src/services/UserService");
@@ -111,7 +113,7 @@ describe('createSession — an app named by id follows the team rule', () => {
111
113
  let restore;
112
114
  let pendingCopy;
113
115
  let finalCaps;
114
- let unblock;
116
+ let release;
115
117
  const create = (c) => __awaiter(void 0, void 0, void 0, function* () {
116
118
  const next = sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
117
119
  driverCaps = JSON.parse(JSON.stringify(c));
@@ -152,8 +154,8 @@ describe('createSession — an app named by id follows the team rule', () => {
152
154
  }));
153
155
  sinon_1.default.stub(pendingSessions, 'removePendingSession').resolves();
154
156
  sinon_1.default.stub(deviceService, 'updateDeviceProgress').resolves();
155
- sinon_1.default.stub(deviceService, 'updatedAllocatedDevice').resolves();
156
- unblock = sinon_1.default.stub(deviceService, 'unblockDevice').resolves();
157
+ sinon_1.default.stub(deviceService, 'claimDeviceForSession').resolves(true);
158
+ release = sinon_1.default.stub(deviceService, 'releasePendingClaim').resolves(true);
157
159
  svc = new SessionLifecycleService_1.SessionLifecycleService();
158
160
  sinon_1.default.stub(svc, 'createSessionInstance').callsFake((...args) => {
159
161
  finalCaps = JSON.parse(JSON.stringify(args[2]));
@@ -286,7 +288,7 @@ describe('createSession — an app named by id follows the team rule', () => {
286
288
  (0, chai_1.expect)(text, where).to.not.match(/"token"|"accessKey"/);
287
289
  }
288
290
  }));
289
- it('a peer node gets the ticket and the credentials; the hub keeps neither', () => __awaiter(void 0, void 0, void 0, function* () {
291
+ it('a peer node gets the ticket and the hub’s token, never the credentials; the hub keeps neither', () => __awaiter(void 0, void 0, void 0, function* () {
290
292
  allocate.resolves({
291
293
  udid: 'u2',
292
294
  host: 'http://10.0.0.2:4723',
@@ -294,18 +296,19 @@ describe('createSession — an app named by id follows the team rule', () => {
294
296
  nodeId: 'node-2',
295
297
  });
296
298
  let forwarded;
297
- sinon_1.default.stub(svc, 'forwardSessionRequest').callsFake((_d, fc) => __awaiter(void 0, void 0, void 0, function* () {
299
+ let hubToken;
300
+ sinon_1.default.stub(svc, 'forwardSessionRequest').callsFake((_d, fc, t) => __awaiter(void 0, void 0, void 0, function* () {
298
301
  forwarded = JSON.parse(JSON.stringify(fc));
302
+ hubToken = t;
299
303
  return { value: ['sess-2', { platformName: 'Android' }] };
300
304
  }));
301
305
  sinon_1.default.stub(svc, 'finalizeSession').resolves();
302
306
  const c = caps('app-a', 'ak_team_a');
303
307
  yield svc.createSession(sinon_1.default.stub(), {}, c);
304
308
  (0, chai_1.expect)(parseAppUrl(forwarded.alwaysMatch['appium:app']).appId).to.equal('app-a');
305
- (0, chai_1.expect)(forwarded.alwaysMatch['xe:options']).to.deep.equal({
306
- accessKey: 'ak_team_a',
307
- token: 'tk',
308
- });
309
+ (0, chai_1.expect)(forwarded.alwaysMatch['xe:options']).to.deep.equal({});
310
+ (0, chai_1.expect)(JSON.stringify(forwarded)).to.not.match(/"token"|"accessKey"/);
311
+ (0, chai_1.expect)(jose.decodeJwt(hubToken)).to.include({ aud: hubSessionToken_1.HUB_CREATE_AUDIENCE, udid: 'u2' });
309
312
  for (const [where, seen] of Object.entries({ pendingCopy, sent: c })) {
310
313
  const text = JSON.stringify(seen);
311
314
  (0, chai_1.expect)(text, where).to.include(PLAIN);
@@ -326,6 +329,7 @@ describe('createSession — an app named by id follows the team rule', () => {
326
329
  }
327
330
  (0, chai_1.expect)(String(err === null || err === void 0 ? void 0 : err.message)).to.match(/app-a.*JWT key service not initialized/);
328
331
  (0, chai_1.expect)(driverCaps, 'the driver was never started').to.equal(undefined);
329
- (0, chai_1.expect)(unblock.calledOnceWith('u1', 'h1')).to.equal(true);
332
+ (0, chai_1.expect)(release.calledOnce).to.equal(true);
333
+ (0, chai_1.expect)(release.firstCall.args[0]).to.include({ udid: 'u1', host: 'h1' });
330
334
  }));
331
335
  });
@@ -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 fs_1 = __importDefault(require("fs"));
21
21
  const os_1 = __importDefault(require("os"));
22
22
  const path_1 = __importDefault(require("path"));
@@ -99,12 +99,12 @@ describe('session files', () => {
99
99
  fs_1.default.rmSync(dir, { recursive: true, force: true });
100
100
  });
101
101
  it("serves a visible session's screenshot to a member", () => __awaiter(void 0, void 0, void 0, function* () {
102
- const r = yield (0, supertest_1.default)(app(member)).get('/session/sess-shared/asset/screenshots/shot.png');
102
+ const r = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-shared/asset/screenshots/shot.png');
103
103
  (0, chai_1.expect)(r.status).to.equal(200);
104
104
  (0, chai_1.expect)(String(r.body)).to.equal('png of sess-shared');
105
105
  }));
106
106
  it('serves a video with Range, for seeking', () => __awaiter(void 0, void 0, void 0, function* () {
107
- const r = yield (0, supertest_1.default)(app(member))
107
+ const r = yield (0, loopbackRequest_1.default)(app(member))
108
108
  .get('/session/sess-shared/asset/video/sess-shared.mp4')
109
109
  .set('Range', 'bytes=2-4');
110
110
  (0, chai_1.expect)(r.status).to.equal(206);
@@ -116,15 +116,15 @@ describe('session files', () => {
116
116
  error: true,
117
117
  message: `Session with id ${id} not found`,
118
118
  });
119
- const hidden = yield (0, supertest_1.default)(app(member)).get('/session/sess-b/asset/screenshots/shot.png');
120
- const unknown = yield (0, supertest_1.default)(app(member)).get('/session/sess-nope/asset/screenshots/shot.png');
119
+ const hidden = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-b/asset/screenshots/shot.png');
120
+ const unknown = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-nope/asset/screenshots/shot.png');
121
121
  (0, chai_1.expect)(unknown.status).to.equal(404);
122
122
  (0, chai_1.expect)(unknown.body).to.deep.equal(unknownBody('sess-nope'));
123
123
  (0, chai_1.expect)(hidden.status).to.equal(404);
124
124
  (0, chai_1.expect)(hidden.body).to.deep.equal(unknownBody('sess-b'));
125
125
  }));
126
126
  it("an admin gets another team's session file", () => __awaiter(void 0, void 0, void 0, function* () {
127
- const r = yield (0, supertest_1.default)(app(admin)).get('/session/sess-b/asset/screenshots/shot.png');
127
+ const r = yield (0, loopbackRequest_1.default)(app(admin)).get('/session/sess-b/asset/screenshots/shot.png');
128
128
  (0, chai_1.expect)(r.status).to.equal(200);
129
129
  (0, chai_1.expect)(String(r.body)).to.equal('png of sess-b');
130
130
  }));
@@ -136,14 +136,14 @@ describe('session files', () => {
136
136
  '/session/sess-shared/asset/screenshots/%2e%2e%2finterceptor%2fsession.har',
137
137
  '/session/sess-shared/asset/video/..%2f..%2frecordings%2fgrp%2fcomposite.mp4',
138
138
  ]) {
139
- const r = yield (0, supertest_1.default)(app(admin)).get(url);
139
+ const r = yield (0, loopbackRequest_1.default)(app(admin)).get(url);
140
140
  (0, chai_1.expect)(r.status, url).to.equal(404);
141
141
  (0, chai_1.expect)(r.text, url).to.not.include('HAR-SECRET');
142
142
  (0, chai_1.expect)(r.text, url).to.not.include('COMPOSITE-SECRET');
143
143
  }
144
144
  }));
145
145
  it('a missing file in a visible session is a JSON 404', () => __awaiter(void 0, void 0, void 0, function* () {
146
- const r = yield (0, supertest_1.default)(app(member)).get('/session/sess-shared/asset/screenshots/none.png');
146
+ const r = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-shared/asset/screenshots/none.png');
147
147
  (0, chai_1.expect)(r.status).to.equal(404);
148
148
  (0, chai_1.expect)(r.body).to.have.property('error');
149
149
  }));
@@ -160,7 +160,7 @@ describe('session files', () => {
160
160
  it('no longer serves session files to a caller with no login', () => __awaiter(void 0, void 0, void 0, function* () {
161
161
  const a = (0, express_1.default)();
162
162
  a.use('/xenon', (0, index_1.createRouter)({ bindHostOrIp: '127.0.0.1', enableDashboard: true }));
163
- const r = yield (0, supertest_1.default)(a).get(`/xenon/session-recordings/${path_1.default.basename(probeDir)}/secret.txt`);
163
+ const r = yield (0, loopbackRequest_1.default)(a).get(`/xenon/session-recordings/${path_1.default.basename(probeDir)}/secret.txt`);
164
164
  (0, chai_1.expect)(r.text).to.not.include(secret);
165
165
  }));
166
166
  });
@@ -36,15 +36,22 @@ const capsWith = (obj) => ({
36
36
  describe('authorizeSessionRequest — identity', () => {
37
37
  let svc;
38
38
  let authDisabledBefore;
39
+ let restore;
39
40
  beforeEach(() => {
40
41
  authDisabledBefore = config_1.config.authDisabled;
41
42
  config_1.config.authDisabled = false;
42
- svc = typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService);
43
+ restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
44
+ // No such user and no teams, as the developer's database answered for
45
+ // these ids: the real lookups read that database. Nothing here is about
46
+ // the owner's teams.
47
+ typedi_1.Container.set(UserService_1.UserService, { findById: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
48
+ sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
49
+ svc = new SessionLifecycleService_1.SessionLifecycleService();
43
50
  });
44
51
  afterEach(() => {
45
52
  config_1.config.authDisabled = authDisabledBefore;
46
53
  sinon_1.default.restore();
47
- typedi_1.Container.reset();
54
+ restore();
48
55
  });
49
56
  it('returns both ids for a valid xe:options pair', () => __awaiter(void 0, void 0, void 0, function* () {
50
57
  typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
@@ -304,6 +311,26 @@ describe('authorizeSessionRequest — leaseAccess', () => {
304
311
  }
305
312
  (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/not allowed/);
306
313
  }));
314
+ it('reads the team from xe:options.team, over a flat cap, and still checks it', () => __awaiter(void 0, void 0, void 0, function* () {
315
+ withKey(keyRow('sessions'), user('MEMBER'));
316
+ teamRows.resolves([{ teamId: 'team_a' }, { teamId: 'team_b' }]);
317
+ const caps = (team) => capsWith({
318
+ 'xe:options': { accessKey: 'ak', token: 'tk', team },
319
+ 'xenon:team': 'team_a',
320
+ });
321
+ (0, chai_1.expect)(yield scope(caps('team_b'))).to.deep.equal({
322
+ callerTeamIds: ['team_b'],
323
+ scoped: true,
324
+ });
325
+ let error;
326
+ try {
327
+ yield invoke(svc, caps('team_c'));
328
+ }
329
+ catch (e) {
330
+ error = e;
331
+ }
332
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("xe:options.team 'team_c' is not allowed for this API key");
333
+ }));
307
334
  it('falls back to the shared pool when the lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
308
335
  withKey(keyRow('sessions'), null);
309
336
  typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
@@ -0,0 +1,255 @@
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 express_1 = __importDefault(require("express"));
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
+ const base_driver_1 = require("@appium/base-driver");
20
+ const sessionCreate_1 = require("../../src/gateway/sessionCreate");
21
+ const createHandoff_1 = require("../../src/gateway/createHandoff");
22
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
23
+ const loopbackServer_1 = require("../helpers/loopbackServer");
24
+ /**
25
+ * `POST <basePath>/session` in the session gateway: allocation happens here,
26
+ * before Appium's route. A remote phone's session is created on its node and
27
+ * answered here; a local phone's allocation goes on to the plugin through the
28
+ * request's async context, and is given back if nothing takes it.
29
+ *
30
+ * Appium's route is stood in for by a plain route, and the plugin by a
31
+ * function that takes the handoff, the way XenonPlugin.createSession does.
32
+ */
33
+ describe('the session gateway’s create layer', () => {
34
+ const loopback = (0, loopbackServer_1.loopbackServers)();
35
+ const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
36
+ let lifecycle;
37
+ let routeHits;
38
+ let pluginTakes;
39
+ let base;
40
+ const grant = { userId: 'alice', udid: 'phone-1', host: 'http://node:4725' };
41
+ const caps = () => ({
42
+ capabilities: { alwaysMatch: { platformName: 'Android' }, firstMatch: [{}] },
43
+ });
44
+ beforeEach(() => {
45
+ routeHits = [];
46
+ pluginTakes = true;
47
+ lifecycle = {
48
+ prepareSession: sinon_1.default.stub().resolves({ remote: false, device: { udid: 'phone-1' } }),
49
+ completeRemoteSession: sinon_1.default
50
+ .stub()
51
+ .resolves({ protocol: 'W3C', value: ['node-s1', { platformName: 'Android' }, 'W3C'] }),
52
+ releaseAllocation: sinon_1.default.stub().resolves(),
53
+ };
54
+ });
55
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
56
+ sinon_1.default.restore();
57
+ yield loopback.closeAll();
58
+ }));
59
+ function serve() {
60
+ return __awaiter(this, arguments, void 0, function* (opts = {}) {
61
+ var _a;
62
+ const basePath = (_a = opts.basePath) !== null && _a !== void 0 ? _a : '/wd/hub';
63
+ const app = (0, express_1.default)();
64
+ app.use(express_1.default.json());
65
+ app.use(`${basePath}/session`, (0, sessionCreate_1.createSessionCreateLayer)({
66
+ lifecycle: () => lifecycle,
67
+ hubGrants: opts.hubGrants,
68
+ authDisabled: () => !!opts.authDisabled,
69
+ logger: quiet,
70
+ }));
71
+ if (opts.beforeRoute)
72
+ app.use(opts.beforeRoute);
73
+ // Appium's route, with the plugin inside it.
74
+ app.post(`${basePath}/session`, (req, res) => __awaiter(this, void 0, void 0, function* () {
75
+ var _a;
76
+ const hit = { headers: Object.assign({}, req.headers), body: req.body };
77
+ routeHits.push(hit);
78
+ yield new Promise((resolve) => setTimeout(resolve, 1));
79
+ if (pluginTakes)
80
+ hit.took = (_a = (0, createHandoff_1.currentCreateHandoff)()) === null || _a === void 0 ? void 0 : _a.take();
81
+ res.json({ value: { sessionId: 'local-s1', capabilities: {} } });
82
+ }));
83
+ app.all('*', (_req, res) => res.status(404).json({ value: { error: 'unknown command' } }));
84
+ const server = yield loopback.serve(app);
85
+ base = `http://127.0.0.1:${server.address().port}`;
86
+ return base;
87
+ });
88
+ }
89
+ describe('what it handles', () => {
90
+ it('only a POST of exactly the create path', () => __awaiter(void 0, void 0, void 0, function* () {
91
+ yield serve();
92
+ yield (0, loopbackRequest_1.default)(base).get('/wd/hub/session').expect(404);
93
+ yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session/abc/url').send({}).expect(404);
94
+ yield (0, loopbackRequest_1.default)(base).post('/wd/hub/sessionx').send(caps()).expect(404);
95
+ (0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
96
+ }));
97
+ it('a body without a capabilities object goes straight on, for Appium to answer', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ yield serve();
99
+ yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send({ desiredCapabilities: {} }).expect(200);
100
+ yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send({ capabilities: 'no' }).expect(200);
101
+ (0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
102
+ }));
103
+ it('matches the path as the router does: letter case, a trailing slash, any base path', () => __awaiter(void 0, void 0, void 0, function* () {
104
+ yield serve({ basePath: '' });
105
+ yield (0, loopbackRequest_1.default)(base).post('/SESSION/').send(caps()).expect(200);
106
+ (0, chai_1.expect)(lifecycle.prepareSession.calledOnce).to.equal(true);
107
+ }));
108
+ it('fills in an omitted firstMatch, as the plugin used to', () => __awaiter(void 0, void 0, void 0, function* () {
109
+ yield serve();
110
+ yield (0, loopbackRequest_1.default)(base)
111
+ .post('/wd/hub/session')
112
+ .send({ capabilities: { alwaysMatch: { platformName: 'Android' } } })
113
+ .expect(200);
114
+ (0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[0].firstMatch).to.deep.equal([{}]);
115
+ (0, chai_1.expect)(routeHits[0].body.capabilities.firstMatch).to.deep.equal([{}]);
116
+ }));
117
+ });
118
+ describe('a remote phone', () => {
119
+ beforeEach(() => lifecycle.prepareSession.resolves({ remote: true }));
120
+ it('is created on its node and answered here, as Appium answers a new session', () => __awaiter(void 0, void 0, void 0, function* () {
121
+ yield serve();
122
+ const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
123
+ (0, chai_1.expect)(res.status).to.equal(200);
124
+ (0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
125
+ (0, chai_1.expect)(res.body).to.deep.equal({
126
+ value: { capabilities: { platformName: 'Android' }, sessionId: 'node-s1' },
127
+ });
128
+ (0, chai_1.expect)(routeHits, 'Appium’s route').to.deep.equal([]);
129
+ (0, chai_1.expect)(lifecycle.completeRemoteSession.calledOnce).to.equal(true);
130
+ }));
131
+ it('a failure is answered as Appium answers the error', () => __awaiter(void 0, void 0, void 0, function* () {
132
+ lifecycle.completeRemoteSession.rejects(new base_driver_1.errors.SessionNotCreatedError('node said no'));
133
+ yield serve();
134
+ const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
135
+ (0, chai_1.expect)(res.status).to.equal(500);
136
+ (0, chai_1.expect)(res.body.value.error).to.equal('session not created');
137
+ (0, chai_1.expect)(res.body.value.message).to.include('node said no');
138
+ (0, chai_1.expect)(routeHits).to.deep.equal([]);
139
+ }));
140
+ });
141
+ describe('a refusal before any phone is allocated', () => {
142
+ it('is answered as Appium answers the error, and Appium never sees the request', () => __awaiter(void 0, void 0, void 0, function* () {
143
+ lifecycle.prepareSession.rejects(new base_driver_1.errors.InvalidArgumentError('session rejected: no'));
144
+ yield serve();
145
+ const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
146
+ (0, chai_1.expect)(res.status).to.equal(400);
147
+ (0, chai_1.expect)(res.body.value).to.include({ error: 'invalid argument' });
148
+ (0, chai_1.expect)(res.body.value.message).to.include('session rejected: no');
149
+ (0, chai_1.expect)(routeHits).to.deep.equal([]);
150
+ }));
151
+ it('a plain error is an unknown error', () => __awaiter(void 0, void 0, void 0, function* () {
152
+ lifecycle.prepareSession.rejects(new Error('No device matching request.'));
153
+ yield serve();
154
+ const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
155
+ (0, chai_1.expect)(res.status).to.equal(500);
156
+ (0, chai_1.expect)(res.body.value.error).to.equal('unknown error');
157
+ }));
158
+ });
159
+ describe('a local phone', () => {
160
+ it('its allocation reaches the plugin through Appium’s route, and is not given back', () => __awaiter(void 0, void 0, void 0, function* () {
161
+ yield serve();
162
+ yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps()).expect(200);
163
+ (0, chai_1.expect)(routeHits[0].took).to.deep.equal({ remote: false, device: { udid: 'phone-1' } });
164
+ (0, chai_1.expect)(lifecycle.prepareSession.calledOnce).to.equal(true);
165
+ (0, chai_1.expect)(lifecycle.releaseAllocation.called).to.equal(false);
166
+ }));
167
+ it('is given back once when Appium answers without the plugin taking it', () => __awaiter(void 0, void 0, void 0, function* () {
168
+ pluginTakes = false;
169
+ yield serve();
170
+ yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps()).expect(200);
171
+ yield new Promise((resolve) => setImmediate(resolve));
172
+ (0, chai_1.expect)(lifecycle.releaseAllocation.callCount).to.equal(1);
173
+ (0, chai_1.expect)(lifecycle.releaseAllocation.firstCall.args[0].device.udid).to.equal('phone-1');
174
+ }));
175
+ it('is given back, and Appium never called, when the client left while it waited', () => __awaiter(void 0, void 0, void 0, function* () {
176
+ let allocated;
177
+ lifecycle.prepareSession.callsFake(() => new Promise((resolve) => {
178
+ allocated = () => resolve({ remote: false, device: { udid: 'phone-1' } });
179
+ }));
180
+ yield serve();
181
+ const req = (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps()).timeout(50);
182
+ yield req.then(() => undefined, () => undefined);
183
+ // The server sees the connection close, then the phone turns up.
184
+ yield new Promise((resolve) => setTimeout(resolve, 20));
185
+ allocated();
186
+ yield new Promise((resolve) => setTimeout(resolve, 20));
187
+ (0, chai_1.expect)(lifecycle.releaseAllocation.callCount).to.equal(1);
188
+ (0, chai_1.expect)(routeHits).to.deep.equal([]);
189
+ }));
190
+ });
191
+ describe('on a node: the hub’s token', () => {
192
+ it('a valid one is the grant the node allocates by; the header goes no further', () => __awaiter(void 0, void 0, void 0, function* () {
193
+ const verifyCreate = sinon_1.default.stub().resolves(grant);
194
+ yield serve({ hubGrants: { verifyCreate } });
195
+ yield (0, loopbackRequest_1.default)(base)
196
+ .post('/wd/hub/session')
197
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good-token')
198
+ .send(caps())
199
+ .expect(200);
200
+ (0, chai_1.expect)(verifyCreate.calledOnceWith('good-token')).to.equal(true);
201
+ (0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[1]).to.deep.equal({ hubGrant: grant });
202
+ (0, chai_1.expect)(routeHits[0].headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
203
+ }));
204
+ it('an invalid one is refused, and nothing is allocated', () => __awaiter(void 0, void 0, void 0, function* () {
205
+ yield serve({ hubGrants: { verifyCreate: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) } });
206
+ const res = yield (0, loopbackRequest_1.default)(base)
207
+ .post('/wd/hub/session')
208
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, 'forged')
209
+ .send(caps());
210
+ (0, chai_1.expect)(res.status).to.equal(400);
211
+ (0, chai_1.expect)(res.body.value.error).to.equal('invalid argument');
212
+ (0, chai_1.expect)(res.body.value.message).to.include('session rejected');
213
+ (0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
214
+ (0, chai_1.expect)(routeHits).to.deep.equal([]);
215
+ }));
216
+ it('one that cannot be checked is a 503, and nothing is allocated', () => __awaiter(void 0, void 0, void 0, function* () {
217
+ yield serve({
218
+ hubGrants: {
219
+ verifyCreate: () => __awaiter(void 0, void 0, void 0, function* () {
220
+ throw new hubSessionToken_1.HubTokenUnavailableError('hub down');
221
+ }),
222
+ },
223
+ });
224
+ const res = yield (0, loopbackRequest_1.default)(base)
225
+ .post('/wd/hub/session')
226
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, 'token')
227
+ .send(caps());
228
+ (0, chai_1.expect)(res.status).to.equal(503);
229
+ (0, chai_1.expect)(res.body.value.error).to.equal('unknown error');
230
+ (0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
231
+ }));
232
+ it('with auth disabled it is not checked, only removed', () => __awaiter(void 0, void 0, void 0, function* () {
233
+ const verifyCreate = sinon_1.default.stub().resolves(null);
234
+ yield serve({ hubGrants: { verifyCreate }, authDisabled: true });
235
+ yield (0, loopbackRequest_1.default)(base)
236
+ .post('/wd/hub/session')
237
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, 'anything')
238
+ .send(caps())
239
+ .expect(200);
240
+ (0, chai_1.expect)(verifyCreate.called).to.equal(false);
241
+ (0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[1]).to.deep.equal({});
242
+ (0, chai_1.expect)(routeHits[0].headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
243
+ }));
244
+ it('a hub, which checks no hub tokens, ignores and removes one', () => __awaiter(void 0, void 0, void 0, function* () {
245
+ yield serve();
246
+ yield (0, loopbackRequest_1.default)(base)
247
+ .post('/wd/hub/session')
248
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, 'anything')
249
+ .send(caps())
250
+ .expect(200);
251
+ (0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[1]).to.deep.equal({});
252
+ (0, chai_1.expect)(routeHits[0].headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
253
+ }));
254
+ });
255
+ });