@xenon-device-management/xenon 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +3 -4
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
  5. package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
  69. package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
  70. package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +11 -104
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/routers/nodePhoneControl.js +301 -0
  84. package/lib/src/app/ws/h264StreamWs.js +43 -12
  85. package/lib/src/app/ws/logcatWs.js +48 -14
  86. package/lib/src/app/ws/nodeSocketRelay.js +167 -0
  87. package/lib/src/app/ws/upgradeRouter.js +215 -0
  88. package/lib/src/dashboard/event-manager.js +7 -6
  89. package/lib/src/data-service/device-service.js +152 -49
  90. package/lib/src/data-service/device-store.js +92 -10
  91. package/lib/src/data-service/deviceClaims.js +116 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  93. package/lib/src/data-service/prisma-store.js +102 -31
  94. package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
  95. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  96. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  97. package/lib/src/device-managers/NodeDevices.js +7 -2
  98. package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
  99. package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
  100. package/lib/src/device-managers/android/previewHold.js +2 -2
  101. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  102. package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
  103. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  104. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  105. package/lib/src/device-utils.js +78 -24
  106. package/lib/src/gateway/createHandoff.js +73 -0
  107. package/lib/src/gateway/defaultGateway.js +80 -0
  108. package/lib/src/gateway/forwardToNode.js +147 -0
  109. package/lib/src/gateway/hubSessionToken.js +356 -0
  110. package/lib/src/gateway/internalCall.js +90 -0
  111. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  112. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  113. package/lib/src/gateway/sessionCreate.js +136 -0
  114. package/lib/src/gateway/sessionGateway.js +201 -0
  115. package/lib/src/gateway/sessionLocator.js +100 -0
  116. package/lib/src/generated/client/edge.js +7 -4
  117. package/lib/src/generated/client/index-browser.js +4 -1
  118. package/lib/src/generated/client/index.d.ts +112 -1
  119. package/lib/src/generated/client/index.js +7 -4
  120. package/lib/src/generated/client/package.json +1 -1
  121. package/lib/src/generated/client/schema.prisma +9 -0
  122. package/lib/src/generated/client/wasm.js +4 -1
  123. package/lib/src/helpers/index.js +5 -1
  124. package/lib/src/logger.js +5 -0
  125. package/lib/src/middleware/authMiddleware.js +80 -0
  126. package/lib/src/middleware/commandAuth.js +31 -17
  127. package/lib/src/middleware/csrfMiddleware.js +3 -2
  128. package/lib/src/middleware/deviceAccessGuard.js +4 -1
  129. package/lib/src/middleware/sessionListingFilter.js +153 -0
  130. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  131. package/lib/src/plugin.js +27 -1
  132. package/lib/src/prisma.js +2 -1
  133. package/lib/src/services/DeviceReconciler.js +5 -2
  134. package/lib/src/services/OrphanSweeper.js +12 -19
  135. package/lib/src/services/PortAllocator.js +75 -16
  136. package/lib/src/services/ServerManager.js +63 -30
  137. package/lib/src/services/SessionLifecycleService.js +335 -76
  138. package/lib/src/services/ShutdownCoordinator.js +7 -1
  139. package/lib/src/services/SocketServer.js +11 -2
  140. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  141. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  142. package/lib/src/services/device-access/ticketActorAccess.js +3 -1
  143. package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
  144. package/lib/src/services/lease/LeaseService.js +19 -5
  145. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  146. package/lib/src/services/session/sessionCredentials.js +7 -59
  147. package/lib/src/sessions/CloudSession.js +12 -0
  148. package/lib/src/sessions/LocalSession.js +52 -26
  149. package/lib/src/sessions/RemoteSession.js +116 -9
  150. package/lib/src/sessions/SessionManager.js +49 -6
  151. package/lib/src/sessions/appiumUmbrella.js +32 -0
  152. package/lib/test/helpers/appium-umbrella.js +84 -0
  153. package/lib/test/helpers/fake-device-table.js +97 -0
  154. package/lib/test/helpers/loki-stores.js +41 -0
  155. package/lib/test/helpers/loopbackRequest.js +77 -0
  156. package/lib/test/helpers/own-node-id.js +29 -0
  157. package/lib/test/helpers/scratch-database.js +100 -0
  158. package/lib/test/helpers/scratch-port-leases.js +119 -0
  159. package/lib/test/helpers/test-container.js +35 -27
  160. package/lib/test/integration/androidDevices.spec.js +5 -4
  161. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  162. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  163. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  164. package/lib/test/setup/scratch-default-database.js +32 -0
  165. package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
  166. package/lib/test/unit/DeviceModel.spec.js +9 -9
  167. package/lib/test/unit/HealingCascade.test.js +5 -1
  168. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  169. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  170. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  171. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  172. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  173. package/lib/test/unit/android-h264-service.spec.js +24 -0
  174. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  175. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  176. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  177. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  178. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  179. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  180. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  181. package/lib/test/unit/authMiddleware.test.js +3 -0
  182. package/lib/test/unit/bug-report/route.spec.js +9 -9
  183. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  184. package/lib/test/unit/clipboard-route.spec.js +22 -8
  185. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  186. package/lib/test/unit/command-auth.spec.js +31 -31
  187. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  188. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  189. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  190. package/lib/test/unit/create-error-message.spec.js +43 -0
  191. package/lib/test/unit/create-handoff.spec.js +86 -0
  192. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  193. package/lib/test/unit/device-access-guard.spec.js +66 -28
  194. package/lib/test/unit/device-service.spec.js +4 -0
  195. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  196. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  197. package/lib/test/unit/device-team-guard.spec.js +20 -20
  198. package/lib/test/unit/device-utils.spec.js +8 -0
  199. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  200. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  201. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  202. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  203. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  204. package/lib/test/unit/helpers.spec.js +27 -0
  205. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  206. package/lib/test/unit/hub-create-once.spec.js +190 -0
  207. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  208. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  209. package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
  210. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  211. package/lib/test/unit/hub-node-race.spec.js +426 -0
  212. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  213. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  214. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  215. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  216. package/lib/test/unit/hub-session-token.spec.js +265 -0
  217. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  218. package/lib/test/unit/install-repository-app-team.spec.js +6 -2
  219. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  220. package/lib/test/unit/internal-call.spec.js +181 -0
  221. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  222. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  223. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  224. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  225. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  226. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  227. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  228. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  229. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
  230. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  231. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  232. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  233. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  234. package/lib/test/unit/logcat-ws.spec.js +12 -1
  235. package/lib/test/unit/loopback-request.spec.js +81 -0
  236. package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
  237. package/lib/test/unit/node-create-caps.spec.js +54 -0
  238. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  239. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  240. package/lib/test/unit/node-phone-control.spec.js +131 -0
  241. package/lib/test/unit/node-socket-relay.spec.js +357 -0
  242. package/lib/test/unit/omni-vision.spec.js +7 -3
  243. package/lib/test/unit/plugin.spec.js +45 -4
  244. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  245. package/lib/test/unit/profile-router.test.js +11 -11
  246. package/lib/test/unit/queue-requester.spec.js +5 -0
  247. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  248. package/lib/test/unit/queue-visibility.spec.js +1 -1
  249. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  250. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  251. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  252. package/lib/test/unit/recording-store.spec.js +9 -3
  253. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  254. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  255. package/lib/test/unit/remote-session-create.spec.js +130 -0
  256. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  257. package/lib/test/unit/reset-link.test.js +14 -14
  258. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  259. package/lib/test/unit/session-assets.spec.js +9 -9
  260. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  261. package/lib/test/unit/session-create-layer.spec.js +255 -0
  262. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  263. package/lib/test/unit/session-credentials.spec.js +0 -47
  264. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  265. package/lib/test/unit/session-locator.spec.js +250 -0
  266. package/lib/test/unit/session-node-calls.spec.js +174 -0
  267. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  268. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  269. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  270. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  271. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  272. package/lib/test/unit/stream-leave-route.spec.js +19 -7
  273. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  274. package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
  275. package/lib/test/unit/stream-stop-recording.spec.js +17 -5
  276. package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
  277. package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
  278. package/lib/test/unit/upgrade-router.spec.js +446 -0
  279. package/lib/test/unit/users-router.test.js +11 -8
  280. package/lib/test/unit/verify-credential.spec.js +4 -1
  281. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  282. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  283. package/lib/tsconfig.tsbuildinfo +1 -1
  284. package/package.json +3 -4
  285. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  286. package/prisma/schema.prisma +9 -0
  287. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  288. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  289. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -0,0 +1,356 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
