@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
@@ -49,7 +49,7 @@ require("reflect-metadata");
49
49
  const chai_1 = require("chai");
50
50
  const sinon_1 = __importDefault(require("sinon"));
51
51
  const express_1 = __importDefault(require("express"));
52
- const supertest_1 = __importDefault(require("supertest"));
52
+ const loopbackRequest_1 = __importDefault(require("../../helpers/loopbackRequest"));
53
53
  const sdk_leases_1 = require("../../../src/app/routers/sdk-leases");
54
54
  const LeaseService_1 = require("../../../src/services/lease/LeaseService");
55
55
  const leaseToken = __importStar(require("../../../src/services/lease/leaseToken"));
@@ -107,7 +107,10 @@ describe('lease token routes reveal nothing without the token', () => {
107
107
  },
108
108
  portLease: { deleteMany: sinon_1.default.stub().resolves({ count: 0 }) },
109
109
  };
110
- store = { updateDevice: sinon_1.default.stub().resolves() };
110
+ store = {
111
+ updateDevice: sinon_1.default.stub().resolves(),
112
+ releaseLeaseLock: sinon_1.default.stub().resolves(true),
113
+ };
111
114
  const svc = new LeaseService_1.LeaseService(db, store, {}, { nodePairAuth: () => __awaiter(void 0, void 0, void 0, function* () { return ({ accessKey: '', token: '' }); }) });
112
115
  const app = (0, express_1.default)();
113
116
  app.use(express_1.default.json());
