@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,446 @@
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
+ const chai_1 = require("chai");
49
+ const events_1 = require("events");
50
+ const http_1 = __importDefault(require("http"));
51
+ const path_1 = __importDefault(require("path"));
52
+ const sinon_1 = __importDefault(require("sinon"));
53
+ const ws_1 = __importStar(require("ws"));
54
+ const upgradeRouter_1 = require("../../src/app/ws/upgradeRouter");
55
+ const h264StreamWs_1 = require("../../src/app/ws/h264StreamWs");
56
+ const H264Multiplexer_1 = require("../../src/device-managers/android/H264Multiplexer");
57
+ const commandCaller_1 = require("../../src/middleware/commandCaller");
58
+ const sessionUpgradeGuard_1 = require("../../src/middleware/sessionUpgradeGuard");
59
+ /**
60
+ * The upgrade router (src/app/ws/upgradeRouter.ts) on its own: which handler
61
+ * each upgrade reaches, what it adds to a server, and its copy of Appium's
62
+ * upgrade listener for Node < 22.21, checked against Appium's own dispatch.
63
+ * ws-upgrade-routing-appium-server.spec.ts runs it inside Appium's server().
64
+ */
65
+ /** base-driver's own upgrade dispatch (tryHandleWebSocketUpgrade), from Appium's install. */
66
+ function appiumDispatch() {
67
+ const appiumDir = path_1.default.dirname(require.resolve('appium/package.json'));
68
+ const baseDriverDir = path_1.default.dirname(require.resolve('@appium/base-driver/package.json', { paths: [appiumDir] }));
69
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
70
+ return require(path_1.default.join(baseDriverDir, 'build/lib/express/middleware'));
71
+ }
72
+ function silentAppiumLog() {
73
+ appiumDispatch();
74
+ const support = require.resolve('@appium/support', {
75
+ paths: [path_1.default.dirname(require.resolve('appium/package.json'))],
76
+ });
77
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
78
+ const log = require(require.resolve('@appium/logger', { paths: [support] })).default;
79
+ const level = log.level;
80
+ log.level = 'silent';
81
+ return () => {
82
+ log.level = level;
83
+ };
84
+ }
85
+ const quiet = () => ({
86
+ infos: [],
87
+ warns: [],
88
+ errors: [],
89
+ info(m) {
90
+ this.infos.push(m);
91
+ },
92
+ warn(m) {
93
+ this.warns.push(m);
94
+ },
95
+ error(m) {
96
+ this.errors.push(m);
97
+ },
98
+ });
99
+ function connect(port, urlPath) {
100
+ return new Promise((resolve) => {
101
+ const ws = new ws_1.default(`ws://127.0.0.1:${port}${urlPath}`);
102
+ ws.on('open', () => {
103
+ resolve({ kind: 'open' });
104
+ ws.terminate();
105
+ });
106
+ ws.on('unexpected-response', (req, res) => {
107
+ var _a;
108
+ resolve({ kind: 'http', status: (_a = res.statusCode) !== null && _a !== void 0 ? _a : 0 });
109
+ req.destroy();
110
+ });
111
+ ws.on('error', () => resolve({ kind: 'closed' }));
112
+ });
113
+ }
114
+ function listen(server) {
115
+ return __awaiter(this, void 0, void 0, function* () {
116
+ const sockets = new Set();
117
+ server.on('connection', (s) => {
118
+ sockets.add(s);
119
+ s.on('close', () => sockets.delete(s));
120
+ });
121
+ yield new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
122
+ return {
123
+ port: server.address().port,
124
+ shut: () => __awaiter(this, void 0, void 0, function* () {
125
+ for (const s of sockets)
126
+ s.destroy();
127
+ server.closeAllConnections();
128
+ yield new Promise((resolve) => server.close(() => resolve()));
129
+ }),
130
+ };
131
+ });
132
+ }
133
+ describe('upgrade router', () => {
134
+ describe("its copy of Appium's upgrade dispatch, against Appium's own", () => {
135
+ let restoreLog;
136
+ before(() => {
137
+ restoreLog = silentAppiumLog();
138
+ });
139
+ after(() => restoreLog());
140
+ /** Which handler each dispatch picks, or whether it threw. */
141
+ function outcome(dispatch, url, upgrade, patterns) {
142
+ const mapping = {};
143
+ for (const p of patterns) {
144
+ mapping[p] = { handleUpgrade: sinon_1.default.stub(), emit: sinon_1.default.stub(), name: p };
145
+ }
146
+ const req = { url, headers: upgrade === undefined ? {} : { upgrade } };
147
+ try {
148
+ const handled = dispatch(req, mapping);
149
+ const by = patterns.filter((p) => mapping[p].handleUpgrade.called);
150
+ return `${handled}:${by.join(',')}`;
151
+ }
152
+ catch (error) {
153
+ return `threw ${error.name}`;
154
+ }
155
+ }
156
+ const PATTERNS = [
157
+ '/wd/hub/bidi',
158
+ '/wd/hub/bidi/:sessionId',
159
+ '/ws/session/abc-123/appium/device/logcat',
160
+ '/ws/session/abc-123/appium/device/syslog',
161
+ ];
162
+ const CASES = [
163
+ ['/wd/hub/bidi', 'websocket'],
164
+ ['/wd/hub/bidi/', 'websocket'],
165
+ ['/wd/hub/bidi/abc-123', 'websocket'],
166
+ ['/wd/hub/bidi/abc-123/', 'websocket'],
167
+ ['/WD/HUB/BIDI/abc-123', 'websocket'],
168
+ ['/wd/hub/bidi/abc-123?x=1', 'websocket'],
169
+ ['/wd/hub/bidi/abc-123/extra', 'websocket'],
170
+ ['/wd/hub/bidi//', 'websocket'],
171
+ ['/wd/hub/./bidi/abc-123', 'websocket'],
172
+ ['/wd/hub/x/../bidi/abc-123', 'websocket'],
173
+ ['/wd/hub/%2e%2e/wd/hub/bidi/abc', 'websocket'],
174
+ ['/wd/hub/bidi/a%20b', 'websocket'],
175
+ ['/wd/hub/bidi/%E0%A4%A', 'websocket'],
176
+ ['/wd/hub/bidi/abc-123', 'WebSocket'],
177
+ ['/wd/hub/bidi/abc-123', 'h2c'],
178
+ ['/wd/hub/bidi/abc-123', undefined],
179
+ ['http://example.com/wd/hub/bidi/abc-123', 'websocket'],
180
+ ['/ws/session/abc-123/appium/device/logcat', 'websocket'],
181
+ ['/ws/session/ABC-123/appium/device/logcat', 'websocket'],
182
+ ['/ws/session/abc-123/appium/device/logcat/', 'websocket'],
183
+ ['/ws/session/abc-123/appium/device/syslog', 'websocket'],
184
+ ['/ws/session/other/appium/device/logcat', 'websocket'],
185
+ ['/xenon/api/control/u1/stream/h264?ticket=t', 'websocket'],
186
+ ['/socket.io/?EIO=4&transport=websocket', 'websocket'],
187
+ ['', 'websocket'],
188
+ ];
189
+ for (const [url, upgrade] of CASES) {
190
+ it(`picks the same handler for ${JSON.stringify(url)} (Upgrade: ${upgrade})`, () => {
191
+ const logger = quiet();
192
+ const theirs = outcome((req, mapping) => appiumDispatch().tryHandleWebSocketUpgrade(req, {}, Buffer.alloc(0), mapping), url, upgrade, PATTERNS);
193
+ const ours = outcome((req, mapping) => (0, upgradeRouter_1.dispatchToAppiumHandlers)(req, {}, Buffer.alloc(0), mapping, logger), url, upgrade, PATTERNS);
194
+ (0, chai_1.expect)(ours).to.equal(theirs);
195
+ });
196
+ }
197
+ it('takes the first matching handler, in registration order, as Appium does', () => {
198
+ const first = { handleUpgrade: sinon_1.default.stub(), emit: sinon_1.default.stub() };
199
+ const second = { handleUpgrade: sinon_1.default.stub(), emit: sinon_1.default.stub() };
200
+ const handled = (0, upgradeRouter_1.dispatchToAppiumHandlers)({ url: '/wd/hub/bidi/s1', headers: { upgrade: 'websocket' } }, {}, Buffer.alloc(0), { '/wd/hub/bidi/:id': first, '/wd/hub/bidi/s1': second }, quiet());
201
+ (0, chai_1.expect)(handled).to.equal(true);
202
+ (0, chai_1.expect)(first.handleUpgrade.calledOnce).to.equal(true);
203
+ (0, chai_1.expect)(second.handleUpgrade.called).to.equal(false);
204
+ });
205
+ it('emits connection on the handler, with the request, as Appium does', () => {
206
+ const ws = {};
207
+ const wss = {
208
+ handleUpgrade: sinon_1.default.stub().callsFake((_r, _s, _h, cb) => cb(ws)),
209
+ emit: sinon_1.default.stub(),
210
+ };
211
+ const req = { url: '/wd/hub/bidi', headers: { upgrade: 'websocket' } };
212
+ (0, upgradeRouter_1.dispatchToAppiumHandlers)(req, {}, Buffer.alloc(0), { '/wd/hub/bidi': wss }, quiet());
213
+ (0, chai_1.expect)(wss.emit.calledOnceWithExactly('connection', ws, req)).to.equal(true);
214
+ });
215
+ it('skips a pattern in syntax it does not speak, and says so once', () => {
216
+ (0, chai_1.expect)((0, upgradeRouter_1.appiumPatternMatcher)('/ws/{:optional}')).to.equal(null);
217
+ (0, chai_1.expect)((0, upgradeRouter_1.appiumPatternMatcher)('/ws/*rest')).to.equal(null);
218
+ const logger = quiet();
219
+ const wss = { handleUpgrade: sinon_1.default.stub(), emit: sinon_1.default.stub() };
220
+ for (let i = 0; i < 2; i++) {
221
+ (0, upgradeRouter_1.dispatchToAppiumHandlers)({ url: '/ws/x', headers: { upgrade: 'websocket' } }, {}, Buffer.alloc(0), { '/ws/*rest': wss }, logger);
222
+ }
223
+ (0, chai_1.expect)(wss.handleUpgrade.called).to.equal(false);
224
+ (0, chai_1.expect)(logger.warns).to.have.length(1);
225
+ });
226
+ it('matches literal text literally, regex characters included', () => {
227
+ const matches = (0, upgradeRouter_1.appiumPatternMatcher)('/base.path/bidi/:id');
228
+ (0, chai_1.expect)(matches === null || matches === void 0 ? void 0 : matches('/base.path/bidi/s1')).to.equal(true);
229
+ (0, chai_1.expect)(matches === null || matches === void 0 ? void 0 : matches('/basexpath/bidi/s1')).to.equal(false);
230
+ });
231
+ });
232
+ describe('what it adds to a server', () => {
233
+ function fakeServer(opts) {
234
+ const server = new events_1.EventEmitter();
235
+ if (opts.callback)
236
+ server.shouldUpgradeCallback = () => true;
237
+ for (let i = 0; i < opts.listeners; i++)
238
+ server.on('upgrade', () => undefined);
239
+ return server;
240
+ }
241
+ it('adds nothing when Appium already listens (Node >= 22.21 / 24.9)', () => {
242
+ const server = fakeServer({ callback: true, listeners: 1 });
243
+ const router = new upgradeRouter_1.UpgradeRouter(server, quiet());
244
+ (0, chai_1.expect)(router.appiumPath).to.equal('appium-listener');
245
+ (0, chai_1.expect)(server.listenerCount('upgrade')).to.equal(1);
246
+ });
247
+ it("adds Appium's listener when this Node has no shouldUpgradeCallback (Node < 22.21)", () => {
248
+ const server = fakeServer({ callback: false, listeners: 0 });
249
+ const router = new upgradeRouter_1.UpgradeRouter(server, quiet());
250
+ (0, chai_1.expect)(router.appiumPath).to.equal('router-listener');
251
+ (0, chai_1.expect)(server.listenerCount('upgrade')).to.equal(1);
252
+ });
253
+ it('adds it on Node < 22.21 even when another listener is already there', () => {
254
+ // Node hands no upgrade to Appium's Express middleware once any listener exists.
255
+ const server = fakeServer({ callback: false, listeners: 1 });
256
+ (0, chai_1.expect)(new upgradeRouter_1.UpgradeRouter(server, quiet()).appiumPath).to.equal('router-listener');
257
+ (0, chai_1.expect)(server.listenerCount('upgrade')).to.equal(2);
258
+ });
259
+ it('adds a listener to a server with none, or no Node would emit upgrade at all', () => {
260
+ const server = fakeServer({ callback: true, listeners: 0 });
261
+ (0, chai_1.expect)(new upgradeRouter_1.UpgradeRouter(server, quiet()).appiumPath).to.equal('router-listener');
262
+ (0, chai_1.expect)(server.listenerCount('upgrade')).to.equal(1);
263
+ });
264
+ it('installs once per server', () => {
265
+ const server = http_1.default.createServer();
266
+ const router = (0, upgradeRouter_1.upgradeRouterFor)(server);
267
+ const emit = server.emit;
268
+ const listeners = server.listenerCount('upgrade');
269
+ (0, chai_1.expect)((0, upgradeRouter_1.upgradeRouterFor)(server)).to.equal(router);
270
+ (0, chai_1.expect)(server.emit).to.equal(emit);
271
+ (0, chai_1.expect)(server.listenerCount('upgrade')).to.equal(listeners);
272
+ });
273
+ it('leaves every other event alone', () => {
274
+ const server = fakeServer({ callback: true, listeners: 1 });
275
+ new upgradeRouter_1.UpgradeRouter(server, quiet());
276
+ const seen = [];
277
+ server.on('request', (a) => seen.push(a));
278
+ (0, chai_1.expect)(server.emit('request', 'r')).to.equal(true);
279
+ (0, chai_1.expect)(seen).to.deep.equal(['r']);
280
+ });
281
+ });
282
+ describe('on a real server', () => {
283
+ let shut;
284
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
285
+ sinon_1.default.restore();
286
+ yield (shut === null || shut === void 0 ? void 0 : shut());
287
+ shut = undefined;
288
+ }));
289
+ /** A server shaped like Appium's on Node >= 22.21: its listener, destroying what it can't place. */
290
+ function appiumLikeServer(mapping) {
291
+ const server = http_1.default.createServer();
292
+ if (typeof server.shouldUpgradeCallback === 'undefined')
293
+ server.shouldUpgradeCallback = () => true;
294
+ server.webSocketsMapping = mapping;
295
+ const appiumSaw = [];
296
+ server.on('upgrade', (req, socket, head) => {
297
+ appiumSaw.push(String(req.url));
298
+ if (!(0, upgradeRouter_1.dispatchToAppiumHandlers)(req, socket, head, server.webSocketsMapping, quiet())) {
299
+ socket.destroy();
300
+ }
301
+ });
302
+ return { server: server, appiumSaw };
303
+ }
304
+ it("gives a route's upgrades to the route alone, and every other upgrade to the listeners", () => __awaiter(void 0, void 0, void 0, function* () {
305
+ const bidi = new ws_1.WebSocketServer({ noServer: true });
306
+ const { server, appiumSaw } = appiumLikeServer({ '/bidi/:id': bidi });
307
+ const xenon = new ws_1.WebSocketServer({ noServer: true });
308
+ const routed = [];
309
+ (0, upgradeRouter_1.upgradeRouterFor)(server).add({
310
+ name: 'test',
311
+ matches: (req) => String(req.url).startsWith('/xenon/'),
312
+ handle: (req, socket, head) => {
313
+ routed.push(String(req.url));
314
+ xenon.handleUpgrade(req, socket, head, () => undefined);
315
+ },
316
+ });
317
+ const listening = yield listen(server);
318
+ shut = listening.shut;
319
+ (0, chai_1.expect)(yield connect(listening.port, '/xenon/x')).to.deep.equal({ kind: 'open' });
320
+ (0, chai_1.expect)(yield connect(listening.port, '/bidi/s1')).to.deep.equal({ kind: 'open' });
321
+ (0, chai_1.expect)(routed).to.deep.equal(['/xenon/x']);
322
+ (0, chai_1.expect)(appiumSaw).to.deep.equal(['/bidi/s1']);
323
+ }));
324
+ it('adopts the upgrade listener an attach adds, and calls it only for its own path', () => __awaiter(void 0, void 0, void 0, function* () {
325
+ const { server, appiumSaw } = appiumLikeServer({});
326
+ const adoptedSaw = [];
327
+ const wss = new ws_1.WebSocketServer({ noServer: true });
328
+ const result = (0, upgradeRouter_1.upgradeRouterFor)(server).adopt('adopted', (req) => String(req.url).startsWith('/mine/'), () => {
329
+ server.on('upgrade', (req, socket, head) => {
330
+ adoptedSaw.push(String(req.url));
331
+ // Like engine.io: anything not its own is ended.
332
+ if (!String(req.url).startsWith('/mine/'))
333
+ return socket.end();
334
+ wss.handleUpgrade(req, socket, head, () => undefined);
335
+ });
336
+ return 'attached';
337
+ });
338
+ (0, chai_1.expect)(result).to.equal('attached');
339
+ (0, chai_1.expect)(server.listenerCount('upgrade')).to.equal(1); // Appium's, not the adopted one
340
+ const listening = yield listen(server);
341
+ shut = listening.shut;
342
+ (0, chai_1.expect)(yield connect(listening.port, '/mine/x')).to.deep.equal({ kind: 'open' });
343
+ (0, chai_1.expect)(yield connect(listening.port, '/other')).to.deep.equal({ kind: 'closed' });
344
+ (0, chai_1.expect)(adoptedSaw).to.deep.equal(['/mine/x']);
345
+ (0, chai_1.expect)(appiumSaw).to.deep.equal(['/other']);
346
+ }));
347
+ it('adds no route when the attach adds no listener', () => {
348
+ const { server } = appiumLikeServer({});
349
+ const router = (0, upgradeRouter_1.upgradeRouterFor)(server);
350
+ router.adopt('nothing', () => true, () => undefined);
351
+ (0, chai_1.expect)(router.routeFor({ url: '/anything' })).to.equal(undefined);
352
+ });
353
+ it("closes the socket and keeps the process up when a route's handler throws", () => __awaiter(void 0, void 0, void 0, function* () {
354
+ const { server } = appiumLikeServer({});
355
+ const router = (0, upgradeRouter_1.upgradeRouterFor)(server);
356
+ const errors = sinon_1.default.stub(router.logger, 'error');
357
+ router.add({
358
+ name: 'broken',
359
+ matches: () => true,
360
+ handle: () => {
361
+ throw new Error('boom');
362
+ },
363
+ });
364
+ const listening = yield listen(server);
365
+ shut = listening.shut;
366
+ (0, chai_1.expect)(yield connect(listening.port, '/x')).to.deep.equal({ kind: 'closed' });
367
+ (0, chai_1.expect)(errors.calledOnce).to.equal(true);
368
+ (0, chai_1.expect)(String(errors.firstCall.args[0])).to.match(/broken.*boom/);
369
+ }));
370
+ it('closes the socket and keeps the process up on a malformed udid in a Xenon path', () => __awaiter(void 0, void 0, void 0, function* () {
371
+ // parseH264WsPath decodes the udid, and decodeURIComponent throws on a
372
+ // malformed escape. Out of a plain `upgrade` listener that ended the process.
373
+ const { server } = appiumLikeServer({});
374
+ const redeem = sinon_1.default.stub().resolves({ actorId: 'a' });
375
+ (0, h264StreamWs_1.attachH264Ws)(server, { redeem, startStream: () => __awaiter(void 0, void 0, void 0, function* () { return new H264Multiplexer_1.H264Multiplexer(); }) });
376
+ sinon_1.default.stub((0, upgradeRouter_1.upgradeRouterFor)(server).logger, 'error');
377
+ const listening = yield listen(server);
378
+ shut = listening.shut;
379
+ (0, chai_1.expect)(yield connect(listening.port, '/xenon/api/control/%E0%A4%A/stream/h264?ticket=t')).to.deep.equal({ kind: 'closed' });
380
+ (0, chai_1.expect)(yield connect(listening.port, '/xenon/api/control/ok/stream/h264?ticket=t')).to.deep.equal({ kind: 'open' });
381
+ (0, chai_1.expect)(redeem.callCount).to.equal(1);
382
+ }));
383
+ it("closes the socket and keeps the process up when Appium's own dispatch throws", () => __awaiter(void 0, void 0, void 0, function* () {
384
+ // path-to-regexp decodes a matched parameter; a malformed escape throws
385
+ // out of Appium's listener on Node >= 22.21.
386
+ const restoreLog = silentAppiumLog();
387
+ try {
388
+ const server = http_1.default.createServer();
389
+ if (typeof server.shouldUpgradeCallback === 'undefined')
390
+ server.shouldUpgradeCallback = () => true;
391
+ const mapping = { '/bidi/:id': new ws_1.WebSocketServer({ noServer: true }) };
392
+ server.on('upgrade', (req, socket, head) => {
393
+ if (!appiumDispatch().tryHandleWebSocketUpgrade(req, socket, head, mapping))
394
+ socket.destroy();
395
+ });
396
+ sinon_1.default.stub((0, upgradeRouter_1.upgradeRouterFor)(server).logger, 'error');
397
+ const listening = yield listen(server);
398
+ shut = listening.shut;
399
+ (0, chai_1.expect)(yield connect(listening.port, '/bidi/%E0%A4%A')).to.deep.equal({ kind: 'closed' });
400
+ (0, chai_1.expect)(yield connect(listening.port, '/bidi/s1')).to.deep.equal({ kind: 'open' });
401
+ }
402
+ finally {
403
+ restoreLog();
404
+ }
405
+ }));
406
+ for (const order of ['guard first', 'router first']) {
407
+ it(`keeps the session guard in front of Appium, and out of Xenon's way (${order})`, () => __awaiter(void 0, void 0, void 0, function* () {
408
+ const bidi = new ws_1.WebSocketServer({ noServer: true });
409
+ const { server, appiumSaw } = appiumLikeServer({ '/bidi/:id': bidi });
410
+ const ownerOf = sinon_1.default.stub().resolves('alice');
411
+ const guard = (0, sessionUpgradeGuard_1.createSessionUpgradeGuard)('', {
412
+ enabled: () => true,
413
+ authDisabled: () => false,
414
+ verifier: new commandCaller_1.CommandCallerVerifier({
415
+ verifyKeyPair: sinon_1.default.stub().resolves(null),
416
+ verifyBearer: sinon_1.default.stub().resolves(null),
417
+ }),
418
+ ownerOf,
419
+ ownersOf: () => __awaiter(void 0, void 0, void 0, function* () { return new Map(); }),
420
+ logger: { info: () => undefined, warn: () => undefined, error: () => undefined },
421
+ });
422
+ const route = {
423
+ name: 'xenon',
424
+ matches: (req) => String(req.url).startsWith('/xenon/'),
425
+ handle: (req, socket, head) => new ws_1.WebSocketServer({ noServer: true }).handleUpgrade(req, socket, head, () => undefined),
426
+ };
427
+ if (order === 'guard first') {
428
+ (0, sessionUpgradeGuard_1.guardUpgradeEvents)(server, guard);
429
+ (0, upgradeRouter_1.upgradeRouterFor)(server).add(route);
430
+ }
431
+ else {
432
+ (0, upgradeRouter_1.upgradeRouterFor)(server).add(route);
433
+ (0, sessionUpgradeGuard_1.guardUpgradeEvents)(server, guard);
434
+ }
435
+ const listening = yield listen(server);
436
+ shut = listening.shut;
437
+ (0, chai_1.expect)(yield connect(listening.port, '/bidi/s1')).to.deep.equal({
438
+ kind: 'http',
439
+ status: 404,
440
+ });
441
+ (0, chai_1.expect)(yield connect(listening.port, '/xenon/x')).to.deep.equal({ kind: 'open' });
442
+ (0, chai_1.expect)(appiumSaw).to.deep.equal([]);
443
+ }));
444
+ }
445
+ });
446
+ });
@@ -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 users_1 = require("../../src/app/routers/users");
21
21
  const typedi_1 = require("typedi");