+ return new (P || (P = Promise))(function (resolve, reject) {
44
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
45
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
46
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
47
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
48
+ });
49
+ };
50
+ var __importDefault = (this && this.__importDefault) || function (mod) {
51
+ return (mod && mod.__esModule) ? mod : { "default": mod };
52
+ };
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.HubSessionTokenVerifier = exports.HubTokenUnavailableError = exports.HubSessionTokenIssuer = exports.HUB_CONTROL_TTL_SECONDS = exports.HUB_CONTROL_AUDIENCE = exports.HUB_CREATE_TTL_SECONDS = exports.HUB_CREATE_AUDIENCE = exports.HUB_TOKEN_TTL_SECONDS = exports.HUB_TOKEN_AUDIENCE = exports.HUB_TOKEN_HEADER = void 0;
55
+ exports.hubJwksUrl = hubJwksUrl;
56
+ const crypto_1 = require("crypto");
57
+ const typedi_1 = require("typedi");
58
+ const jose = __importStar(require("jose"));
59
+ const logger_1 = __importDefault(require("../logger"));
60
+ const JwtKeyService_1 = require("../services/token/JwtKeyService");
61
+ const singleUseLedger_1 = require("../services/token/singleUseLedger");
62
+ /**
63
+ * The credential a hub's calls to a node carry.
64
+ *
65
+ * Auth is enforced at the hub: it checks the client's credentials, then
66
+ * forwards the command to the node that runs the session. Each instance has
67
+ * its own database, so the node cannot check the client's key itself. It
68
+ * trusts the hub instead: every call the hub makes about a session (forwarded
69
+ * commands, and its own screenshot, source, recording and heartbeat calls)
70
+ * carries a short-lived RS256 token the hub signs for that one session. The
71
+ * node verifies it against the hub's public keys (`/xenon/api/auth/jwks.json`)
72
+ * when its own per-command auth is on.
73
+ *
74
+ * The audience is `xenon-node`, which /auth/token will not mint, and the token
75
+ * names its session (`sid`), so a token for one session opens no other.
76
+ *
77
+ * A create has no session yet, so it carries a token of its own kind:
78
+ * audience `xenon-node-create`, naming the owner the hub verified (`sub`,
79
+ * absent when the create is unattributed), the phone the hub allocated
80
+ * (`udid`) and that phone's node (`host`, the phone row's host). A separate
81
+ * audience rather than a claim on the `xenon-node` token, so neither kind can
82
+ * be taken for the other whatever claims a verifier reads: jose refuses the
83
+ * other kind's audience before any claim is looked at. The node treats a valid
84
+ * one as the session's credential (sessionCreate.ts).
85
+ *
86
+ * A hub's own device-control call to a node's phone (`/xenon/api/control/
87
+ * <udid>/tap`, swipe, text, keyevent, touchAndHold, which the hub forwards to
88
+ * the phone's node) carries a third kind: audience `xenon-node-control`, one
89
+ * minute, naming the hub user it acts for (`sub`), whether they are an admin
90
+ * (`adm`), the phone (`udid`) and its node (`host`). The hub checked the
91
+ * caller, the team rule and ownership before forwarding. The node accepts it
92
+ * only for `/control` on that phone (authMiddleware), then runs its own
93
+ * ownership guard with that user.
94
+ *
95
+ * A create token is single-use. The hub sends a create once (it is not safe
96
+ * to repeat), and each token has its own id (`jti`), which the node's
97
+ * verifier remembers until the token expires: the same token again is
98
+ * refused. A token with no id, from a hub before this rule, is accepted as
99
+ * before, without that check.
100
+ */
101
+ exports.HUB_TOKEN_HEADER = 'x-xenon-hub-token';
102
+ exports.HUB_TOKEN_AUDIENCE = 'xenon-node';
103
+ exports.HUB_TOKEN_TTL_SECONDS = 300;
104
+ exports.HUB_CREATE_AUDIENCE = 'xenon-node-create';
105
+ /**
106
+ * Long enough for the create to reach the node, which checks the token as the
107
+ * request arrives; short enough that a copied token is soon worthless. It
108
+ * opens one phone, on one node, once.
109
+ */
110
+ exports.HUB_CREATE_TTL_SECONDS = 120;
111
+ exports.HUB_CONTROL_AUDIENCE = 'xenon-node-control';
112
+ /** A forwarded tap is sent at once; a minute covers clock skew and a slow node. */
113
+ exports.HUB_CONTROL_TTL_SECONDS = 60;
114
+ /** A cached token is re-minted once it has less than this left. */
115
+ const RENEW_BEFORE_EXPIRY_MS = 60000;
116
+ const MAX_CACHED_TOKENS = 1000;
117
+ /** Clock skew tolerated between hub and node, as JwtKeyService.verify allows. */
118
+ const CLOCK_TOLERANCE_SECONDS = 60;
119
+ /** Hub side: mints a session's token (reused while it lasts), a create's and a control call's (fresh each time). */
120
+ let HubSessionTokenIssuer = class HubSessionTokenIssuer {
121
+ constructor() {
122
+ this.logger = logger_1.default.scope('HubSessionToken');
123
+ this.cache = new Map();
124
+ this.signer = () => typedi_1.Container.get(JwtKeyService_1.JwtKeyService);
125
+ this.warned = false;
126
+ }
127
+ /** For tests: sign with this instead of the server's JwtKeyService. */
128
+ useSigner(signer) {
129
+ this.signer = () => signer;
130
+ this.cache.clear();
131
+ }
132
+ /**
133
+ * The token for a session, or null when this server cannot sign (its JWT
134
+ * key failed to load). A node with per-command auth off needs none, so the
135
+ * call still goes out; one with it on refuses it, and the warning says why.
136
+ */
137
+ tokenFor(sessionId) {
138
+ return __awaiter(this, void 0, void 0, function* () {
139
+ if (!sessionId)
140
+ return null;
141
+ const now = Date.now();
142
+ const hit = this.cache.get(sessionId);
143
+ if (hit && hit.renewAt > now)
144
+ return hit.token;
145
+ let token;
146
+ try {
147
+ token = yield this.signer().sign({ sid: sessionId }, { audience: exports.HUB_TOKEN_AUDIENCE, ttlSeconds: exports.HUB_TOKEN_TTL_SECONDS });
148
+ }
149
+ catch (err) {
150
+ this.warnCannotSign(err);
151
+ return null;
152
+ }
153
+ this.cache.delete(sessionId);
154
+ if (this.cache.size >= MAX_CACHED_TOKENS) {
155
+ const oldest = this.cache.keys().next().value;
156
+ this.cache.delete(oldest);
157
+ }
158
+ this.cache.set(sessionId, {
159
+ token,
160
+ renewAt: now + exports.HUB_TOKEN_TTL_SECONDS * 1000 - RENEW_BEFORE_EXPIRY_MS,
161
+ });
162
+ return token;
163
+ });
164
+ }
165
+ forget(sessionId) {
166
+ this.cache.delete(sessionId);
167
+ }
168
+ /**
169
+ * The token for a create the hub forwards to the phone's node, or null when
170
+ * this server cannot sign. Minted fresh for each create, never cached, with
171
+ * an id of its own: the node takes it once.
172
+ */
173
+ createTokenFor(grant) {
174
+ return __awaiter(this, void 0, void 0, function* () {
175
+ const claims = { udid: grant.udid, host: grant.host };
176
+ if (grant.userId)
177
+ claims.sub = grant.userId;
178
+ try {
179
+ return yield this.signer().sign(claims, {
180
+ audience: exports.HUB_CREATE_AUDIENCE,
181
+ ttlSeconds: exports.HUB_CREATE_TTL_SECONDS,
182
+ jti: (0, crypto_1.randomUUID)(),
183
+ });
184
+ }
185
+ catch (err) {
186
+ this.warnCannotSign(err);
187
+ return null;
188
+ }
189
+ });
190
+ }
191
+ /**
192
+ * The token for a device-control call the hub forwards to the phone's
193
+ * node, or null when this server cannot sign. Fresh for each call.
194
+ */
195
+ controlTokenFor(grant) {
196
+ return __awaiter(this, void 0, void 0, function* () {
197
+ try {
198
+ return yield this.signer().sign({ sub: grant.userId, adm: grant.isAdmin, udid: grant.udid, host: grant.host }, { audience: exports.HUB_CONTROL_AUDIENCE, ttlSeconds: exports.HUB_CONTROL_TTL_SECONDS });
199
+ }
200
+ catch (err) {
201
+ this.warnCannotSign(err);
202
+ return null;
203
+ }
204
+ });
205
+ }
206
+ warnCannotSign(err) {
207
+ var _a;
208
+ if (this.warned)
209
+ return;
210
+ this.warned = true;
211
+ this.logger.warn(`Cannot sign session tokens for nodes (${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}). Calls to nodes go ` +
212
+ 'without one, and a node with XENON_REQUIRE_COMMAND_AUTH or ' +
213
+ 'XENON_REQUIRE_SESSION_TOKEN on will refuse them.');
214
+ }
215
+ };
216
+ exports.HubSessionTokenIssuer = HubSessionTokenIssuer;
217
+ exports.HubSessionTokenIssuer = HubSessionTokenIssuer = __decorate([
218
+ (0, typedi_1.Service)()
219
+ ], HubSessionTokenIssuer);
220
+ /** The hub's public keys, which JwtKeyService serves without a login. */
221
+ function hubJwksUrl(hub) {
222
+ return new URL('/xenon/api/auth/jwks.json', hub);
223
+ }
224
+ /** The node could not check a hub token (the hub's keys could not be fetched). */
225
+ class HubTokenUnavailableError extends Error {
226
+ }
227
+ exports.HubTokenUnavailableError = HubTokenUnavailableError;
228
+ // jose's codes for a token that was checked and is not good: forged, expired,
229
+ // for another audience, malformed, or signed by a key the hub does not have.
230
+ // Anything else (the key set could not be fetched, or was not a key set) means
231
+ // the check did not run.
232
+ const VERDICT_CODES = new Set([
233
+ 'ERR_JWKS_NO_MATCHING_KEY',
234
+ 'ERR_JWKS_MULTIPLE_MATCHING_KEYS',
235
+ 'ERR_JOSE_ALG_NOT_ALLOWED',
236
+ 'ERR_JOSE_NOT_SUPPORTED',
237
+ ]);
238
+ function isVerdict(err) {
239
+ const code = err === null || err === void 0 ? void 0 : err.code;
240
+ if (typeof code !== 'string')
241
+ return false;
242
+ return code.startsWith('ERR_JWT_') || code.startsWith('ERR_JWS_') || VERDICT_CODES.has(code);
243
+ }
244
+ /**
245
+ * Node side: is this a token the hub signed for this session?
246
+ *
247
+ * Resolves false for any token that is not (forged, expired, another
248
+ * session's, another audience's), and throws HubTokenUnavailableError when
249
+ * the hub's keys cannot be fetched, so the caller can answer 503 rather than
250
+ * pretend the token was checked. jose caches the key set, refetching it when
251
+ * a token names a key it does not have (a hub that rotated its key).
252
+ */
253
+ class HubSessionTokenVerifier {
254
+ constructor(hubUrl, keys) {
255
+ /** The create tokens already taken, by id, until each one expires. */
256
+ this.usedCreates = new singleUseLedger_1.SingleUseLedger(exports.HUB_CREATE_TTL_SECONDS);
257
+ this.keys =
258
+ keys !== null && keys !== void 0 ? keys : jose.createRemoteJWKSet(hubJwksUrl(hubUrl), {
259
+ timeoutDuration: 5000,
260
+ cooldownDuration: 30000,
261
+ cacheMaxAge: 10 * 60000,
262
+ });
263
+ }
264
+ verify(token, sessionId) {
265
+ return __awaiter(this, void 0, void 0, function* () {
266
+ var _a;
267
+ try {
268
+ const { payload } = yield jose.jwtVerify(token, this.keys, {
269
+ audience: exports.HUB_TOKEN_AUDIENCE,
270
+ algorithms: ['RS256'],
271
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
272
+ });
273
+ return typeof payload.sid === 'string' && payload.sid === sessionId;
274
+ }
275
+ catch (err) {
276
+ if (isVerdict(err))
277
+ return false;
278
+ throw new HubTokenUnavailableError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
279
+ }
280
+ });
281
+ }
282
+ /**
283
+ * The grant in a hub's create token, or null for any token that is not one
284
+ * (forged, expired, another audience's, missing the phone or its node, or
285
+ * already taken: a create token is taken once). Throws
286
+ * HubTokenUnavailableError when the hub's keys cannot be fetched.
287
+ */
288
+ verifyCreate(token) {
289
+ return __awaiter(this, void 0, void 0, function* () {
290
+ var _a;
291
+ let payload;
292
+ try {
293
+ ({ payload } = yield jose.jwtVerify(token, this.keys, {
294
+ audience: exports.HUB_CREATE_AUDIENCE,
295
+ algorithms: ['RS256'],
296
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
297
+ }));
298
+ }
299
+ catch (err) {
300
+ if (isVerdict(err))
301
+ return null;
302
+ throw new HubTokenUnavailableError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
303
+ }
304
+ const nonEmpty = (value) => typeof value === 'string' && value.length > 0;
305
+ if (!nonEmpty(payload.udid) || !nonEmpty(payload.host))
306
+ return null;
307
+ if (nonEmpty(payload.jti)) {
308
+ try {
309
+ this.usedCreates.consume(payload);
310
+ }
311
+ catch (_b) {
312
+ return null;
313
+ }
314
+ }
315
+ return {
316
+ userId: nonEmpty(payload.sub) ? payload.sub : null,
317
+ udid: payload.udid,
318
+ host: payload.host,
319
+ };
320
+ });
321
+ }
322
+ /**
323
+ * The grant in a hub's control token, or null for any token that is not
324
+ * one (forged, expired, another audience's, missing the user, the phone or
325
+ * its node). Throws HubTokenUnavailableError when the hub's keys cannot be
326
+ * fetched.
327
+ */
328
+ verifyControl(token) {
329
+ return __awaiter(this, void 0, void 0, function* () {
330
+ var _a;
331
+ let payload;
332
+ try {
333
+ ({ payload } = yield jose.jwtVerify(token, this.keys, {
334
+ audience: exports.HUB_CONTROL_AUDIENCE,
335
+ algorithms: ['RS256'],
336
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
337
+ }));
338
+ }
339
+ catch (err) {
340
+ if (isVerdict(err))
341
+ return null;
342
+ throw new HubTokenUnavailableError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
343
+ }
344
+ const nonEmpty = (value) => typeof value === 'string' && value.length > 0;
345
+ if (!nonEmpty(payload.sub) || !nonEmpty(payload.udid) || !nonEmpty(payload.host))
346
+ return null;
347
+ return {
348
+ userId: payload.sub,
349
+ isAdmin: payload.adm === true,
350
+ udid: payload.udid,
351
+ host: payload.host,
352
+ };
353
+ });
354
+ }
355
+ }
356
+ exports.HubSessionTokenVerifier = HubSessionTokenVerifier;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INTERNAL_CALL_MARKER = exports.INTERNAL_CALL_HEADER = void 0;
4
+ exports.internalCallHeaders = internalCallHeaders;
5
+ exports.hasInternalCallSecret = hasInternalCallSecret;
6
+ exports.isInternalCall = isInternalCall;
7
+ exports.internalCallBaseUrl = internalCallBaseUrl;
8
+ exports.createInternalCallLayer = createInternalCallLayer;
9
+ const crypto_1 = require("crypto");
10
+ const appiumBasePath_1 = require("../app/appiumBasePath");
11
+ /**
12
+ * Xenon's own loopback calls to its Appium server.
13
+ *
14
+ * A LocalSession asks its own server for a few things over HTTP (a page
15
+ * source or a screen recording when the in-process driver call fails). Those
16
+ * go to `<basePath>/wd-internal/session/<id>/...`, carrying this header with
17
+ * a secret made once per process. The session gateway recognises such a call
18
+ * only when both are present: it strips `/wd-internal` so Appium's own route
19
+ * answers, and lets the call skip per-command auth, since it is Xenon asking
20
+ * about a session it already runs.
21
+ *
22
+ * The path proves nothing on its own. Without the secret a `/wd-internal`
23
+ * request is left exactly as it came, matches no route, and gets Appium's
24
+ * unknown-route answer. Before this, the path alone was the marker, which on a
25
+ * server with per-command auth on was a way around it.
26
+ *
27
+ * The secret never leaves the process: it is sent only to this server's own
28
+ * address, with any HTTP proxy bypassed, and the gateway removes the header
29
+ * before anything after it sees the request.
30
+ */
31
+ exports.INTERNAL_CALL_HEADER = 'x-xenon-internal';
32
+ exports.INTERNAL_CALL_MARKER = '/wd-internal';
33
+ const SECRET = Buffer.from((0, crypto_1.randomBytes)(32).toString('base64url'));
34
+ const INTERNAL = Symbol('xenon.internalCall');
35
+ /** The headers a loopback call to this process's own server carries. */
36
+ function internalCallHeaders() {
37
+ return { [exports.INTERNAL_CALL_HEADER]: SECRET.toString() };
38
+ }
39
+ /** Whether these headers carry this process's secret. Constant-time. */
40
+ function hasInternalCallSecret(headers) {
41
+ const presented = headers[exports.INTERNAL_CALL_HEADER];
42
+ if (typeof presented !== 'string')
43
+ return false;
44
+ const candidate = Buffer.from(presented);
45
+ return candidate.length === SECRET.length && (0, crypto_1.timingSafeEqual)(candidate, SECRET);
46
+ }
47
+ /** Whether the internal-call layer accepted this request as Xenon's own. */
48
+ function isInternalCall(req) {
49
+ return !!req && req[INTERNAL] === true;
50
+ }
51
+ /**
52
+ * Where a local session's loopback calls go: this server, under its base
53
+ * path, marked `/wd-internal`. A wildcard bind address is reached on 127.0.0.1.
54
+ */
55
+ function internalCallBaseUrl(address, port, basePath) {
56
+ const wildcard = !address || address === '0.0.0.0' || address === '::';
57
+ const host = wildcard ? '127.0.0.1' : address.includes(':') ? `[${address}]` : address;
58
+ return `http://${host}:${port}${(0, appiumBasePath_1.normalizeBasePath)(basePath)}${exports.INTERNAL_CALL_MARKER}`;
59
+ }
60
+ /**
61
+ * The layer that recognises internal calls. Path-less, because Express puts a
62
+ * mount path back onto `req.url` when a mounted layer calls `next()`, which
63
+ * would undo the strip. The marker is compared exactly as Xenon writes it:
64
+ * any other spelling is not an internal call and falls through untouched.
65
+ */
66
+ function createInternalCallLayer(basePath) {
67
+ const base = (0, appiumBasePath_1.normalizeBasePath)(basePath);
68
+ const prefix = `${base}${exports.INTERNAL_CALL_MARKER}`;
69
+ /** Accept the call as Xenon's own, rewriting it in place; false leaves it untouched. */
70
+ function accept(req) {
71
+ const url = req.url;
72
+ const rest = url.startsWith(prefix) ? url.slice(prefix.length) : undefined;
73
+ if (rest === undefined || (rest !== '' && rest[0] !== '/' && rest[0] !== '?'))
74
+ return false;
75
+ if (!hasInternalCallSecret(req.headers))
76
+ return false;
77
+ delete req.headers[exports.INTERNAL_CALL_HEADER];
78
+ req[INTERNAL] = true;
79
+ req.url = `${base}${rest}` || '/';
80
+ req.originalUrl = req.url;
81
+ return true;
82
+ }
83
+ // One call to next() for every outcome: Appium's 404 carries a stack trace,
84
+ // and a second call site would make a refused /wd-internal request's answer
85
+ // differ from any other unknown route's by that one frame.
86
+ return function xenonInternalCalls(req, _res, next) {
87
+ accept(req);
88
+ next();
89
+ };
90
+ }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.NodeSessionProbeSupport = exports.UNSUPPORTED_RECHECK_MS = exports.NODE_SESSION_STATUS_HEADER = exports.NODE_SESSION_STATUS_PATH = exports.NODE_SESSION_STATUS_ROUTE = void 0;
13
+ exports.nodeSessionStatusHandler = nodeSessionStatusHandler;
14
+ exports.registerNodeSessionStatus = registerNodeSessionStatus;
15
+ const typedi_1 = require("typedi");
16
+ const logger_1 = __importDefault(require("../logger"));
17
+ const config_1 = require("../config");
18
+ const commandAuth_1 = require("../middleware/commandAuth");
19
+ const appiumUmbrella_1 = require("../sessions/appiumUmbrella");
20
+ const hubSessionToken_1 = require("./hubSessionToken");
21
+ /**
22
+ * "Does session X still exist?", asked by a hub of a node without touching it.
23
+ *
24
+ * A hub checks each session it routes to a node every ~30 s
25
+ * (SessionHeartbeatService, RemoteSession.checkHealth). It used to ask with a
26
+ * WebDriver command (`GET <basePath>/session/<id>/timeouts`), and any command
27
+ * restarts the node driver's new-command timeout and Xenon's idle clock there,
28
+ * so a session its client had abandoned, and its phone, stayed alive for as
29
+ * long as the hub did. A node's own sessions were fixed the same way in 2.1
30
+ * (LocalSession.checkHealth asks the umbrella).
31
+ *
32
+ * A node answers `GET /xenon/api/node/sessions/<id>` from Appium's umbrella
33
+ * (`sessionExists`), which runs no command: `200 { value: { sessionId,
34
+ * exists } }`. It is outside the login, like `/xenon/api/webdriver`: the hub
35
+ * holds no credentials for the node. It asks what a command to that session
36
+ * asks, never more, so it is never stricter than the probe it replaces. With
37
+ * per-command auth on (XENON_REQUIRE_COMMAND_AUTH, auth enabled) it needs the
38
+ * hub's session token for that session (`x-xenon-hub-token`, audience
39
+ * `xenon-node`, claim `sid`), the one the hub sends with every call about it:
40
+ * any other caller gets the unknown-session answer, and a JWKS that can't be
41
+ * fetched is `503`. With it off a command needs no credential, so neither does
42
+ * this. Otherwise a hub that can't sign (its JWT key failed to load) would
43
+ * see every node session as gone, and end them, where its command probe
44
+ * worked.
45
+ *
46
+ * Every answer carries NODE_SESSION_STATUS_HEADER, so a hub can tell a node
47
+ * that has this route from one that doesn't (an older Xenon, whose answer is a
48
+ * 404 or a 401 from its login). For those the hub keeps the old WebDriver
49
+ * probe and says so once per node (NodeSessionProbeSupport).
50
+ */
51
+ exports.NODE_SESSION_STATUS_ROUTE = '/node/sessions/:sessionId';
52
+ /** The route under a server's origin, for a hub building the URL. */
53
+ exports.NODE_SESSION_STATUS_PATH = '/xenon/api/node/sessions';
54
+ exports.NODE_SESSION_STATUS_HEADER = 'x-xenon-node-sessions';
55
+ /** How long a hub keeps using the old probe for a node without the route before asking again. */
56
+ exports.UNSUPPORTED_RECHECK_MS = 10 * 60000;
57
+ function nodeSessionStatusHandler(deps) {
58
+ return (req, res) => {
59
+ var _a, _b;
60
+ res.setHeader(exports.NODE_SESSION_STATUS_HEADER, '1');
61
+ const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
62
+ const answer = () => res.status(200).json({ value: { sessionId, exists: deps.hasSession(sessionId) } });
63
+ if (!deps.enforced())
64
+ return answer();
65
+ const presented = req.headers[hubSessionToken_1.HUB_TOKEN_HEADER];
66
+ const token = typeof presented === 'string' ? presented : undefined;
67
+ if (token === undefined) {
68
+ res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
69
+ return;
70
+ }
71
+ deps.hubTokens
72
+ .verify(token, sessionId)
73
+ .then((valid) => {
74
+ if (valid)
75
+ return answer();
76
+ deps.logger.warn(`Session status for ${sessionId} refused: the hub token is not valid for it`);
77
+ res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
78
+ })
79
+ .catch((error) => {
80
+ deps.logger.error(`Session status for ${sessionId} unavailable: hub token check failed: ${(0, commandAuth_1.summarize)(error)}`);
81
+ if (!res.headersSent) {
82
+ res.status(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_STATUS).json(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
83
+ }
84
+ });
85
+ };
86
+ }
87
+ /**
88
+ * A node's route, on the `/xenon/api` router ahead of its login. Only a node
89
+ * (a server with `hub`) has it; a hub or a standalone server answers it as an
90
+ * unknown API route.
91
+ */
92
+ function registerNodeSessionStatus(router, pluginArgs) {
93
+ if (pluginArgs.hub === undefined)
94
+ return;
95
+ router.get(exports.NODE_SESSION_STATUS_ROUTE, nodeSessionStatusHandler({
96
+ hubTokens: new hubSessionToken_1.HubSessionTokenVerifier(pluginArgs.hub),
97
+ enforced: () => (0, commandAuth_1.commandAuthEnabled)() && config_1.config.authDisabled !== true,
98
+ hasSession: (sessionId) => typedi_1.Container.get(appiumUmbrella_1.AppiumUmbrella).hasSession(sessionId),
99
+ logger: logger_1.default.scope('NodeSessionStatus'),
100
+ }));
101
+ }
102
+ /**
103
+ * Hub side: which nodes don't have the route. Such a node is probed the old
104
+ * way, with a WebDriver command, and the hub logs that once for it. It is
105
+ * asked again after UNSUPPORTED_RECHECK_MS, so a node upgraded in place is
106
+ * picked up without restarting the hub.
107
+ */
108
+ let NodeSessionProbeSupport = class NodeSessionProbeSupport {
109
+ constructor() {
110
+ this.logger = logger_1.default.scope('RemoteSession');
111
+ this.now = () => Date.now();
112
+ this.unsupportedUntil = new Map();
113
+ this.warned = new Set();
114
+ }
115
+ /** Whether to ask this node its route (true unless it recently had none). */
116
+ shouldAsk(origin) {
117
+ const until = this.unsupportedUntil.get(origin);
118
+ if (until === undefined)
119
+ return true;
120
+ if (until > this.now())
121
+ return false;
122
+ this.unsupportedUntil.delete(origin);
123
+ return true;
124
+ }
125
+ /** The node answered without the route: use the WebDriver probe for it for a while. */
126
+ unsupported(origin, status) {
127
+ this.unsupportedUntil.set(origin, this.now() + exports.UNSUPPORTED_RECHECK_MS);
128
+ if (this.warned.has(origin))
129
+ return;
130
+ this.warned.add(origin);
131
+ this.logger.warn(`Node ${origin} has no ${exports.NODE_SESSION_STATUS_PATH} route (answered ${status !== null && status !== void 0 ? status : 'nothing'}); ` +
132
+ 'an older Xenon. Its sessions are checked with a WebDriver command, which restarts ' +
133
+ 'their new-command timeout and idle clock there, so an abandoned session is kept ' +
134
+ 'alive. Upgrade the node.');
135
+ }
136
+ };
137
+ exports.NodeSessionProbeSupport = NodeSessionProbeSupport;
138
+ exports.NodeSessionProbeSupport = NodeSessionProbeSupport = __decorate([
139
+ (0, typedi_1.Service)()
140
+ ], NodeSessionProbeSupport);