@xenon-device-management/xenon 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +3 -4
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
  5. package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
  69. package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
  70. package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +11 -104
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/routers/nodePhoneControl.js +301 -0
  84. package/lib/src/app/ws/h264StreamWs.js +43 -12
  85. package/lib/src/app/ws/logcatWs.js +48 -14
  86. package/lib/src/app/ws/nodeSocketRelay.js +167 -0
  87. package/lib/src/app/ws/upgradeRouter.js +215 -0
  88. package/lib/src/dashboard/event-manager.js +7 -6
  89. package/lib/src/data-service/device-service.js +152 -49
  90. package/lib/src/data-service/device-store.js +92 -10
  91. package/lib/src/data-service/deviceClaims.js +116 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  93. package/lib/src/data-service/prisma-store.js +102 -31
  94. package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
  95. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  96. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  97. package/lib/src/device-managers/NodeDevices.js +7 -2
  98. package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
  99. package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
  100. package/lib/src/device-managers/android/previewHold.js +2 -2
  101. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  102. package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
  103. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  104. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  105. package/lib/src/device-utils.js +78 -24
  106. package/lib/src/gateway/createHandoff.js +73 -0
  107. package/lib/src/gateway/defaultGateway.js +80 -0
  108. package/lib/src/gateway/forwardToNode.js +147 -0
  109. package/lib/src/gateway/hubSessionToken.js +356 -0
  110. package/lib/src/gateway/internalCall.js +90 -0
  111. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  112. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  113. package/lib/src/gateway/sessionCreate.js +136 -0
  114. package/lib/src/gateway/sessionGateway.js +201 -0
  115. package/lib/src/gateway/sessionLocator.js +100 -0
  116. package/lib/src/generated/client/edge.js +7 -4
  117. package/lib/src/generated/client/index-browser.js +4 -1
  118. package/lib/src/generated/client/index.d.ts +112 -1
  119. package/lib/src/generated/client/index.js +7 -4
  120. package/lib/src/generated/client/package.json +1 -1
  121. package/lib/src/generated/client/schema.prisma +9 -0
  122. package/lib/src/generated/client/wasm.js +4 -1
  123. package/lib/src/helpers/index.js +5 -1
  124. package/lib/src/logger.js +5 -0
  125. package/lib/src/middleware/authMiddleware.js +80 -0
  126. package/lib/src/middleware/commandAuth.js +31 -17
  127. package/lib/src/middleware/csrfMiddleware.js +3 -2
  128. package/lib/src/middleware/deviceAccessGuard.js +4 -1
  129. package/lib/src/middleware/sessionListingFilter.js +153 -0
  130. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  131. package/lib/src/plugin.js +27 -1
  132. package/lib/src/prisma.js +2 -1
  133. package/lib/src/services/DeviceReconciler.js +5 -2
  134. package/lib/src/services/OrphanSweeper.js +12 -19
  135. package/lib/src/services/PortAllocator.js +75 -16
  136. package/lib/src/services/ServerManager.js +63 -30
  137. package/lib/src/services/SessionLifecycleService.js +335 -76
  138. package/lib/src/services/ShutdownCoordinator.js +7 -1
  139. package/lib/src/services/SocketServer.js +11 -2
  140. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  141. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  142. package/lib/src/services/device-access/ticketActorAccess.js +3 -1
  143. package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
  144. package/lib/src/services/lease/LeaseService.js +19 -5
  145. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  146. package/lib/src/services/session/sessionCredentials.js +7 -59
  147. package/lib/src/sessions/CloudSession.js +12 -0
  148. package/lib/src/sessions/LocalSession.js +52 -26
  149. package/lib/src/sessions/RemoteSession.js +116 -9
  150. package/lib/src/sessions/SessionManager.js +49 -6
  151. package/lib/src/sessions/appiumUmbrella.js +32 -0
  152. package/lib/test/helpers/appium-umbrella.js +84 -0
  153. package/lib/test/helpers/fake-device-table.js +97 -0
  154. package/lib/test/helpers/loki-stores.js +41 -0
  155. package/lib/test/helpers/loopbackRequest.js +77 -0
  156. package/lib/test/helpers/own-node-id.js +29 -0
  157. package/lib/test/helpers/scratch-database.js +100 -0
  158. package/lib/test/helpers/scratch-port-leases.js +119 -0
  159. package/lib/test/helpers/test-container.js +35 -27
  160. package/lib/test/integration/androidDevices.spec.js +5 -4
  161. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  162. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  163. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  164. package/lib/test/setup/scratch-default-database.js +32 -0
  165. package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
  166. package/lib/test/unit/DeviceModel.spec.js +9 -9
  167. package/lib/test/unit/HealingCascade.test.js +5 -1
  168. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  169. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  170. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  171. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  172. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  173. package/lib/test/unit/android-h264-service.spec.js +24 -0
  174. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  175. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  176. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  177. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  178. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  179. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  180. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  181. package/lib/test/unit/authMiddleware.test.js +3 -0
  182. package/lib/test/unit/bug-report/route.spec.js +9 -9
  183. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  184. package/lib/test/unit/clipboard-route.spec.js +22 -8
  185. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  186. package/lib/test/unit/command-auth.spec.js +31 -31
  187. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  188. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  189. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  190. package/lib/test/unit/create-error-message.spec.js +43 -0
  191. package/lib/test/unit/create-handoff.spec.js +86 -0
  192. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  193. package/lib/test/unit/device-access-guard.spec.js +66 -28
  194. package/lib/test/unit/device-service.spec.js +4 -0
  195. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  196. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  197. package/lib/test/unit/device-team-guard.spec.js +20 -20
  198. package/lib/test/unit/device-utils.spec.js +8 -0
  199. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  200. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  201. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  202. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  203. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  204. package/lib/test/unit/helpers.spec.js +27 -0
  205. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  206. package/lib/test/unit/hub-create-once.spec.js +190 -0
  207. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  208. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  209. package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
  210. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  211. package/lib/test/unit/hub-node-race.spec.js +426 -0
  212. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  213. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  214. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  215. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  216. package/lib/test/unit/hub-session-token.spec.js +265 -0
  217. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  218. package/lib/test/unit/install-repository-app-team.spec.js +6 -2
  219. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  220. package/lib/test/unit/internal-call.spec.js +181 -0
  221. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  222. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  223. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  224. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  225. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  226. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  227. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  228. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  229. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
  230. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  231. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  232. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  233. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  234. package/lib/test/unit/logcat-ws.spec.js +12 -1
  235. package/lib/test/unit/loopback-request.spec.js +81 -0
  236. package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
  237. package/lib/test/unit/node-create-caps.spec.js +54 -0
  238. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  239. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  240. package/lib/test/unit/node-phone-control.spec.js +131 -0
  241. package/lib/test/unit/node-socket-relay.spec.js +357 -0
  242. package/lib/test/unit/omni-vision.spec.js +7 -3
  243. package/lib/test/unit/plugin.spec.js +45 -4
  244. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  245. package/lib/test/unit/profile-router.test.js +11 -11
  246. package/lib/test/unit/queue-requester.spec.js +5 -0
  247. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  248. package/lib/test/unit/queue-visibility.spec.js +1 -1
  249. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  250. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  251. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  252. package/lib/test/unit/recording-store.spec.js +9 -3
  253. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  254. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  255. package/lib/test/unit/remote-session-create.spec.js +130 -0
  256. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  257. package/lib/test/unit/reset-link.test.js +14 -14
  258. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  259. package/lib/test/unit/session-assets.spec.js +9 -9
  260. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  261. package/lib/test/unit/session-create-layer.spec.js +255 -0
  262. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  263. package/lib/test/unit/session-credentials.spec.js +0 -47
  264. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  265. package/lib/test/unit/session-locator.spec.js +250 -0
  266. package/lib/test/unit/session-node-calls.spec.js +174 -0
  267. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  268. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  269. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  270. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  271. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  272. package/lib/test/unit/stream-leave-route.spec.js +19 -7
  273. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  274. package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
  275. package/lib/test/unit/stream-stop-recording.spec.js +17 -5
  276. package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
  277. package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
  278. package/lib/test/unit/upgrade-router.spec.js +446 -0
  279. package/lib/test/unit/users-router.test.js +11 -8
  280. package/lib/test/unit/verify-credential.spec.js +4 -1
  281. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  282. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  283. package/lib/tsconfig.tsbuildinfo +1 -1
  284. package/package.json +3 -4
  285. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  286. package/prisma/schema.prisma +9 -0
  287. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  288. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  289. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -0,0 +1,591 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const fs_1 = __importDefault(require("fs"));
