@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
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const typedi_1 = require("typedi");
21
21
  const fs_1 = __importDefault(require("fs"));
22
22
  const os_1 = __importDefault(require("os"));
@@ -29,6 +29,8 @@ const device_store_1 = require("../../src/data-service/device-store");
29
29
  const UserService_1 = require("../../src/services/UserService");
30
30
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
31
31
  const UserSessionService_1 = require("../../src/services/UserSessionService");
32
+ const container_registration_1 = require("../helpers/container-registration");
33
+ const loopbackServer_1 = require("../helpers/loopbackServer");
32
34
  function fakeRes() {
33
35
  const res = { statusCode: 200 };
34
36
  res.status = (c) => {
@@ -46,7 +48,9 @@ describe('authMiddleware — stream-ticket branch', () => {
46
48
  let dir;
47
49
  let keySvc;
48
50
  let ticketSvc;
51
+ let restoreContainer;
49
52
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
53
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, StreamTicketService_1.StreamTicketService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
50
54
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-authmw-ticket-'));
51
55
  keySvc = new JwtKeyService_1.JwtKeyService();
52
56
  yield keySvc.init(dir);
@@ -62,7 +66,7 @@ describe('authMiddleware — stream-ticket branch', () => {
62
66
  }));
63
67
  afterEach(() => {
64
68
  sinon_1.default.restore();
65
- typedi_1.Container.reset();
69
+ restoreContainer();
66
70
  fs_1.default.rmSync(dir, { recursive: true, force: true });
67
71
  });
68
72
  // req.path here mirrors what authMiddleware actually sees at runtime:
@@ -194,7 +198,12 @@ describe('authMiddleware — stream-ticket branch (real mount-order, supertest)'
194
198
  let dir;
195
199
  let app;
196
200
  let ticketSvc;
201
+ let restoreContainer;
202
+ // Served on 127.0.0.1: request(app) listens on every address and connects
203
+ // to 127.0.0.1, where another process's listener on that port can answer.
204
+ const loopback = (0, loopbackServer_1.loopbackServers)();
197
205
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
206
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, StreamTicketService_1.StreamTicketService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
198
207
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-authmw-mount-'));
199
208
  const keySvc = new JwtKeyService_1.JwtKeyService();
200
209
  yield keySvc.init(dir);
@@ -222,21 +231,24 @@ describe('authMiddleware — stream-ticket branch (real mount-order, supertest)'
222
231
  app = (0, express_1.default)();
223
232
  app.use('/xenon/api', apiRouter);
224
233
  }));
225
- afterEach(() => {
234
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
226
235
  sinon_1.default.restore();
227
- typedi_1.Container.reset();
236
+ restoreContainer();
237
+ yield loopback.closeAll();
228
238
  fs_1.default.rmSync(dir, { recursive: true, force: true });
229
- });
239
+ }));
230
240
  it('a valid ticket on GET /xenon/api/control/:udid/stream reaches the route (past 401)', () => __awaiter(void 0, void 0, void 0, function* () {
231
241
  const ticket = yield ticketSvc.mint('UDID-1', 'actor-1');
232
- const res = yield (0, supertest_1.default)(app).get('/xenon/api/control/UDID-1/stream').query({ ticket });
242
+ const res = yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
243
+ .get('/xenon/api/control/UDID-1/stream')
244
+ .query({ ticket });
233
245
  // The ticket branch fired: request cleared auth and hit the handler's
234
246
  // device lookup (404), rather than being rejected at the middleware (401).
235
247
  (0, chai_1.expect)(res.status).to.not.equal(401);
236
248
  (0, chai_1.expect)(res.status).to.equal(404); // 'Device not found' from the stubbed store
237
249
  }));