22
22
  const UserService_1 = require("../../src/services/UserService");
@@ -36,7 +36,7 @@ describe('/users router', () => {
36
36
  it('GET /users returns the listUsers result, filtered by caller role', () => __awaiter(void 0, void 0, void 0, function* () {
37
37
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'listUsers').resolves([{ id: 'u1' }]);
38
38
  const app = appWithAuth({ userId: 'admin', role: 'ADMIN' });
39
- const r = yield (0, supertest_1.default)(app).get('/users');
39
+ const r = yield (0, loopbackRequest_1.default)(app).get('/users');
40
40
  (0, chai_1.expect)(r.status).to.equal(200);
41
41
  (0, chai_1.expect)(r.body[0].id).to.equal('u1');
42
42
  }));
@@ -50,23 +50,26 @@ describe('/users router', () => {
50
50
  accessKey: 'xen_NEW0000NEW00',
51
51
  });
52
52
  const app = appWithAuth({ userId: 'admin', role: 'ADMIN' });
53
- const r = yield (0, supertest_1.default)(app)
53
+ const r = yield (0, loopbackRequest_1.default)(app)
54
54
  .post('/users')
55
55
  .send({ email: 'new@x.local', name: 'New', role: 'MEMBER' });
56
56
  (0, chai_1.expect)(r.status).to.equal(201);
57
57
  (0, chai_1.expect)(r.body.id).to.equal('new-user');
58
58
  (0, chai_1.expect)(r.body.temporaryPassword).to.match(/^[A-Za-z0-9_-]{12,}$/);
59
+ // The admin hands the new user their key; the logger's redaction of
60
+ // accessKey must not reach a response.
61
+ (0, chai_1.expect)(r.body.accessKey).to.equal('xen_NEW0000NEW00');
59
62
  }));
