@xenon-device-management/xenon 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +1 -2
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
  5. package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
  69. package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
  70. package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +57 -13
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/ws/h264StreamWs.js +13 -8
  84. package/lib/src/app/ws/logcatWs.js +12 -7
  85. package/lib/src/app/ws/upgradeRouter.js +215 -0
  86. package/lib/src/dashboard/event-manager.js +7 -6
  87. package/lib/src/data-service/device-service.js +152 -49
  88. package/lib/src/data-service/device-store.js +81 -10
  89. package/lib/src/data-service/deviceClaims.js +98 -0
  90. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  91. package/lib/src/data-service/prisma-store.js +94 -31
  92. package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
  93. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  94. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  95. package/lib/src/device-managers/NodeDevices.js +7 -2
  96. package/lib/src/device-managers/android/AndroidH264StreamService.js +3 -3
  97. package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
  98. package/lib/src/device-managers/android/previewHold.js +2 -2
  99. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  100. package/lib/src/device-managers/ios/IOSStreamService.js +158 -38
  101. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  102. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  103. package/lib/src/device-utils.js +78 -24
  104. package/lib/src/gateway/createHandoff.js +73 -0
  105. package/lib/src/gateway/defaultGateway.js +80 -0
  106. package/lib/src/gateway/forwardToNode.js +147 -0
  107. package/lib/src/gateway/hubSessionToken.js +356 -0
  108. package/lib/src/gateway/internalCall.js +90 -0
  109. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  110. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  111. package/lib/src/gateway/sessionCreate.js +136 -0
  112. package/lib/src/gateway/sessionGateway.js +201 -0
  113. package/lib/src/gateway/sessionLocator.js +100 -0
  114. package/lib/src/generated/client/edge.js +7 -4
  115. package/lib/src/generated/client/index-browser.js +4 -1
  116. package/lib/src/generated/client/index.d.ts +112 -1
  117. package/lib/src/generated/client/index.js +7 -4
  118. package/lib/src/generated/client/package.json +1 -1
  119. package/lib/src/generated/client/schema.prisma +9 -0
  120. package/lib/src/generated/client/wasm.js +4 -1
  121. package/lib/src/helpers/index.js +5 -1
  122. package/lib/src/logger.js +5 -0
  123. package/lib/src/middleware/authMiddleware.js +80 -0
  124. package/lib/src/middleware/commandAuth.js +31 -17
  125. package/lib/src/middleware/csrfMiddleware.js +3 -2
  126. package/lib/src/middleware/sessionListingFilter.js +153 -0
  127. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  128. package/lib/src/plugin.js +27 -1
  129. package/lib/src/prisma.js +2 -1
  130. package/lib/src/services/DeviceReconciler.js +5 -2
  131. package/lib/src/services/OrphanSweeper.js +12 -19
  132. package/lib/src/services/PortAllocator.js +75 -16
  133. package/lib/src/services/ServerManager.js +50 -30
  134. package/lib/src/services/SessionLifecycleService.js +335 -76
  135. package/lib/src/services/ShutdownCoordinator.js +7 -1
  136. package/lib/src/services/SocketServer.js +11 -2
  137. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  138. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  139. package/lib/src/services/lease/LeaseOrphanSweeper.js +32 -6
  140. package/lib/src/services/lease/LeaseService.js +19 -5
  141. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  142. package/lib/src/services/session/sessionCredentials.js +7 -59
  143. package/lib/src/sessions/CloudSession.js +12 -0
  144. package/lib/src/sessions/LocalSession.js +52 -26
  145. package/lib/src/sessions/RemoteSession.js +116 -9
  146. package/lib/src/sessions/SessionManager.js +49 -6
  147. package/lib/src/sessions/appiumUmbrella.js +32 -0
  148. package/lib/test/helpers/appium-umbrella.js +84 -0
  149. package/lib/test/helpers/fake-device-table.js +97 -0
  150. package/lib/test/helpers/loki-stores.js +41 -0
  151. package/lib/test/helpers/loopbackRequest.js +77 -0
  152. package/lib/test/helpers/scratch-database.js +100 -0
  153. package/lib/test/helpers/scratch-port-leases.js +119 -0
  154. package/lib/test/helpers/test-container.js +35 -27
  155. package/lib/test/integration/androidDevices.spec.js +5 -4
  156. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  157. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  158. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  159. package/lib/test/unit/AndroidDeviceManager.spec.js +8 -1
  160. package/lib/test/unit/DeviceModel.spec.js +9 -9
  161. package/lib/test/unit/HealingCascade.test.js +5 -1
  162. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  163. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  164. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  165. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  166. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  167. package/lib/test/unit/android-h264-service.spec.js +6 -0
  168. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  169. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  170. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  171. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  172. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  173. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  174. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  175. package/lib/test/unit/authMiddleware.test.js +3 -0
  176. package/lib/test/unit/bug-report/route.spec.js +9 -9
  177. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  178. package/lib/test/unit/clipboard-route.spec.js +18 -8
  179. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  180. package/lib/test/unit/command-auth.spec.js +31 -31
  181. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  182. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  183. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  184. package/lib/test/unit/create-error-message.spec.js +43 -0
  185. package/lib/test/unit/create-handoff.spec.js +86 -0
  186. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  187. package/lib/test/unit/device-access-guard.spec.js +28 -28
  188. package/lib/test/unit/device-service.spec.js +4 -0
  189. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  190. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  191. package/lib/test/unit/device-team-guard.spec.js +20 -20
  192. package/lib/test/unit/device-utils.spec.js +8 -0
  193. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  194. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  195. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  196. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  197. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  198. package/lib/test/unit/helpers.spec.js +27 -0
  199. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  200. package/lib/test/unit/hub-create-once.spec.js +190 -0
  201. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  202. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  203. package/lib/test/unit/hub-node-control-forward.spec.js +291 -0
  204. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  205. package/lib/test/unit/hub-node-race.spec.js +401 -0
  206. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  207. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  208. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  209. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  210. package/lib/test/unit/hub-session-token.spec.js +265 -0
  211. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  212. package/lib/test/unit/install-repository-app-team.spec.js +2 -2
  213. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  214. package/lib/test/unit/internal-call.spec.js +181 -0
  215. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  216. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  217. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  218. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  219. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  220. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  221. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  222. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  223. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +92 -0
  224. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  225. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  226. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  227. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  228. package/lib/test/unit/logcat-ws.spec.js +4 -1
  229. package/lib/test/unit/loopback-request.spec.js +81 -0
  230. package/lib/test/unit/node-create-caps.spec.js +54 -0
  231. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  232. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  233. package/lib/test/unit/omni-vision.spec.js +7 -3
  234. package/lib/test/unit/plugin.spec.js +45 -4
  235. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  236. package/lib/test/unit/profile-router.test.js +11 -11
  237. package/lib/test/unit/queue-requester.spec.js +5 -0
  238. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  239. package/lib/test/unit/queue-visibility.spec.js +1 -1
  240. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  241. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  242. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  243. package/lib/test/unit/recording-store.spec.js +9 -3
  244. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  245. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  246. package/lib/test/unit/remote-session-create.spec.js +130 -0
  247. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  248. package/lib/test/unit/reset-link.test.js +14 -14
  249. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  250. package/lib/test/unit/session-assets.spec.js +9 -9
  251. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  252. package/lib/test/unit/session-create-layer.spec.js +255 -0
  253. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  254. package/lib/test/unit/session-credentials.spec.js +0 -47
  255. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  256. package/lib/test/unit/session-locator.spec.js +250 -0
  257. package/lib/test/unit/session-node-calls.spec.js +174 -0
  258. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  259. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  260. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  261. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  262. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  263. package/lib/test/unit/stream-leave-route.spec.js +15 -7
  264. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  265. package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
  266. package/lib/test/unit/stream-stop-recording.spec.js +13 -5
  267. package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
  268. package/lib/test/unit/upgrade-router.spec.js +446 -0
  269. package/lib/test/unit/users-router.test.js +11 -8
  270. package/lib/test/unit/verify-credential.spec.js +4 -1
  271. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  272. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  273. package/lib/tsconfig.tsbuildinfo +1 -1
  274. package/package.json +1 -2
  275. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  276. package/prisma/schema.prisma +9 -0
  277. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  278. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  279. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const fs_1 = __importDefault(require("fs"));
