@xenon-device-management/xenon 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +1 -2
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
  5. package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
  69. package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
  70. package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +57 -13
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/ws/h264StreamWs.js +13 -8
  84. package/lib/src/app/ws/logcatWs.js +12 -7
  85. package/lib/src/app/ws/upgradeRouter.js +215 -0
  86. package/lib/src/dashboard/event-manager.js +7 -6
  87. package/lib/src/data-service/device-service.js +152 -49
  88. package/lib/src/data-service/device-store.js +81 -10
  89. package/lib/src/data-service/deviceClaims.js +98 -0
  90. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  91. package/lib/src/data-service/prisma-store.js +94 -31
  92. package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
  93. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  94. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  95. package/lib/src/device-managers/NodeDevices.js +7 -2
  96. package/lib/src/device-managers/android/AndroidH264StreamService.js +3 -3
  97. package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
  98. package/lib/src/device-managers/android/previewHold.js +2 -2
  99. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  100. package/lib/src/device-managers/ios/IOSStreamService.js +158 -38
  101. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  102. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  103. package/lib/src/device-utils.js +78 -24
  104. package/lib/src/gateway/createHandoff.js +73 -0
  105. package/lib/src/gateway/defaultGateway.js +80 -0
  106. package/lib/src/gateway/forwardToNode.js +147 -0
  107. package/lib/src/gateway/hubSessionToken.js +356 -0
  108. package/lib/src/gateway/internalCall.js +90 -0
  109. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  110. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  111. package/lib/src/gateway/sessionCreate.js +136 -0
  112. package/lib/src/gateway/sessionGateway.js +201 -0
  113. package/lib/src/gateway/sessionLocator.js +100 -0
  114. package/lib/src/generated/client/edge.js +7 -4
  115. package/lib/src/generated/client/index-browser.js +4 -1
  116. package/lib/src/generated/client/index.d.ts +112 -1
  117. package/lib/src/generated/client/index.js +7 -4
  118. package/lib/src/generated/client/package.json +1 -1
  119. package/lib/src/generated/client/schema.prisma +9 -0
  120. package/lib/src/generated/client/wasm.js +4 -1
  121. package/lib/src/helpers/index.js +5 -1
  122. package/lib/src/logger.js +5 -0
  123. package/lib/src/middleware/authMiddleware.js +80 -0
  124. package/lib/src/middleware/commandAuth.js +31 -17
  125. package/lib/src/middleware/csrfMiddleware.js +3 -2
  126. package/lib/src/middleware/sessionListingFilter.js +153 -0
  127. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  128. package/lib/src/plugin.js +27 -1
  129. package/lib/src/prisma.js +2 -1
  130. package/lib/src/services/DeviceReconciler.js +5 -2
  131. package/lib/src/services/OrphanSweeper.js +12 -19
  132. package/lib/src/services/PortAllocator.js +75 -16
  133. package/lib/src/services/ServerManager.js +50 -30
  134. package/lib/src/services/SessionLifecycleService.js +335 -76
  135. package/lib/src/services/ShutdownCoordinator.js +7 -1
  136. package/lib/src/services/SocketServer.js +11 -2
  137. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  138. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  139. package/lib/src/services/lease/LeaseOrphanSweeper.js +32 -6
  140. package/lib/src/services/lease/LeaseService.js +19 -5
  141. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  142. package/lib/src/services/session/sessionCredentials.js +7 -59
  143. package/lib/src/sessions/CloudSession.js +12 -0
  144. package/lib/src/sessions/LocalSession.js +52 -26
  145. package/lib/src/sessions/RemoteSession.js +116 -9
  146. package/lib/src/sessions/SessionManager.js +49 -6
  147. package/lib/src/sessions/appiumUmbrella.js +32 -0
  148. package/lib/test/helpers/appium-umbrella.js +84 -0
  149. package/lib/test/helpers/fake-device-table.js +97 -0
  150. package/lib/test/helpers/loki-stores.js +41 -0
  151. package/lib/test/helpers/loopbackRequest.js +77 -0
  152. package/lib/test/helpers/scratch-database.js +100 -0
  153. package/lib/test/helpers/scratch-port-leases.js +119 -0
  154. package/lib/test/helpers/test-container.js +35 -27
  155. package/lib/test/integration/androidDevices.spec.js +5 -4
  156. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  157. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  158. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  159. package/lib/test/unit/AndroidDeviceManager.spec.js +8 -1
  160. package/lib/test/unit/DeviceModel.spec.js +9 -9
  161. package/lib/test/unit/HealingCascade.test.js +5 -1
  162. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  163. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  164. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  165. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  166. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  167. package/lib/test/unit/android-h264-service.spec.js +6 -0
  168. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  169. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  170. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  171. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  172. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  173. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  174. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  175. package/lib/test/unit/authMiddleware.test.js +3 -0
  176. package/lib/test/unit/bug-report/route.spec.js +9 -9
  177. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  178. package/lib/test/unit/clipboard-route.spec.js +18 -8
  179. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  180. package/lib/test/unit/command-auth.spec.js +31 -31
  181. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  182. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  183. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  184. package/lib/test/unit/create-error-message.spec.js +43 -0
  185. package/lib/test/unit/create-handoff.spec.js +86 -0
  186. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  187. package/lib/test/unit/device-access-guard.spec.js +28 -28
  188. package/lib/test/unit/device-service.spec.js +4 -0
  189. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  190. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  191. package/lib/test/unit/device-team-guard.spec.js +20 -20
  192. package/lib/test/unit/device-utils.spec.js +8 -0
  193. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  194. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  195. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  196. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  197. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  198. package/lib/test/unit/helpers.spec.js +27 -0
  199. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  200. package/lib/test/unit/hub-create-once.spec.js +190 -0
  201. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  202. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  203. package/lib/test/unit/hub-node-control-forward.spec.js +291 -0
  204. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  205. package/lib/test/unit/hub-node-race.spec.js +401 -0
  206. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  207. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  208. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  209. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  210. package/lib/test/unit/hub-session-token.spec.js +265 -0
  211. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  212. package/lib/test/unit/install-repository-app-team.spec.js +2 -2
  213. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  214. package/lib/test/unit/internal-call.spec.js +181 -0
  215. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  216. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  217. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  218. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  219. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  220. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  221. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  222. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  223. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +92 -0
  224. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  225. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  226. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  227. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  228. package/lib/test/unit/logcat-ws.spec.js +4 -1
  229. package/lib/test/unit/loopback-request.spec.js +81 -0
  230. package/lib/test/unit/node-create-caps.spec.js +54 -0
  231. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  232. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  233. package/lib/test/unit/omni-vision.spec.js +7 -3
  234. package/lib/test/unit/plugin.spec.js +45 -4
  235. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  236. package/lib/test/unit/profile-router.test.js +11 -11
  237. package/lib/test/unit/queue-requester.spec.js +5 -0
  238. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  239. package/lib/test/unit/queue-visibility.spec.js +1 -1
  240. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  241. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  242. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  243. package/lib/test/unit/recording-store.spec.js +9 -3
  244. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  245. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  246. package/lib/test/unit/remote-session-create.spec.js +130 -0
  247. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  248. package/lib/test/unit/reset-link.test.js +14 -14
  249. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  250. package/lib/test/unit/session-assets.spec.js +9 -9
  251. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  252. package/lib/test/unit/session-create-layer.spec.js +255 -0
  253. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  254. package/lib/test/unit/session-credentials.spec.js +0 -47
  255. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  256. package/lib/test/unit/session-locator.spec.js +250 -0
  257. package/lib/test/unit/session-node-calls.spec.js +174 -0
  258. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  259. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  260. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  261. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  262. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  263. package/lib/test/unit/stream-leave-route.spec.js +15 -7
  264. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  265. package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
  266. package/lib/test/unit/stream-stop-recording.spec.js +13 -5
  267. package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
  268. package/lib/test/unit/upgrade-router.spec.js +446 -0
  269. package/lib/test/unit/users-router.test.js +11 -8
  270. package/lib/test/unit/verify-credential.spec.js +4 -1
  271. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  272. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  273. package/lib/tsconfig.tsbuildinfo +1 -1
  274. package/package.json +1 -2
  275. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  276. package/prisma/schema.prisma +9 -0
  277. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  278. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  279. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const fs_1 = __importDefault(require("fs"));