@@ -136,8 +139,8 @@ describe('lease token routes reveal nothing without the token', () => {
136
139
  function call(op, id, token) {
137
140
  const url = `/xenon/api/sdk/leases/${id}`;
138
141
  if (op === 'release')
139
- return (0, supertest_1.default)(server).delete(url).set('x-xenon-lease-token', token);
140
- const r = (0, supertest_1.default)(server).post(`${url}/${op}`).set('x-xenon-lease-token', token);
142
+ return (0, loopbackRequest_1.default)(server).delete(url).set('x-xenon-lease-token', token);
143
+ const r = (0, loopbackRequest_1.default)(server).post(`${url}/${op}`).set('x-xenon-lease-token', token);
141
144
  return op === 'extend' ? r.send({ durationMs: 60000 }) : r;
142
145
  }
143
146
  const answer = (r) => ({
@@ -177,6 +180,7 @@ describe('lease token routes reveal nothing without the token', () => {
177
180
  (0, chai_1.expect)(db.lease.updateMany.called).to.equal(false);
178
181
  (0, chai_1.expect)(db.portLease.deleteMany.called).to.equal(false);
179
182
  (0, chai_1.expect)(store.updateDevice.called).to.equal(false);
183
+ (0, chai_1.expect)(store.releaseLeaseLock.called).to.equal(false);
180
184
  }));
181
185
  it(`the right token on an expired lease gets ${GONE[op]}`, () => __awaiter(void 0, void 0, void 0, function* () {
182
186
  (0, chai_1.expect)((yield call(op, 'lse_expired', TOKEN)).status).to.equal(GONE[op]);
@@ -189,9 +193,18 @@ describe('lease token routes reveal nothing without the token', () => {
189
193
  }));
190
194
  });
191
195
  }
192
- it('heartbeat: the right token on a lease past its expiry, not yet swept, gets 410', () => __awaiter(void 0, void 0, void 0, function* () {
193
- (0, chai_1.expect)((yield call('heartbeat', 'lse_lapsed', TOKEN)).status).to.equal(410);
194
- }));
196
+ // Neither may keep a lapsed lease alive: extend used to push its expiresAt
197
+ // out again and reset its heartbeat clock, so the sweeper never took it.
198
+ for (const op of ['heartbeat', 'extend']) {
199
+ it(`${op}: the right token on a lease past its expiry, not yet swept, gets 410 and changes nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
200
+ const r = yield call(op, 'lse_lapsed', TOKEN);
201
+ (0, chai_1.expect)({ status: r.status, error: r.body.error }).to.deep.equal({
202
+ status: 410,
203
+ error: 'gone',
204
+ });
205
+ (0, chai_1.expect)(db.lease.updateMany.called).to.equal(false);
206
+ }));
207
+ }
195
208
  it('an unknown id still costs one token comparison, against one fixed hash', () => __awaiter(void 0, void 0, void 0, function* () {
196
209
  const verify = sinon_1.default.spy(leaseToken, 'verifyToken');
197
210
  yield call('heartbeat', 'lse_nope', WRONG);
@@ -65,6 +65,7 @@ const logger_1 = require("../../../src/logger");
65
65
  const RequestLogService_1 = require("../../../src/services/RequestLogService");
66
66
  const EventBus_1 = require("../../../src/services/EventBus");
67
67
  const JwtKeyService_1 = require("../../../src/services/token/JwtKeyService");
68
+ const hubSessionToken_1 = require("../../../src/gateway/hubSessionToken");
68
69
  const prisma_1 = require("../../../src/prisma");
69
70
  const container_registration_1 = require("../../helpers/container-registration");
70
71
  // createSession end to end, from the capabilities a client sends to the
@@ -184,7 +185,7 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
184
185
  }));
185
186
  sinon_1.default.stub(pendingSessions, 'removePendingSession').resolves();
186
187
  sinon_1.default.stub(deviceService, 'updateDeviceProgress').resolves();
187
- sinon_1.default.stub(deviceService, 'updatedAllocatedDevice').resolves();
188
+ sinon_1.default.stub(deviceService, 'claimDeviceForSession').resolves(true);
188
189
  svc = new SessionLifecycleService_1.SessionLifecycleService();
189
190
  // Everything past finalizeSession's own bookkeeping is out of scope here;
190
191
  // what matters is the session response that becomes the Session row.
@@ -343,11 +344,15 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
343
344
  forwardedCaps = JSON.parse(JSON.stringify(c));
344
345
  return { protocol: 'W3C', value: ['sess-1', {}, 'W3C'] };
345
346
  }));
347
+ // The hub's token needs its signing key, which this spec has no use for.
348
+ sinon_1.default.stub(hubSessionToken_1.HubSessionTokenIssuer.prototype, 'createTokenFor').resolves('HUB-TOKEN');
346
349
  });
347
- it('forwards the token to a peer Xenon node, which runs the same check', () => __awaiter(void 0, void 0, void 0, function* () {
350
+ it('forwards neither the lease id nor its token to a peer Xenon node: the hub resolved the lease to its phone', () => __awaiter(void 0, void 0, void 0, function* () {
348
351
  device.nodeId = 'node-peer';
349
352
  yield create(sessionCaps({ leaseToken: TOKEN }));
350
- (0, chai_1.expect)(forwardedCaps.alwaysMatch['xe:options'].leaseToken).to.equal(TOKEN);
353
+ (0, chai_1.expect)(JSON.stringify(forwardedCaps)).to.not.include(TOKEN);
354
+ (0, chai_1.expect)(JSON.stringify(forwardedCaps)).to.not.include('lse_1');
355
+ (0, chai_1.expect)(forwardedCaps.alwaysMatch['appium:udid']).to.equal('u1');
351
356
  (0, chai_1.expect)(JSON.stringify(finalCaps)).to.not.include(TOKEN);
352
357
  (0, chai_1.expect)(JSON.stringify(pendingCopy)).to.not.include(TOKEN);
353
358
  }));
@@ -92,7 +92,10 @@ function harness(over) {
92
92
  if (over.maxBufferedBytes !== undefined)
93
93
  deps.maxBufferedBytes = over.maxBufferedBytes;
94
94
  (0, logcatWs_1.attachLogcatWs)(server, deps);
95
- yield new Promise((r) => server.listen(0, r));
95
+ // 127.0.0.1, where the client connects. A listener on every address can
96
+ // share its port with another process's 127.0.0.1 listener, which then
97
+ // answers the handshake instead ("Unexpected server response: 501").
98
+ yield new Promise((r) => server.listen(0, '127.0.0.1', r));
96
99
  const port = server.address().port;
97
100
  return {
98
101
  port,
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const chai_1 = require("chai");
16
+ const express_1 = __importDefault(require("express"));
17
+ const http_1 = __importDefault(require("http"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
+ // The drop-in for supertest's request(app) that listens on 127.0.0.1, where
20
+ // it connects, instead of on every address. See test/helpers/loopbackRequest.ts.
21
+ describe('loopbackRequest', () => {
22
+ function echoApp() {
23
+ const app = (0, express_1.default)();
24
+ app.use(express_1.default.json());
25
+ app.all('/echo', (req, res) => {
26
+ res.json({ method: req.method, query: req.query, body: req.body });
27
+ });
28
+ return app;
29
+ }
30
+ it('serves the app on 127.0.0.1, not on every address', () => __awaiter(void 0, void 0, void 0, function* () {
31
+ const server = http_1.default.createServer(echoApp());
32
+ let bound;
33
+ server.on('listening', () => {
34
+ bound = server.address();
35
+ });
36
+ const res = yield (0, loopbackRequest_1.default)(server).get('/echo');
37
+ (0, chai_1.expect)(res.status).to.equal(200);
38
+ (0, chai_1.expect)(bound === null || bound === void 0 ? void 0 : bound.address).to.equal('127.0.0.1');
39
+ }));
40
+ it('closes the server it started once the request is answered', () => __awaiter(void 0, void 0, void 0, function* () {
41
+ const server = http_1.default.createServer(echoApp());
42
+ yield (0, loopbackRequest_1.default)(server).get('/echo');
43
+ (0, chai_1.expect)(server.listening).to.equal(false);
44
+ }));
45
+ it('keeps what supertest does with a request: method, query, body', () => __awaiter(void 0, void 0, void 0, function* () {
46
+ const res = yield (0, loopbackRequest_1.default)(echoApp()).post('/echo').query({ a: '1' }).send({ b: 2 });
47
+ (0, chai_1.expect)(res.body).to.deep.equal({ method: 'POST', query: { a: '1' }, body: { b: 2 } });
48
+ }));
49
+ it('answers several requests sent to one server at once', () => __awaiter(void 0, void 0, void 0, function* () {
50
+ const server = http_1.default.createServer(echoApp());
51
+ const answers = yield Promise.all([
52
+ (0, loopbackRequest_1.default)(server).get('/echo').query({ n: '1' }),
53
+ (0, loopbackRequest_1.default)(server).get('/echo').query({ n: '2' }),
54
+ ]);
55
+ (0, chai_1.expect)(answers.map((r) => r.body.query.n)).to.deep.equal(['1', '2']);
56
+ }));
57
+ it('uses a server that is already listening, and leaves it open', () => __awaiter(void 0, void 0, void 0, function* () {
58
+ const server = http_1.default.createServer(echoApp());
59
+ yield new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
60
+ try {
61
+ const res = yield (0, loopbackRequest_1.default)(server).get('/echo');
62
+ (0, chai_1.expect)(res.status).to.equal(200);
63
+ (0, chai_1.expect)(server.listening).to.equal(true);
64
+ }
65
+ finally {
66
+ yield new Promise((resolve) => server.close(resolve));
67
+ }
68
+ }));
69
+ it('sends to a URL as given', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ const server = http_1.default.createServer(echoApp());
71
+ yield new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
72
+ try {
73
+ const { port } = server.address();
74
+ const res = yield (0, loopbackRequest_1.default)(`http://127.0.0.1:${port}`).get('/echo');
75
+ (0, chai_1.expect)(res.body.method).to.equal('GET');
76
+ }
77
+ finally {
78
+ yield new Promise((resolve) => server.close(resolve));
79
+ }
80
+ }));
81
+ });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const nodeCreateCaps_1 = require("../../src/services/session/nodeCreateCaps");
5
+ /**
6
+ * The capabilities a hub sends the node whose phone it allocated. The hub has
7
+ * checked the credentials, applied the team rule and resolved the lease; the
8
+ * node gets a request that stands on its own: that one phone, and nothing the
9
+ * node can't act on.
10
+ */
11
+ describe('capsForNode — the copy of a create a node gets', () => {
12
+ const sent = () => ({
13
+ alwaysMatch: {
14
+ platformName: 'Android',
15
+ 'appium:udids': 'phone-1,phone-2',
16
+ 'xe:options': { leaseId: 'lse_1', buildId: 'b-1', token: 'SECRET' },
17
+ },
18
+ firstMatch: [
19
+ { 'appium:udid': 'phone-1', 'appium:systemPort': 8201 },
20
+ { 'appium:udid': 'phone-2', 'xenon:options': { leaseId: 'lse_1', leaseToken: 'SECRET' } },
21
+ ],
22
+ });
23
+ it('names exactly the phone the hub allocated, once, in alwaysMatch', () => {
24
+ const copy = (0, nodeCreateCaps_1.capsForNode)(sent(), 'phone-1');
25
+ (0, chai_1.expect)(copy.alwaysMatch['appium:udid']).to.equal('phone-1');
26
+ (0, chai_1.expect)(copy.alwaysMatch['appium:udids']).to.equal(undefined);
27
+ for (const entry of copy.firstMatch) {
28
+ (0, chai_1.expect)(entry['appium:udid']).to.equal(undefined);
29
+ (0, chai_1.expect)(entry['appium:udids']).to.equal(undefined);
30
+ }
31
+ // What the hub allocated for it travels.
32
+ (0, chai_1.expect)(copy.firstMatch[0]['appium:systemPort']).to.equal(8201);
33
+ });
34
+ it('drops the lease id, which only the hub can resolve, from both namespaces', () => {
35
+ const copy = (0, nodeCreateCaps_1.capsForNode)(sent(), 'phone-1');
36
+ (0, chai_1.expect)(JSON.stringify(copy)).to.not.include('lse_1');
37
+ (0, chai_1.expect)(copy.alwaysMatch['xe:options']).to.deep.equal({ buildId: 'b-1' });
38
+ });
39
+ it('never carries a credential', () => {
40
+ (0, chai_1.expect)(JSON.stringify((0, nodeCreateCaps_1.capsForNode)(sent(), 'phone-1'))).to.not.include('SECRET');
41
+ });
42
+ it('does not modify the caps the hub keeps', () => {
43
+ const caps = sent();
44
+ const before = JSON.stringify(caps);
45
+ (0, nodeCreateCaps_1.capsForNode)(caps, 'phone-1');
46
+ (0, chai_1.expect)(JSON.stringify(caps)).to.equal(before);
47
+ });
48
+ it('copes with missing buckets', () => {
49
+ (0, chai_1.expect)((0, nodeCreateCaps_1.capsForNode)({ alwaysMatch: undefined }, 'p')).to.deep.equal({
50
+ alwaysMatch: { 'appium:udid': 'p' },
51
+ firstMatch: [{}],
52
+ });
53
+ });
54
+ });
@@ -0,0 +1,354 @@
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 fs_1 = __importDefault(require("fs"));
18
+ const http_1 = __importDefault(require("http"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const sinon_1 = __importDefault(require("sinon"));
22
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
23
+ const typedi_1 = require("typedi");
24
+ const plugin_1 = require("../../src/plugin");
25
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
26
+ const defaultGateway_1 = require("../../src/gateway/defaultGateway");
27
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
28
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
29
+ const PluginContext_1 = require("../../src/PluginContext");
30
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
31
+ const device_managers_1 = require("../../src/device-managers");
32
+ const device_store_1 = require("../../src/data-service/device-store");
33
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
34
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
35
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
36
+ const config_1 = require("../../src/config");
37
+ const container_registration_1 = require("../helpers/container-registration");
38
+ const scratch_database_1 = require("../helpers/scratch-database");
39
+ const appium_umbrella_1 = require("../helpers/appium-umbrella");
40
+ /**
41
+ * A Xenon node's side of a create the hub forwards, in Appium 3's own
42
+ * server(): Appium's real umbrella with Xenon's real plugin, the production
43
+ * node wiring (nodeGatewayOptions), the real SessionLifecycleService and a
44
+ * scratch SQLite database of the node's own.
45
+ *
46
+ * Each instance has its own database, so the node cannot check the client's
47
+ * key: the hub did. The hub's create carries a token it signed instead
48
+ * (x-xenon-hub-token, audience xenon-node-create), naming the verified owner,
49
+ * the phone and its node. The node checks it against the hub's JWKS, and it is
50
+ * the session's credential: it attributes the session and passes
51
+ * XENON_REQUIRE_SESSION_TOKEN.
52
+ *
53
+ * The hub here is only what the node talks to: an Appium server serving the
54
+ * hub's public keys. What the hub sends is hub-gateway-create-appium-server.spec.ts.
55
+ */
56
+ const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
57
+ const NODE_ID = 'node-1';
58
+ describe('a create the hub forwards, on the node, in Appium 3’s own server()', function () {
59
+ this.timeout(60000);
60
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
61
+ const servers = [];
62
+ let dirs;
63
+ let hubKeys;
64
+ let appiumLogs;
65
+ let restore;
66
+ let saved;
67
+ let authDisabledBefore;
68
+ let gateBefore;
69
+ let umbrella;
70
+ let hubUrl;
71
+ let nodeOrigin;
72
+ let nodeAuthDisabled;
73
+ function keyService() {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-node-create-keys-'));
76
+ dirs.push(dir);
77
+ const keys = new JwtKeyService_1.JwtKeyService();
78
+ yield keys.init(dir);
79
+ return keys;
80
+ });
81
+ }
82
+ before(() => __awaiter(this, void 0, void 0, function* () {
83
+ appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
84
+ dirs = [];
85
+ hubKeys = yield keyService();
86
+ }));
87
+ after(() => {
88
+ appiumLogs.restore();
89
+ for (const dir of dirs)
90
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
91
+ });
92
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
93
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager);
94
+ // The hub's keys sign here only because the test mints what the hub would send.
95
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
96
+ typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
97
+ typedi_1.Container.set(device_managers_1.XenonManager, {
98
+ getMaxSessionCount: () => undefined,
99
+ deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
100
+ });
101
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
102
+ saved = {
103
+ store: device_store_1.DeviceStoreFactory._deviceStore,
104
+ pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
105
+ context: Object.assign({}, context),
106
+ };
107
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
108
+ device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
109
+ authDisabledBefore = config_1.config.authDisabled;
110
+ config_1.config.authDisabled = false;
111
+ nodeAuthDisabled = false;
112
+ gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
113
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
114
+ yield scratch.db.pendingSession.deleteMany({});
115
+ yield scratch.db.device.deleteMany({});
116
+ yield boot();
117
+ }));
118
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
119
+ for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
120
+ SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
121
+ device_store_1.DeviceStoreFactory._deviceStore = saved.store;
122
+ device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
123
+ Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
124
+ config_1.config.authDisabled = authDisabledBefore;
125
+ if (gateBefore === undefined)
126
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
127
+ else
128
+ process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
129
+ sinon_1.default.restore();
130
+ restore();
131
+ for (const s of servers.splice(0)) {
132
+ s.closeAllConnections();
133
+ yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
134
+ }
135
+ }));
136
+ const port = (s) => s.address().port;
137
+ function boot() {
138
+ return __awaiter(this, void 0, void 0, function* () {
139
+ const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
140
+ // The hub: only its public keys matter to the node.
141
+ const hubServer = yield baseDriver.server({
142
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
143
+ port: 0,
144
+ hostname: '127.0.0.1',
145
+ basePath: '/wd/hub',
146
+ cliArgs: { basePath: '/wd/hub' },
147
+ serverUpdaters: [
148
+ (app) => {
149
+ app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
150
+ },
151
+ ],
152
+ });
153
+ servers.push(hubServer);
154
+ hubUrl = `http://127.0.0.1:${port(hubServer)}`;
155
+ umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
156
+ const nodeServer = yield baseDriver.server({
157
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
158
+ port: 0,
159
+ hostname: '127.0.0.1',
160
+ basePath: '/node',
161
+ cliArgs: { basePath: '/node' },
162
+ serverUpdaters: [
163
+ (app) => {
164
+ (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({
165
+ enabled: () => false,
166
+ authDisabled: () => nodeAuthDisabled,
167
+ logger: quiet,
168
+ }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
169
+ },
170
+ ],
171
+ });
172
+ servers.push(nodeServer);
173
+ nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
174
+ typedi_1.Container.get(PluginContext_1.PluginContext).setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1',
175
+ // Registers the node's LocalSession in SESSION_MANAGER, where its owner is read.
176
+ enableDashboard: true, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), port(nodeServer), NODE_ID, '/node');
177
+ for (const udid of ['phone-1', 'phone-2']) {
178
+ yield scratch.db.device.create({
179
+ data: {
180
+ udid,
181
+ host: nodeOrigin,
182
+ nodeId: NODE_ID,
183
+ platform: 'android',
184
+ name: udid,
185
+ sdk: '14',
186
+ busy: false,
187
+ userBlocked: false,
188
+ offline: false,
189
+ },
190
+ });
191
+ }
192
+ });
193
+ }
194
+ /** What the hub mints for alice's create on phone-1. */
195
+ const hubToken = (grant = {}) => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor(Object.assign({ userId: 'alice', udid: 'phone-1', host: nodeOrigin }, grant));
196
+ /** The capabilities as the hub forwards them: no credentials, the phone pinned. */
197
+ const forwarded = (alwaysMatch = {}) => ({
198
+ capabilities: {
199
+ alwaysMatch: Object.assign({ platformName: 'Android', 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION, 'appium:newCommandTimeout': 0, 'appium:udid': 'phone-1',
200
+ // No video: it would start Xenon's real capture pipeline for the phone.
201
+ 'xe:options': { recordVideo: false } }, alwaysMatch),
202
+ firstMatch: [{}],
203
+ },
204
+ });
205
+ const create = (token_1, ...args_1) => __awaiter(this, [token_1, ...args_1], void 0, function* (token, body = forwarded()) {
206
+ const req = (0, loopbackRequest_1.default)(nodeOrigin).post('/node/session');
207
+ if (token !== undefined)
208
+ req.set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
209
+ return req.send(body);
210
+ });
211
+ const row = (udid) => scratch.db.device.findFirst({ where: { udid } });
212
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
213
+ function expectNothingCreated() {
214
+ return __awaiter(this, void 0, void 0, function* () {
215
+ yield settle();
216
+ (0, chai_1.expect)(Object.keys(umbrella.sessions), 'driver sessions').to.deep.equal([]);
217
+ (0, chai_1.expect)(yield row('phone-1')).to.include({ busy: false, session_id: null });
218
+ (0, chai_1.expect)(yield row('phone-2')).to.include({ busy: false, session_id: null });
219
+ (0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
220
+ });
221
+ }
222
+ it('attributes the session to the owner the hub’s token names', () => __awaiter(this, void 0, void 0, function* () {
223
+ const res = yield create(yield hubToken());
224
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
225
+ const sessionId = res.body.value.sessionId;
226
+ (0, chai_1.expect)(Object.keys(umbrella.sessions)).to.deep.equal([sessionId]);
227
+ const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
228
+ (0, chai_1.expect)(session.userId).to.equal('alice');
229
+ (0, chai_1.expect)(session.apiKeyId).to.equal(null);
230
+ (0, chai_1.expect)(yield row('phone-1')).to.include({ busy: true, session_id: sessionId });
231
+ (0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
232
+ }));
233
+ it('an unattributed create from the hub stays unattributed', () => __awaiter(this, void 0, void 0, function* () {
234
+ const res = yield create(yield hubToken({ userId: null }));
235
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
236
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(res.body.value.sessionId).userId).to.equal(null);
237
+ }));
238
+ describe('with XENON_REQUIRE_SESSION_TOKEN on', () => {
239
+ beforeEach(() => {
240
+ process.env.XENON_REQUIRE_SESSION_TOKEN = '1';
241
+ });
242
+ it('the hub’s token admits the session', () => __awaiter(this, void 0, void 0, function* () {
243
+ const res = yield create(yield hubToken());
244
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
245
+ }));
246
+ it('a create with no credentials at all is refused, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
247
+ // Before the gate is asked: on a node, only the hub's creates are taken.
248
+ const res = yield create(undefined);
249
+ (0, chai_1.expect)(res.status).to.equal(500);
250
+ (0, chai_1.expect)(res.body.value.error).to.equal('session not created');
251
+ (0, chai_1.expect)(res.body.value.message).to.include('through the hub');
252
+ yield expectNothingCreated();
253
+ }));
254
+ it('client credentials the node cannot check are neither needed nor passed on', () => __awaiter(this, void 0, void 0, function* () {
255
+ const res = yield create(yield hubToken(), forwarded({
256
+ 'xe:options': { accessKey: 'ak-unknown', token: 'CLIENT-SECRET', recordVideo: false },
257
+ }));
258
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
259
+ const session = SessionManager_1.SESSION_MANAGER.getSession(res.body.value.sessionId);
260
+ (0, chai_1.expect)(session.userId).to.equal('alice');
261
+ const inner = umbrella.sessions[res.body.value.sessionId];
262
+ (0, chai_1.expect)(JSON.stringify(inner.originalCaps)).to.not.include('CLIENT-SECRET');
263
+ }));
264
+ });
265
+ describe('a token that is not the hub’s grant for this create', () => {
266
+ it('is refused when forged, expired or a command token, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
267
+ const forger = yield keyService();
268
+ const claims = { sub: 'alice', udid: 'phone-1', host: nodeOrigin };
269
+ const tokens = [
270
+ yield forger.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
271
+ yield hubKeys.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: -3600 }),
272
+ yield hubKeys.sign({ sid: 's-1' }, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE, ttlSeconds: 300 }),
273
+ 'not-a-jwt',
274
+ ];
275
+ for (const token of tokens) {
276
+ const res = yield create(token);
277
+ (0, chai_1.expect)(res.status, token).to.equal(400);
278
+ (0, chai_1.expect)(res.body.value.error).to.equal('invalid argument');
279
+ (0, chai_1.expect)(res.body.value.message).to.include('session rejected');
280
+ }
281
+ yield expectNothingCreated();
282
+ }));
283
+ it('is refused the second time: a create token is taken once', () => __awaiter(this, void 0, void 0, function* () {
284
+ const token = yield hubToken();
285
+ const first = yield create(token);
286
+ (0, chai_1.expect)(first.status, JSON.stringify(first.body)).to.equal(200);
287
+ const ended = yield (0, loopbackRequest_1.default)(nodeOrigin).delete(`/node/session/${first.body.value.sessionId}`);
288
+ (0, chai_1.expect)(ended.status, JSON.stringify(ended.body)).to.equal(200);
289
+ const again = yield create(token);
290
+ (0, chai_1.expect)(again.status).to.equal(400);
291
+ (0, chai_1.expect)(again.body.value.error).to.equal('invalid argument');
292
+ (0, chai_1.expect)(again.body.value.message).to.include('session rejected');
293
+ yield expectNothingCreated();
294
+ }));
295
+ it('is refused when the capabilities name another phone than the grant', () => __awaiter(this, void 0, void 0, function* () {
296
+ const res = yield create(yield hubToken(), forwarded({ 'appium:udid': 'phone-2' }));
297
+ (0, chai_1.expect)(res.status).to.equal(400);
298
+ (0, chai_1.expect)(res.body.value.message).to.include('session rejected');
299
+ yield expectNothingCreated();
300
+ }));
301
+ it('is refused, and the phone given back, when it names another node’s phone', () => __awaiter(this, void 0, void 0, function* () {
302
+ const res = yield create(yield hubToken({ host: 'http://10.9.9.9:4725' }));
303
+ (0, chai_1.expect)(res.status).to.equal(400);
304
+ (0, chai_1.expect)(res.body.value.message).to.include('session rejected');
305
+ yield expectNothingCreated();
306
+ }));
307
+ it('cannot be checked while the hub’s keys cannot be fetched: 503, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
308
+ const token = yield hubToken();
309
+ const hub = servers[0];
310
+ hub.closeAllConnections();
311
+ yield new Promise((resolve) => http_1.default.Server.prototype.close.call(hub, () => resolve()));
312
+ const res = yield create(token);
313
+ (0, chai_1.expect)(res.status).to.equal(503);
314
+ (0, chai_1.expect)(res.body.value.error).to.equal('unknown error');
315
+ yield expectNothingCreated();
316
+ }));
317
+ });
318
+ describe('a create that did not come from the hub', () => {
319
+ const direct = () => create(undefined, forwarded({
320
+ 'xe:options': { accessKey: 'ak-node', token: 'node-secret', recordVideo: false },
321
+ }));
322
+ it('is refused, saying to create sessions through the hub, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
323
+ const res = yield direct();
324
+ (0, chai_1.expect)(res.status).to.equal(500);
325
+ (0, chai_1.expect)(res.body.value.error).to.equal('session not created');
326
+ (0, chai_1.expect)(res.body.value.message).to.include('through the hub');
327
+ (0, chai_1.expect)(res.body.value.message).to.include(hubUrl);
328
+ yield expectNothingCreated();
329
+ }));
330
+ it('is refused when it reaches the plugin without the gateway too', () => __awaiter(this, void 0, void 0, function* () {
331
+ const lifecycle = new SessionLifecycleService_1.SessionLifecycleService();
332
+ const caps = forwarded().capabilities;
333
+ let refused;
334
+ yield lifecycle
335
+ .createSession(() => ({}), umbrella, caps, { localOnly: true })
336
+ .catch((error) => (refused = error));
337
+ (0, chai_1.expect)(refused === null || refused === void 0 ? void 0 : refused.message).to.include('through the hub');
338
+ yield expectNothingCreated();
339
+ }));
340
+ it('still works on a node with auth disabled, as it did (local development)', () => __awaiter(this, void 0, void 0, function* () {
341
+ nodeAuthDisabled = true;
342
+ config_1.config.authDisabled = true;
343
+ const res = yield direct();
344
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
345
+ }));
346
+ });
347
+ it('with the node’s auth disabled, a token is not checked and the session is unattributed', () => __awaiter(this, void 0, void 0, function* () {
348
+ nodeAuthDisabled = true;
349
+ config_1.config.authDisabled = true;
350
+ const res = yield create('not-a-jwt');
351
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
352
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(res.body.value.sessionId).userId).to.equal(null);
353
+ }));
354
+ });