60
63
  it('POST /users — ADMIN cannot create an ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
61
64
  const app = appWithAuth({ userId: 'admin', role: 'ADMIN' });
62
- const r = yield (0, supertest_1.default)(app)
65
+ const r = yield (0, loopbackRequest_1.default)(app)
63
66
  .post('/users')
64
67
  .send({ email: 'wannabe@x.local', name: 'Wannabe', role: 'ADMIN' });
65
68
  (0, chai_1.expect)(r.status).to.equal(403);
66
69
  }));
67
70
  it('PATCH /users/:id — refuses self role-change', () => __awaiter(void 0, void 0, void 0, function* () {
68
71
  const app = appWithAuth({ userId: 'me', role: 'SUPER_ADMIN' });
69
- const r = yield (0, supertest_1.default)(app).patch('/users/me').send({ role: 'MEMBER' });
72
+ const r = yield (0, loopbackRequest_1.default)(app).patch('/users/me').send({ role: 'MEMBER' });
70
73
  (0, chai_1.expect)(r.status).to.equal(400);
71
74
  (0, chai_1.expect)(r.body.error).to.match(/role-change yourself/);
72
75
  }));
@@ -80,7 +83,7 @@ describe('/users router', () => {
80
83
  status: 'ACTIVE',
81
84
  });
