@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
@@ -54,12 +54,15 @@ const PluginContext_1 = require("../../src/PluginContext");
54
54
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
55
55
  const UserService_1 = require("../../src/services/UserService");
56
56
  const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
57
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
57
58
  const pendingSessions = __importStar(require("../../src/data-service/pending-sessions-service"));
58
59
  const deviceService = __importStar(require("../../src/data-service/device-service"));
59
60
  const deviceUtils = __importStar(require("../../src/device-utils"));
60
61
  const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
61
62
  const config_1 = require("../../src/config");
63
+ const prisma_1 = require("../../src/prisma");
62
64
  const container_registration_1 = require("../helpers/container-registration");
65
+ const loki_stores_1 = require("../helpers/loki-stores");
63
66
  /**
64
67
  * createSession end to end: the credentials a session presents in
65
68
  * `xe:options` (or its alias `xenon:options`) are read once, then taken out of
@@ -85,6 +88,10 @@ const caps = (always, firstMatch = [{}]) => ({
85
88
  });
86
89
  const leaked = (value) => SECRETS.filter((s) => JSON.stringify(value !== null && value !== void 0 ? value : null).includes(s));
87
90
  describe('createSession — credentials never reach the driver or storage', () => {
91
+ // The in-memory stores, also when this file runs on its own: without
92
+ // NODE_ENV=test the factory handed out the Prisma stores, which are the
93
+ // developer's ~/.cache/xenon/xenon.db.
94
+ (0, loki_stores_1.useLokiStores)();
88
95
  let svc;
89
96
  let device;
90
97
  let context;
@@ -125,7 +132,7 @@ describe('createSession — credentials never reach the driver or storage', () =
125
132
  context.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, { bindHostOrIp: '127.0.0.1' });
126
133
  context.nodeId = 'node-hub';
127
134
  device = { udid: 'u1', host: 'h1', platform: 'android', nodeId: 'node-hub', teamId: null };
128
- restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, UserService_1.UserService, JwtKeyService_1.JwtKeyService);
135
+ restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, UserService_1.UserService, JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer);
129
136
  typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
130
137
  verifyPair: sinon_1.default
131
138
  .stub()
@@ -142,6 +149,8 @@ describe('createSession — credentials never reach the driver or storage', () =
142
149
  return { sub: t.slice('jwt:'.length), teamId: null };
143
150
  }),
144
151
  });
152
+ // The owner's teams: none, and not read from the developer's database.
153
+ sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
145
154
  seen = {};
146
155
  sinon_1.default.stub(pendingSessions, 'addNewPendingSession').callsFake((c) => __awaiter(void 0, void 0, void 0, function* () {
147
156
  seen.pending = JSON.parse(JSON.stringify(c));
@@ -246,32 +255,39 @@ describe('createSession — credentials never reach the driver or storage', () =
246
255
  }));
247
256
  });
248
257
  describe('when the device is on another node', () => {
258
+ let grants;
249
259
  beforeEach(() => {
250
260
  sinon_1.default.stub(svc, 'finalizeSession').callsFake((...args) => __awaiter(void 0, void 0, void 0, function* () {
251
261
  seen.sessionResponse = { desired: JSON.parse(JSON.stringify(args[2])) };
252
262
  }));
253
- sinon_1.default.stub(svc, 'forwardSessionRequest').callsFake((_d, c) => __awaiter(void 0, void 0, void 0, function* () {
263
+ sinon_1.default.stub(svc, 'forwardSessionRequest').callsFake((_d, c, t) => __awaiter(void 0, void 0, void 0, function* () {
254
264
  seen.forwarded = JSON.parse(JSON.stringify(c));
265
+ seen.hubToken = t;
255
266
  return { protocol: 'W3C', value: ['sess-1', {}, 'W3C'] };
256
267
  }));
268
+ grants = sinon_1.default.stub().resolves('HUB-CREATE-TOKEN');
269
+ typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, {
270
+ createTokenFor: grants,
271
+ forget: () => undefined,
272
+ });
257
273
  });
258
- it('a peer Xenon node gets them, since it authenticates the session itself', () => __awaiter(void 0, void 0, void 0, function* () {
274
+ it('a peer Xenon node never gets them: it gets the hub’s token for the verified owner', () => __awaiter(void 0, void 0, void 0, function* () {
259
275
  device.nodeId = 'node-peer';
260
276
  yield create(caps({ 'xe:options': { accessKey: KEY, token: TOKEN, sessionToken: JWT } }));
261
- (0, chai_1.expect)(seen.forwarded.alwaysMatch['xe:options']).to.deep.equal({
262
- accessKey: KEY,
263
- token: TOKEN,
264
- sessionToken: JWT,
265
- });
277
+ (0, chai_1.expect)(leaked(seen.forwarded), 'forwarded').to.deep.equal([]);
278
+ (0, chai_1.expect)(seen.hubToken).to.equal('HUB-CREATE-TOKEN');
279
+ (0, chai_1.expect)(grants.firstCall.args[0]).to.deep.equal({ userId: 'usr_key', udid: 'u1', host: 'h1' });
266
280
  for (const where of ['pending', 'sent', 'sessionResponse']) {
267
281
  (0, chai_1.expect)(leaked(seen[where]), where).to.deep.equal([]);
268
282
  }
269
283
  }));
270
- it('a cloud provider never does', () => __awaiter(void 0, void 0, void 0, function* () {
284
+ it('a cloud provider gets neither', () => __awaiter(void 0, void 0, void 0, function* () {
271
285
  device.nodeId = undefined;
272
286
  device.cloud = true;
273
287
  yield create(caps({ 'xe:options': { accessKey: KEY, token: TOKEN, sessionToken: JWT } }));
274
288
  (0, chai_1.expect)(leaked(seen.forwarded)).to.deep.equal([]);
289
+ (0, chai_1.expect)(seen.hubToken).to.equal(null);
290
+ (0, chai_1.expect)(grants.called).to.equal(false);
275
291
  }));
276
292
  });
277
293
  });
@@ -100,50 +100,3 @@ describe('takeSessionCredentials', () => {
100
100
  (0, chai_1.expect)(() => (0, sessionCredentials_1.takeSessionCredentials)({ alwaysMatch: null, firstMatch: 'x' })).to.not.throw();
101
101
  });
102
102
  });
103
- describe('capsWithCredentials — the copy a peer Xenon node gets', () => {
104
- // A peer node re-runs createSession: it authenticates and attributes the
105
- // session and checks the lease itself, so it needs what the hub read. It
106
- // takes the credentials out again before its own driver sees them.
107
- const take = (caps) => ({ caps, credentials: (0, sessionCredentials_1.takeSessionCredentials)(caps) });
108
- it('puts every credential back, so the node reads the same ones the hub did', () => {
109
- const { caps, credentials } = take({
110
- alwaysMatch: {
111
- 'xenon:options': { leaseId: 'lse_1', accessKey: 'ak_old', token: 'tk_old' },
112
- 'xe:options': { token: SECRETS.token, sessionToken: SECRETS.sessionToken },
113
- },
114
- firstMatch: [{}],
115
- });
116
- const forwarded = (0, sessionCredentials_1.capsWithCredentials)(caps, credentials);
117
- (0, chai_1.expect)((0, sessionCredentials_1.takeSessionCredentials)(JSON.parse(JSON.stringify(forwarded)))).to.deep.equal(credentials);
118
- });
119
- it('writes them into the namespace that wins, in the bucket it is in', () => {
120
- const { caps, credentials } = take({
121
- alwaysMatch: { 'xenon:options': { leaseId: 'lse_1', leaseToken: SECRETS.leaseToken } },
122
- firstMatch: [{ 'xe:options': { accessKey: SECRETS.accessKey, token: SECRETS.token } }],
123
- });
124
- const forwarded = (0, sessionCredentials_1.capsWithCredentials)(caps, credentials);
125
- (0, chai_1.expect)(forwarded.firstMatch[0]['xe:options']).to.deep.equal({
126
- accessKey: SECRETS.accessKey,
127
- token: SECRETS.token,
128
- leaseToken: SECRETS.leaseToken,
129
- });
130
- (0, chai_1.expect)(forwarded.alwaysMatch['xenon:options']).to.deep.equal({ leaseId: 'lse_1' });
131
- });
132
- it('does not modify the caps it copies', () => {
133
- const { caps, credentials } = take({
134
- alwaysMatch: { 'xe:options': Object.assign({ leaseId: 'lse_1' }, SECRETS) },
135
- firstMatch: [{}],
136
- });
137
- const before = JSON.stringify(caps);
138
- (0, sessionCredentials_1.capsWithCredentials)(caps, credentials);
139
- (0, chai_1.expect)(JSON.stringify(caps)).to.equal(before);
140
- (0, chai_1.expect)(leaks(caps)).to.deep.equal([]);
141
- });
142
- it('returns the caps unchanged when there is nothing to put back', () => {
143
- const { caps, credentials } = take({
144
- alwaysMatch: { 'xe:options': { leaseId: 'lse_1' } },
145
- firstMatch: [{}],
146
- });
147
- (0, chai_1.expect)((0, sessionCredentials_1.capsWithCredentials)(caps, credentials)).to.equal(caps);
148
- });
149
- });
@@ -0,0 +1,345 @@
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
+ const chai_1 = require("chai");
16
+ const path_1 = __importDefault(require("path"));
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const express_1 = __importDefault(require("express"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const commandAuth_1 = require("../../src/middleware/commandAuth");
21
+ const commandCaller_1 = require("../../src/middleware/commandCaller");
22
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
23
+ const loopbackServer_1 = require("../helpers/loopbackServer");
24
+ /**
25
+ * Appium 3's session listing (`GET <basePath>/appium/sessions`) under
26
+ * XENON_REQUIRE_COMMAND_AUTH.
27
+ *
28
+ * Appium answers it for anyone with every live session's id and capabilities,
29
+ * which is the easiest way to find an id to use. With the setting on, the
30
+ * answer is filtered: an override admin sees everything, a verified caller sees
31
+ * the sessions they own, and a caller without valid credentials sees an empty
32
+ * list in Appium's own shape. Appium still produces the listing; Xenon only
33
+ * removes entries from it.
34
+ *
35
+ * The apps are built as Appium builds its own (middleware, routes, then the
36
+ * plugin's updater), and the listing route records that it ran.
37
+ */
38
+ function appiumExpress() {
39
+ const appiumDir = path_1.default.dirname(require.resolve('appium/package.json'));
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ return require(require.resolve('express', { paths: [appiumDir] }));
42
+ }
43
+ const USERS = {
44
+ alice: { id: 'alice', role: 'MEMBER', status: 'ACTIVE' },
45
+ bob: { id: 'bob', role: 'MEMBER', status: 'ACTIVE' },
46
+ carol: { id: 'carol', role: 'ADMIN', status: 'ACTIVE' },
47
+ root: { id: 'root', role: 'SUPER_ADMIN', status: 'ACTIVE' },
48
+ };
49
+ // token -> [owner, scopes]
50
+ const KEYS = {
51
+ 'alice-token': ['alice', 'devices,sessions,read'],
52
+ 'bob-token': ['bob', 'devices,sessions,read'],
53
+ 'carol-ordinary': ['carol', 'devices,sessions,read'],
54
+ 'carol-admin': ['carol', 'admin,devices,sessions,read'],
55
+ };
56
+ const JWTS = {
57
+ 'alice-jwt': ['alice', 'devices,sessions,read'],
58
+ 'root-jwt': ['root', 'sessions'],
59
+ };
60
+ const OWNERS = {
61
+ 's-alice': 'alice',
62
+ 's-bob': 'bob',
63
+ 's-anon': null,
64
+ 's-alice-2': 'alice',
65
+ };
66
+ /** What Appium's getAppiumSessions answers: id, creation time, capabilities. */
67
+ const LISTING = [
68
+ { id: 's-alice', created: 1, capabilities: { platformName: 'Android' } },
69
+ { id: 's-bob', created: 2, capabilities: { platformName: 'iOS' } },
70
+ { id: 's-anon', created: 3, capabilities: { platformName: 'Android' } },
71
+ { id: 's-alice-2', created: 4, capabilities: { platformName: 'iOS' } },
72
+ ];
73
+ const ALL_IDS = LISTING.map((s) => s.id);
74
+ const as = (token) => ({
75
+ 'x-xenon-access-key': `ak-${KEYS[token][0]}`,
76
+ 'x-xenon-token': token,
77
+ });
78
+ const bearer = (jwt) => ({ authorization: `Bearer ${jwt}` });
79
+ const idsOf = (res) => res.body.value.map((s) => s.id);
80
+ const shapes = [
81
+ ['Express 4', () => (0, express_1.default)()],
82
+ ['Express 5 (Appium 3)', () => appiumExpress()()],
83
+ ];
84
+ describe('session listing under per-command auth (GET <basePath>/appium/sessions)', () => {
85
+ it('is the listing route under the normalised base path', () => {
86
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)('/wd/hub')).to.equal('/wd/hub/appium/sessions');
87
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)('wd/hub/')).to.equal('/wd/hub/appium/sessions');
88
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)('')).to.equal('/appium/sessions');
89
+ (0, chai_1.expect)((0, registerCommandAuth_1.sessionListingPath)(undefined)).to.equal('/appium/sessions');
90
+ });
91
+ for (const [shapeName, makeExpress] of shapes) {
92
+ describe(shapeName, () => {
93
+ let enabled;
94
+ let authDisabled;
95
+ let verifyKeyPair;
96
+ let verifyBearer;
97
+ let ownerOf;
98
+ let ownersOf;
99
+ let infos;
100
+ let warnings;
101
+ let errors;
102
+ /** What the listing route answers; a test may swap in an error. */
103
+ let answer;
104
+ const loopback = (0, loopbackServer_1.loopbackServers)();
105
+ afterEach(() => loopback.closeAll());
106
+ function lab() {
107
+ return __awaiter(this, void 0, void 0, function* () {
108
+ const ran = [];
109
+ const app = makeExpress();
110
+ app.use(express_1.default.json());
111
+ app.get('/wd/hub/appium/sessions', (_req, res) => {
112
+ ran.push('getAppiumSessions');
113
+ res.status(answer.status).json(answer.body);
114
+ });
115
+ app.get('/wd/hub/status', (_req, res) => {
116
+ ran.push('getStatus');
117
+ res.json({ value: { ready: true } });
118
+ });
119
+ const result = (0, registerCommandAuth_1.registerCommandAuth)(app, { basePath: '/wd/hub' }, {
120
+ enabled: () => enabled,
121
+ authDisabled: () => authDisabled,
122
+ verifier: new commandCaller_1.CommandCallerVerifier({ verifyKeyPair, verifyBearer }),
123
+ ownerOf,
124
+ ownersOf,
125
+ logger: {
126
+ info: (m) => infos.push(m),
127
+ warn: (m) => warnings.push(m),
128
+ error: (m) => errors.push(m),
129
+ },
130
+ });
131
+ (0, chai_1.expect)(result.placed, 'placed ahead of the routes').to.equal(true);
132
+ // Only what requests log counts below; drop the registration's own line.
133
+ infos.splice(0);
134
+ return { app: yield loopback.serve(app), ran };
135
+ });
136
+ }
137
+ beforeEach(() => {
138
+ enabled = true;
139
+ authDisabled = false;
140
+ infos = [];
141
+ warnings = [];
142
+ errors = [];
143
+ answer = { status: 200, body: { value: LISTING } };
144
+ verifyKeyPair = sinon_1.default.stub().callsFake((accessKey, token) => __awaiter(void 0, void 0, void 0, function* () {
145
+ const key = KEYS[token];
146
+ if (!key || accessKey !== `ak-${key[0]}`)
147
+ return null;
148
+ return {
149
+ row: { id: `key-${token}`, userId: key[0], scopes: key[1], expiresAt: null },
150
+ user: USERS[key[0]],
151
+ };
152
+ }));
153
+ verifyBearer = sinon_1.default.stub().callsFake((jwt) => __awaiter(void 0, void 0, void 0, function* () {
154
+ const t = JWTS[jwt];
155
+ if (!t)
156
+ return null;
157
+ return { payload: { sub: t[0], scopes: t[1] }, user: USERS[t[0]] };
158
+ }));
159
+ ownerOf = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { return (id in OWNERS ? OWNERS[id] : null); }));
160
+ ownersOf = sinon_1.default
161
+ .stub()
162
+ .callsFake((ids) => __awaiter(void 0, void 0, void 0, function* () { return new Map(ids.map((id) => [id, id in OWNERS ? OWNERS[id] : null])); }));
163
+ });
164
+ describe('who sees what', () => {
165
+ it('shows the owner only the sessions they own', () => __awaiter(void 0, void 0, void 0, function* () {
166
+ const { app, ran } = yield lab();
167
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
168
+ (0, chai_1.expect)(res.status).to.equal(200);
169
+ (0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
170
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [LISTING[0], LISTING[3]] });
171
+ (0, chai_1.expect)(ran).to.deep.equal(['getAppiumSessions']);
172
+ }));
173
+ it('decides every listed session with one owner lookup, not one per session', () => __awaiter(void 0, void 0, void 0, function* () {
174
+ const { app } = yield lab();
175
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token')).expect(200);
176
+ (0, chai_1.expect)(ownersOf.callCount).to.equal(1);
177
+ (0, chai_1.expect)(ownersOf.firstCall.args[0]).to.have.members(ALL_IDS);
178
+ (0, chai_1.expect)(ownerOf.called).to.equal(false);
179
+ }));
180
+ it('shows the owner their sessions for a Bearer token too', () => __awaiter(void 0, void 0, void 0, function* () {
181
+ const { app } = yield lab();
182
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(bearer('alice-jwt'));
183
+ (0, chai_1.expect)(idsOf(res)).to.deep.equal(['s-alice', 's-alice-2']);
184
+ }));
185
+ it('shows another user only theirs', () => __awaiter(void 0, void 0, void 0, function* () {
186
+ const { app } = yield lab();
187
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('bob-token'));
188
+ (0, chai_1.expect)(idsOf(res)).to.deep.equal(['s-bob']);
189
+ }));
190
+ it('logs the filtering at info with the caller and the counts, never the credential', () => __awaiter(void 0, void 0, void 0, function* () {
191
+ const { app } = yield lab();
192
+ yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('bob-token')).expect(200);
193
+ (0, chai_1.expect)(infos).to.have.length(1);
194
+ (0, chai_1.expect)(infos[0]).to.include('bob');
195
+ (0, chai_1.expect)(infos[0]).to.include('1 of 4');
196
+ (0, chai_1.expect)(infos[0]).not.to.include('bob-token');
197
+ (0, chai_1.expect)(warnings).to.deep.equal([]);
198
+ }));
199
+ it('shows an admin-scoped key every session, without looking up owners', () => __awaiter(void 0, void 0, void 0, function* () {
200
+ const { app } = yield lab();
201
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-admin'));
202
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: LISTING });
203
+ (0, chai_1.expect)(ownersOf.called || ownerOf.called).to.equal(false);
204
+ }));
205
+ it('shows a SUPER_ADMIN every session', () => __awaiter(void 0, void 0, void 0, function* () {
206
+ const { app } = yield lab();
207
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(bearer('root-jwt'));
208
+ (0, chai_1.expect)(idsOf(res)).to.deep.equal(ALL_IDS);
209
+ }));
210
+ it("shows an ADMIN's ordinary key only their own sessions, here none", () => __awaiter(void 0, void 0, void 0, function* () {
211
+ const { app } = yield lab();
212
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-ordinary'));
213
+ (0, chai_1.expect)(res.status).to.equal(200);
214
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [] });
215
+ }));
216
+ it('never shows a session nobody owns to anyone but an override admin', () => __awaiter(void 0, void 0, void 0, function* () {
217
+ const { app } = yield lab();
218
+ for (const who of ['alice-token', 'bob-token', 'carol-ordinary']) {
219
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as(who));
220
+ (0, chai_1.expect)(idsOf(res), who).not.to.include('s-anon');
221
+ }
222
+ const admin = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-admin'));
223
+ (0, chai_1.expect)(idsOf(admin)).to.include('s-anon');
224
+ }));
225
+ });
226
+ describe('callers without valid credentials', () => {
227
+ it("get an empty list in Appium's normal shape, with no lookups at all", () => __awaiter(void 0, void 0, void 0, function* () {
228
+ const { app, ran } = yield lab();
229
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
230
+ (0, chai_1.expect)(res.status).to.equal(200);
231
+ (0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
232
+ (0, chai_1.expect)(res.text).to.equal('{"value":[]}');
233
+ (0, chai_1.expect)(ran).to.deep.equal(['getAppiumSessions']);
234
+ (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called).to.equal(false);
235
+ (0, chai_1.expect)(ownersOf.called || ownerOf.called).to.equal(false);
236
+ (0, chai_1.expect)(warnings).to.have.length(1);
237
+ (0, chai_1.expect)(warnings[0]).to.include('no credentials');
238
+ }));
239
+ it('get an empty list for credentials that do not verify, logged without the secret', () => __awaiter(void 0, void 0, void 0, function* () {
240
+ const { app } = yield lab();
241
+ const res = yield (0, loopbackRequest_1.default)(app)
242
+ .get('/wd/hub/appium/sessions')
243
+ .set({ 'x-xenon-access-key': 'ak-alice', 'x-xenon-token': 'guessed' });
244
+ (0, chai_1.expect)(res.status).to.equal(200);
245
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [] });
246
+ (0, chai_1.expect)(ownersOf.called).to.equal(false);
247
+ (0, chai_1.expect)(warnings).to.have.length(1);
248
+ (0, chai_1.expect)(warnings[0]).to.include('invalid credentials');
249
+ (0, chai_1.expect)(warnings[0]).not.to.include('guessed');
250
+ }));
251
+ it('get the same answer as when no session is running', () => __awaiter(void 0, void 0, void 0, function* () {
252
+ const { app } = yield lab();
253
+ const filtered = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
254
+ answer = { status: 200, body: { value: [] } };
255
+ const empty = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('carol-admin'));
256
+ (0, chai_1.expect)(filtered.status).to.equal(empty.status);
257
+ (0, chai_1.expect)(filtered.headers['content-type']).to.equal(empty.headers['content-type']);
258
+ (0, chai_1.expect)(filtered.text).to.equal(empty.text);
259
+ }));
260
+ it('are filtered on the path in another letter case and with a trailing slash', () => __awaiter(void 0, void 0, void 0, function* () {
261
+ const { app, ran } = yield lab();
262
+ for (const url of ['/WD/HUB/APPIUM/SESSIONS', '/wd/hub/appium/sessions/']) {
263
+ const res = yield (0, loopbackRequest_1.default)(app).get(url);
264
+ (0, chai_1.expect)(res.status, url).to.equal(200);
265
+ (0, chai_1.expect)(res.body, url).to.deep.equal({ value: [] });
266
+ }
267
+ (0, chai_1.expect)(ran).to.deep.equal(['getAppiumSessions', 'getAppiumSessions']);
268
+ }));
269
+ });
270
+ describe('what it leaves alone', () => {
271
+ it('does nothing at all when the setting is off (the default)', () => __awaiter(void 0, void 0, void 0, function* () {
272
+ enabled = false;
273
+ const { app } = yield lab();
274
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
275
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: LISTING });
276
+ (0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called).to.equal(false);
277
+ (0, chai_1.expect)(ownersOf.called || ownerOf.called).to.equal(false);
278
+ (0, chai_1.expect)(infos.concat(warnings, errors)).to.deep.equal([]);
279
+ }));
280
+ it('does nothing when auth is disabled, even with the setting on', () => __awaiter(void 0, void 0, void 0, function* () {
281
+ authDisabled = true;
282
+ const { app } = yield lab();
283
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions');
284
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: LISTING });
285
+ (0, chai_1.expect)(ownersOf.called).to.equal(false);
286
+ }));
287
+ it("passes Appium's error answer through untouched (session_discovery not enabled)", () => __awaiter(void 0, void 0, void 0, function* () {
288
+ answer = {
289
+ status: 400,
290
+ body: {
291
+ value: {
292
+ error: 'invalid argument',
293
+ message: 'Potentially insecure feature session_discovery has not been enabled',
294
+ stacktrace: '',
295
+ },
296
+ },
297
+ };
298
+ const { app } = yield lab();
299
+ for (const headers of [{}, as('alice-token')]) {
300
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(headers);
301
+ (0, chai_1.expect)(res.status).to.equal(400);
302
+ (0, chai_1.expect)(res.body).to.deep.equal(answer.body);
303
+ }
304
+ (0, chai_1.expect)(ownersOf.called).to.equal(false);
305
+ }));
306
+ it('leaves other requests alone', () => __awaiter(void 0, void 0, void 0, function* () {
307
+ const { app, ran } = yield lab();
308
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/status');
309
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: { ready: true } });
310
+ (0, chai_1.expect)(ran).to.deep.equal(['getStatus']);
311
+ (0, chai_1.expect)(warnings).to.deep.equal([]);
312
+ }));
313
+ });
314
+ describe('failing closed', () => {
315
+ it('answers 503 when the credential check fails, before the listing runs', () => __awaiter(void 0, void 0, void 0, function* () {
316
+ verifyKeyPair.rejects(new Error('database is locked'));
317
+ const { app, ran } = yield lab();
318
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
319
+ (0, chai_1.expect)(res.status).to.equal(503);
320
+ (0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
321
+ (0, chai_1.expect)(ran).to.deep.equal([]);
322
+ (0, chai_1.expect)(errors).to.have.length(1);
323
+ (0, chai_1.expect)(errors[0]).to.include('database is locked');
324
+ }));
325
+ it('answers 503 when the owner lookup fails, never the unfiltered list', () => __awaiter(void 0, void 0, void 0, function* () {
326
+ ownersOf.rejects(new Error('database is locked\nSELECT secret FROM ...'));
327
+ const { app } = yield lab();
328
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
329
+ (0, chai_1.expect)(res.status).to.equal(503);
330
+ (0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
331
+ (0, chai_1.expect)(res.text).not.to.include('s-bob');
332
+ (0, chai_1.expect)(errors).to.have.length(1);
333
+ (0, chai_1.expect)(errors[0]).to.include('database is locked');
334
+ (0, chai_1.expect)(errors[0]).not.to.include('SELECT');
335
+ }));
336
+ it('drops a listed entry it cannot attribute rather than showing it', () => __awaiter(void 0, void 0, void 0, function* () {
337
+ answer = { status: 200, body: { value: [LISTING[0], { created: 5 }, 'junk'] } };
338
+ const { app } = yield lab();
339
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').set(as('alice-token'));
340
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: [LISTING[0]] });
341
+ }));
342
+ });
343
+ });
344
+ }
345
+ });