238
250
  it('the SAME request without a ticket is rejected 401 (proves the guard is non-vacuous)', () => __awaiter(void 0, void 0, void 0, function* () {
239
- const res = yield (0, supertest_1.default)(app).get('/xenon/api/control/UDID-1/stream');
251
+ const res = yield (0, loopbackRequest_1.default)(yield loopback.serve(app)).get('/xenon/api/control/UDID-1/stream');
240
252
  (0, chai_1.expect)(res.status).to.equal(401);
241
253
  (0, chai_1.expect)(res.body).to.deep.equal({ error: 'unauthenticated' });
242
254
  }));
@@ -92,6 +92,9 @@ describe('authMiddleware', () => {
92
92
  role: 'MEMBER',
93
93
  status: 'ACTIVE',
94
94
  });
95
+ // A member's teams are looked up; without this the lookup read the
96
+ // developer's own database.
97
+ sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
95
98
  const req = { headers: { cookie: 'xenon_dashboard_session=legacy-key-id' } };
96
99
  let called = false;
97
100
  yield (0, authMiddleware_1.authMiddleware)(req, mkRes(), () => {
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../../helpers/loopbackRequest"));
19
19
  const sinon_1 = __importDefault(require("sinon"));
20
20
  const BugReportService_1 = require("../../../src/services/bug-report/BugReportService");
21
21
  const bug_report_1 = __importDefault(require("../../../src/app/routers/bug-report"));
@@ -63,8 +63,8 @@ describe('bug-report route', () => {
63
63
  throw new Error(`Session ${opts.sessionId} not found`);
64
64
  throw new Error('built a report');
65
65
  }));
66
- const hidden = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-b/bug-report?mode=full');
67
- const unknown = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-nope/bug-report?mode=full');
66
+ const hidden = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-b/bug-report?mode=full');
67
+ const unknown = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-nope/bug-report?mode=full');
68
68
  (0, chai_1.expect)(hidden.status).to.equal(404);
69
69
  (0, chai_1.expect)(unknown.status).to.equal(404);
70
70
  (0, chai_1.expect)(hidden.body).to.deep.equal({ error: 'Session sess-b not found' });
@@ -78,26 +78,26 @@ describe('bug-report route', () => {
78
78
  entries: [{ name: 'a.txt', source: { kind: 'buffer', data: Buffer.from('hi') } }],
79
79
  cleanup: () => __awaiter(void 0, void 0, void 0, function* () { }),
80
80
  });
81
- const res = yield (0, supertest_1.default)(makeApp({ admin: true })).post('/sessions/sess-b/bug-report?mode=full');
81
+ const res = yield (0, loopbackRequest_1.default)(makeApp({ admin: true })).post('/sessions/sess-b/bug-report?mode=full');
82
82
  (0, chai_1.expect)(res.status).to.equal(200);
83
83
  }));
84
84
  it('401 when the caller is not signed in', () => __awaiter(void 0, void 0, void 0, function* () {
85
- const res = yield (0, supertest_1.default)(makeApp({ signedIn: false })).post('/sessions/sess-1/bug-report?mode=full');
85
+ const res = yield (0, loopbackRequest_1.default)(makeApp({ signedIn: false })).post('/sessions/sess-1/bug-report?mode=full');
86
86
  (0, chai_1.expect)(res.status).to.equal(401);
87
87
  }));
88
88
  it('400 on invalid mode', () => __awaiter(void 0, void 0, void 0, function* () {
89
- const res = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=bogus');
89
+ const res = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=bogus');
90
90
  (0, chai_1.expect)(res.status).to.equal(400);
91
91
  }));
92
92
  it('400 on out-of-range windowSec', () => __awaiter(void 0, void 0, void 0, function* () {
93
- const res = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=slice&windowSec=9999');
93
+ const res = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=slice&windowSec=9999');
94
94
  (0, chai_1.expect)(res.status).to.equal(400);
95
95
  }));
96
96
  it('404 when service throws not-found', () => __awaiter(void 0, void 0, void 0, function* () {
97
97
  sinon_1.default
98
98
  .stub(BugReportService_1.BugReportService.prototype, 'assemble')
99
99
  .rejects(new Error('Session sess-1 not found'));
100
- const res = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=full');
100
+ const res = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=full');
101
101
  (0, chai_1.expect)(res.status).to.equal(404);
102
102
  }));