82
85
  const app = appWithAuth({ userId: 'admin', role: 'ADMIN' });
83
- const r = yield (0, supertest_1.default)(app).patch('/users/t1').send({ email: 'evil@x.local', name: 'NewName' });
86
+ const r = yield (0, loopbackRequest_1.default)(app).patch('/users/t1').send({ email: 'evil@x.local', name: 'NewName' });
84
87
  (0, chai_1.expect)(r.status).to.equal(200);
85
88
  const passedPatch = update.firstCall.args[1];
86
89
  (0, chai_1.expect)(passedPatch).to.not.have.property('email');
@@ -88,7 +91,7 @@ describe('/users router', () => {
88
91
  }));
89
92
  it('DELETE /users/:id — refuses self-delete', () => __awaiter(void 0, void 0, void 0, function* () {
90
93
  const app = appWithAuth({ userId: 'me', role: 'SUPER_ADMIN' });
91
- const r = yield (0, supertest_1.default)(app).delete('/users/me');
94
+ const r = yield (0, loopbackRequest_1.default)(app).delete('/users/me');
92
95
  (0, chai_1.expect)(r.status).to.equal(400);
93
96
  (0, chai_1.expect)(r.body.error).to.match(/delete yourself/);
94
97
  }));
@@ -98,7 +101,7 @@ describe('/users router', () => {
98
101
  .resolves({ id: 'last-sa', role: 'SUPER_ADMIN' });
99
102
  sinon_1.default.stub(prisma_1.prisma.user, 'count').resolves(0);
100
103
  const app = appWithAuth({ userId: 'me', role: 'SUPER_ADMIN' });
101
- const r = yield (0, supertest_1.default)(app).delete('/users/last-sa');
104
+ const r = yield (0, loopbackRequest_1.default)(app).delete('/users/last-sa');
102
105
  (0, chai_1.expect)(r.status).to.equal(400);
103
106
  (0, chai_1.expect)(r.body.error).to.match(/last active super-admin/);
104
107
  }));
