@xenon-device-management/xenon 2.0.0 → 2.1.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 (279) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +1 -2
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
  5. package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
  69. package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
  70. package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.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 +57 -13
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/ws/h264StreamWs.js +13 -8
  84. package/lib/src/app/ws/logcatWs.js +12 -7
  85. package/lib/src/app/ws/upgradeRouter.js +215 -0
  86. package/lib/src/dashboard/event-manager.js +7 -6
  87. package/lib/src/data-service/device-service.js +152 -49
  88. package/lib/src/data-service/device-store.js +81 -10
  89. package/lib/src/data-service/deviceClaims.js +98 -0
  90. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  91. package/lib/src/data-service/prisma-store.js +94 -31
  92. package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
  93. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  94. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  95. package/lib/src/device-managers/NodeDevices.js +7 -2
  96. package/lib/src/device-managers/android/AndroidH264StreamService.js +3 -3
  97. package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
  98. package/lib/src/device-managers/android/previewHold.js +2 -2
  99. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  100. package/lib/src/device-managers/ios/IOSStreamService.js +158 -38
  101. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  102. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  103. package/lib/src/device-utils.js +78 -24
  104. package/lib/src/gateway/createHandoff.js +73 -0
  105. package/lib/src/gateway/defaultGateway.js +80 -0
  106. package/lib/src/gateway/forwardToNode.js +147 -0
  107. package/lib/src/gateway/hubSessionToken.js +356 -0
  108. package/lib/src/gateway/internalCall.js +90 -0
  109. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  110. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  111. package/lib/src/gateway/sessionCreate.js +136 -0
  112. package/lib/src/gateway/sessionGateway.js +201 -0
  113. package/lib/src/gateway/sessionLocator.js +100 -0
  114. package/lib/src/generated/client/edge.js +7 -4
  115. package/lib/src/generated/client/index-browser.js +4 -1
  116. package/lib/src/generated/client/index.d.ts +112 -1
  117. package/lib/src/generated/client/index.js +7 -4
  118. package/lib/src/generated/client/package.json +1 -1
  119. package/lib/src/generated/client/schema.prisma +9 -0
  120. package/lib/src/generated/client/wasm.js +4 -1
  121. package/lib/src/helpers/index.js +5 -1
  122. package/lib/src/logger.js +5 -0
  123. package/lib/src/middleware/authMiddleware.js +80 -0
  124. package/lib/src/middleware/commandAuth.js +31 -17
  125. package/lib/src/middleware/csrfMiddleware.js +3 -2
  126. package/lib/src/middleware/sessionListingFilter.js +153 -0
  127. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  128. package/lib/src/plugin.js +27 -1
  129. package/lib/src/prisma.js +2 -1
  130. package/lib/src/services/DeviceReconciler.js +5 -2
  131. package/lib/src/services/OrphanSweeper.js +12 -19
  132. package/lib/src/services/PortAllocator.js +75 -16
  133. package/lib/src/services/ServerManager.js +50 -30
  134. package/lib/src/services/SessionLifecycleService.js +335 -76
  135. package/lib/src/services/ShutdownCoordinator.js +7 -1
  136. package/lib/src/services/SocketServer.js +11 -2
  137. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  138. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  139. package/lib/src/services/lease/LeaseOrphanSweeper.js +32 -6
  140. package/lib/src/services/lease/LeaseService.js +19 -5
  141. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  142. package/lib/src/services/session/sessionCredentials.js +7 -59
  143. package/lib/src/sessions/CloudSession.js +12 -0
  144. package/lib/src/sessions/LocalSession.js +52 -26
  145. package/lib/src/sessions/RemoteSession.js +116 -9
  146. package/lib/src/sessions/SessionManager.js +49 -6
  147. package/lib/src/sessions/appiumUmbrella.js +32 -0
  148. package/lib/test/helpers/appium-umbrella.js +84 -0
  149. package/lib/test/helpers/fake-device-table.js +97 -0
  150. package/lib/test/helpers/loki-stores.js +41 -0
  151. package/lib/test/helpers/loopbackRequest.js +77 -0
  152. package/lib/test/helpers/scratch-database.js +100 -0
  153. package/lib/test/helpers/scratch-port-leases.js +119 -0
  154. package/lib/test/helpers/test-container.js +35 -27
  155. package/lib/test/integration/androidDevices.spec.js +5 -4
  156. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  157. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  158. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  159. package/lib/test/unit/AndroidDeviceManager.spec.js +8 -1
  160. package/lib/test/unit/DeviceModel.spec.js +9 -9
  161. package/lib/test/unit/HealingCascade.test.js +5 -1
  162. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  163. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  164. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  165. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  166. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  167. package/lib/test/unit/android-h264-service.spec.js +6 -0
  168. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  169. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  170. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  171. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  172. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  173. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  174. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  175. package/lib/test/unit/authMiddleware.test.js +3 -0
  176. package/lib/test/unit/bug-report/route.spec.js +9 -9
  177. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  178. package/lib/test/unit/clipboard-route.spec.js +18 -8
  179. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  180. package/lib/test/unit/command-auth.spec.js +31 -31
  181. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  182. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  183. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  184. package/lib/test/unit/create-error-message.spec.js +43 -0
  185. package/lib/test/unit/create-handoff.spec.js +86 -0
  186. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  187. package/lib/test/unit/device-access-guard.spec.js +28 -28
  188. package/lib/test/unit/device-service.spec.js +4 -0
  189. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  190. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  191. package/lib/test/unit/device-team-guard.spec.js +20 -20
  192. package/lib/test/unit/device-utils.spec.js +8 -0
  193. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  194. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  195. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  196. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  197. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  198. package/lib/test/unit/helpers.spec.js +27 -0
  199. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  200. package/lib/test/unit/hub-create-once.spec.js +190 -0
  201. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  202. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  203. package/lib/test/unit/hub-node-control-forward.spec.js +291 -0
  204. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  205. package/lib/test/unit/hub-node-race.spec.js +401 -0
  206. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  207. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  208. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  209. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  210. package/lib/test/unit/hub-session-token.spec.js +265 -0
  211. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  212. package/lib/test/unit/install-repository-app-team.spec.js +2 -2
  213. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  214. package/lib/test/unit/internal-call.spec.js +181 -0
  215. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  216. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  217. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  218. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  219. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  220. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  221. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  222. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  223. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +92 -0
  224. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  225. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  226. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  227. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  228. package/lib/test/unit/logcat-ws.spec.js +4 -1
  229. package/lib/test/unit/loopback-request.spec.js +81 -0
  230. package/lib/test/unit/node-create-caps.spec.js +54 -0
  231. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  232. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  233. package/lib/test/unit/omni-vision.spec.js +7 -3
  234. package/lib/test/unit/plugin.spec.js +45 -4
  235. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  236. package/lib/test/unit/profile-router.test.js +11 -11
  237. package/lib/test/unit/queue-requester.spec.js +5 -0
  238. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  239. package/lib/test/unit/queue-visibility.spec.js +1 -1
  240. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  241. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  242. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  243. package/lib/test/unit/recording-store.spec.js +9 -3
  244. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  245. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  246. package/lib/test/unit/remote-session-create.spec.js +130 -0
  247. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  248. package/lib/test/unit/reset-link.test.js +14 -14
  249. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  250. package/lib/test/unit/session-assets.spec.js +9 -9
  251. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  252. package/lib/test/unit/session-create-layer.spec.js +255 -0
  253. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  254. package/lib/test/unit/session-credentials.spec.js +0 -47
  255. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  256. package/lib/test/unit/session-locator.spec.js +250 -0
  257. package/lib/test/unit/session-node-calls.spec.js +174 -0
  258. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  259. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  260. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  261. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  262. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  263. package/lib/test/unit/stream-leave-route.spec.js +15 -7
  264. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  265. package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
  266. package/lib/test/unit/stream-stop-recording.spec.js +13 -5
  267. package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
  268. package/lib/test/unit/upgrade-router.spec.js +446 -0
  269. package/lib/test/unit/users-router.test.js +11 -8
  270. package/lib/test/unit/verify-credential.spec.js +4 -1
  271. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  272. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  273. package/lib/tsconfig.tsbuildinfo +1 -1
  274. package/package.json +1 -2
  275. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  276. package/prisma/schema.prisma +9 -0
  277. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  278. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  279. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -0,0 +1,345 @@
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 path_1 = __importDefault(require("path"));
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const express_1 = __importDefault(require("express"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const commandAuth_1 = require("../../src/middleware/commandAuth");
21
+ const commandCaller_1 = require("../../src/middleware/commandCaller");
22
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
23
+ const loopbackServer_1 = require("../helpers/loopbackServer");
24
+ /**
25
+ * Appium 3's session listing (`GET <basePath>/appium/sessions`) under
26
+ * XENON_REQUIRE_COMMAND_AUTH.
27
+ *
28
+ * Appium answers it for anyone with every live session's id and capabilities,
29
+ * which is the easiest way to find an id to use. With the setting on, the
30
+ * answer is filtered: an override admin sees everything, a verified caller sees
31
+ * the sessions they own, and a caller without valid credentials sees an empty
32
+ * list in Appium's own shape. Appium still produces the listing; Xenon only
33
+ * removes entries from it.
34
+ *
35
+ * The apps are built as Appium builds its own (middleware, routes, then the
36
+ * plugin's updater), and the listing route records that it ran.
37
+ */
38
+ function appiumExpress() {
39
+ const appiumDir = path_1.default.dirname(require.resolve('appium/package.json'));
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ return require(require.resolve('express', { paths: [appiumDir] }));
42
+ }
43
+ const USERS = {
44
+ alice: { id: 'alice', role: 'MEMBER', status: 'ACTIVE' },
45
+ bob: { id: 'bob', role: 'MEMBER', status: 'ACTIVE' },
46
+ carol: { id: 'carol', role: 'ADMIN', status: 'ACTIVE' },
47
+ root: { id: 'root', role: 'SUPER_ADMIN', status: 'ACTIVE' },
48
+ };
49
+ // token -> [owner, scopes]
50
+ const KEYS = {
51
+ 'alice-token': ['alice', 'devices,sessions,read'],
52
+ 'bob-token': ['bob', 'devices,sessions,read'],
53
+ 'carol-ordinary': ['carol', 'devices,sessions,read'],
54
+ 'carol-admin': ['carol', 'admin,devices,sessions,read'],
55
+ };
56
+ const JWTS = {
57
+ 'alice-jwt': ['alice', 'devices,sessions,read'],
58
+ 'root-jwt': ['root', 'sessions'],
59
+ };
60
+ const OWNERS = {
61
+ 's-alice': 'alice',
62
+ 's-bob': 'bob',
63
+ 's-anon': null,
64
+ 's-alice-2': 'alice',
65
+ };
66
+ /** What Appium's getAppiumSessions answers: id, creation time, capabilities. */
67
+ const LISTING = [
68
+ { id: 's-alice', created: 1, capabilities: { platformName: 'Android' } },
69
+ { id: 's-bob', created: 2, capabilities: { platformName: 'iOS' } },
70
+ { id: 's-anon', created: 3, capabilities: { platformName: 'Android' } },
71
+ { id: 's-alice-2', created: 4, capabilities: { platformName: 'iOS' } },
72
+ ];
73
+ const ALL_IDS = LISTING.map((s) => s.id);
74
+ const as = (token) => ({
75
+ 'x-xenon-access-key': `ak-${KEYS[token][0]}`,
76
+ 'x-xenon-token': token,
77
+ });
78
+ const bearer = (jwt) => ({ authorization: `Bearer ${jwt}` });
79
+ const idsOf = (res) => res.body.value.map((s) => s.id);
80
+ const shapes = [
81
+ ['Express 4', () => (0, express_1.default)()],
82
+ ['Express 5 (Appium 3)', () => appiumExpress()()],
83
+ ];
84
+ describe('session listing under per-command auth (GET <basePath>/appium/sessions)', () => {
85
+ it('is the listing route under the normalised base path', () => {
86
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)('/wd/hub')).to.equal('/wd/hub/appium/sessions');
87
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)('wd/hub/')).to.equal('/wd/hub/appium/sessions');
88
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)('')).to.equal('/appium/sessions');
89
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)(undefined)).to.equal('/appium/sessions');
90
+ });
91
+ for (const [shapeName, makeExpress] of shapes) {
92
+ describe(shapeName, () => {
93
+ let enabled;
94
+ let authDisabled;
95
+ let verifyKeyPair;
96
+ let verifyBearer;
97
+ let ownerOf;
98
+ let ownersOf;
99
+ let infos;
100
+ let warnings;
101
+ let errors;
102
+ /** What the listing route answers; a test may swap in an error. */
103
+ let answer;
104
+ const loopback = (0, loopbackServer_1.loopbackServers)();
105
+ afterEach(() => loopback.closeAll());
106
+ function lab() {
107
+ return __awaiter(this, void 0, void 0, function* () {
108
+ const ran = [];
109
+ const app = makeExpress();
110
+ app.use(express_1.default.json());
111
+ app.get('/wd/hub/appium/sessions', (_req, res) => {
112
+ ran.push('getAppiumSessions');
113
+ res.status(answer.status).json(answer.body);
114
+ });
115
+ app.get('/wd/hub/status', (_req, res) => {
116
+ ran.push('getStatus');
117
+ res.json({ value: { ready: true } });
118
+ });
119
+ const result = (0, registerCommandAuth_1.registerCommandAuth)(app, { basePath: '/wd/hub' }, {
120
+ enabled: () => enabled,
121
+ authDisabled: () => authDisabled,
122
+ verifier: new commandCaller_1.CommandCallerVerifier({ verifyKeyPair, verifyBearer }),
123
+ ownerOf,
124
+ ownersOf,
125
+ logger: {
126
+ info: (m) => infos.push(m),
127
+ warn: (m) => warnings.push(m),
128
+ error: (m) => errors.push(m),
129
+ },
130
+ });
131
+ (0, chai_1.expect)(result.placed, 'placed ahead of the routes').to.equal(true);
132
+ // Only what requests log counts below; drop the registration's own line.
133
+ infos.splice(0);
134
+ return { app: yield loopback.serve(app), ran };
135
+ });
136
+ }
137
+ beforeEach(() => {
138
+ enabled = true;
139
+ authDisabled = false;
140
+ infos = [];
141
+ warnings = [];
142
+ errors = [];
143
+ answer = { status: 200, body: { value: LISTING } };
144
+ verifyKeyPair = sinon_1.default.stub().callsFake((accessKey, token) => __awaiter(void 0, void 0, void 0, function* () {
145
+ const key = KEYS[token];
146
+ if (!key || accessKey !== `ak-${key[0]}`)
147
+ return null;
148
+ return {
149
+ row: { id: `key-${token}`, userId: key[0], scopes: key[1], expiresAt: null },
150
+ user: USERS[key[0]],
151
+ };
152
+ }));
153
+ verifyBearer = sinon_1.default.stub().callsFake((jwt) => __awaiter(void 0, void 0, void 0, function* () {
154
+ const t = JWTS[jwt];
155
+ if (!t)
156
+ return null;
157
+ return { payload: { sub: t[0], scopes: t[1] }, user: USERS[t[0]] };
158
+ }));
159
+ ownerOf = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { return (id in OWNERS ? OWNERS[id] : null); }));
160
+ ownersOf = sinon_1.default
161
+ .stub()
162
+ .callsFake((ids) => __awaiter(void 0, void 0, void 0, function* () { return new Map(ids.map((id) => [id, id in OWNERS ? OWNERS[id] : null])); }));
163
+ });
164
+ describe('who sees what', () => {
165
+ it('shows the owner only the sessions they own', () => __awaiter(void 0, void 0, void 0, function* () {
166
+ const { app, ran } = yield lab();
167
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
168
+ (0, chai_1.expect)(res.status).to.equal(200);
169
+ (0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
170
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [LISTING[0], LISTING[3]] });
171
+ (0, chai_1.expect)(ran).to.deep.equal(['getAppiumSessions']);
172
+ }));
173
+ it('decides every listed session with one owner lookup, not one per session', () => __awaiter(void 0, void 0, void 0, function* () {
174
+ const { app } = yield lab();
175
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token')).expect(200);
176
+ (0, chai_1.expect)(ownersOf.callCount).to.equal(1);
177
+ (0, chai_1.expect)(ownersOf.firstCall.args[0]).to.have.members(ALL_IDS);
178
+ (0, chai_1.expect)(ownerOf.called).to.equal(false);
179
+ }));
180
+ it('shows the owner their sessions for a Bearer token too', () => __awaiter(void 0, void 0, void 0, function* () {
181
+ const { app } = yield lab();
182
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(bearer('alice-jwt'));
183
+ (0, chai_1.expect)(idsOf(res)).to.deep.equal(['s-alice', 's-alice-2']);
184
+ }));
185
+ it('shows another user only theirs', () => __awaiter(void 0, void 0, void 0, function* () {
186
+ const { app } = yield lab();
187
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('bob-token'));
188
+ (0, chai_1.expect)(idsOf(res)).to.deep.equal(['s-bob']);
189
+ }));
190
+ it('logs the filtering at info with the caller and the counts, never the credential', () => __awaiter(void 0, void 0, void 0, function* () {
191
+ const { app } = yield lab();
192
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('bob-token')).expect(200);
193
+ (0, chai_1.expect)(infos).to.have.length(1);
194
+ (0, chai_1.expect)(infos[0]).to.include('bob');
195
+ (0, chai_1.expect)(infos[0]).to.include('1 of 4');
196
+ (0, chai_1.expect)(infos[0]).not.to.include('bob-token');
197
+ (0, chai_1.expect)(warnings).to.deep.equal([]);
198
+ }));
199
+ it('shows an admin-scoped key every session, without looking up owners', () => __awaiter(void 0, void 0, void 0, function* () {
200
+ const { app } = yield lab();
201
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-admin'));
202
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: LISTING });
203
+ (0, chai_1.expect)(ownersOf.called || ownerOf.called).to.equal(false);
204
+ }));
205
+ it('shows a SUPER_ADMIN every session', () => __awaiter(void 0, void 0, void 0, function* () {
206
+ const { app } = yield lab();
207
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(bearer('root-jwt'));
208
+ (0, chai_1.expect)(idsOf(res)).to.deep.equal(ALL_IDS);
209
+ }));
210
+ it("shows an ADMIN's ordinary key only their own sessions, here none", () => __awaiter(void 0, void 0, void 0, function* () {
211
+ const { app } = yield lab();
212
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-ordinary'));
213
+ (0, chai_1.expect)(res.status).to.equal(200);
214
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [] });
215
+ }));
216
+ it('never shows a session nobody owns to anyone but an override admin', () => __awaiter(void 0, void 0, void 0, function* () {
217
+ const { app } = yield lab();
218
+ for (const who of ['alice-token', 'bob-token', 'carol-ordinary']) {
219
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as(who));
220
+ (0, chai_1.expect)(idsOf(res), who).not.to.include('s-anon');
221
+ }
222
+ const admin = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-admin'));
223
+ (0, chai_1.expect)(idsOf(admin)).to.include('s-anon');
224
+ }));
225
+ });
226
+ describe('callers without valid credentials', () => {
227
+ it("get an empty list in Appium's normal shape, with no lookups at all", () => __awaiter(void 0, void 0, void 0, function* () {
228
+ const { app, ran } = yield lab();
229
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
230
+ (0, chai_1.expect)(res.status).to.equal(200);
231
+ (0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
232
+ (0, chai_1.expect)(res.text).to.equal('{"value":[]}');
233
+ (0, chai_1.expect)(ran).to.deep.equal(['getAppiumSessions']);
234
+ (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called).to.equal(false);
235
+ (0, chai_1.expect)(ownersOf.called || ownerOf.called).to.equal(false);
236
+ (0, chai_1.expect)(warnings).to.have.length(1);
237
+ (0, chai_1.expect)(warnings[0]).to.include('no credentials');
238
+ }));
239
+ it('get an empty list for credentials that do not verify, logged without the secret', () => __awaiter(void 0, void 0, void 0, function* () {
240
+ const { app } = yield lab();
241
+ const res = yield (0, loopbackRequest_1.default)(app)
242
+ .get('/wd/hub/appium/sessions')
243
+ .set({ 'x-xenon-access-key': 'ak-alice', 'x-xenon-token': 'guessed' });
244
+ (0, chai_1.expect)(res.status).to.equal(200);
245
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [] });
246
+ (0, chai_1.expect)(ownersOf.called).to.equal(false);
247
+ (0, chai_1.expect)(warnings).to.have.length(1);
248
+ (0, chai_1.expect)(warnings[0]).to.include('invalid credentials');
249
+ (0, chai_1.expect)(warnings[0]).not.to.include('guessed');
250
+ }));
251
+ it('get the same answer as when no session is running', () => __awaiter(void 0, void 0, void 0, function* () {
252
+ const { app } = yield lab();
253
+ const filtered = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
254
+ answer = { status: 200, body: { value: [] } };
255
+ const empty = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-admin'));
256
+ (0, chai_1.expect)(filtered.status).to.equal(empty.status);
257
+ (0, chai_1.expect)(filtered.headers['content-type']).to.equal(empty.headers['content-type']);
258
+ (0, chai_1.expect)(filtered.text).to.equal(empty.text);
259
+ }));
260
+ it('are filtered on the path in another letter case and with a trailing slash', () => __awaiter(void 0, void 0, void 0, function* () {
261
+ const { app, ran } = yield lab();
262
+ for (const url of ['/WD/HUB/APPIUM/SESSIONS', '/wd/hub/appium/sessions/']) {
263
+ const res = yield (0, loopbackRequest_1.default)(app).get(url);
264
+ (0, chai_1.expect)(res.status, url).to.equal(200);
265
+ (0, chai_1.expect)(res.body, url).to.deep.equal({ value: [] });
266
+ }
267
+ (0, chai_1.expect)(ran).to.deep.equal(['getAppiumSessions', 'getAppiumSessions']);
268
+ }));
269
+ });
270
+ describe('what it leaves alone', () => {
271
+ it('does nothing at all when the setting is off (the default)', () => __awaiter(void 0, void 0, void 0, function* () {
272
+ enabled = false;
273
+ const { app } = yield lab();
274
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
275
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: LISTING });
276
+ (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called).to.equal(false);
277
+ (0, chai_1.expect)(ownersOf.called || ownerOf.called).to.equal(false);
278
+ (0, chai_1.expect)(infos.concat(warnings, errors)).to.deep.equal([]);
279
+ }));
280
+ it('does nothing when auth is disabled, even with the setting on', () => __awaiter(void 0, void 0, void 0, function* () {
281
+ authDisabled = true;
282
+ const { app } = yield lab();
283
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
284
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: LISTING });
285
+ (0, chai_1.expect)(ownersOf.called).to.equal(false);
286
+ }));
287
+ it("passes Appium's error answer through untouched (session_discovery not enabled)", () => __awaiter(void 0, void 0, void 0, function* () {
288
+ answer = {
289
+ status: 400,
290
+ body: {
291
+ value: {
292
+ error: 'invalid argument',
293
+ message: 'Potentially insecure feature session_discovery has not been enabled',
294
+ stacktrace: '',
295
+ },
296
+ },
297
+ };
298
+ const { app } = yield lab();
299
+ for (const headers of [{}, as('alice-token')]) {
300
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(headers);
301
+ (0, chai_1.expect)(res.status).to.equal(400);
302
+ (0, chai_1.expect)(res.body).to.deep.equal(answer.body);
303
+ }
304
+ (0, chai_1.expect)(ownersOf.called).to.equal(false);
305
+ }));
306
+ it('leaves other requests alone', () => __awaiter(void 0, void 0, void 0, function* () {
307
+ const { app, ran } = yield lab();
308
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/status');
309
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: { ready: true } });
310
+ (0, chai_1.expect)(ran).to.deep.equal(['getStatus']);
311
+ (0, chai_1.expect)(warnings).to.deep.equal([]);
312
+ }));
313
+ });
314
+ describe('failing closed', () => {
315
+ it('answers 503 when the credential check fails, before the listing runs', () => __awaiter(void 0, void 0, void 0, function* () {
316
+ verifyKeyPair.rejects(new Error('database is locked'));
317
+ const { app, ran } = yield lab();
318
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
319
+ (0, chai_1.expect)(res.status).to.equal(503);
320
+ (0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
321
+ (0, chai_1.expect)(ran).to.deep.equal([]);
322
+ (0, chai_1.expect)(errors).to.have.length(1);
323
+ (0, chai_1.expect)(errors[0]).to.include('database is locked');
324
+ }));
325
+ it('answers 503 when the owner lookup fails, never the unfiltered list', () => __awaiter(void 0, void 0, void 0, function* () {
326
+ ownersOf.rejects(new Error('database is locked\nSELECT secret FROM ...'));
327
+ const { app } = yield lab();
328
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
329
+ (0, chai_1.expect)(res.status).to.equal(503);
330
+ (0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
331
+ (0, chai_1.expect)(res.text).not.to.include('s-bob');
332
+ (0, chai_1.expect)(errors).to.have.length(1);
333
+ (0, chai_1.expect)(errors[0]).to.include('database is locked');
334
+ (0, chai_1.expect)(errors[0]).not.to.include('SELECT');
335
+ }));
336
+ it('drops a listed entry it cannot attribute rather than showing it', () => __awaiter(void 0, void 0, void 0, function* () {
337
+ answer = { status: 200, body: { value: [LISTING[0], { created: 5 }, 'junk'] } };
338
+ const { app } = yield lab();
339
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
340
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [LISTING[0]] });
341
+ }));
342
+ });
343
+ });
344
+ }
345
+ });
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const express_1 = __importDefault(require("express"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const typedi_1 = require("typedi");
21
+ const SessionType_1 = __importDefault(require("../../src/enums/SessionType"));
22
+ const sessionLocator_1 = require("../../src/gateway/sessionLocator");
23
+ const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
24
+ const container_registration_1 = require("../helpers/container-registration");
25
+ const loopbackServer_1 = require("../helpers/loopbackServer");
26
+ /**
27
+ * Where a session runs, as the hub's gateway asks it: SESSION_MANAGER for a
28
+ * live session, the database after a restart, and never a routing table of
29
+ * its own. And where a node's WebDriver API lives, which a node says itself.
30
+ */
31
+ const NODE = 'http://10.0.0.7:4725';
32
+ function fakeSession(type, url, host = NODE) {
33
+ return {
34
+ getType: () => type,
35
+ getWebDriverUrl: () => url,
36
+ getDevice: () => ({ udid: 'u1', host }),
37
+ };
38
+ }
39
+ describe('SessionLocator', () => {
40
+ let live;
41
+ let rows;
42
+ let devices;
43
+ let findOpenSession;
44
+ let findDevice;
45
+ let webDriverUrlOf;
46
+ function locator(maxRemembered) {
47
+ return new sessionLocator_1.SessionLocator({
48
+ getSession: (id) => live[id],
49
+ findOpenSession,
50
+ findDevice,
51
+ isLocalDevice: (device) => device.host === 'http://10.0.0.1:4724',
52
+ webDriverUrlOf,
53
+ maxRemembered,
54
+ });
55
+ }
56
+ beforeEach(() => {
57
+ live = {};
58
+ rows = {};
59
+ devices = {};
60
+ findOpenSession = sinon_1.default.spy((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = rows[id]) !== null && _a !== void 0 ? _a : null; }));
61
+ findDevice = sinon_1.default.spy((udid) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = devices[udid]) !== null && _a !== void 0 ? _a : null; }));
62
+ webDriverUrlOf = sinon_1.default.spy((device) => __awaiter(void 0, void 0, void 0, function* () { return `${device.host}/node-base`; }));
63
+ });
64
+ it('answers a live local session from SESSION_MANAGER, with no lookup', () => __awaiter(void 0, void 0, void 0, function* () {
65
+ live.s = fakeSession(SessionType_1.default.LOCAL, 'http://10.0.0.1:4724/wd/hub/wd-internal');
66
+ (0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({ kind: 'local' });
67
+ (0, chai_1.expect)(findOpenSession.called).to.equal(false);
68
+ }));
69
+ it('answers a live remote session with its node URL and the session', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ live.s = fakeSession(SessionType_1.default.REMOTE, `${NODE}/wd/hub`);
71
+ const location = yield locator().locate('s');
72
+ (0, chai_1.expect)(location).to.include({ kind: 'remote', url: `${NODE}/wd/hub`, cloud: false });
73
+ (0, chai_1.expect)(location.session).to.equal(live.s);
74
+ (0, chai_1.expect)(findOpenSession.called).to.equal(false);
75
+ }));
76
+ it('marks a cloud session, which gets no hub token', () => __awaiter(void 0, void 0, void 0, function* () {
77
+ live.s = fakeSession(SessionType_1.default.CLOUD, 'https://cloud.example/wd/hub', 'https://cloud.example');
78
+ (0, chai_1.expect)(yield locator().locate('s')).to.include({ kind: 'remote', cloud: true });
79
+ }));
80
+ it('never routes a session back to this server’s own phone', () => __awaiter(void 0, void 0, void 0, function* () {
81
+ live.s = fakeSession(SessionType_1.default.REMOTE, 'http://10.0.0.1:4724/wd/hub', 'http://10.0.0.1:4724');
82
+ (0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({ kind: 'local' });
83
+ }));
84
+ it('after a restart, routes an open session from its row and its phone’s row', () => __awaiter(void 0, void 0, void 0, function* () {
85
+ rows.s = { device_udid: 'u1', node_id: 'node-1' };
86
+ devices.u1 = { udid: 'u1', host: NODE, nodeId: 'node-1' };
87
+ (0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({
88
+ kind: 'remote',
89
+ url: `${NODE}/node-base`,
90
+ cloud: false,
91
+ });
92
+ (0, chai_1.expect)(findDevice.firstCall.args).to.deep.equal(['u1', 'node-1']);
93
+ }));
94
+ it('remembers the database’s answer, which cannot change, until forgotten', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ rows.s = { device_udid: 'u1', node_id: null };
96
+ devices.u1 = { udid: 'u1', host: NODE };
97
+ const l = locator();
98
+ yield l.locate('s');
99
+ yield l.locate('s');
100
+ (0, chai_1.expect)(findOpenSession.callCount).to.equal(1);
101
+ l.forget('s');
102
+ yield l.locate('s');
103
+ (0, chai_1.expect)(findOpenSession.callCount).to.equal(2);
104
+ }));
105
+ it('answers local for a session nobody here knows, so Appium gives its own answer', () => __awaiter(void 0, void 0, void 0, function* () {
106
+ const l = locator();
107
+ (0, chai_1.expect)(yield l.locate('nobody')).to.deep.equal({ kind: 'local' });
108
+ yield l.locate('nobody');
109
+ (0, chai_1.expect)(findOpenSession.callCount).to.equal(1);
110
+ }));
111
+ it('answers local for an open session on this server’s own phone', () => __awaiter(void 0, void 0, void 0, function* () {
112
+ rows.s = { device_udid: 'u1', node_id: null };
113
+ devices.u1 = { udid: 'u1', host: 'http://10.0.0.1:4724' };
114
+ (0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({ kind: 'local' });
115
+ (0, chai_1.expect)(webDriverUrlOf.called).to.equal(false);
116
+ }));
117
+ it('bounds what it remembers', () => __awaiter(void 0, void 0, void 0, function* () {
118
+ const l = locator(2);
119
+ yield l.locate('a');
120
+ yield l.locate('b');
121
+ yield l.locate('c');
122
+ yield l.locate('a');
123
+ (0, chai_1.expect)(findOpenSession.callCount).to.equal(4);
124
+ }));
125
+ it('lets a database error through for the gateway to answer', () => __awaiter(void 0, void 0, void 0, function* () {
126
+ const l = new sessionLocator_1.SessionLocator({
127
+ getSession: () => undefined,
128
+ findOpenSession: () => __awaiter(void 0, void 0, void 0, function* () {
129
+ throw new Error('database is locked');
130
+ }),
131
+ findDevice,
132
+ isLocalDevice: () => false,
133
+ webDriverUrlOf,
134
+ });
135
+ let thrown;
136
+ try {
137
+ yield l.locate('s');
138
+ }
139
+ catch (err) {
140
+ thrown = err;
141
+ }
142
+ (0, chai_1.expect)(thrown.message).to.equal('database is locked');
143
+ }));
144
+ describe('sessionRowsFrom (Prisma)', () => {
145
+ it('reads only an open session, and prefers the phone on the session’s node', () => __awaiter(void 0, void 0, void 0, function* () {
146
+ const db = {
147
+ session: { findFirst: sinon_1.default.stub().resolves({ device_udid: 'u1', node_id: 'n1' }) },
148
+ device: {
149
+ findFirst: sinon_1.default
150
+ .stub()
151
+ .callsFake((_a) => __awaiter(void 0, [_a], void 0, function* ({ where }) { return where.nodeId ? { udid: 'u1', host: NODE, nodeId: 'n1' } : { udid: 'u1', host: 'x' }; })),
152
+ },
153
+ };
154
+ const rowsFrom = (0, sessionLocator_1.sessionRowsFrom)(db);
155
+ (0, chai_1.expect)(yield rowsFrom.findOpenSession('s')).to.deep.equal({
156
+ device_udid: 'u1',
157
+ node_id: 'n1',
158
+ });
159
+ (0, chai_1.expect)(db.session.findFirst.firstCall.args[0].where).to.deep.equal({
160
+ id: 's',
161
+ endTime: null,
162
+ });
163
+ (0, chai_1.expect)(yield rowsFrom.findDevice('u1', 'n1')).to.include({ host: NODE });
164
+ (0, chai_1.expect)(yield rowsFrom.findDevice('u1', null)).to.include({ host: 'x' });
165
+ }));
166
+ });
167
+ });
168
+ describe('NodeBasePathResolver', () => {
169
+ const loopback = (0, loopbackServer_1.loopbackServers)();
170
+ let restore;
171
+ let hits;
172
+ beforeEach(() => {
173
+ restore = (0, container_registration_1.saveRegistrations)(nodeWebDriverUrl_1.NodeBasePathResolver);
174
+ typedi_1.Container.set(nodeWebDriverUrl_1.NodeBasePathResolver, new nodeWebDriverUrl_1.NodeBasePathResolver());
175
+ hits = 0;
176
+ });
177
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
178
+ restore();
179
+ yield loopback.closeAll();
180
+ }));
181
+ function node(basePath) {
182
+ return __awaiter(this, void 0, void 0, function* () {
183
+ const app = (0, express_1.default)();
184
+ if (basePath !== undefined) {
185
+ app.get(nodeWebDriverUrl_1.WEBDRIVER_INFO_PATH, (req, res, next) => {
186
+ hits++;
187
+ (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => basePath)(req, res, next);
188
+ });
189
+ }
190
+ const server = yield loopback.serve(app);
191
+ return `http://127.0.0.1:${server.address().port}`;
192
+ });
193
+ }
194
+ it('the node says its base path, normalised as Appium does', () => __awaiter(void 0, void 0, void 0, function* () {
195
+ const origin = yield node('node/base/');
196
+ const res = yield (0, loopbackRequest_1.default)(origin).get(nodeWebDriverUrl_1.WEBDRIVER_INFO_PATH).expect(200);
197
+ (0, chai_1.expect)(res.body).to.deep.equal({ basePath: '/node/base' });
198
+ }));
199
+ it('uses the base path the node says, however the hub’s differs', () => __awaiter(void 0, void 0, void 0, function* () {
200
+ const origin = yield node('');
201
+ (0, chai_1.expect)(yield typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver).resolve(origin, '/wd/hub')).to.equal('');
202
+ }));
203
+ it('falls back to the hub’s own for a node that does not say (an older Xenon)', () => __awaiter(void 0, void 0, void 0, function* () {
204
+ const origin = yield node(undefined);
205
+ (0, chai_1.expect)(yield typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver).resolve(origin, 'wd/hub')).to.equal('/wd/hub');
206
+ }));
207
+ it('falls back to the hub’s own for a node that cannot be reached', () => __awaiter(void 0, void 0, void 0, function* () {
208
+ const origin = yield node('/x');
209
+ yield loopback.closeAll();
210
+ (0, chai_1.expect)(yield typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver).resolve(origin, '/wd/hub')).to.equal('/wd/hub');
211
+ }));
212
+ it('asks once for concurrent callers, and again only after the answer expires', () => __awaiter(void 0, void 0, void 0, function* () {
213
+ const origin = yield node('/node');
214
+ const resolver = typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver);
215
+ let now = 1000;
216
+ resolver.now = () => now;
217
+ const answers = yield Promise.all([
218
+ resolver.resolve(origin, '/wd/hub'),
219
+ resolver.resolve(origin, '/wd/hub'),
220
+ ]);
221
+ (0, chai_1.expect)(answers).to.deep.equal(['/node', '/node']);
222
+ (0, chai_1.expect)(hits).to.equal(1);
223
+ now += nodeWebDriverUrl_1.BASE_PATH_TTL_MS - 1;
224
+ yield resolver.resolve(origin, '/wd/hub');
225
+ (0, chai_1.expect)(hits).to.equal(1);
226
+ now += 2;
227
+ yield resolver.resolve(origin, '/wd/hub');
228
+ (0, chai_1.expect)(hits).to.equal(2);
229
+ }));
230
+ it('asks again soon after a fallback', () => __awaiter(void 0, void 0, void 0, function* () {
231
+ const origin = yield node(undefined);
232
+ const resolver = typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver);
233
+ let now = 1000;
234
+ resolver.now = () => now;
235
+ yield resolver.resolve(origin, '/wd/hub');
236
+ now += nodeWebDriverUrl_1.FALLBACK_TTL_MS + 1;
237
+ const lookUp = sinon_1.default.spy(resolver, 'lookUp');
238
+ yield resolver.resolve(origin, '/wd/hub');
239
+ (0, chai_1.expect)(lookUp.callCount).to.equal(1);
240
+ }));
241
+ it('nodeWebDriverUrl: a node’s origin plus its own base path', () => __awaiter(void 0, void 0, void 0, function* () {
242
+ const origin = yield node('/node');
243
+ (0, chai_1.expect)(yield (0, nodeWebDriverUrl_1.nodeWebDriverUrl)({ udid: 'u', host: `${origin}/` }, '/wd/hub')).to.equal(`${origin}/node`);
244
+ }));
245
+ it('nodeWebDriverUrl: a cloud provider’s own URL, with no lookup', () => __awaiter(void 0, void 0, void 0, function* () {
246
+ const url = yield (0, nodeWebDriverUrl_1.nodeWebDriverUrl)({ udid: 'u', host: 'https://cloud.example', cloud: 'pCloudy' }, '/wd/hub');
247
+ (0, chai_1.expect)(url).to.equal('https://cloud.example/wd/hub');
248
+ (0, chai_1.expect)(hits).to.equal(0);
249
+ }));
250
+ });