103
103
  it('200 streams zip on success', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -107,7 +107,7 @@ describe('bug-report route', () => {
107
107
  entries: [{ name: 'a.txt', source: { kind: 'buffer', data: Buffer.from('hi') } }],
108
108
  cleanup: () => __awaiter(void 0, void 0, void 0, function* () { }),
109
109
  });
110
- const res = yield (0, supertest_1.default)(makeApp())
110
+ const res = yield (0, loopbackRequest_1.default)(makeApp())
111
111
  .post('/sessions/sess-1/bug-report?mode=full')
112
112
  .buffer(true)
113
113
  .parse((r, cb) => {
@@ -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 typedi_1 = require("typedi");
54
54
  const prisma_1 = require("../../src/prisma");
55
55
  const config_1 = require("../../src/config");
@@ -125,7 +125,7 @@ describe('GET /xenon/api/cliArgs', () => {
125
125
  sinon_1.default.restore();
126
126
  config_1.config.authDisabled = savedAuthDisabled;
127
127
  });
128
- const asCookie = (sessionId) => (0, supertest_1.default)(app).get('/xenon/api/cliArgs').set('Cookie', `xenon_dashboard_session=${sessionId}`);
128
+ const asCookie = (sessionId) => (0, loopbackRequest_1.default)(app).get('/xenon/api/cliArgs').set('Cookie', `xenon_dashboard_session=${sessionId}`);
129
129
  function asApiKey(scopes, ownerId) {
130
130
  sinon_1.default.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'verifyPair').resolves({
131
131
  id: 'key-1',
@@ -138,7 +138,7 @@ describe('GET /xenon/api/cliArgs', () => {
138
138
  userId: ownerId,
139
139
  teamId: null,
140
140
  });
141
- return (0, supertest_1.default)(app)
141
+ return (0, loopbackRequest_1.default)(app)
142
142
  .get('/xenon/api/cliArgs')
143
143
  .set('x-xenon-access-key', 'ak')
144
144
  .set('x-xenon-token', 'tok');
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const typedi_1 = require("typedi");
21
21
  const control_1 = __importDefault(require("../../src/app/routers/control"));
22
22
  const device_store_1 = require("../../src/data-service/device-store");
@@ -24,6 +24,9 @@ const ApiKeyService_1 = require("../../src/services/ApiKeyService");
24
24
  const device_managers_1 = require("../../src/device-managers");
25
25
  const clipboardErrors_1 = require("../../src/device-managers/clipboardErrors");
26
26
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
27
+ const container_registration_1 = require("../helpers/container-registration");
28
+ const loopbackServer_1 = require("../helpers/loopbackServer");
29
+ const own_node_id_1 = require("../helpers/own-node-id");
27
30
  /**
28
31
  * GET/POST /control/:udid/clipboard answered 200 whatever happened on the
29
32
  * device: the Android manager swallowed its errors, and its "set" had no
@@ -56,14 +59,24 @@ function buildApp() {
56
59
  return app;
57
60
  }
58
61
  describe('GET/POST /control/:udid/clipboard', () => {
62
+ // The fake rows are this server's own phones.
63
+ (0, own_node_id_1.useOwnNodeId)();
59
64
  let manager;
65
+ let restoreContainer;
66
+ // Served on 127.0.0.1, not by request(app): that listens on every address
67
+ // and connects to 127.0.0.1, where another process's listener on the same
68
+ // port answers instead (a stray 501 or a socket hang-up in a full run).
69
+ const loopback = (0, loopbackServer_1.loopbackServers)();
70
+ const app = () => __awaiter(void 0, void 0, void 0, function* () { return loopback.serve(buildApp()); });
60
71
  beforeEach(() => {
72
+ restoreContainer = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, device_managers_1.XenonManager);
61
73
  manager = new AndroidDeviceManager();
62
74
  sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
63
75
  findDevice: () => __awaiter(void 0, void 0, void 0, function* () {
64
76
  return ({
65
77
  udid: UDID,
66
78
  host: '127.0.0.1',
79
+ nodeId: own_node_id_1.OWN_NODE_ID,
67
80
  platform: 'android',
68
81
  busy: false,
69
82
  session_id: null,
@@ -73,19 +86,20 @@ describe('GET/POST /control/:udid/clipboard', () => {
73
86
  typedi_1.Container.set(ApiKeyService_1.ApiKeyService, new ApiKeyService_1.ApiKeyService());
74
87
  typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(void 0, void 0, void 0, function* () { return [manager]; }) });
75
88
  });
76
- afterEach(() => {
89
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
77
90
  sinon_1.default.restore();
78
- typedi_1.Container.reset();
79
- });
91
+ restoreContainer();
92
+ yield loopback.closeAll();
93
+ }));
80
94
  it('returns the clipboard text', () => __awaiter(void 0, void 0, void 0, function* () {
81
95
  manager.getClipboard.resolves('xenonread');
82
- const res = yield (0, supertest_1.default)(buildApp()).get(`/xenon/api/control/${UDID}/clipboard`);
96
+ const res = yield (0, loopbackRequest_1.default)(yield app()).get(`/xenon/api/control/${UDID}/clipboard`);
83
97
  (0, chai_1.expect)(res.status).to.equal(200);
84
98
  (0, chai_1.expect)(res.body).to.deep.equal({ content: 'xenonread' });
85
99
  }));
86
100
  it('says why a read failed, with a 500', () => __awaiter(void 0, void 0, void 0, function* () {
87
101
  manager.getClipboard.rejects(new Error('Appium Settings didn’t return the clipboard.'));
88
- const res = yield (0, supertest_1.default)(buildApp()).get(`/xenon/api/control/${UDID}/clipboard`);
102
+ const res = yield (0, loopbackRequest_1.default)(yield app()).get(`/xenon/api/control/${UDID}/clipboard`);
89
103
  (0, chai_1.expect)(res.status).to.equal(500);
90
104
  (0, chai_1.expect)(res.body.error).to.match(/Appium Settings/);
91
105
  }));
@@ -93,7 +107,7 @@ describe('GET/POST /control/:udid/clipboard', () => {
93
107
  // won't help. 501 says exactly that.
94
108
  it('answers 501 when the device cannot set its clipboard', () => __awaiter(void 0, void 0, void 0, function* () {
95
109
  manager.setClipboard.rejects(new clipboardErrors_1.ClipboardUnsupportedError('Appium Settings can only read it.'));
96
- const res = yield (0, supertest_1.default)(buildApp())
110
+ const res = yield (0, loopbackRequest_1.default)(yield app())
97
111
  .post(`/xenon/api/control/${UDID}/clipboard`)
98
112
  .send({ content: 'x' });
99
113
  (0, chai_1.expect)(res.status).to.equal(501);
@@ -101,7 +115,7 @@ describe('GET/POST /control/:udid/clipboard', () => {
101
115
  }));
102
116
  it('still answers 500 for any other write failure', () => __awaiter(void 0, void 0, void 0, function* () {
103
117
  manager.setClipboard.rejects(new Error('WDA is not running'));
104
- const res = yield (0, supertest_1.default)(buildApp())
118
+ const res = yield (0, loopbackRequest_1.default)(yield app())
105
119
  .post(`/xenon/api/control/${UDID}/clipboard`)
106
120
  .send({ content: 'x' });
107
121
  (0, chai_1.expect)(res.status).to.equal(500);
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
36
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
37
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -16,7 +49,8 @@ const chai_1 = require("chai");
16
49
  const http_1 = __importDefault(require("http"));
17
50
  const path_1 = __importDefault(require("path"));
18
51
  const sinon_1 = __importDefault(require("sinon"));
19
- const supertest_1 = __importDefault(require("supertest"));
52
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
53
+ const ws_1 = __importStar(require("ws"));
20
54
  const commandCaller_1 = require("../../src/middleware/commandCaller");
21
55
  const commandAuth_1 = require("../../src/middleware/commandAuth");
22
56
  const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
@@ -29,9 +63,22 @@ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
29
63
  * Express 5 as Appium builds it: its middleware, then every WebDriver route,
30
64
  * then the plugin updaters, then its catch-all 404. The fake driver records
31
65
  * each command that reaches it, which shows whether the check ran first.
66
+ *
67
+ * The session listing and the BiDi WebSocket go through the same server:
68
+ * registerSessionUpgradeGuard is a server updater too, and the BiDi handler is
69
+ * added after boot with addWebSocketHandler, as Appium's main adds it.
32
70
  */
33
71
  const EXACT_UNKNOWN_SESSION = JSON.stringify(commandAuth_1.UNKNOWN_SESSION_BODY);
34
72
  const LIVE_SESSION = 'live-session';
73
+ const BOBS_SESSION = 'bobs-session';
74
+ const LISTING = [
75
+ { id: LIVE_SESSION, created: 1, capabilities: { platformName: 'Fake' } },
76
+ { id: BOBS_SESSION, created: 2, capabilities: { platformName: 'Fake' } },
77
+ ];
78
+ const OWNERS = { [LIVE_SESSION]: 'alice', [BOBS_SESSION]: 'bob' };
79
+ function nativelyUsesUpgradeListener() {
80
+ return typeof http_1.default.createServer().shouldUpgradeCallback !== 'undefined';
81
+ }
35
82
  function appiumBaseDriver() {
36
83
  const appiumDir = path_1.default.dirname(require.resolve('appium/package.json'));
37
84
  // eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -47,11 +94,14 @@ describe("per-command auth in Appium 3's own server()", () => {
47
94
  let server;
48
95
  let commands;
49
96
  let enabled;
97
+ let infos;
50
98
  let warnings;
99
+ let bidiConnections;
51
100
  function boot() {
52
- return __awaiter(this, void 0, void 0, function* () {
101
+ return __awaiter(this, arguments, void 0, function* (opts = {}) {
53
102
  const baseDriver = appiumBaseDriver();
54
103
  commands = [];
104
+ bidiConnections = [];
55
105
  const driver = {
56
106
  log: quiet,
57
107
  protocol: 'W3C',
@@ -63,44 +113,88 @@ describe("per-command auth in Appium 3's own server()", () => {
63
113
  commands.push(command);
64
114
  if (command === 'createSession')
65
115
  return [LIVE_SESSION, {}];
116
+ if (command === 'getAppiumSessions')
117
+ return LISTING;
66
118
  return 'about:blank';
67
119
  }),
68
120
  };
121
+ const credentials = { 'alice-token': 'alice', 'bob-token': 'bob' };
69
122
  const verifier = new commandCaller_1.CommandCallerVerifier({
70
123
  verifyKeyPair: sinon_1.default.stub().callsFake((_ak, token) => __awaiter(this, void 0, void 0, function* () {
71
- return token === 'alice-token'
124
+ return credentials[token]
72
125
  ? {
73
- row: { id: 'k', userId: 'alice', scopes: 'sessions', expiresAt: null },
74
- user: { id: 'alice', role: 'MEMBER', status: 'ACTIVE' },
126
+ row: { id: 'k', userId: credentials[token], scopes: 'sessions', expiresAt: null },
127
+ user: { id: credentials[token], role: 'MEMBER', status: 'ACTIVE' },
75
128
  }
76
129
  : null;
77
130
  })),
78
131
  verifyBearer: sinon_1.default.stub().resolves(null),
79
132
  });
80
- server = yield baseDriver.server({
81
- routeConfiguringFunction: baseDriver.routeConfiguringFunction(driver),
82
- port: 0,
83
- hostname: '127.0.0.1',
84
- basePath: '/wd/hub',
85
- // Appium hands plugins the raw CLI args; the base path here is normalised
86
- // by registerCommandAuth itself.
87
- cliArgs: { basePath: 'wd/hub/' },
88
- serverUpdaters: [
89
- (app, _httpServer, cliArgs) => {
90
- (0, registerCommandAuth_1.registerCommandAuth)(app, cliArgs, {
91
- enabled: () => enabled,
92
- authDisabled: () => false,
93
- verifier,
94
- ownerOf: (id) => __awaiter(this, void 0, void 0, function* () { return (id === LIVE_SESSION ? 'alice' : null); }),
95
- logger: {
96
- info: () => undefined,
97
- warn: (m) => warnings.push(m),
98
- error: () => undefined,
99
- },
100
- });
101
- },
102
- ],
133
+ const deps = {
134
+ enabled: () => enabled,
135
+ authDisabled: () => false,
136
+ verifier,
137
+ ownerOf: (id) => __awaiter(this, void 0, void 0, function* () { var _a; return (_a = OWNERS[id]) !== null && _a !== void 0 ? _a : null; }),
138
+ ownersOf: (ids) => __awaiter(this, void 0, void 0, function* () { return new Map(ids.map((id) => { var _a; return [id, (_a = OWNERS[id]) !== null && _a !== void 0 ? _a : null]; })); }),
139
+ logger: {
140
+ info: (m) => infos.push(m),
141
+ warn: (m) => warnings.push(m),
142
+ error: () => undefined,
143
+ },
144
+ };
145
+ const createServer = http_1.default.createServer;
146
+ const forced = opts.upgrades === 'listener' ? sinon_1.default.stub(http_1.default, 'createServer') : undefined;
147
+ forced === null || forced === void 0 ? void 0 : forced.callsFake(((app) => {
148
+ const s = createServer(app);
149
+ if (typeof s.shouldUpgradeCallback === 'undefined') {
150
+ s.shouldUpgradeCallback = () => true;
151
+ }
152
+ return s;
153
+ }));
154
+ try {
155
+ server = yield baseDriver.server({
156
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction(driver),
157
+ port: 0,
158
+ hostname: '127.0.0.1',
159
+ basePath: '/wd/hub',
160
+ // Appium hands plugins the raw CLI args; the base path here is normalised
161
+ // by registerCommandAuth itself.
162
+ cliArgs: { basePath: 'wd/hub/' },
163
+ serverUpdaters: [
164
+ (app, httpServer, cliArgs) => {
165
+ (0, registerCommandAuth_1.registerCommandAuth)(app, cliArgs, deps);
166
+ (0, registerCommandAuth_1.registerSessionUpgradeGuard)(httpServer, app, cliArgs, deps);
167
+ },
168
+ ],
169
+ });
170
+ }
171
+ finally {
172
+ forced === null || forced === void 0 ? void 0 : forced.restore();
173
+ }
174
+ // Appium's main adds its BiDi handler right after server() resolves.
175
+ const bidi = new ws_1.WebSocketServer({ noServer: true });
176
+ bidi.on('connection', (ws, req) => {
177
+ bidiConnections.push(String(req.url));
178
+ ws.on('error', () => undefined);
179
+ });
180
+ yield booted().addWebSocketHandler('/wd/hub/bidi', bidi);
181
+ yield booted().addWebSocketHandler('/wd/hub/bidi/:sessionId', bidi);
182
+ });
183
+ }
184
+ function upgrade(urlPath, headers = {}) {
185
+ const { port } = booted().address();
186
+ return new Promise((resolve) => {
187
+ const ws = new ws_1.default(`ws://127.0.0.1:${port}${urlPath}`, { headers });
188
+ ws.on('open', () => {
189
+ resolve('open');
190
+ ws.terminate();
191
+ });
192
+ ws.on('unexpected-response', (req, res) => {
193
+ var _a;
194
+ resolve((_a = res.statusCode) !== null && _a !== void 0 ? _a : 0);
195
+ req.destroy();
103
196
  });
197
+ ws.on('error', (err) => resolve(err.message));
104
198
  });
105
199
  }
106
200
  // Appium's HTTP logger writes every request to stdout; quiet it here only.
@@ -130,6 +224,7 @@ describe("per-command auth in Appium 3's own server()", () => {
130
224
  }
131
225
  beforeEach(() => {
132
226
  enabled = true;
227
+ infos = [];
133
228
  warnings = [];
134
229
  });
135
230
  afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
@@ -144,7 +239,7 @@ describe("per-command auth in Appium 3's own server()", () => {
144
239
  }));
145
240
  it("refuses a session command before Appium's route runs", () => __awaiter(void 0, void 0, void 0, function* () {
146
241
  yield boot();
147
- const res = yield (0, supertest_1.default)(booted()).get(`/wd/hub/session/${LIVE_SESSION}/url`);
242
+ const res = yield (0, loopbackRequest_1.default)(booted()).get(`/wd/hub/session/${LIVE_SESSION}/url`);
148
243
  (0, chai_1.expect)(res.status).to.equal(404);
149
244
  (0, chai_1.expect)(res.text).to.equal(EXACT_UNKNOWN_SESSION);
150
245
  (0, chai_1.expect)(commands).to.deep.equal([]);
@@ -152,7 +247,7 @@ describe("per-command auth in Appium 3's own server()", () => {
152
247
  }));
153
248
  it("lets the owner through to Appium's route and the driver", () => __awaiter(void 0, void 0, void 0, function* () {
154
249
  yield boot();
155
- const res = yield (0, supertest_1.default)(booted())
250
+ const res = yield (0, loopbackRequest_1.default)(booted())
156
251
  .get(`/wd/hub/session/${LIVE_SESSION}/url`)
157
252
  .set({ 'x-xenon-access-key': 'ak', 'x-xenon-token': 'alice-token' });
158
253
  (0, chai_1.expect)(res.status).to.equal(200);
@@ -161,7 +256,7 @@ describe("per-command auth in Appium 3's own server()", () => {
161
256
  }));
162
257
  it('lets createSession reach the driver without credentials', () => __awaiter(void 0, void 0, void 0, function* () {
163
258
  yield boot();
164
- yield (0, supertest_1.default)(booted())
259
+ yield (0, loopbackRequest_1.default)(booted())
165
260
  .post('/wd/hub/session')
166
261
  .send({ capabilities: { alwaysMatch: { platformName: 'Fake' } } })
167
262
  .expect(200);
@@ -170,7 +265,7 @@ describe("per-command auth in Appium 3's own server()", () => {
170
265
  it("with the setting off, Appium gives its own unknown-session answer, which the refusal matches but for Appium's stack trace", () => __awaiter(void 0, void 0, void 0, function* () {
171
266
  enabled = false;
172
267
  yield boot();
173
- const res = yield (0, supertest_1.default)(booted()).get('/wd/hub/session/no-such-session/url');
268
+ const res = yield (0, loopbackRequest_1.default)(booted()).get('/wd/hub/session/no-such-session/url');
174
269
  (0, chai_1.expect)(res.status).to.equal(404);
175
270
  (0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
176
271
  (0, chai_1.expect)(res.body.value.error).to.equal(commandAuth_1.UNKNOWN_SESSION_BODY.value.error);
@@ -181,4 +276,64 @@ describe("per-command auth in Appium 3's own server()", () => {
181
276
  (0, chai_1.expect)(res.body.value.stacktrace).to.match(/^NoSuchDriverError: A session is either terminated or not started/);
182
277
  (0, chai_1.expect)(warnings).to.deep.equal([]);
183
278
  }));
279
+ describe("Appium's session listing (GET /appium/sessions)", () => {
280
+ it('answers a caller without credentials an empty list, though Appium produced the full one', () => __awaiter(void 0, void 0, void 0, function* () {
281
+ yield boot();
282
+ const res = yield (0, loopbackRequest_1.default)(booted()).get('/wd/hub/appium/sessions');
283
+ (0, chai_1.expect)(res.status).to.equal(200);
284
+ (0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
285
+ (0, chai_1.expect)(res.text).to.equal('{"value":[]}');
286
+ (0, chai_1.expect)(commands).to.deep.equal(['getAppiumSessions']);
287
+ (0, chai_1.expect)(warnings).to.have.length(1);
288
+ }));
289
+ it('shows the owner only their own session', () => __awaiter(void 0, void 0, void 0, function* () {
290
+ yield boot();
291
+ const res = yield (0, loopbackRequest_1.default)(booted())
292
+ .get('/wd/hub/appium/sessions')
293
+ .set({ 'x-xenon-access-key': 'ak', 'x-xenon-token': 'alice-token' });
294
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [LISTING[0]] });
295
+ (0, chai_1.expect)(infos.some((m) => m.includes('1 of 2'))).to.equal(true);
296
+ }));
297
+ it("with the setting off, answers Appium's listing untouched", () => __awaiter(void 0, void 0, void 0, function* () {
298
+ enabled = false;
299
+ yield boot();
300
+ const res = yield (0, loopbackRequest_1.default)(booted()).get('/wd/hub/appium/sessions');
301
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: LISTING });
302
+ }));
303
+ });
304
+ for (const upgrades of ['native', 'listener']) {
305
+ const how = upgrades === 'listener' || nativelyUsesUpgradeListener()
306
+ ? 'with its upgrade listener'
307
+ : 'with its Express upgrade middleware';
308
+ describe(`the BiDi WebSocket, Appium dispatching upgrades ${how} (${upgrades})`, () => {
309
+ it('refuses a session upgrade without credentials with 404, before Appium attaches it', () => __awaiter(void 0, void 0, void 0, function* () {
310
+ yield boot({ upgrades });
311
+ (0, chai_1.expect)(yield upgrade(`/wd/hub/bidi/${LIVE_SESSION}`)).to.equal(404);
312
+ (0, chai_1.expect)(bidiConnections).to.deep.equal([]);
313
+ (0, chai_1.expect)(warnings).to.have.length(1);
314
+ }));
315
+ it('refuses another user with 404', () => __awaiter(void 0, void 0, void 0, function* () {
316
+ yield boot({ upgrades });
317
+ (0, chai_1.expect)(yield upgrade(`/wd/hub/bidi/${LIVE_SESSION}`, {
318
+ 'x-xenon-access-key': 'ak',
319
+ 'x-xenon-token': 'bob-token',
320
+ })).to.equal(404);
321
+ (0, chai_1.expect)(bidiConnections).to.deep.equal([]);
322
+ }));
323
+ it("lets the owner attach through Appium's own handler", () => __awaiter(void 0, void 0, void 0, function* () {
324
+ yield boot({ upgrades });
325
+ (0, chai_1.expect)(yield upgrade(`/wd/hub/bidi/${LIVE_SESSION}`, {
326
+ 'x-xenon-access-key': 'ak',
327
+ 'x-xenon-token': 'alice-token',
328
+ })).to.equal('open');
329
+ (0, chai_1.expect)(bidiConnections).to.deep.equal([`/wd/hub/bidi/${LIVE_SESSION}`]);
330
+ }));
331
+ it('with the setting off, lets anyone attach, as Appium does', () => __awaiter(void 0, void 0, void 0, function* () {
332
+ enabled = false;
333
+ yield boot({ upgrades });
334
+ (0, chai_1.expect)(yield upgrade(`/wd/hub/bidi/${LIVE_SESSION}`)).to.equal('open');
335
+ (0, chai_1.expect)(warnings).to.deep.equal([]);
336
+ }));
337
+ });
338
+ }
184
339
  });