51
+ const http_1 = __importDefault(require("http"));
52
+ const os_1 = __importDefault(require("os"));
53
+ const path_1 = __importDefault(require("path"));
54
+ const sinon_1 = __importDefault(require("sinon"));
55
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
56
+ const jose = __importStar(require("jose"));
57
+ const typedi_1 = require("typedi");
58
+ const base_driver_1 = require("@appium/base-driver");
59
+ const plugin_1 = require("../../src/plugin");
60
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
61
+ const insertBeforeRoutes_1 = require("../../src/app/insertBeforeRoutes");
62
+ const defaultGateway_1 = require("../../src/gateway/defaultGateway");
63
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
64
+ const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
65
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
66
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
67
+ const SessionOwnerResolver_1 = require("../../src/services/device-access/SessionOwnerResolver");
68
+ const ApiKeyService_1 = require("../../src/services/ApiKeyService");
69
+ const PluginContext_1 = require("../../src/PluginContext");
70
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
71
+ const device_managers_1 = require("../../src/device-managers");
72
+ const device_store_1 = require("../../src/data-service/device-store");
73
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
74
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
75
+ const event_manager_1 = require("../../src/dashboard/event-manager");
76
+ const SessionType_1 = __importDefault(require("../../src/enums/SessionType"));
77
+ const deviceUtils = __importStar(require("../../src/device-utils"));
78
+ const leaseToken_1 = require("../../src/services/lease/leaseToken");
79
+ const config_1 = require("../../src/config");
80
+ const container_registration_1 = require("../helpers/container-registration");
81
+ const scratch_database_1 = require("../helpers/scratch-database");
82
+ const appium_umbrella_1 = require("../helpers/appium-umbrella");
83
+ /**
84
+ * `POST /session` through a Xenon hub, in Appium 3's own server(): the hub is
85
+ * Appium's real umbrella driver with Xenon's real plugin, the session gateway
86
+ * from the production wiring (hubGatewayOptions), the real
87
+ * SessionLifecycleService and a scratch SQLite database. The node runs the
88
+ * production node wiring too (nodeGatewayOptions), so the hub's token is
89
+ * checked by the node's real create layer against the hub's JWKS; what the
90
+ * node then does with the grant is node-gateway-create-appium-server.spec.ts.
91
+ * Here a stand-in records it and the node's fake driver answers.
92
+ */
93
+ const quiet = {
94
+ debug: () => undefined,
95
+ info: () => undefined,
96
+ warn: () => undefined,
97
+ error: () => undefined,
98
+ };
99
+ const HUB_NODE_ID = 'hub-node-id';
100
+ const SECRETS = ['SECRET-SESSION-JWT', 'SECRET-LEASE-TOKEN'];
101
+ /** Another plugin, ahead of Xenon's in the chain, that can refuse a create. */
102
+ class GatekeeperPlugin {
103
+ constructor(name) {
104
+ this.name = name;
105
+ }
106
+ createSession(next, _driver, ...args) {
107
+ return __awaiter(this, void 0, void 0, function* () {
108
+ var _a, _b;
109
+ if ((_b = (_a = args[2]) === null || _a === void 0 ? void 0 : _a.alwaysMatch) === null || _b === void 0 ? void 0 : _b['gatekeeper:refuse']) {
110
+ throw new ((0, appium_umbrella_1.appiumBaseDriver)().errors.InvalidArgumentError)('refused by another plugin');
111
+ }
112
+ return next();
113
+ });
114
+ }
115
+ }
116
+ describe('POST /session through a hub, in Appium 3’s own server()', function () {
117
+ this.timeout(60000);
118
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
119
+ const servers = [];
120
+ let keyDir;
121
+ let hubKeys;
122
+ let appiumLogs;
123
+ let restore;
124
+ let saved;
125
+ let authDisabledBefore;
126
+ let gateBefore;
127
+ let umbrella;
128
+ let hubUrl;
129
+ let nodeOrigin;
130
+ let node;
131
+ let nodeRequests;
132
+ let nodeCreates;
133
+ let nodeRefuses;
134
+ let keys;
135
+ let keyIds;
136
+ let nodeSessions = 0;
137
+ before(() => __awaiter(this, void 0, void 0, function* () {
138
+ appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
139
+ keyDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-hub-create-keys-'));
140
+ hubKeys = new JwtKeyService_1.JwtKeyService();
141
+ yield hubKeys.init(keyDir);
142
+ }));
143
+ after(() => {
144
+ appiumLogs.restore();
145
+ fs_1.default.rmSync(keyDir, { recursive: true, force: true });
146
+ });
147
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
148
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, nodeWebDriverUrl_1.NodeBasePathResolver, SessionOwnerResolver_1.SessionOwnerResolver, device_managers_1.XenonManager);
149
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
150
+ typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
151
+ typedi_1.Container.set(nodeWebDriverUrl_1.NodeBasePathResolver, new nodeWebDriverUrl_1.NodeBasePathResolver());
152
+ typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
153
+ // No adb here: no health check, no extra device info.
154
+ typedi_1.Container.set(device_managers_1.XenonManager, {
155
+ getMaxSessionCount: () => undefined,
156
+ deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
157
+ });
158
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
159
+ saved = {
160
+ store: device_store_1.DeviceStoreFactory._deviceStore,
161
+ pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
162
+ context: Object.assign({}, context),
163
+ };
164
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
165
+ device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
166
+ authDisabledBefore = config_1.config.authDisabled;
167
+ config_1.config.authDisabled = false;
168
+ gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
169
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
170
+ for (const model of [
171
+ 'pendingSession',
172
+ 'lease',
173
+ 'session',
174
+ 'device',
175
+ 'apiKey',
176
+ 'teamMember',
177
+ 'team',
178
+ 'user',
179
+ ]) {
180
+ yield scratch.db[model].deleteMany({});
181
+ }
182
+ nodeRequests = [];
183
+ nodeCreates = [];
184
+ nodeRefuses = false;
185
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
186
+ }));
187
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
188
+ for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
189
+ SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
190
+ device_store_1.DeviceStoreFactory._deviceStore = saved.store;
191
+ device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
192
+ Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
193
+ config_1.config.authDisabled = authDisabledBefore;
194
+ if (gateBefore === undefined)
195
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
196
+ else
197
+ process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
198
+ sinon_1.default.restore();
199
+ restore();
200
+ for (const s of servers.splice(0)) {
201
+ s.closeAllConnections();
202
+ yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
203
+ }
204
+ }));
205
+ const port = (s) => s.address().port;
206
+ /** A fake node driver: a session is whatever it is asked to create. */
207
+ function fakeNodeDriver() {
208
+ const commands = [];
209
+ const sessions = new Set();
210
+ const driver = {
211
+ log: quiet,
212
+ protocol: 'W3C',
213
+ sessionExists: (id) => sessions.has(id),
214
+ proxyActive: () => false,
215
+ canProxy: () => false,
216
+ proxyRouteIsAvoided: () => false,
217
+ executeCommand: (command) => __awaiter(this, void 0, void 0, function* () {
218
+ commands.push(command);
219
+ if (command === 'createSession') {
220
+ const id = `node-session-${++nodeSessions}`;
221
+ sessions.add(id);
222
+ return [id, { platformName: 'Android' }];
223
+ }
224
+ return null;
225
+ }),
226
+ };
227
+ return { commands, driver };
228
+ }
229
+ function boot() {
230
+ return __awaiter(this, arguments, void 0, function* (opts = {}) {
231
+ var _a, _b;
232
+ const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
233
+ const hubBase = (_a = opts.hubBase) !== null && _a !== void 0 ? _a : '/wd/hub';
234
+ const nodeBase = (_b = opts.nodeBase) !== null && _b !== void 0 ? _b : '/node';
235
+ umbrella = (0, appium_umbrella_1.umbrellaWith)([
236
+ [plugin_1.XenonPlugin, 'xenon'],
237
+ [GatekeeperPlugin, 'gatekeeper'],
238
+ ]);
239
+ const hubServer = yield baseDriver.server({
240
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
241
+ port: 0,
242
+ hostname: '127.0.0.1',
243
+ basePath: hubBase,
244
+ cliArgs: { basePath: hubBase },
245
+ serverUpdaters: [
246
+ (app) => {
247
+ app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
248
+ (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: hubBase }, (0, registerCommandAuth_1.commandAuthDeps)({
249
+ enabled: () => false,
250
+ authDisabled: () => false,
251
+ logger: quiet,
252
+ }), (0, defaultGateway_1.hubGatewayOptions)({
253
+ basePath: hubBase,
254
+ isLocalHost: (host) => host === hubUrl,
255
+ nodeId: () => HUB_NODE_ID,
256
+ dashboard: () => false,
257
+ }));
258
+ },
259
+ ],
260
+ });
261
+ servers.push(hubServer);
262
+ hubUrl = `http://127.0.0.1:${port(hubServer)}`;
263
+ typedi_1.Container.get(PluginContext_1.PluginContext).setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), port(hubServer), HUB_NODE_ID, hubBase);
264
+ node = fakeNodeDriver();
265
+ // The node's own create layer, from the production wiring; its lifecycle
266
+ // is a stand-in that records the grant (see the node's own spec).
267
+ const nodeLifecycle = {
268
+ prepareSession: (caps, opts) => __awaiter(this, void 0, void 0, function* () {
269
+ nodeCreates.push({ caps: JSON.parse(JSON.stringify(caps)), grant: opts === null || opts === void 0 ? void 0 : opts.hubGrant });
270
+ if (nodeRefuses)
271
+ throw new base_driver_1.errors.InvalidArgumentError('the node refused');
272
+ return { remote: false };
273
+ }),
274
+ completeRemoteSession: () => __awaiter(this, void 0, void 0, function* () {
275
+ throw new Error('a node never forwards a create');
276
+ }),
277
+ releaseAllocation: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
278
+ };
279
+ const nodeOptions = (0, defaultGateway_1.nodeGatewayOptions)(hubUrl);
280
+ const nodeServer = yield baseDriver.server({
281
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction(node.driver),
282
+ port: 0,
283
+ hostname: '127.0.0.1',
284
+ basePath: nodeBase,
285
+ cliArgs: { basePath: nodeBase },
286
+ serverUpdaters: [
287
+ (app) => {
288
+ (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, '/', (req, _res, next) => {
289
+ if (!req.url.startsWith('/xenon/')) {
290
+ nodeRequests.push({
291
+ method: req.method,
292
+ url: req.url,
293
+ headers: Object.assign({}, req.headers),
294
+ body: req.body,
295
+ });
296
+ }
297
+ next();
298
+ });
299
+ app.get(nodeWebDriverUrl_1.WEBDRIVER_INFO_PATH, (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => nodeBase));
300
+ (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: nodeBase }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => false, authDisabled: () => false, logger: quiet }), Object.assign(Object.assign({}, nodeOptions), { create: Object.assign(Object.assign({}, nodeOptions.create), { lifecycle: () => nodeLifecycle }) }));
301
+ },
302
+ ],
303
+ });
304
+ servers.push(nodeServer);
305
+ nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
306
+ });
307
+ }
308
+ /** alice (team A) and bob (team B), members with an ordinary sessions key each. */
309
+ function seedUsers() {
310
+ return __awaiter(this, void 0, void 0, function* () {
311
+ keys = {};
312
+ keyIds = {};
313
+ for (const [id, team] of [
314
+ ['alice', 'team-a'],
315
+ ['bob', 'team-b'],
316
+ ]) {
317
+ yield scratch.db.team.create({ data: { id: team, name: team } });
318
+ yield scratch.db.user.create({
319
+ data: {
320
+ id,
321
+ email: `${id}@xenon.test`,
322
+ name: id,
323
+ passwordHash: 'x',
324
+ accessKey: `ak-${id}`,
325
+ role: 'MEMBER',
326
+ },
327
+ });
328
+ yield scratch.db.teamMember.create({ data: { teamId: team, userId: id } });
329
+ const key = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).create({
330
+ name: id,
331
+ scopes: ['sessions'],
332
+ userId: id,
333
+ });
334
+ keys[id] = key.raw;
335
+ keyIds[id] = key.id;
336
+ }
337
+ });
338
+ }
339
+ function phone(udid_1, where_1) {
340
+ return __awaiter(this, arguments, void 0, function* (udid, where, extra = {}) {
341
+ yield scratch.db.device.create({
342
+ data: Object.assign({ udid, host: where === 'node' ? nodeOrigin : hubUrl, nodeId: where === 'node' ? 'node-1' : HUB_NODE_ID, platform: 'android', name: udid, sdk: '14', busy: false, userBlocked: false, offline: false }, extra),
343
+ });
344
+ });
345
+ }
346
+ const row = (udid) => scratch.db.device.findFirst({ where: { udid } });
347
+ function capsFor(who, xeOptions = {}, alwaysMatch = {}) {
348
+ return {
349
+ capabilities: {
350
+ alwaysMatch: Object.assign({ platformName: 'Android', 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION, 'appium:newCommandTimeout': 0,
351
+ // No video: it would start Xenon's real capture pipeline for the phone.
352
+ 'xe:options': Object.assign({ accessKey: `ak-${who}`, token: keys[who], recordVideo: false }, xeOptions) }, alwaysMatch),
353
+ firstMatch: [{}],
354
+ },
355
+ };
356
+ }
357
+ const creates = () => nodeRequests.filter((r) => r.method === 'POST' && /\/session$/.test(r.url));
358
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
359
+ describe('a node’s phone', () => {
360
+ it('is created on the node and answered by the hub; Appium’s createSession never runs', () => __awaiter(this, void 0, void 0, function* () {
361
+ yield boot();
362
+ yield seedUsers();
363
+ yield phone('node-phone', 'node');
364
+ const umbrellaCommands = sinon_1.default.spy(umbrella, 'executeCommand');
365
+ const pluginCreate = sinon_1.default.spy(plugin_1.XenonPlugin.prototype, 'createSession');
366
+ const res = yield (0, loopbackRequest_1.default)(hubUrl).post('/wd/hub/session').send(capsFor('alice'));
367
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
368
+ const sessionId = res.body.value.sessionId;
369
+ (0, chai_1.expect)(sessionId).to.match(/^node-session-/);
370
+ // The node's capabilities, plus the `desired` ones finalizeSession adds,
371
+ // as a session created through Appium's route always had.
372
+ (0, chai_1.expect)(res.body.value.capabilities.platformName).to.equal('Android');
373
+ (0, chai_1.expect)(res.body.value.capabilities.desired['appium:udid']).to.equal('node-phone');
374
+ (0, chai_1.expect)(JSON.stringify(res.body)).to.not.include(keys.alice);
375
+ // Appium never saw it: no umbrella command, no plugin, no promoted plugin.
376
+ (0, chai_1.expect)(umbrellaCommands.called).to.equal(false);
377
+ (0, chai_1.expect)(pluginCreate.called).to.equal(false);
378
+ (0, chai_1.expect)(umbrella.sessionlessPlugins).to.deep.equal([]);
379
+ (0, chai_1.expect)(Object.keys(umbrella.sessionPlugins)).to.deep.equal([]);
380
+ (0, chai_1.expect)(Object.keys(umbrella.sessions)).to.deep.equal([]);
381
+ // The node created it, under its own base path.
382
+ (0, chai_1.expect)(node.commands).to.deep.equal(['createSession']);
383
+ (0, chai_1.expect)(creates().map((r) => r.url)).to.deep.equal(['/node/session']);
384
+ // The hub's record of it.
385
+ (0, chai_1.expect)(yield row('node-phone')).to.include({ busy: true, session_id: sessionId });
386
+ const live = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
387
+ (0, chai_1.expect)(live === null || live === void 0 ? void 0 : live.getType()).to.equal(SessionType_1.default.REMOTE);
388
+ (0, chai_1.expect)(live.userId).to.equal('alice');
389
+ (0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
390
+ // Its commands then go through the gateway to the node.
391
+ yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${sessionId}/url`).expect(200);
392
+ (0, chai_1.expect)(node.commands).to.deep.equal(['createSession', 'getUrl']);
393
+ (0, chai_1.expect)(umbrellaCommands.called).to.equal(false);
394
+ }));
395
+ it('the node gets the hub’s token for the verified owner, never the client’s credentials', () => __awaiter(this, void 0, void 0, function* () {
396
+ yield boot();
397
+ yield seedUsers();
398
+ yield phone('node-phone', 'node');
399
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
400
+ .post('/wd/hub/session')
401
+ .set('x-xenon-access-key', 'ak-alice')
402
+ .set('x-xenon-token', keys.alice)
403
+ .set('authorization', 'Bearer SECRET-SESSION-JWT')
404
+ .set('cookie', 'xenon_dashboard_session=SECRET-LEASE-TOKEN')
405
+ .send(capsFor('alice', { sessionToken: SECRETS[0], leaseToken: SECRETS[1], buildId: 'b-1' }));
406
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
407
+ const [sent] = creates();
408
+ const everything = JSON.stringify({ headers: sent.headers, body: sent.body });
409
+ for (const secret of [...SECRETS, keys.alice])
410
+ (0, chai_1.expect)(everything).to.not.include(secret);
411
+ for (const header of ['x-xenon-access-key', 'x-xenon-token', 'authorization', 'cookie']) {
412
+ (0, chai_1.expect)(sent.headers[header], header).to.equal(undefined);
413
+ }
414
+ // Options that are not secrets still travel.
415
+ (0, chai_1.expect)(sent.body.capabilities.alwaysMatch['xe:options']).to.deep.equal({
416
+ recordVideo: false,
417
+ buildId: 'b-1',
418
+ });
419
+ const claims = jose.decodeJwt(String(sent.headers[hubSessionToken_1.HUB_TOKEN_HEADER]));
420
+ (0, chai_1.expect)(claims).to.include({
421
+ aud: hubSessionToken_1.HUB_CREATE_AUDIENCE,
422
+ sub: 'alice',
423
+ udid: 'node-phone',
424
+ host: nodeOrigin,
425
+ });
426
+ // The node's real create layer checked it against the hub's keys.
427
+ (0, chai_1.expect)(nodeCreates).to.have.length(1);
428
+ (0, chai_1.expect)(nodeCreates[0].grant).to.deep.equal({
429
+ userId: 'alice',
430
+ udid: 'node-phone',
431
+ host: nodeOrigin,
432
+ });
433
+ (0, chai_1.expect)(JSON.stringify(nodeCreates[0].caps)).to.not.include(keys.alice);
434
+ }));
435
+ it('a node that refuses it: the hub answers the error and frees the phone', () => __awaiter(this, void 0, void 0, function* () {
436
+ yield boot();
437
+ yield seedUsers();
438
+ yield phone('node-phone', 'node');
439
+ nodeRefuses = true;
440
+ const res = yield (0, loopbackRequest_1.default)(hubUrl).post('/wd/hub/session').send(capsFor('alice'));
441
+ (0, chai_1.expect)(res.status).to.equal(500);
442
+ (0, chai_1.expect)(res.body.value.message).to.include('invalid argument');
443
+ (0, chai_1.expect)(yield row('node-phone')).to.include({ busy: false, session_id: null });
444
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getAllSessions()).to.deep.equal([]);
445
+ (0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
446
+ }));
447
+ });
448
+ describe('the hub’s own phone', () => {
449
+ it('is allocated once, in the gateway, and created by Appium through the plugin', () => __awaiter(this, void 0, void 0, function* () {
450
+ yield boot();
451
+ yield seedUsers();
452
+ yield phone('hub-phone', 'hub');
453
+ const lifecycle = typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService);
454
+ const allocate = sinon_1.default.spy(deviceUtils, 'allocateDeviceForSession');
455
+ const prepare = sinon_1.default.spy(lifecycle, 'prepareSession');
456
+ const completeLocal = sinon_1.default.spy(lifecycle, 'completeLocalSession');
457
+ const fallback = sinon_1.default.spy(lifecycle, 'createSession');
458
+ const res = yield (0, loopbackRequest_1.default)(hubUrl).post('/wd/hub/session').send(capsFor('alice'));
459
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
460
+ const sessionId = res.body.value.sessionId;
461
+ (0, chai_1.expect)(Object.keys(umbrella.sessions)).to.deep.equal([sessionId]);
462
+ (0, chai_1.expect)(allocate.callCount, 'allocations').to.equal(1);
463
+ (0, chai_1.expect)(prepare.callCount).to.equal(1);
464
+ (0, chai_1.expect)(completeLocal.callCount).to.equal(1);
465
+ (0, chai_1.expect)(fallback.called, 'the plugin’s own allocation').to.equal(false);
466
+ (0, chai_1.expect)(yield row('hub-phone')).to.include({ busy: true, session_id: sessionId });
467
+ (0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
468
+ (0, chai_1.expect)(creates(), 'the node').to.deep.equal([]);
469
+ // The driver never saw the credentials.
470
+ const inner = umbrella.sessions[sessionId];
471
+ (0, chai_1.expect)(JSON.stringify(inner.originalCaps)).to.not.include(keys.alice);
472
+ yield (0, loopbackRequest_1.default)(hubUrl).delete(`/wd/hub/session/${sessionId}`).expect(200);
473
+ (0, chai_1.expect)(yield row('hub-phone')).to.include({ busy: false, session_id: null });
474
+ }));
475
+ it('is freed when Appium rejects the create after the plugin (no such driver)', () => __awaiter(this, void 0, void 0, function* () {
476
+ yield boot();
477
+ yield seedUsers();
478
+ yield phone('hub-phone', 'hub');
479
+ const lookup = sinon_1.default.spy(umbrella.driverConfig, 'findMatchingDriver');
480
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
481
+ .post('/wd/hub/session')
482
+ .send(capsFor('alice', {}, { 'appium:automationName': 'NoSuchDriver' }));
483
+ (0, chai_1.expect)(res.status).to.equal(500);
484
+ // Appium looked for the driver, after Xenon's plugin, and found none.
485
+ (0, chai_1.expect)(lookup.calledOnce).to.equal(true);
486
+ (0, chai_1.expect)(yield lookup.firstCall.returnValue.catch((e) => e.message)).to.include('NoSuchDriver');
487
+ yield settle();
488
+ (0, chai_1.expect)(yield row('hub-phone')).to.include({ busy: false, session_id: null });
489
+ (0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
490
+ (0, chai_1.expect)(Object.keys(umbrella.sessions)).to.deep.equal([]);
491
+ }));
492
+ it('is freed when Appium rejects the create before the plugin runs', () => __awaiter(this, void 0, void 0, function* () {
493
+ yield boot();
494
+ yield seedUsers();
495
+ yield phone('hub-phone', 'hub');
496
+ const pluginCreate = sinon_1.default.spy(plugin_1.XenonPlugin.prototype, 'createSession');
497
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
498
+ .post('/wd/hub/session')
499
+ .send(capsFor('alice', {}, { 'gatekeeper:refuse': true }));
500
+ (0, chai_1.expect)(res.status).to.equal(400);
501
+ (0, chai_1.expect)(res.body.value.message).to.include('refused by another plugin');
502
+ (0, chai_1.expect)(pluginCreate.called, 'Xenon’s plugin').to.equal(false);
503
+ yield settle();
504
+ (0, chai_1.expect)(yield row('hub-phone')).to.include({ busy: false, session_id: null });
505
+ (0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
506
+ }));
507
+ });
508
+ describe('a lease through the hub', () => {
509
+ const LEASE_TOKEN = 'SECRET-LEASE-TOKEN';
510
+ function leaseNodePhone() {
511
+ return __awaiter(this, void 0, void 0, function* () {
512
+ yield phone('node-phone', 'node', { busy: true });
513
+ yield scratch.db.lease.create({
514
+ data: {
515
+ id: 'lse_1',
516
+ tokenHash: (0, leaseToken_1.hashToken)(LEASE_TOKEN),
517
+ deviceUdid: 'node-phone',
518
+ deviceHost: nodeOrigin,
519
+ actorId: keyIds.alice,
520
+ status: 'active',
521
+ expiresAt: Date.now() + 600000,
522
+ lastHeartbeatAt: Date.now(),
523
+ allocatedPorts: '{}',
524
+ capabilityBag: '{}',
525
+ },
526
+ });
527
+ });
528
+ }
529
+ it('its token opens the leased node phone; the node gets neither the lease id nor its token', () => __awaiter(this, void 0, void 0, function* () {
530
+ yield boot();
531
+ yield seedUsers();
532
+ yield leaseNodePhone();
533
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
534
+ .post('/wd/hub/session')
535
+ .send(capsFor('bob', { leaseId: 'lse_1', leaseToken: LEASE_TOKEN }));
536
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
537
+ const [sent] = creates();
538
+ const body = JSON.stringify(sent.body);
539
+ (0, chai_1.expect)(body).to.not.include(LEASE_TOKEN);
540
+ (0, chai_1.expect)(body).to.not.include('lse_1');
541
+ (0, chai_1.expect)(sent.body.capabilities.alwaysMatch['appium:udid']).to.equal('node-phone');
542
+ (0, chai_1.expect)(nodeCreates[0].grant).to.deep.include({ userId: 'bob', udid: 'node-phone' });
543
+ (0, chai_1.expect)(yield row('node-phone')).to.include({ session_id: res.body.value.sessionId });
544
+ }));
545
+ it('without its token or its creator, is refused at the hub; the node sees nothing', () => __awaiter(this, void 0, void 0, function* () {
546
+ yield boot();
547
+ yield seedUsers();
548
+ yield leaseNodePhone();
549
+ const res = yield (0, loopbackRequest_1.default)(hubUrl)
550
+ .post('/wd/hub/session')
551
+ .send(capsFor('bob', { leaseId: 'lse_1' }));
552
+ (0, chai_1.expect)(res.status).to.equal(500);
553
+ (0, chai_1.expect)(res.body.value.message).to.include('lease lse_1 is not active');
554
+ (0, chai_1.expect)(creates()).to.deep.equal([]);
555
+ (0, chai_1.expect)(yield row('node-phone')).to.include({ busy: true, session_id: null });
556
+ }));
557
+ });
558
+ describe('a team-scoped member through the hub', () => {
559
+ it('reaches their team’s node phone, and another team’s member does not', () => __awaiter(this, void 0, void 0, function* () {
560
+ yield boot();
561
+ yield seedUsers();
562
+ yield phone('team-a-phone', 'node', { teamId: 'team-a' });
563
+ const refused = yield (0, loopbackRequest_1.default)(hubUrl).post('/wd/hub/session').send(capsFor('bob'));
564
+ (0, chai_1.expect)(refused.status).to.equal(500);
565
+ (0, chai_1.expect)(refused.body.value.message).to.include('No device matching request');
566
+ (0, chai_1.expect)(creates(), 'the node, for bob').to.deep.equal([]);
567
+ (0, chai_1.expect)(yield row('team-a-phone')).to.include({ busy: false, session_id: null });
568
+ const allowed = yield (0, loopbackRequest_1.default)(hubUrl).post('/wd/hub/session').send(capsFor('alice'));
569
+ (0, chai_1.expect)(allowed.status, JSON.stringify(allowed.body)).to.equal(200);
570
+ (0, chai_1.expect)(nodeCreates.map((c) => c.grant.userId)).to.deep.equal(['alice']);
571
+ }));
572
+ });
573
+ describe('base paths', () => {
574
+ it('a hub at the root creates on a node under its own base path', () => __awaiter(this, void 0, void 0, function* () {
575
+ yield boot({ hubBase: '', nodeBase: '/node' });
576
+ yield seedUsers();
577
+ yield phone('node-phone', 'node');
578
+ const res = yield (0, loopbackRequest_1.default)(hubUrl).post('/session').send(capsFor('alice'));
579
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
580
+ (0, chai_1.expect)(creates().map((r) => r.url)).to.deep.equal(['/node/session']);
581
+ }));
582
+ it('a node at the root is created at /session', () => __awaiter(this, void 0, void 0, function* () {
583
+ yield boot({ hubBase: '/wd/hub', nodeBase: '' });
584
+ yield seedUsers();
585
+ yield phone('node-phone', 'node');
586
+ const res = yield (0, loopbackRequest_1.default)(hubUrl).post('/wd/hub/session').send(capsFor('alice'));
587
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
588
+ (0, chai_1.expect)(creates().map((r) => r.url)).to.deep.equal(['/session']);
589
+ }));
590
+ });
591
+ });