52
+ const os_1 = __importDefault(require("os"));
53
+ const path_1 = __importDefault(require("path"));
54
+ const express_1 = __importDefault(require("express"));
55
+ const jose = __importStar(require("jose"));
56
+ const typedi_1 = require("typedi");
57
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
58
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
59
+ const container_registration_1 = require("../helpers/container-registration");
60
+ const loopbackServer_1 = require("../helpers/loopbackServer");
61
+ /**
62
+ * The hub's session token: what a node accepts, with its own per-command auth
63
+ * on, in place of the client's credentials the hub already checked. Signed by
64
+ * the hub's JwtKeyService, checked by the node against the hub's public keys.
65
+ */
66
+ describe('hub session tokens', () => {
67
+ const loopback = (0, loopbackServer_1.loopbackServers)();
68
+ let dirs;
69
+ let restore;
70
+ let hubKeys;
71
+ let hubUrl;
72
+ function keyService() {
73
+ return __awaiter(this, void 0, void 0, function* () {
74
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-hub-token-'));
75
+ dirs.push(dir);
76
+ const keys = new JwtKeyService_1.JwtKeyService();
77
+ yield keys.init(dir);
78
+ return keys;
79
+ });
80
+ }
81
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
82
+ dirs = [];
83
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer);
84
+ hubKeys = yield keyService();
85
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
86
+ // The hub serves its keys the way the real route does.
87
+ const hub = (0, express_1.default)();
88
+ hub.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
89
+ const server = yield loopback.serve(hub);
90
+ hubUrl = `http://127.0.0.1:${server.address().port}`;
91
+ }));
92
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
93
+ sinon_1.default.restore();
94
+ restore();
95
+ yield loopback.closeAll();
96
+ for (const dir of dirs)
97
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
98
+ }));
99
+ describe('HubSessionTokenIssuer (hub)', () => {
100
+ it('signs a token for one session, audience xenon-node, for five minutes', () => __awaiter(void 0, void 0, void 0, function* () {
101
+ const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
102
+ const token = yield issuer.tokenFor('s-1');
103
+ (0, chai_1.expect)(token).to.be.a('string');
104
+ const claims = jose.decodeJwt(token);
105
+ (0, chai_1.expect)(claims.aud).to.equal(hubSessionToken_1.HUB_TOKEN_AUDIENCE);
106
+ (0, chai_1.expect)(claims.sid).to.equal('s-1');
107
+ (0, chai_1.expect)(claims.exp - claims.iat).to.equal(hubSessionToken_1.HUB_TOKEN_TTL_SECONDS);
108
+ // The hub's own key verifies it.
109
+ yield hubKeys.verify(token, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE });
110
+ }));
111
+ it('reuses a session’s token until it is forgotten', () => __awaiter(void 0, void 0, void 0, function* () {
112
+ const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
113
+ const sign = sinon_1.default.spy(hubKeys, 'sign');
114
+ const first = yield issuer.tokenFor('s-1');
115
+ (0, chai_1.expect)(yield issuer.tokenFor('s-1')).to.equal(first);
116
+ (0, chai_1.expect)(sign.callCount).to.equal(1);
117
+ yield issuer.tokenFor('s-2');
118
+ (0, chai_1.expect)(sign.callCount).to.equal(2);
119
+ issuer.forget('s-1');
120
+ yield issuer.tokenFor('s-1');
121
+ (0, chai_1.expect)(sign.callCount).to.equal(3);
122
+ }));
123
+ it('answers null, and warns once, when this server cannot sign', () => __awaiter(void 0, void 0, void 0, function* () {
124
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, new JwtKeyService_1.JwtKeyService()); // never initialised
125
+ const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
126
+ const warn = sinon_1.default.stub(issuer.logger, 'warn');
127
+ (0, chai_1.expect)(yield issuer.tokenFor('s-1')).to.equal(null);
128
+ (0, chai_1.expect)(yield issuer.tokenFor('s-2')).to.equal(null);
129
+ (0, chai_1.expect)(warn.callCount).to.equal(1);
130
+ }));
131
+ it('is an audience /auth/token will not mint for a user', () => {
132
+ var _a, _b;
133
+ const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../src/app/routers/auth.ts'), 'utf8');
134
+ const mintable = (_b = (_a = /MINTABLE_AUDIENCES = \[([^\]]*)\]/.exec(source)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
135
+ (0, chai_1.expect)(mintable).to.include('xenon-rest');
136
+ (0, chai_1.expect)(mintable).not.to.include(hubSessionToken_1.HUB_TOKEN_AUDIENCE);
137
+ });
138
+ });
139
+ describe('HubSessionTokenVerifier (node)', () => {
140
+ it('fetches the keys from the hub’s public JWKS route', () => {
141
+ (0, chai_1.expect)((0, hubSessionToken_1.hubJwksUrl)('http://hub:4724').toString()).to.equal('http://hub:4724/xenon/api/auth/jwks.json');
142
+ });
143
+ it('accepts the hub’s token for its session', () => __awaiter(void 0, void 0, void 0, function* () {
144
+ const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().tokenFor('s-1'));
145
+ (0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-1')).to.equal(true);
146
+ }));
147
+ it('refuses the hub’s token for another session', () => __awaiter(void 0, void 0, void 0, function* () {
148
+ const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().tokenFor('s-1'));
149
+ (0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-2')).to.equal(false);
150
+ }));
151
+ it('refuses an expired token', () => __awaiter(void 0, void 0, void 0, function* () {
152
+ const token = yield hubKeys.sign({ sid: 's-1' }, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE, ttlSeconds: -3600 });
153
+ (0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-1')).to.equal(false);
154
+ }));
155
+ it('refuses a token signed by another key, even under the hub’s key id', () => __awaiter(void 0, void 0, void 0, function* () {
156
+ const other = yield keyService();
157
+ const foreign = yield other.sign({ sid: 's-1' }, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE, ttlSeconds: 300 });
158
+ const verifier = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
159
+ (0, chai_1.expect)(yield verifier.verify(foreign, 's-1')).to.equal(false);
160
+ const hubKid = hubKeys.jwks().keys[0].kid;
161
+ const { privateKey } = yield jose.generateKeyPair('RS256');
162
+ const forged = yield new jose.SignJWT({ sid: 's-1' })
163
+ .setProtectedHeader({ alg: 'RS256', kid: hubKid })
164
+ .setAudience(hubSessionToken_1.HUB_TOKEN_AUDIENCE)
165
+ .setIssuedAt()
166
+ .setExpirationTime('5m')
167
+ .sign(privateKey);
168
+ (0, chai_1.expect)(yield verifier.verify(forged, 's-1')).to.equal(false);
169
+ }));
170
+ it('refuses the hub’s token for another audience', () => __awaiter(void 0, void 0, void 0, function* () {
171
+ const rest = yield hubKeys.sign({ sid: 's-1', sub: 'alice' }, { audience: 'xenon-rest', ttlSeconds: 300 });
172
+ (0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(rest, 's-1')).to.equal(false);
173
+ }));
174
+ it('refuses what is not a token at all', () => __awaiter(void 0, void 0, void 0, function* () {
175
+ (0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify('not-a-jwt', 's-1')).to.equal(false);
176
+ }));
177
+ it('throws HubTokenUnavailableError when the hub’s keys cannot be fetched', () => __awaiter(void 0, void 0, void 0, function* () {
178
+ const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().tokenFor('s-1'));
179
+ yield loopback.closeAll();
180
+ let thrown;
181
+ try {
182
+ yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-1');
183
+ }
184
+ catch (err) {
185
+ thrown = err;
186
+ }
187
+ (0, chai_1.expect)(thrown).to.be.instanceOf(hubSessionToken_1.HubTokenUnavailableError);
188
+ }));
189
+ });
190
+ describe('the token for a create (hub → node)', () => {
191
+ const grant = { userId: 'alice', udid: 'phone-1', host: 'http://10.0.0.5:4725' };
192
+ it('names the owner, the phone and its node, audience xenon-node-create, for two minutes', () => __awaiter(void 0, void 0, void 0, function* () {
193
+ const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(grant));
194
+ const claims = jose.decodeJwt(token);
195
+ (0, chai_1.expect)(claims.aud).to.equal(hubSessionToken_1.HUB_CREATE_AUDIENCE);
196
+ (0, chai_1.expect)(claims).to.include({ sub: 'alice', udid: 'phone-1', host: 'http://10.0.0.5:4725' });
197
+ (0, chai_1.expect)(claims.sid, 'no session yet').to.equal(undefined);
198
+ (0, chai_1.expect)(claims.exp - claims.iat).to.equal(hubSessionToken_1.HUB_CREATE_TTL_SECONDS);
199
+ }));
200
+ it('carries no owner for an unattributed create', () => __awaiter(void 0, void 0, void 0, function* () {
201
+ const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(Object.assign(Object.assign({}, grant), { userId: null })));
202
+ (0, chai_1.expect)(jose.decodeJwt(token).sub).to.equal(undefined);
203
+ (0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verifyCreate(token)).to.deep.equal(Object.assign(Object.assign({}, grant), { userId: null }));
204
+ }));
205
+ it('is minted fresh for every create', () => __awaiter(void 0, void 0, void 0, function* () {
206
+ const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
207
+ const sign = sinon_1.default.spy(hubKeys, 'sign');
208
+ yield issuer.createTokenFor(grant);
209
+ yield issuer.createTokenFor(grant);
210
+ (0, chai_1.expect)(sign.callCount).to.equal(2);
211
+ }));
212
+ it('answers null when this server cannot sign', () => __awaiter(void 0, void 0, void 0, function* () {
213
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, new JwtKeyService_1.JwtKeyService()); // never initialised
214
+ const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
215
+ sinon_1.default.stub(issuer.logger, 'warn');
216
+ (0, chai_1.expect)(yield issuer.createTokenFor(grant)).to.equal(null);
217
+ }));
218
+ it('the node reads the grant back from it', () => __awaiter(void 0, void 0, void 0, function* () {
219
+ const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(grant));
220
+ (0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verifyCreate(token)).to.deep.equal(grant);
221
+ }));
222
+ it('is no command token, and a command token is no create token', () => __awaiter(void 0, void 0, void 0, function* () {
223
+ const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
224
+ const verifier = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
225
+ const create = (yield issuer.createTokenFor(grant));
226
+ const command = (yield issuer.tokenFor('s-1'));
227
+ (0, chai_1.expect)(yield verifier.verify(create, 's-1')).to.equal(false);
228
+ (0, chai_1.expect)(yield verifier.verifyCreate(command)).to.equal(null);
229
+ }));
230
+ it('refuses a forged, an expired, or an incomplete one', () => __awaiter(void 0, void 0, void 0, function* () {
231
+ const verifier = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
232
+ const forger = yield keyService();
233
+ const claims = { sub: 'alice', udid: 'phone-1', host: grant.host };
234
+ const tokens = [
235
+ yield forger.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
236
+ yield hubKeys.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: -3600 }),
237
+ yield hubKeys.sign({ sub: 'alice', host: grant.host }, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
238
+ yield hubKeys.sign({ sub: 'alice', udid: 'phone-1' }, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
239
+ yield hubKeys.sign(claims, { audience: 'xenon-rest', ttlSeconds: 120 }),
240
+ 'not-a-jwt',
241
+ ];
242
+ for (const token of tokens)
243
+ (0, chai_1.expect)(yield verifier.verifyCreate(token)).to.equal(null);
244
+ }));
245
+ it('throws HubTokenUnavailableError when the hub’s keys cannot be fetched', () => __awaiter(void 0, void 0, void 0, function* () {
246
+ const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(grant));
247
+ yield loopback.closeAll();
248
+ let thrown;
249
+ try {
250
+ yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verifyCreate(token);
251
+ }
252
+ catch (err) {
253
+ thrown = err;
254
+ }
255
+ (0, chai_1.expect)(thrown).to.be.instanceOf(hubSessionToken_1.HubTokenUnavailableError);
256
+ }));
257
+ it('is an audience /auth/token will not mint for a user', () => {
258
+ var _a, _b;
259
+ const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../src/app/routers/auth.ts'), 'utf8');
260
+ const mintable = (_b = (_a = /MINTABLE_AUDIENCES = \[([^\]]*)\]/.exec(source)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
261
+ (0, chai_1.expect)(mintable).to.include('xenon-rest');
262
+ (0, chai_1.expect)(mintable).not.to.include(hubSessionToken_1.HUB_CREATE_AUDIENCE);
263
+ });
264
+ });
265
+ });
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const chai_1 = require("chai");
16
16
  const path_1 = __importDefault(require("path"));
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const insertBeforeRoutes_1 = require("../../src/app/insertBeforeRoutes");
20
20
  const loopbackServer_1 = require("../helpers/loopbackServer");