@@ -23,6 +23,7 @@ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
23
23
  const UserService_1 = require("../../src/services/UserService");
24
24
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
25
25
  const verifyCredential_1 = require("../../src/middleware/verifyCredential");
26
+ const container_registration_1 = require("../helpers/container-registration");
26
27
  /**
27
28
  * The credential checks authMiddleware applies to the header pair and to a
28
29
  * Bearer JWT, pulled out so the WebDriver per-command check verifies exactly
@@ -36,7 +37,9 @@ describe('verifyCredential', () => {
36
37
  let dir;
37
38
  let keys;
38
39
  const active = { id: 'u1', role: 'MEMBER', status: 'ACTIVE' };
40
+ let restoreContainer;
39
41
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
42
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, UserService_1.UserService, ApiKeyService_1.ApiKeyService);
40
43
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-verify-cred-'));
41
44
  keys = new JwtKeyService_1.JwtKeyService();
42
45
  yield keys.init(dir);
@@ -46,7 +49,7 @@ describe('verifyCredential', () => {
46
49
  }));
47
50
  afterEach(() => {
48
51
  sinon_1.default.restore();
49
- typedi_1.Container.reset();
52
+ restoreContainer();
50
53
  fs_1.default.rmSync(dir, { recursive: true, force: true });
51
54
  });
52
55
  describe('verifyBearerCredential', () => {
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const express_1 = __importDefault(require("express"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
+ const typedi_1 = require("typedi");
20
+ const config_1 = require("../../src/config");
21
+ const index_1 = require("../../src/app/index");
22
+ const PluginContext_1 = require("../../src/PluginContext");
23
+ /**
24
+ * GET /xenon/api/webdriver: where this server's WebDriver API lives. A hub
25
+ * asks a node before sending it a session or a command, because a phone's
26
+ * `host` names only the node's origin and the two need not share a base path.
27
+ * The hub holds no credentials for the node, so the answer needs no login;
28
+ * the base path is part of every client's URL anyway.
29
+ *
30
+ * Driven through the real /xenon/api stack (createRouter + authMiddleware).
31
+ */
32
+ describe('GET /xenon/api/webdriver', () => {
33
+ let app;
34
+ let savedAuthDisabled;
35
+ let savedBasePath;
36
+ before(() => {
37
+ app = (0, express_1.default)();
38
+ app.use('/xenon', (0, index_1.createRouter)({ bindHostOrIp: '127.0.0.1', enableDashboard: false }));
39
+ });
40
+ beforeEach(() => {
41
+ savedAuthDisabled = config_1.config.authDisabled;
42
+ config_1.config.authDisabled = false;
43
+ savedBasePath = typedi_1.Container.get(PluginContext_1.PluginContext).nodeBasePath;
44
+ });
45
+ afterEach(() => {
46
+ config_1.config.authDisabled = savedAuthDisabled;
47
+ typedi_1.Container.get(PluginContext_1.PluginContext).nodeBasePath = savedBasePath;
48
+ });
49
+ it("answers this server's base path, normalised as Appium does, without a login", () => __awaiter(void 0, void 0, void 0, function* () {
50
+ typedi_1.Container.get(PluginContext_1.PluginContext).nodeBasePath = 'wd/hub/';
51
+ const res = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/webdriver');
52
+ (0, chai_1.expect)(res.status).to.equal(200);
53
+ (0, chai_1.expect)(res.body).to.deep.equal({ basePath: '/wd/hub' });
54
+ }));
55
+ it("answers Appium 3's default, no base path, as an empty one", () => __awaiter(void 0, void 0, void 0, function* () {
56
+ typedi_1.Container.get(PluginContext_1.PluginContext).nodeBasePath = '';
57
+ const res = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/webdriver');
58
+ (0, chai_1.expect)(res.body).to.deep.equal({ basePath: '' });
59
+ }));
60
+ it('while the rest of /xenon/api still needs one', () => __awaiter(void 0, void 0, void 0, function* () {
61
+ const res = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/cliArgs');
62
+ (0, chai_1.expect)(res.status).to.equal(401);
63
+ }));
64
+ });