51
+ const os_1 = __importDefault(require("os"));
52
+ const path_1 = __importDefault(require("path"));
53
+ const sinon_1 = __importDefault(require("sinon"));
54
+ const express_1 = __importDefault(require("express"));
55
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
56
+ const jose = __importStar(require("jose"));
57
+ const typedi_1 = require("typedi");
58
+ const control_1 = __importDefault(require("../../src/app/routers/control"));
59
+ const auth_1 = require("../../src/app/routers/auth");
60
+ const authMiddleware_1 = require("../../src/middleware/authMiddleware");
61
+ const csrfMiddleware_1 = require("../../src/middleware/csrfMiddleware");
62
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
63
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
64
+ const PluginContext_1 = require("../../src/PluginContext");
65
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
66
+ const device_managers_1 = require("../../src/device-managers");
67
+ const device_store_1 = require("../../src/data-service/device-store");
68
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
69
+ const SessionOwnerResolver_1 = require("../../src/services/device-access/SessionOwnerResolver");
70
+ const LiveSessionOwners_1 = require("../../src/services/device-access/LiveSessionOwners");
71
+ const config_1 = require("../../src/config");
72
+ const container_registration_1 = require("../helpers/container-registration");
73
+ const scratch_database_1 = require("../helpers/scratch-database");
74
+ const loopbackServer_1 = require("../helpers/loopbackServer");
75
+ /**
76
+ * A hub forwards five of its /control actions on a node's phone (tap, swipe,
77
+ * text, keyevent, touchAndHold) to the phone's node. It sent them with no
78
+ * credential at all, so a node with auth enabled refused every one (its CSRF
79
+ * check, then its login), and device control of a node's phone only worked
80
+ * with the node's auth off.
81
+ *
82
+ * The hub now signs each forwarded call for the user it acts for: audience
83
+ * xenon-node-control, one minute, naming the user, whether they are an admin,
84
+ * the phone and its node. The node accepts it for /control on that phone
85
+ * only, then runs its own ownership guard with that user, whose session it
86
+ * knows (LiveSessionOwners).
87
+ */
88
+ // Named like the real manager: /control picks one by constructor.name.
89
+ class AndroidDeviceManager {
90
+ constructor() {
91
+ this.taps = [];
92
+ this.tap = (udid) => __awaiter(this, void 0, void 0, function* () {
93
+ this.taps.push(udid);
94
+ });
95
+ }
96
+ }
97
+ describe('the hub’s device control on a node’s phone', () => {
98
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
99
+ const loopback = (0, loopbackServer_1.loopbackServers)();
100
+ let dirs;
101
+ let hubKeys;
102
+ let manager;
103
+ let context;
104
+ let saved;
105
+ let restore;
106
+ let hubUrl;
107
+ let hubApp;
108
+ let nodeOrigin;
109
+ let nodePort;
110
+ let nodeArgs;
111
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
112
+ dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-control-keys-'))];
113
+ hubKeys = new JwtKeyService_1.JwtKeyService();
114
+ yield hubKeys.init(dirs[0]);
115
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionOwnerResolver_1.SessionOwnerResolver, LiveSessionOwners_1.LiveSessionOwners);
116
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
117
+ typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
118
+ typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
119
+ typedi_1.Container.set(LiveSessionOwners_1.LiveSessionOwners, new LiveSessionOwners_1.LiveSessionOwners());
120
+ manager = new AndroidDeviceManager();
121
+ typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(void 0, void 0, void 0, function* () { return [manager]; }) });
122
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
123
+ saved = {
124
+ context: Object.assign({}, context),
125
+ store: device_store_1.DeviceStoreFactory._deviceStore,
126
+ authDisabled: config_1.config.authDisabled,
127
+ };
128
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
129
+ config_1.config.authDisabled = false;
130
+ // The hub: its public keys, and its own /control with a caller named by a header.
131
+ const hub = (0, express_1.default)();
132
+ hub.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
133
+ hubUrl = `http://127.0.0.1:${(yield loopback.serve(hub)).address().port}`;
134
+ hubApp = (0, express_1.default)();
135
+ // One process plays both servers, and PluginContext is a singleton: each
136
+ // app takes its own identity per request, so the hub's forward decision
137
+ // (the phone's nodeId against this server's) sees the hub, not the node.
138
+ hubApp.use((_req, _res, next) => {
139
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), 1, 'hub-1', '');
140
+ next();
141
+ });
142
+ hubApp.use(express_1.default.json());
143
+ hubApp.use((req, _res, next) => {
144
+ var _a;
145
+ const user = String((_a = req.headers['x-test-user']) !== null && _a !== void 0 ? _a : '');
146
+ const admin = user.startsWith('admin-');
147
+ req.auth = {
148
+ kind: 'user-session',
149
+ userId: user,
150
+ role: admin ? 'ADMIN' : 'MEMBER',
151
+ scopes: admin ? 'admin,devices,sessions,read' : 'devices,sessions,read',
152
+ rateLimit: 300,
153
+ teamIds: undefined,
154
+ };
155
+ next();
156
+ });
157
+ const hubApi = express_1.default.Router();
158
+ control_1.default.register(hubApi);
159
+ hubApp.use('/xenon/api', hubApi);
160
+ // The node: its CSRF check, its login and its /control, as ServerManager mounts them.
161
+ const node = (0, express_1.default)();
162
+ node.use((_req, _res, next) => {
163
+ context.setContext(nodeArgs, nodePort, 'node-1', '');
164
+ next();
165
+ });
166
+ node.use(express_1.default.json());
167
+ const nodeApi = express_1.default.Router();
168
+ nodeApi.use(csrfMiddleware_1.csrfMiddleware);
169
+ nodeApi.use(authMiddleware_1.authMiddleware);
170
+ nodeApi.get('/whoami', (req, res) => res.json(req.auth));
171
+ control_1.default.register(nodeApi);
172
+ node.use('/xenon/api', nodeApi);
173
+ nodePort = (yield loopback.serve(node)).address().port;
174
+ nodeOrigin = `http://127.0.0.1:${nodePort}`;
175
+ nodeArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1' });
176
+ context.setContext(nodeArgs, nodePort, 'node-1', '');
177
+ yield scratch.db.device.deleteMany({});
178
+ // alice's session runs on the node's phone.
179
+ for (const udid of ['phone-1', 'phone-2']) {
180
+ yield scratch.db.device.create({
181
+ data: {
182
+ udid,
183
+ host: nodeOrigin,
184
+ nodeId: 'node-1',
185
+ platform: 'android',
186
+ name: udid,
187
+ busy: udid === 'phone-1',
188
+ session_id: udid === 'phone-1' ? 's-1' : null,
189
+ },
190
+ });
191
+ }
192
+ typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).record('s-1', 'alice');
193
+ }));
194
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
195
+ Object.assign(context, saved.context);
196
+ device_store_1.DeviceStoreFactory._deviceStore = saved.store;
197
+ config_1.config.authDisabled = saved.authDisabled;
198
+ sinon_1.default.restore();
199
+ restore();
200
+ yield loopback.closeAll();
201
+ for (const dir of dirs)
202
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
203
+ }));
204
+ const tapThroughHub = (user) => (0, loopbackRequest_1.default)(hubApp)
205
+ .post('/xenon/api/control/phone-1/tap')
206
+ .set('x-test-user', user)
207
+ .send({ x: 10, y: 20 });
208
+ const controlToken = (grant = {}) => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).controlTokenFor(Object.assign({ userId: 'alice', isAdmin: false, udid: 'phone-1', host: nodeOrigin }, grant));
209
+ const toNode = (method, url, token) => {
210
+ const req = (0, loopbackRequest_1.default)(nodeOrigin)[method](url);
211
+ if (token)
212
+ req.set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
213
+ return method === 'post' ? req.send({ x: 1, y: 2 }) : req;
214
+ };
215
+ it('the owner’s tap through the hub reaches the node’s phone', () => __awaiter(void 0, void 0, void 0, function* () {
216
+ const res = yield tapThroughHub('alice');
217
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
218
+ (0, chai_1.expect)(manager.taps).to.deep.equal(['phone-1']);
219
+ }));
220
+ it('the hub signs the forwarded call for its user, that phone and its node', () => __awaiter(void 0, void 0, void 0, function* () {
221
+ const seen = [];
222
+ sinon_1.default.stub(typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer), 'controlTokenFor').callsFake((grant) => __awaiter(void 0, void 0, void 0, function* () {
223
+ seen.push(JSON.stringify(grant));
224
+ return hubKeys.sign({ sub: grant.userId, adm: grant.isAdmin, udid: grant.udid, host: grant.host }, { audience: hubSessionToken_1.HUB_CONTROL_AUDIENCE, ttlSeconds: 60 });
225
+ }));
226
+ yield tapThroughHub('alice').expect(200);
227
+ (0, chai_1.expect)(seen.map((g) => JSON.parse(g))).to.deep.equal([
228
+ { userId: 'alice', isAdmin: false, udid: 'phone-1', host: nodeOrigin },
229
+ ]);
230
+ }));
231
+ it('is a one-minute token /auth/token will never mint', () => __awaiter(void 0, void 0, void 0, function* () {
232
+ const claims = jose.decodeJwt(yield controlToken());
233
+ (0, chai_1.expect)(claims.aud).to.equal(hubSessionToken_1.HUB_CONTROL_AUDIENCE);
234
+ (0, chai_1.expect)(claims.exp - claims.iat).to.equal(60);
235
+ let refused;
236
+ yield (0, auth_1.issueToken)({ userId: 'alice', role: 'ADMIN', scopes: 'admin' }, { audience: hubSessionToken_1.HUB_CONTROL_AUDIENCE }).catch((error) => (refused = error));
237
+ (0, chai_1.expect)(refused === null || refused === void 0 ? void 0 : refused.message).to.include('unsupported audience');
238
+ }));
239
+ describe('on the node', () => {
240
+ it('the node’s own ownership guard judges the user the hub named', () => __awaiter(void 0, void 0, void 0, function* () {
241
+ const asBob = yield controlToken({ userId: 'bob' });
242
+ const bob = yield toNode('post', '/xenon/api/control/phone-1/tap', asBob);
243
+ (0, chai_1.expect)(bob.status).to.equal(409);
244
+ (0, chai_1.expect)(bob.body.error).to.equal('device_in_use_by_session');
245
+ const admin = yield controlToken({ userId: 'admin-carol', isAdmin: true });
246
+ yield toNode('post', '/xenon/api/control/phone-1/tap', admin).expect(200);
247
+ (0, chai_1.expect)(manager.taps).to.deep.equal(['phone-1']);
248
+ }));
249
+ it('is accepted for that phone’s /control only', () => __awaiter(void 0, void 0, void 0, function* () {
250
+ const token = yield controlToken();
251
+ const otherPhone = yield toNode('post', '/xenon/api/control/phone-2/tap', token);
252
+ (0, chai_1.expect)(otherPhone.status).to.equal(401);
253
+ const notControl = yield toNode('get', '/xenon/api/whoami', token);
254
+ (0, chai_1.expect)(notControl.status).to.equal(401);
255
+ (0, chai_1.expect)(manager.taps).to.deep.equal([]);
256
+ }));
257
+ it('is refused when it names another node, or is another kind of hub token', () => __awaiter(void 0, void 0, void 0, function* () {
258
+ const issuer = typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer);
259
+ const tokens = [
260
+ yield controlToken({ host: 'http://10.9.9.9:4725' }),
261
+ (yield issuer.tokenFor('s-1')),
262
+ (yield issuer.createTokenFor({
263
+ userId: 'alice',
264
+ udid: 'phone-1',
265
+ host: nodeOrigin,
266
+ })),
267
+ yield hubKeys.sign({ sub: 'alice', adm: false, udid: 'phone-1', host: nodeOrigin }, { audience: hubSessionToken_1.HUB_CONTROL_AUDIENCE, ttlSeconds: -3600 }),
268
+ 'not-a-jwt',
269
+ ];
270
+ for (const token of tokens) {
271
+ const res = yield toNode('post', '/xenon/api/control/phone-1/tap', token);
272
+ (0, chai_1.expect)(res.status, token.slice(0, 20)).to.equal(401);
273
+ }
274
+ (0, chai_1.expect)(manager.taps).to.deep.equal([]);
275
+ }));
276
+ it('cannot be checked while the hub’s keys cannot be fetched: 503', () => __awaiter(void 0, void 0, void 0, function* () {
277
+ const token = yield controlToken();
278
+ nodeArgs = Object.assign(Object.assign({}, nodeArgs), { hub: 'http://127.0.0.1:1' });
279
+ const res = yield toNode('post', '/xenon/api/control/phone-1/tap', token);
280
+ (0, chai_1.expect)(res.status).to.equal(503);
281
+ (0, chai_1.expect)(manager.taps).to.deep.equal([]);
282
+ }));
283
+ it('a server that is not a node ignores it', () => __awaiter(void 0, void 0, void 0, function* () {
284
+ const token = yield controlToken();
285
+ nodeArgs = Object.assign(Object.assign({}, nodeArgs), { hub: undefined });
286
+ const res = yield toNode('post', '/xenon/api/control/phone-1/tap', token);
287
+ (0, chai_1.expect)(res.status).to.not.equal(200);
288
+ (0, chai_1.expect)(manager.taps).to.deep.equal([]);
289
+ }));
290
+ });
291
+ });