21
21
  /**
@@ -80,7 +80,7 @@ describe('insertBeforeRoutes', () => {
80
80
  next();
81
81
  });
82
82
  (0, chai_1.expect)(result.placed).to.equal(true);
83
- yield (0, supertest_1.default)(yield loopback.serve(app))
83
+ yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
84
84
  .get('/wd/hub/session/abc/url')
85
85
  .expect(200);
86
86
  (0, chai_1.expect)(ran).to.deep.equal(['appium-middleware', 'guard:abc', 'route:abc']);
@@ -90,7 +90,7 @@ describe('insertBeforeRoutes', () => {
90
90
  (0, insertBeforeRoutes_1.insertBeforeRoutes)(app, '/wd/hub/session/:sessionId', (_req, res) => {
91
91
  res.status(404).json({ refused: true });
92
92
  });
93
- const res = yield (0, supertest_1.default)(yield loopback.serve(app))
93
+ const res = yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
94
94
  .get('/wd/hub/session/abc/url')
95
95
  .expect(404);
96
96
  (0, chai_1.expect)(res.body).to.deep.equal({ refused: true });
@@ -102,7 +102,7 @@ describe('insertBeforeRoutes', () => {
102
102
  ran.push('guard');
103
103
  next();
104
104
  });
105
- yield (0, supertest_1.default)(yield loopback.serve(app))
105
+ yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
106
106
  .post('/wd/hub/session')
107
107
  .send({})
108
108
  .expect(200);
@@ -133,7 +133,7 @@ describe('insertBeforeRoutes', () => {
133
133
  res.json({});
134
134
  });
135
135
  (0, chai_1.expect)(result.placed).to.equal(true);
136
- yield (0, supertest_1.default)(yield loopback.serve(app))
136
+ yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
137
137
  .get('/wd/hub/session/abc/url')
138
138
  .expect(200);
139
139
  (0, chai_1.expect)(ran).to.deep.equal(['guard', 'route']);
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const typedi_1 = require("typedi");
21
21
  const control_1 = __importDefault(require("../../src/app/routers/control"));
22
22
  const device_store_1 = require("../../src/data-service/device-store");
@@ -89,7 +89,7 @@ describe('POST /control/:udid/install-repository-app follows the app team rule',
89
89
  sinon_1.default.restore();
90
90
  restore();
91
91
  });
92
- const install = (teamIds, appId) => (0, supertest_1.default)(buildApp(teamIds))
92
+ const install = (teamIds, appId) => (0, loopbackRequest_1.default)(buildApp(teamIds))
93
93
  .post(`/xenon/api/control/${UDID}/install-repository-app`)
94
94
  .send({ appId });
95
95
  it("installs a member's team app and a shared one", () => __awaiter(void 0, void 0, void 0, function* () {
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const chai_1 = require("chai");
7
+ const sinon_1 = __importDefault(require("sinon"));
4
8
  const MitmProxyHost_1 = require("../../../src/services/interceptor/MitmProxyHost");
5
9
  const MockEngine_1 = require("../../../src/services/interceptor/MockEngine");
6
10
  function makeHost(mockEngine = new MockEngine_1.MockEngine()) {
@@ -251,16 +255,25 @@ describe('MitmProxyHost.handleProxyError ring edge cases', () => {
251
255
  });
252
256
  // Tied to RECENT_CONNECT_TTL_MS in the source. If that constant changes, update here.
253
257
  it('honors the TTL boundary: entry exactly at the cutoff is still attributed', () => {
254
- const { host, emitted } = makeHost();
255
- host.recentConnects.push({
256
- host: 'edge.example.com',
257
- // strict `>` in the source means equal-to-TTL is still attributable
258
- ts: Date.now() - 30000,
259
- consumed: false,
260
- });
261
- host.handleProxyError(null, new Error('x'), 'HTTPS_CLIENT_ERROR');
262
- (0, chai_1.expect)(emitted).to.have.length(1);
263
- (0, chai_1.expect)(emitted[0].host).to.equal('edge.example.com');
258
+ // Freeze the clock: the source reads Date.now() again, and one millisecond
259
+ // passing in between (it does, under the full suite's load) puts the entry
260
+ // past the cutoff.
261
+ const clock = sinon_1.default.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
262
+ try {
263
+ const { host, emitted } = makeHost();
264
+ host.recentConnects.push({
265
+ host: 'edge.example.com',
266
+ // strict `>` in the source means equal-to-TTL is still attributable
267
+ ts: Date.now() - 30000,
268
+ consumed: false,
269
+ });
270
+ host.handleProxyError(null, new Error('x'), 'HTTPS_CLIENT_ERROR');
271
+ (0, chai_1.expect)(emitted).to.have.length(1);
272
+ (0, chai_1.expect)(emitted[0].host).to.equal('edge.example.com');
273
+ }
274
+ finally {
275
+ clock.restore();
276
+ }
264
277
  });
265
278
  it('honors the TTL boundary: entry one ms past the cutoff is skipped', () => {
266
279
  const { host, emitted } = makeHost();
@@ -0,0 +1,181 @@
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 commandCaller_1 = require("../../src/middleware/commandCaller");
21
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
22
+ const insertBeforeRoutes_1 = require("../../src/app/insertBeforeRoutes");
23
+ const internalCall_1 = require("../../src/gateway/internalCall");
24
+ const loopbackServer_1 = require("../helpers/loopbackServer");
25
+ /**
26
+ * Xenon's own loopback calls (`<basePath>/wd-internal/...`).
27
+ *
28
+ * A call that carries this process's secret header is Xenon's own: the
29
+ * `/wd-internal` marker is stripped and the call skips per-command auth. The
30
+ * path alone proves nothing, so without the secret a `/wd-internal` request
31
+ * is left exactly as it came and ends where any unknown route ends.
32
+ */
33
+ function appiumExpress() {
34
+ const appiumDir = path_1.default.dirname(require.resolve('appium/package.json'));
35
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
36
+ return require(require.resolve('express', { paths: [appiumDir] }));
37
+ }
38
+ const shapes = [
39
+ ['Express 4', () => (0, express_1.default)()],
40
+ ['Express 5 (Appium 3)', () => appiumExpress()()],
41
+ ];
42
+ // Appium's catch-all, in shape: every unknown route gets the same answer.
43
+ const UNKNOWN_ROUTE = {
44
+ value: { error: 'unknown command', message: 'no route', stacktrace: '' },
45
+ };
46
+ describe('internal calls (/wd-internal with the per-process secret)', () => {
47
+ describe('the secret', () => {
48
+ it('is sent by internalCallHeaders and recognised by hasInternalCallSecret', () => {
49
+ const headers = (0, internalCall_1.internalCallHeaders)();
50
+ (0, chai_1.expect)(Object.keys(headers)).to.deep.equal([internalCall_1.INTERNAL_CALL_HEADER]);
51
+ (0, chai_1.expect)(headers[internalCall_1.INTERNAL_CALL_HEADER]).to.have.length.greaterThan(30);
52
+ (0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)(headers)).to.equal(true);
53
+ });
54
+ it('is refused when absent, wrong, a different length, or repeated', () => {
55
+ const secret = (0, internalCall_1.internalCallHeaders)()[internalCall_1.INTERNAL_CALL_HEADER];
56
+ (0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({})).to.equal(false);
57
+ (0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({ [internalCall_1.INTERNAL_CALL_HEADER]: `${secret.slice(1)}x` })).to.equal(false);
58
+ (0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({ [internalCall_1.INTERNAL_CALL_HEADER]: 'short' })).to.equal(false);
59
+ (0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({ [internalCall_1.INTERNAL_CALL_HEADER]: [secret, secret] })).to.equal(false);
60
+ });
61
+ it('builds the loopback base URL under the normalised base path', () => {
62
+ (0, chai_1.expect)((0, internalCall_1.internalCallBaseUrl)('0.0.0.0', 4723, 'wd/hub/')).to.equal('http://127.0.0.1:4723/wd/hub/wd-internal');
63
+ (0, chai_1.expect)((0, internalCall_1.internalCallBaseUrl)('10.0.0.5', 4723, '')).to.equal('http://10.0.0.5:4723/wd-internal');
64
+ });
65
+ });
66
+ for (const [shapeName, makeExpress] of shapes) {
67
+ describe(shapeName, () => {
68
+ const loopback = (0, loopbackServer_1.loopbackServers)();
69
+ let ran;
70
+ let ownerOf;
71
+ let warnings;
72
+ afterEach(() => loopback.closeAll());
73
+ beforeEach(() => {
74
+ ran = [];
75
+ warnings = [];
76
+ ownerOf = sinon_1.default.stub().resolves('alice');
77
+ });
78
+ function lab() {
79
+ return __awaiter(this, arguments, void 0, function* (basePath = '/wd/hub') {
80
+ const app = makeExpress();
81
+ app.use(express_1.default.json());
82
+ const route = (name) => (req, res) => {
83
+ var _a;
84
+ ran.push({ name, url: req.originalUrl, secret: req.headers[internalCall_1.INTERNAL_CALL_HEADER] });
85
+ res.json({ value: (_a = req.params.sessionId) !== null && _a !== void 0 ? _a : null });
86
+ };
87
+ app.get(`${basePath}/session/:sessionId/url`, route('getUrl'));
88
+ app.get(`${basePath}/session/:sessionId/timeouts`, route('getTimeouts'));
89
+ app.post(`${basePath}/session`, route('createSession'));
90
+ const result = (0, registerCommandAuth_1.registerCommandAuth)(app, { basePath }, {
91
+ enabled: () => true,
92
+ authDisabled: () => false,
93
+ verifier: new commandCaller_1.CommandCallerVerifier({
94
+ verifyKeyPair: sinon_1.default.stub().resolves(null),
95
+ verifyBearer: sinon_1.default.stub().resolves(null),
96
+ }),
97
+ ownerOf,
98
+ logger: {
99
+ info: () => undefined,
100
+ warn: (m) => warnings.push(m),
101
+ error: () => undefined,
102
+ },
103
+ });
104
+ (0, chai_1.expect)(result.placed).to.equal(true);
105
+ // Appium adds its catch-all after every updater.
106
+ app.use((_req, res) => res.status(404).json(UNKNOWN_ROUTE));
107
+ return { app: yield loopback.serve(app), raw: app };
108
+ });
109
+ }
110
+ it('without the secret, answers a /wd-internal command as an unknown route', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ const { app } = yield lab();
112
+ const internal = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/wd-internal/session/s1/url');
113
+ const unknown = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/no-such-prefix/session/s1/url');
114
+ (0, chai_1.expect)(internal.status).to.equal(404);
115
+ (0, chai_1.expect)(internal.status).to.equal(unknown.status);
116
+ (0, chai_1.expect)(internal.text).to.equal(unknown.text);
117
+ (0, chai_1.expect)(ran).to.deep.equal([]);
118
+ }));
119
+ it('with a wrong secret, answers as an unknown route too', () => __awaiter(void 0, void 0, void 0, function* () {
120
+ const { app } = yield lab();
121
+ const res = yield (0, loopbackRequest_1.default)(app)
122
+ .get('/wd/hub/wd-internal/session/s1/url')
123
+ .set(internalCall_1.INTERNAL_CALL_HEADER, 'not-the-secret');
124
+ (0, chai_1.expect)(res.status).to.equal(404);
125
+ (0, chai_1.expect)(res.body).to.deep.equal(UNKNOWN_ROUTE);
126
+ (0, chai_1.expect)(ran).to.deep.equal([]);
127
+ }));
128
+ it('with the secret, strips the marker and skips per-command auth', () => __awaiter(void 0, void 0, void 0, function* () {
129
+ const { app } = yield lab();
130
+ const res = yield (0, loopbackRequest_1.default)(app)
131
+ .get('/wd/hub/wd-internal/session/s1/timeouts')
132
+ .set((0, internalCall_1.internalCallHeaders)());
133
+ (0, chai_1.expect)(res.status).to.equal(200);
134
+ (0, chai_1.expect)(res.body).to.deep.equal({ value: 's1' });
135
+ // The route saw the canonical path and never the secret.
136
+ (0, chai_1.expect)(ran).to.deep.equal([
137
+ { name: 'getTimeouts', url: '/wd/hub/session/s1/timeouts', secret: undefined },
138
+ ]);
139
+ (0, chai_1.expect)(ownerOf.called).to.equal(false);
140
+ (0, chai_1.expect)(warnings).to.deep.equal([]);
141
+ }));
142
+ it('the same session command without the marker still needs credentials', () => __awaiter(void 0, void 0, void 0, function* () {
143
+ const { app } = yield lab();
144
+ const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s1/url').set((0, internalCall_1.internalCallHeaders)());
145
+ (0, chai_1.expect)(res.status).to.equal(404);
146
+ (0, chai_1.expect)(res.body.value.error).to.equal('invalid session id');
147
+ (0, chai_1.expect)(ran).to.deep.equal([]);
148
+ }));
149
+ it('works under an empty base path', () => __awaiter(void 0, void 0, void 0, function* () {
150
+ const { app } = yield lab('');
151
+ const res = yield (0, loopbackRequest_1.default)(app)
152
+ .get('/wd-internal/session/s1/url')
153
+ .set((0, internalCall_1.internalCallHeaders)());
154
+ (0, chai_1.expect)(res.status).to.equal(200);
155
+ (0, chai_1.expect)(ran.map((r) => r.url)).to.deep.equal(['/session/s1/url']);
156
+ }));
157
+ it('recognises only the exact marker, never a look-alike', () => __awaiter(void 0, void 0, void 0, function* () {
158
+ const { app } = yield lab();
159
+ for (const url of [
160
+ '/wd/hub/wd-internalx/session/s1/url',
161
+ '/WD/HUB/WD-INTERNAL/session/s1/url',
162
+ '/wd-internal/wd/hub/session/s1/url',
163
+ ]) {
164
+ const res = yield (0, loopbackRequest_1.default)(app).get(url).set((0, internalCall_1.internalCallHeaders)());
165
+ (0, chai_1.expect)(res.status, url).to.equal(404);
166
+ }
167
+ (0, chai_1.expect)(ran).to.deep.equal([]);
168
+ }));
169
+ it('puts the internal-call layer ahead of the session layer, both ahead of the routes', () => __awaiter(void 0, void 0, void 0, function* () {
170
+ const { raw } = yield lab();
171
+ const stack = (0, insertBeforeRoutes_1.routerStackOf)(raw);
172
+ const firstRoute = stack.findIndex((layer) => !!layer.route);
173
+ const names = stack.slice(0, firstRoute).map((layer) => { var _a; return (_a = layer.handle) === null || _a === void 0 ? void 0 : _a.name; });
174
+ const internal = names.indexOf('xenonInternalCalls');
175
+ const gateway = names.indexOf('xenonSessionGateway');
176
+ (0, chai_1.expect)(internal, 'internal-call layer placed').to.be.greaterThan(-1);
177
+ (0, chai_1.expect)(gateway, 'session gateway placed').to.be.greaterThan(internal);
178
+ }));
179
+ });
180
+ }
181
+ });