@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
@@ -1,4 +1,4 @@
1
- import{c as w,b as k,r as a,j as e,$ as C}from"./index-BR1wTIPC.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-Drd7ifJx.js";import{f as U}from"./time-B8FIgwPE.js";import{P}from"./page-header-CP2UkykG.js";import{B as y}from"./button-xtAhYR-f.js";import{I as N}from"./input-DejcCiTI.js";import{S as g}from"./select-Uc9E5pye.js";import{P as B}from"./plus-BTD0dnQ0.js";import{T as L}from"./trash-2-BorJv5_u.js";import"./utils-CDN07tui.js";/**
1
+ import{c as w,b as k,r as a,j as e,$ as C}from"./index-3uxl3o7K.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-BU0nXIVe.js";import{f as U}from"./time-B8FIgwPE.js";import{P}from"./page-header-CfR4Sm9V.js";import{B as y}from"./button-_Y78wXiP.js";import{I as N}from"./input-CzTWvUXZ.js";import{S as g}from"./select-Cqx7pEou.js";import{P as B}from"./plus-DOS8P9CQ.js";import{T as L}from"./trash-2-BV4hSmXJ.js";import"./utils-CDN07tui.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
1
- import{c as o}from"./index-BR1wTIPC.js";/**
1
+ import{c as o}from"./index-3uxl3o7K.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1 +1 @@
1
- import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-BR1wTIPC.js";/* empty css */import{F as S}from"./FieldGroup-BduMPM9K.js";import{P as O}from"./page-header-CP2UkykG.js";import{T as V}from"./trash-2-BorJv5_u.js";import{P as x}from"./plus-BTD0dnQ0.js";import{C as B}from"./circle-x-CZH6t9z-.js";import{C as H}from"./circle-check-CZdg8mMa.js";import{Z as I}from"./zap-4yN8Jdfw.js";import{A as J}from"./activity-D17TUloi.js";const C=[{id:"device_offline",label:"Device offline",icon:U,tone:"red"},{id:"device_new",label:"New device",icon:x,tone:"green"},{id:"session_failed",label:"Session failed",icon:B,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:D,tone:"green"}],X=["udid","host","name","sessionId","failureReason","eventType","platform"],ae=()=>{const{toast:o}=P(),[g,T]=r.useState([]),[A,w]=r.useState(!0),[l,j]=r.useState(""),[d,k]=r.useState(["device_offline","session_failed"]),[h,f]=r.useState(!1),[v,N]=r.useState(!1),[n,_]=r.useState(!1),[y,m]=r.useState("");r.useEffect(()=>{p()},[]);const p=async()=>{w(!0);try{const s=await c.getWebhookConfigs();T(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{w(!1)}},z=async()=>{if(!l.trim()){o("Webhook URL is required.","error");return}if(d.length===0){o("Select at least one trigger event.","error");return}f(!0);try{await c.addWebhookConfig(l.trim(),d,"slack",y||void 0),j(""),m(""),_(!1),k(["device_offline","session_failed"]),await p(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{f(!1)}},E=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await c.deleteWebhookConfig(s),await p(),o("Webhook removed.","success")}catch(a){console.error("Failed to delete webhook",a),o(`Failed to delete: ${a.message||"unknown error"}`,"error")}},L=async()=>{if(l.trim()){N(!0);try{await c.testWebhook(l.trim(),"slack"),o("Test payload delivered.","success")}catch(s){o(`Test failed: ${s.message||"check the URL"}`,"error")}finally{N(!1)}}},$=s=>{k(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},R=s=>{m(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(O,{icon:u,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[A?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(b,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):g.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(u,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No webhooks configured"}),e.jsx("p",{className:"empty-state__copy",children:"Wire up a Slack incoming webhook or any HTTPS endpoint to receive alerts when devices go offline, new devices register, or sessions fail. Configure your first webhook below."})]}):e.jsx("div",{className:"webhook-list-grid",children:g.map(s=>{const t=(()=>{try{return JSON.parse(s.events)}catch{return[]}})();return e.jsxs("div",{className:"webhook-row-card",children:[e.jsxs("div",{className:"webhook-row-card__header",children:[e.jsx("span",{className:`pill-chip ${s.payloadTemplate?"pill-chip--admin":"pill-chip--scope"}`,children:s.payloadTemplate?"CUSTOM":"SLACK"}),e.jsx("span",{className:"webhook-row-card__url",title:s.url,children:s.url}),e.jsxs("button",{type:"button",className:"row-action row-action--danger",onClick:()=>E(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(V,{size:13}),e.jsx("span",{children:"Remove"})]})]}),e.jsx("div",{className:"webhook-row-card__events",children:t.length===0?e.jsx("span",{className:"webhook-row-card__no-events",children:"No triggers"}):t.map(a=>{const i=C.find(F=>F.id===a),W=(i==null?void 0:i.icon)??u;return e.jsxs("span",{className:`event-pill event-pill--${(i==null?void 0:i.tone)||"gray"}`,children:[e.jsx(W,{size:11}),(i==null?void 0:i.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(x,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(S,{label:"Webhook URL",description:"The endpoint we POST webhook events to.",htmlFor:"webhook-url",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{id:"webhook-url",type:"url",placeholder:"https://hooks.slack.com/services/...",value:l,onChange:s=>j(s.target.value)})})}),e.jsx(S,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:C.map(s=>{const t=d.includes(s.id),a=s.icon;return e.jsxs("button",{type:"button",role:"checkbox","aria-checked":t,className:`event-toggle event-toggle--${s.tone} ${t?"is-selected":""}`,onClick:()=>$(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(H,{size:14,className:"event-toggle__check"})]},s.id)})})}),e.jsxs("div",{className:"template-section",children:[e.jsxs("button",{type:"button",className:"template-section__toggle",onClick:()=>_(!n),"aria-expanded":n,children:[e.jsx(I,{size:14,className:n?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:n?"−":"+"})]}),n&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a variable below to insert it."}),e.jsx("div",{className:"template-editor__chips",children:X.map(s=>e.jsx("button",{type:"button",className:"template-editor__chip",onClick:()=>R(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:y,onChange:s=>m(s.target.value),rows:4})]})]})]}),e.jsxs("div",{className:"add-webhook-card__footer",children:[e.jsxs("button",{type:"button",className:"page-header-action page-header-action--ghost",onClick:L,disabled:!l.trim()||v,children:[v?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(J,{size:14}),e.jsx("span",{children:"Test payload"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:z,disabled:h||!l.trim(),children:[h?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(x,{size:14}),e.jsx("span",{children:h?"Saving…":"Save webhook"})]})]})]})]})]})};export{ae as WebhookSettings};
1
+ import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-3uxl3o7K.js";/* empty css */import{F as S}from"./FieldGroup-5bCutWrs.js";import{P as O}from"./page-header-CfR4Sm9V.js";import{T as V}from"./trash-2-BV4hSmXJ.js";import{P as x}from"./plus-DOS8P9CQ.js";import{C as B}from"./circle-x-B4IOD1DE.js";import{C as H}from"./circle-check-D3Qq0tKp.js";import{Z as I}from"./zap-B2bH2vKX.js";import{A as J}from"./activity-DxRFWvkN.js";const C=[{id:"device_offline",label:"Device offline",icon:U,tone:"red"},{id:"device_new",label:"New device",icon:x,tone:"green"},{id:"session_failed",label:"Session failed",icon:B,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:D,tone:"green"}],X=["udid","host","name","sessionId","failureReason","eventType","platform"],ae=()=>{const{toast:o}=P(),[g,T]=r.useState([]),[A,w]=r.useState(!0),[l,j]=r.useState(""),[d,k]=r.useState(["device_offline","session_failed"]),[h,f]=r.useState(!1),[v,N]=r.useState(!1),[n,_]=r.useState(!1),[y,m]=r.useState("");r.useEffect(()=>{p()},[]);const p=async()=>{w(!0);try{const s=await c.getWebhookConfigs();T(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{w(!1)}},z=async()=>{if(!l.trim()){o("Webhook URL is required.","error");return}if(d.length===0){o("Select at least one trigger event.","error");return}f(!0);try{await c.addWebhookConfig(l.trim(),d,"slack",y||void 0),j(""),m(""),_(!1),k(["device_offline","session_failed"]),await p(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{f(!1)}},E=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await c.deleteWebhookConfig(s),await p(),o("Webhook removed.","success")}catch(a){console.error("Failed to delete webhook",a),o(`Failed to delete: ${a.message||"unknown error"}`,"error")}},L=async()=>{if(l.trim()){N(!0);try{await c.testWebhook(l.trim(),"slack"),o("Test payload delivered.","success")}catch(s){o(`Test failed: ${s.message||"check the URL"}`,"error")}finally{N(!1)}}},$=s=>{k(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},R=s=>{m(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(O,{icon:u,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[A?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(b,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):g.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(u,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No webhooks configured"}),e.jsx("p",{className:"empty-state__copy",children:"Wire up a Slack incoming webhook or any HTTPS endpoint to receive alerts when devices go offline, new devices register, or sessions fail. Configure your first webhook below."})]}):e.jsx("div",{className:"webhook-list-grid",children:g.map(s=>{const t=(()=>{try{return JSON.parse(s.events)}catch{return[]}})();return e.jsxs("div",{className:"webhook-row-card",children:[e.jsxs("div",{className:"webhook-row-card__header",children:[e.jsx("span",{className:`pill-chip ${s.payloadTemplate?"pill-chip--admin":"pill-chip--scope"}`,children:s.payloadTemplate?"CUSTOM":"SLACK"}),e.jsx("span",{className:"webhook-row-card__url",title:s.url,children:s.url}),e.jsxs("button",{type:"button",className:"row-action row-action--danger",onClick:()=>E(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(V,{size:13}),e.jsx("span",{children:"Remove"})]})]}),e.jsx("div",{className:"webhook-row-card__events",children:t.length===0?e.jsx("span",{className:"webhook-row-card__no-events",children:"No triggers"}):t.map(a=>{const i=C.find(F=>F.id===a),W=(i==null?void 0:i.icon)??u;return e.jsxs("span",{className:`event-pill event-pill--${(i==null?void 0:i.tone)||"gray"}`,children:[e.jsx(W,{size:11}),(i==null?void 0:i.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(x,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(S,{label:"Webhook URL",description:"The endpoint we POST webhook events to.",htmlFor:"webhook-url",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{id:"webhook-url",type:"url",placeholder:"https://hooks.slack.com/services/...",value:l,onChange:s=>j(s.target.value)})})}),e.jsx(S,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:C.map(s=>{const t=d.includes(s.id),a=s.icon;return e.jsxs("button",{type:"button",role:"checkbox","aria-checked":t,className:`event-toggle event-toggle--${s.tone} ${t?"is-selected":""}`,onClick:()=>$(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(H,{size:14,className:"event-toggle__check"})]},s.id)})})}),e.jsxs("div",{className:"template-section",children:[e.jsxs("button",{type:"button",className:"template-section__toggle",onClick:()=>_(!n),"aria-expanded":n,children:[e.jsx(I,{size:14,className:n?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:n?"−":"+"})]}),n&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a variable below to insert it."}),e.jsx("div",{className:"template-editor__chips",children:X.map(s=>e.jsx("button",{type:"button",className:"template-editor__chip",onClick:()=>R(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:y,onChange:s=>m(s.target.value),rows:4})]})]})]}),e.jsxs("div",{className:"add-webhook-card__footer",children:[e.jsxs("button",{type:"button",className:"page-header-action page-header-action--ghost",onClick:L,disabled:!l.trim()||v,children:[v?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(J,{size:14}),e.jsx("span",{children:"Test payload"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:z,disabled:h||!l.trim(),children:[h?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(x,{size:14}),e.jsx("span",{children:h?"Saving…":"Save webhook"})]})]})]})]})]})};export{ae as WebhookSettings};
@@ -1,4 +1,4 @@
1
- import{c as a}from"./index-BR1wTIPC.js";/**
1
+ import{c as a}from"./index-3uxl3o7K.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -36,7 +36,7 @@
36
36
  Learn how to configure a non-root public URL by running `npm run build`.
37
37
  -->
38
38
  <title>Xenon</title>
39
- <script type="module" crossorigin src="/xenon/assets/index-BR1wTIPC.js"></script>
39
+ <script type="module" crossorigin src="/xenon/assets/index-3uxl3o7K.js"></script>
40
40
  <link rel="stylesheet" crossorigin href="/xenon/assets/index-hymrhlEZ.css">
41
41
  </head>
42
42
 
@@ -55,7 +55,8 @@ const logger_1 = __importDefault(require("./logger"));
55
55
  *
56
56
  * Features:
57
57
  * - Keep-alive connections for performance
58
- * - Automatic retry with exponential backoff
58
+ * - Automatic retry with exponential backoff, except for a request that sets
59
+ * `retry: false` (InternalRequestConfig)
59
60
  * - Request/response logging for debugging
60
61
  * - Correlation ID tracking
61
62
  */
@@ -178,6 +179,9 @@ class InternalHttpClient {
178
179
  logger_1.default.warn(`[HTTP ←] ${(_c = config === null || config === void 0 ? void 0 : config.method) === null || _c === void 0 ? void 0 : _c.toUpperCase()} ${config === null || config === void 0 ? void 0 : config.url} ` +
179
180
  `[${status || 'ERR'}] ${duration}ms - ${error.message}`);
180
181
  }
182
+ if (config.retry === false) {
183
+ return Promise.reject(error);
184
+ }
181
185
  const decision = InternalHttpClient.classifyRetry(error);
182
186
  if (!decision.retryable || config.retryCount >= InternalHttpClient.MAX_RETRIES) {
183
187
  return Promise.reject(error);
@@ -206,9 +206,23 @@ function iOSCapabilities(caps, freeDevice) {
206
206
  deleteMatch.forEach((value) => deleteAlwaysMatch(caps, value));
207
207
  });
208
208
  }
209
+ /**
210
+ * The team a session asks to be placed in, or undefined. Documented as
211
+ * `xe:options.team` (`teamId` is read too), with `xenon:options` as the
212
+ * alias, through xenonOptionsOf like every other Xenon option. The flat caps
213
+ * older clients send (`xenon:team`, `xe:teamId`, ...) still work; an options
214
+ * field wins over them. `df:` is never read.
215
+ *
216
+ * The merge is field by field, so if a request spells it `team` in one
217
+ * namespace and `teamId` in the other, `team` wins.
218
+ */
209
219
  function extractTeamCap(caps) {
210
220
  var _a;
211
- const merged = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
221
+ const options = (0, xenonOptions_1.xenonOptionsOf)(caps);
222
+ const fromOptions = (_a = (0, xenonOptions_1.stringOption)(options, 'team')) !== null && _a !== void 0 ? _a : (0, xenonOptions_1.stringOption)(options, 'teamId');
223
+ if (fromOptions)
224
+ return fromOptions;
225
+ const merged = (0, xenonOptions_1.mergedFirstMatch)(caps);
212
226
  const prefixes = ['xenon:', 'xe:', 'appium:', ''];
213
227
  const names = ['teamId', 'team_id', 'team'];
214
228
  for (const prefix of prefixes) {
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeBasePath = normalizeBasePath;
4
+ exports.appiumPathname = appiumPathname;
5
+ /**
6
+ * Appium's base-path rule (base-driver `normalizeBasePath`): drop one trailing
7
+ * '/', and add a leading '/' unless the path is empty. `cliArgs.basePath`
8
+ * reaches a plugin as the operator wrote it, while Appium normalises its own
9
+ * copy before building routes and WebSocket paths, so anything placed in front
10
+ * of those must normalise the same way or it would miss what it guards.
11
+ */
12
+ function normalizeBasePath(basePath) {
13
+ let normalized = typeof basePath === 'string' ? basePath : '';
14
+ normalized = normalized.replace(/\/$/, '');
15
+ if (normalized !== '' && !normalized.startsWith('/'))
16
+ normalized = `/${normalized}`;
17
+ return normalized;
18
+ }
19
+ /**
20
+ * The pathname Appium matches an upgrade against, computed as
21
+ * base-driver's tryHandleWebSocketUpgrade does: WHATWG URL parsing, which
22
+ * resolves dot segments (also `%2e%2e`) and drops the query and an absolute
23
+ * form's host.
24
+ */
25
+ function appiumPathname(url) {
26
+ try {
27
+ return new URL(url, 'http://localhost').pathname;
28
+ }
29
+ catch (_a) {
30
+ return url;
31
+ }
32
+ }
@@ -86,6 +86,9 @@ const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware");
86
86
  const csrfMiddleware_1 = require("../middleware/csrfMiddleware");
87
87
  const swagger_1 = require("./swagger");
88
88
  const typedi_1 = require("typedi");
89
+ const PluginContext_1 = require("../PluginContext");
90
+ const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
91
+ const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
89
92
  const dashboardPluginUrl = null;
90
93
  const ASYNC_LOCK = new async_lock_1.default();
91
94
  const router = express_1.default.Router(), apiRouter = express_1.default.Router(), staticFilesRouter = express_1.default.Router();
@@ -228,6 +231,15 @@ function createRouter(pluginArgs) {
228
231
  apiRouter.use(csrfMiddleware_1.csrfMiddleware);
229
232
  // Health endpoint: no auth, no rate limit
230
233
  apiRouter.get('/health', (_req, res) => res.json({ ok: true }));
234
+ // Where this server's WebDriver API lives: a hub asks a node before sending
235
+ // it a session or a command (gateway/nodeWebDriverUrl.ts). No login: the
236
+ // hub holds no credentials for the node, and the base path is in every
237
+ // client's URL anyway.
238
+ apiRouter.get('/webdriver', (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => typedi_1.Container.get(PluginContext_1.PluginContext).nodeBasePath));
239
+ // On a node: whether a session exists, for the hub's heartbeat, answered
240
+ // without sending the session a command. No login (the hub holds no
241
+ // credentials for the node); it takes the hub's session token instead.
242
+ (0, nodeSessionStatus_1.registerNodeSessionStatus)(apiRouter, pluginArgs);
231
243
  // Dashboard login: unauthenticated (rate-limited internally via separate IP logic)
232
244
  apiRouter.use('/auth', (0, auth_1.authPublicRouter)()); // login, logout — unauthenticated
233
245
  // All remaining /api/* requires authentication (cookie session for
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.routerStackOf = routerStackOf;
4
4
  exports.insertBeforeRoutes = insertBeforeRoutes;
5
+ exports.insertAtStart = insertAtStart;
5
6
  function isRouter(candidate) {
6
7
  return !!candidate && Array.isArray(candidate.stack);
7
8
  }
@@ -28,7 +29,11 @@ function routerStackOf(app) {
28
29
  var _a;
29
30
  return (_a = findRouter(app)) === null || _a === void 0 ? void 0 : _a.router.stack;
30
31
  }
31
- function insertBeforeRoutes(app, path, handler) {
32
+ /**
33
+ * Let `app.use()` build one layer, then move it to `positionOf(stack)` (the
34
+ * stack without the new layer).
35
+ */
36
+ function spliceNewLayer(app, use, positionOf) {
32
37
  const found = findRouter(app);
33
38
  if (!found) {
34
39
  return {
@@ -38,7 +43,7 @@ function insertBeforeRoutes(app, path, handler) {
38
43
  }
39
44
  const { stack } = found.router;
40
45
  const lengthBefore = stack.length;
41
- app.use(path, handler);
46
+ use();
42
47
  const added = stack.splice(lengthBefore);
43
48
  if (added.length !== 1) {
44
49
  // app.use() added its layer somewhere other than the stack we found, or
@@ -50,8 +55,23 @@ function insertBeforeRoutes(app, path, handler) {
50
55
  reason: `app.use() added ${added.length} layers to the router stack, expected 1`,
51
56
  };
52
57
  }
53
- const firstRoute = stack.findIndex((layer) => !!layer.route);
54
- const index = firstRoute === -1 ? stack.length : firstRoute;
58
+ const index = positionOf(stack);
55
59
  stack.splice(index, 0, added[0]);
56
60
  return { placed: true, shape: found.shape, index };
57
61
  }
62
+ function insertBeforeRoutes(app, path, handler) {
63
+ return spliceNewLayer(app, () => app.use(path, handler), (stack) => {
64
+ const firstRoute = stack.findIndex((layer) => !!layer.route);
65
+ return firstRoute === -1 ? stack.length : firstRoute;
66
+ });
67
+ }
68
+ /**
69
+ * Put a path-less middleware at the very front of the router stack, ahead of
70
+ * middleware as well as routes. For a check that must run before one of
71
+ * Appium's own middlewares (its WebSocket `handleUpgrade`), which sit ahead of
72
+ * the routes. On Express 4 that is also ahead of `query` and `expressInit`, so
73
+ * the handler must use only Node's own request and response API.
74
+ */
75
+ function insertAtStart(app, handler) {
76
+ return spliceNewLayer(app, () => app.use(handler), () => 0);
77
+ }
@@ -1,67 +1,186 @@
1
1
  "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
2
13
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
15
  };
5
16
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.normalizeBasePath = normalizeBasePath;
17
+ exports.normalizeBasePath = void 0;
7
18
  exports.sessionCommandPath = sessionCommandPath;
19
+ exports.sessionListingPath = sessionListingPath;
20
+ exports.commandAuthDeps = commandAuthDeps;
21
+ exports.sessionCreatePath = sessionCreatePath;
22
+ exports.registerSessionGateway = registerSessionGateway;
8
23
  exports.registerCommandAuth = registerCommandAuth;
24
+ exports.registerSessionUpgradeGuard = registerSessionUpgradeGuard;
9
25
  const typedi_1 = require("typedi");
10
26
  const logger_1 = __importDefault(require("../logger"));
11
27
  const config_1 = require("../config");
12
28
  const SessionOwnerResolver_1 = require("../services/device-access/SessionOwnerResolver");
13
29
  const commandAuth_1 = require("../middleware/commandAuth");
14
30
  const commandCaller_1 = require("../middleware/commandCaller");
31
+ const sessionListingFilter_1 = require("../middleware/sessionListingFilter");
32
+ const sessionUpgradeGuard_1 = require("../middleware/sessionUpgradeGuard");
33
+ const internalCall_1 = require("../gateway/internalCall");
34
+ const sessionGateway_1 = require("../gateway/sessionGateway");
35
+ const sessionCreate_1 = require("../gateway/sessionCreate");
36
+ const appiumBasePath_1 = require("./appiumBasePath");
37
+ Object.defineProperty(exports, "normalizeBasePath", { enumerable: true, get: function () { return appiumBasePath_1.normalizeBasePath; } });
15
38
  const insertBeforeRoutes_1 = require("./insertBeforeRoutes");
16
- /**
17
- * Appium's base-path rule (base-driver `normalizeBasePath`): drop one trailing
18
- * '/', and add a leading '/' unless the path is empty. `cliArgs.basePath`
19
- * reaches a plugin as the operator wrote it, while Appium normalises its own
20
- * copy before building routes, so the guard must normalise the same way or it
21
- * would miss the routes it is meant to sit in front of.
22
- */
23
- function normalizeBasePath(basePath) {
24
- let normalized = typeof basePath === 'string' ? basePath : '';
25
- normalized = normalized.replace(/\/$/, '');
26
- if (normalized !== '' && !normalized.startsWith('/'))
27
- normalized = `/${normalized}`;
28
- return normalized;
29
- }
30
39
  /**
31
40
  * Everything under `<basePath>/session/:sessionId`, every method. The create
32
41
  * route (`POST <basePath>/session`) has no id segment and does not match.
33
42
  */
34
43
  function sessionCommandPath(basePath) {
35
- return `${normalizeBasePath(basePath)}/session/:sessionId`;
44
+ return `${(0, appiumBasePath_1.normalizeBasePath)(basePath)}/session/:sessionId`;
45
+ }
46
+ /** Appium 3's session listing route (base-driver `getAppiumSessions`). */
47
+ function sessionListingPath(basePath) {
48
+ return `${(0, appiumBasePath_1.normalizeBasePath)(basePath)}/appium/sessions`;
36
49
  }
37
50
  /**
38
- * Install per-command auth (commandAuth.ts) ahead of Appium's routes. Called
39
- * from ServerManager.registerRoutes, which Appium runs after adding them.
51
+ * The dependencies per-command auth runs with. ServerManager builds one set
52
+ * and hands it to both registrations below, so commands, the session listing
53
+ * and session WebSockets share one credential cache.
54
+ */
55
+ function commandAuthDeps(overrides = {}) {
56
+ var _a, _b, _c, _d, _e, _f;
57
+ return {
58
+ enabled: (_a = overrides.enabled) !== null && _a !== void 0 ? _a : (() => (0, commandAuth_1.commandAuthEnabled)()),
59
+ authDisabled: (_b = overrides.authDisabled) !== null && _b !== void 0 ? _b : (() => config_1.config.authDisabled === true),
60
+ verifier: (_c = overrides.verifier) !== null && _c !== void 0 ? _c : new commandCaller_1.CommandCallerVerifier(),
61
+ ownerOf: (_d = overrides.ownerOf) !== null && _d !== void 0 ? _d : ((sessionId) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).ownerOf(sessionId)),
62
+ ownersOf: (_e = overrides.ownersOf) !== null && _e !== void 0 ? _e : ((sessionIds) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).ownersOf(sessionIds)),
63
+ logger: (_f = overrides.logger) !== null && _f !== void 0 ? _f : logger_1.default.scope('CommandAuth'),
64
+ };
65
+ }
66
+ const refuseToStart = (what) => new Error(`XENON_REQUIRE_COMMAND_AUTH is on, but ${what}. Refusing to start rather than ` +
67
+ 'serve sessions unchecked.');
68
+ /** The create route (`POST <basePath>/session`), where the create layer is mounted. */
69
+ function sessionCreatePath(basePath) {
70
+ return `${(0, appiumBasePath_1.normalizeBasePath)(basePath)}/session`;
71
+ }
72
+ /**
73
+ * Install the session gateway (gateway/sessionGateway.ts) and the
74
+ * session-listing filter (sessionListingFilter.ts) ahead of Appium's routes.
75
+ * Called from ServerManager.registerRoutes, which Appium runs after adding
76
+ * them. The gateway is three adjacent layers:
77
+ *
78
+ * 1. the internal-call layer (gateway/internalCall.ts), path-less, which turns
79
+ * Xenon's own `<basePath>/wd-internal/...` calls, when they carry this
80
+ * process's secret, into the plain session command;
81
+ * 2. with `options.create`, the create layer at `<basePath>/session`
82
+ * (gateway/sessionCreate.ts), which allocates the phone for
83
+ * `POST <basePath>/session` and creates a remote phone's session itself;
84
+ * 3. the session layer at `<basePath>/session/:sessionId`: per-command auth,
85
+ * then, on a hub, forwarding of the sessions other servers run.
86
+ *
87
+ * The first comes first, so the others see an internal call by its real path
88
+ * and know it for one. It is path-less because Express puts a mount path back
89
+ * onto the URL when a mounted layer calls next(), which would undo the strip.
90
+ * The others are mounted, so their path matching is the router's own.
91
+ *
92
+ * All of it is always installed and reads the setting per request, so a
93
+ * server with per-command auth off pays one env check per session command and
94
+ * does no lookups for it. If a layer cannot be placed ahead of the routes the
95
+ * error is logged, and with the setting on (and auth enabled) the server
96
+ * refuses to start: an operator who asked for the check must not get a server
97
+ * that silently serves session commands without it.
40
98
  *
41
- * The middleware is always installed and reads the setting per request, so a
42
- * server with the setting off pays one env check per session command and does
43
- * no lookups. If it cannot be placed ahead of the routes the error is logged,
44
- * and with the setting on (and auth enabled) the server refuses to start:
45
- * an operator who asked for the check must not get a server that silently
46
- * serves session commands without it.
99
+ * Returns where the session layer was placed.
47
100
  */
48
- function registerCommandAuth(app, cliArgs, overrides = {}) {
49
- const deps = Object.assign({ enabled: () => (0, commandAuth_1.commandAuthEnabled)(), authDisabled: () => config_1.config.authDisabled === true, verifier: new commandCaller_1.CommandCallerVerifier(), ownerOf: (sessionId) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).ownerOf(sessionId), logger: logger_1.default.scope('CommandAuth') }, overrides);
50
- const path = sessionCommandPath(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
51
- const result = (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, path, (0, commandAuth_1.createCommandAuthMiddleware)(deps));
101
+ function registerSessionGateway(app, cliArgs, deps, options = {}) {
102
+ var _a;
52
103
  const active = deps.enabled() && !deps.authDisabled();
104
+ const basePath = (0, appiumBasePath_1.normalizeBasePath)(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
105
+ const { create } = options, gateway = __rest(options, ["create"]);
106
+ const path = sessionCommandPath(basePath);
107
+ const internal = (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, '/', (0, internalCall_1.createInternalCallLayer)(basePath));
108
+ const created = internal.placed && create
109
+ ? (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, sessionCreatePath(basePath), (0, sessionCreate_1.createSessionCreateLayer)(Object.assign(Object.assign({}, create), { authDisabled: deps.authDisabled, logger: (_a = create.logger) !== null && _a !== void 0 ? _a : deps.logger })))
110
+ : internal;
111
+ const result = created.placed
112
+ ? (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, path, (0, sessionGateway_1.createSessionGatewayLayer)(Object.assign(Object.assign({}, gateway), { auth: deps })))
113
+ : created;
53
114
  if (!result.placed) {
54
115
  deps.logger.error(`Per-command auth could not be placed ahead of Appium's routes for ${path}: ${result.reason}.`);
55
116
  if (active) {
56
- throw new Error('XENON_REQUIRE_COMMAND_AUTH is on, but the per-command check could not be placed ahead ' +
57
- "of Appium's WebDriver routes, so it would never run. Refusing to start rather than " +
58
- 'serve session commands unchecked.');
117
+ throw refuseToStart("the per-command check could not be placed ahead of Appium's WebDriver routes, " +
118
+ 'so it would never run');
119
+ }
120
+ return result;
121
+ }
122
+ const listingPath = sessionListingPath(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
123
+ const listing = (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, listingPath, (0, sessionListingFilter_1.createSessionListingFilter)(deps));
124
+ if (!listing.placed) {
125
+ deps.logger.error("The session-listing filter could not be placed ahead of Appium's routes for " +
126
+ `${listingPath}: ${listing.reason}.`);
127
+ if (active) {
128
+ throw refuseToStart(`the session-listing filter could not be placed ahead of ${listingPath}`);
59
129
  }
60
130
  return result;
61
131
  }
62
132
  deps.logger.info(active
63
133
  ? `Per-command auth is ON: every request under ${path} needs the session owner's ` +
64
- 'credentials or an override admin (XENON_REQUIRE_COMMAND_AUTH).'
65
- : `Per-command auth is off for ${path} (XENON_REQUIRE_COMMAND_AUTH not set, or auth disabled).`);
134
+ `credentials or an override admin, and ${listingPath} lists only the caller's ` +
135
+ 'sessions (XENON_REQUIRE_COMMAND_AUTH).'
136
+ : `Per-command auth is off for ${path} and ${listingPath} ` +
137
+ '(XENON_REQUIRE_COMMAND_AUTH not set, or auth disabled).');
66
138
  return result;
67
139
  }
140
+ /**
141
+ * Per-command auth (commandAuth.ts) and the session-listing filter, through the
142
+ * session gateway with no routing: a server that runs every session itself.
143
+ */
144
+ function registerCommandAuth(app, cliArgs, overrides = {}) {
145
+ return registerSessionGateway(app, cliArgs, commandAuthDeps(overrides));
146
+ }
147
+ /**
148
+ * Install the session WebSocket guard (sessionUpgradeGuard.ts) on both of the
149
+ * paths Appium can take an upgrade by: the http.Server's `upgrade` event and
150
+ * the front of the Express stack. Called from ServerManager.updateServer with
151
+ * the http.Server Appium hands plugins.
152
+ *
153
+ * Like registerCommandAuth, it is always installed and reads the setting per
154
+ * upgrade; with the setting off every upgrade passes straight through. If
155
+ * either hook cannot be installed the error is logged, and with the setting on
156
+ * the server refuses to start.
157
+ */
158
+ function registerSessionUpgradeGuard(httpServer, app, cliArgs, overrides = {}) {
159
+ const deps = commandAuthDeps(overrides);
160
+ const active = deps.enabled() && !deps.authDisabled();
161
+ const basePath = (0, appiumBasePath_1.normalizeBasePath)(cliArgs === null || cliArgs === void 0 ? void 0 : cliArgs.basePath);
162
+ const guard = (0, sessionUpgradeGuard_1.createSessionUpgradeGuard)(basePath, deps);
163
+ const problems = [];
164
+ if (httpServer && typeof httpServer.emit === 'function') {
165
+ (0, sessionUpgradeGuard_1.guardUpgradeEvents)(httpServer, guard);
166
+ }
167
+ else {
168
+ problems.push('no HTTP server was given to guard');
169
+ }
170
+ const front = (0, insertBeforeRoutes_1.insertAtStart)(app, guard.middleware);
171
+ if (!front.placed)
172
+ problems.push(`the Express guard was not placed: ${front.reason}`);
173
+ if (problems.length > 0) {
174
+ const reason = problems.join('; ');
175
+ deps.logger.error(`The session WebSocket guard could not be installed: ${reason}.`);
176
+ if (active) {
177
+ throw refuseToStart(`session WebSockets (${basePath}/bidi/<id>, /ws/session/<id>/...) could not be guarded`);
178
+ }
179
+ return { placed: false, shape: front.shape, reason };
180
+ }
181
+ deps.logger.info(active
182
+ ? `Session WebSocket guard is ON: upgrades to ${basePath}/bidi/:sessionId and ` +
183
+ "/ws/session/:sessionId/... need the session owner's credentials or an override admin."
184
+ : 'Session WebSocket guard is off (XENON_REQUIRE_COMMAND_AUTH not set, or auth disabled).');
185
+ return front;
186
+ }