@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,223 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const express_1 = __importDefault(require("express"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const http_1 = __importDefault(require("http"));
20
+ const os_1 = __importDefault(require("os"));
21
+ const path_1 = __importDefault(require("path"));
22
+ const sinon_1 = __importDefault(require("sinon"));
23
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
24
+ const typedi_1 = require("typedi");
25
+ const plugin_1 = require("../../src/plugin");
26
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
27
+ const defaultGateway_1 = require("../../src/gateway/defaultGateway");
28
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
29
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
30
+ const PluginContext_1 = require("../../src/PluginContext");
31
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
32
+ const device_managers_1 = require("../../src/device-managers");
33
+ const device_store_1 = require("../../src/data-service/device-store");
34
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
35
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
36
+ const SessionOwnerResolver_1 = require("../../src/services/device-access/SessionOwnerResolver");
37
+ const LiveSessionOwners_1 = require("../../src/services/device-access/LiveSessionOwners");
38
+ const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
39
+ const config_1 = require("../../src/config");
40
+ const container_registration_1 = require("../helpers/container-registration");
41
+ const scratch_database_1 = require("../helpers/scratch-database");
42
+ const appium_umbrella_1 = require("../helpers/appium-umbrella");
43
+ /**
44
+ * Who owns a session the hub created on a node, as the node's own checks see
45
+ * it: SessionOwnerResolver, behind the /control ownership guard, the logcat
46
+ * WebSocket and the session listing.
47
+ *
48
+ * The node writes no Session row for a hub's session (the hub keeps the
49
+ * record), so the resolver, which read only rows, found no owner, and the
50
+ * fail-closed rule refused the device to everyone but admins, its owner
51
+ * included. The owner the hub's create token named is now known for as long
52
+ * as the session lives.
53
+ */
54
+ const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
55
+ const NODE_ID = 'node-1';
56
+ describe('the owner of a hub’s session, on the node', function () {
57
+ this.timeout(60000);
58
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
59
+ const servers = [];
60
+ let dirs;
61
+ let hubKeys;
62
+ let appiumLogs;
63
+ let restore;
64
+ let saved;
65
+ let authDisabledBefore;
66
+ let nodeOrigin;
67
+ before(() => __awaiter(this, void 0, void 0, function* () {
68
+ appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
69
+ dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-node-owner-keys-'))];
70
+ hubKeys = new JwtKeyService_1.JwtKeyService();
71
+ yield hubKeys.init(dirs[0]);
72
+ }));
73
+ after(() => {
74
+ appiumLogs.restore();
75
+ for (const dir of dirs)
76
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
77
+ });
78
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
79
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionOwnerResolver_1.SessionOwnerResolver, LiveSessionOwners_1.LiveSessionOwners);
80
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
81
+ typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
82
+ typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
83
+ typedi_1.Container.set(LiveSessionOwners_1.LiveSessionOwners, new LiveSessionOwners_1.LiveSessionOwners());
84
+ typedi_1.Container.set(device_managers_1.XenonManager, {
85
+ getMaxSessionCount: () => undefined,
86
+ deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
87
+ });
88
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
89
+ saved = {
90
+ store: device_store_1.DeviceStoreFactory._deviceStore,
91
+ pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
92
+ context: Object.assign({}, context),
93
+ };
94
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
95
+ device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
96
+ authDisabledBefore = config_1.config.authDisabled;
97
+ config_1.config.authDisabled = false;
98
+ yield scratch.db.pendingSession.deleteMany({});
99
+ yield scratch.db.session.deleteMany({});
100
+ yield scratch.db.device.deleteMany({});
101
+ yield boot();
102
+ }));
103
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
104
+ for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
105
+ SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
106
+ device_store_1.DeviceStoreFactory._deviceStore = saved.store;
107
+ device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
108
+ Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
109
+ config_1.config.authDisabled = authDisabledBefore;
110
+ sinon_1.default.restore();
111
+ restore();
112
+ for (const s of servers.splice(0)) {
113
+ s.closeAllConnections();
114
+ yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
115
+ }
116
+ }));
117
+ const port = (s) => s.address().port;
118
+ function boot() {
119
+ return __awaiter(this, void 0, void 0, function* () {
120
+ const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
121
+ const hubServer = yield baseDriver.server({
122
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
123
+ port: 0,
124
+ hostname: '127.0.0.1',
125
+ basePath: '/wd/hub',
126
+ cliArgs: { basePath: '/wd/hub' },
127
+ serverUpdaters: [
128
+ (app) => {
129
+ app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
130
+ },
131
+ ],
132
+ });
133
+ servers.push(hubServer);
134
+ const hubUrl = `http://127.0.0.1:${port(hubServer)}`;
135
+ const nodeServer = yield baseDriver.server({
136
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']])),
137
+ port: 0,
138
+ hostname: '127.0.0.1',
139
+ basePath: '/node',
140
+ cliArgs: { basePath: '/node' },
141
+ serverUpdaters: [
142
+ (app) => {
143
+ (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => false, authDisabled: () => false, logger: quiet }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
144
+ // The node's /control ownership guard, with a caller named by a header.
145
+ const control = express_1.default.Router();
146
+ control.use((0, deviceAccessGuard_1.deviceAccessGuard)());
147
+ control.post('/:udid/tap', (_req, res) => res.json({ success: true }));
148
+ app.use('/control', (req, _res, next) => {
149
+ req.auth = { userId: req.headers['x-test-user'], role: 'MEMBER', scopes: 'devices' };
150
+ next();
151
+ }, control);
152
+ },
153
+ ],
154
+ });
155
+ servers.push(nodeServer);
156
+ nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
157
+ // The node's dashboard is off, as it usually is.
158
+ typedi_1.Container.get(PluginContext_1.PluginContext).setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), port(nodeServer), NODE_ID, '/node');
159
+ yield scratch.db.device.create({
160
+ data: {
161
+ udid: 'phone-1',
162
+ host: nodeOrigin,
163
+ nodeId: NODE_ID,
164
+ platform: 'android',
165
+ name: 'phone-1',
166
+ sdk: '14',
167
+ busy: false,
168
+ userBlocked: false,
169
+ offline: false,
170
+ },
171
+ });
172
+ });
173
+ }
174
+ /** alice's session on phone-1, created as the hub creates it. */
175
+ function hubSession() {
176
+ return __awaiter(this, void 0, void 0, function* () {
177
+ const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
178
+ userId: 'alice',
179
+ udid: 'phone-1',
180
+ host: nodeOrigin,
181
+ });
182
+ const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
183
+ .post('/node/session')
184
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, token)
185
+ .send({
186
+ capabilities: {
187
+ alwaysMatch: {
188
+ platformName: 'Android',
189
+ 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION,
190
+ 'appium:newCommandTimeout': 0,
191
+ 'appium:udid': 'phone-1',
192
+ 'xe:options': { recordVideo: false },
193
+ },
194
+ firstMatch: [{}],
195
+ },
196
+ });
197
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
198
+ return res.body.value.sessionId;
199
+ });
200
+ }
201
+ const tap = (user) => (0, loopbackRequest_1.default)(nodeOrigin).post('/control/phone-1/tap').set('x-test-user', user).send({ x: 1, y: 1 });
202
+ it('is the owner the hub’s create token named, while the session lives', () => __awaiter(this, void 0, void 0, function* () {
203
+ const sessionId = yield hubSession();
204
+ (0, chai_1.expect)(yield scratch.db.session.count(), 'Session rows on the node').to.equal(0);
205
+ const owners = typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver);
206
+ (0, chai_1.expect)(yield owners.ownerOf(sessionId)).to.equal('alice');
207
+ (0, chai_1.expect)((yield owners.ownersOf([sessionId])).get(sessionId)).to.equal('alice');
208
+ }));
209
+ it('lets its owner use the phone on the node, and nobody else', () => __awaiter(this, void 0, void 0, function* () {
210
+ yield hubSession();
211
+ const alice = yield tap('alice');
212
+ (0, chai_1.expect)(alice.status, JSON.stringify(alice.body)).to.equal(200);
213
+ const bob = yield tap('bob');
214
+ (0, chai_1.expect)(bob.status).to.equal(409);
215
+ (0, chai_1.expect)(bob.body.error).to.equal('device_in_use_by_session');
216
+ }));
217
+ it('is forgotten when the session ends', () => __awaiter(this, void 0, void 0, function* () {
218
+ const sessionId = yield hubSession();
219
+ (0, chai_1.expect)(typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).ownerOf(sessionId)).to.equal('alice');
220
+ yield (0, loopbackRequest_1.default)(nodeOrigin).delete(`/node/session/${sessionId}`).expect(200);
221
+ (0, chai_1.expect)(typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).ownerOf(sessionId)).to.equal(undefined);
222
+ }));
223
+ });
@@ -19,13 +19,16 @@ const typedi_1 = require("typedi");
19
19
  const OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
20
20
  const tesseract_js_1 = __importDefault(require("tesseract.js"));
21
21
  const AIService_1 = require("../../src/services/AIService");
22
+ const container_registration_1 = require("../helpers/container-registration");
22
23
  describe('OmniVisionService Unit Tests', () => {
23
24
  let omniService;
24
25
  let mockDriver;
26
+ let restoreContainer;
25
27
  beforeEach(() => {
26
- // Reset container to ensure clean state
27
- typedi_1.Container.reset();
28
- omniService = typedi_1.Container.get(OmniVisionService_1.OmniVisionService);
28
+ // A fresh service for each test, registered only until it ends.
29
+ restoreContainer = (0, container_registration_1.saveRegistrations)(OmniVisionService_1.OmniVisionService);
30
+ omniService = new OmniVisionService_1.OmniVisionService();
31
+ typedi_1.Container.set(OmniVisionService_1.OmniVisionService, omniService);
29
32
  mockDriver = {
30
33
  getScreenshot: sinon_1.default.stub().resolves('mock_screenshot_base64'),
31
34
  sessionId: 'test_session',
@@ -33,6 +36,7 @@ describe('OmniVisionService Unit Tests', () => {
33
36
  });
34
37
  afterEach(() => {
35
38
  sinon_1.default.restore();
39
+ restoreContainer();
36
40
  });
37
41
  const createMockWorker = () => ({
38
42
  setParameters: sinon_1.default.stub().resolves(),
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
36
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
37
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -12,14 +45,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
45
  require("reflect-metadata");
13
46
  const device_utils_1 = require("../../src/device-utils");
14
47
  const chai_1 = require("chai");
15
- const pluginArgs_1 = require("../../src/data-service/pluginArgs");
16
48
  const cliArgs_1 = require("../integration/cliArgs");
17
49
  const db_1 = require("../../src/data-service/db");
18
50
  const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
51
+ const loki_stores_1 = require("../helpers/loki-stores");
19
52
  const pluginArgs = IPluginArgs_1.DefaultPluginArgs;
20
53
  describe('Device filter tests', () => {
54
+ // The in-memory stores, also when this file runs on its own: without
55
+ // NODE_ENV=test the factory handed out the Prisma stores, which are the
56
+ // developer's ~/.cache/xenon/xenon.db.
57
+ (0, loki_stores_1.useLokiStores)();
21
58
  it('Get Device filters for real device', () => __awaiter(void 0, void 0, void 0, function* () {
22
- yield (0, pluginArgs_1.addCLIArgs)(cliArgs_1.serverCliArgs);
59
+ // Imported here, not at the top: the module keeps the store it gets when
60
+ // it loads, and only here is that the in-memory one on a file run alone.
61
+ const { addCLIArgs } = yield Promise.resolve().then(() => __importStar(require('../../src/data-service/pluginArgs')));
62
+ yield addCLIArgs(cliArgs_1.serverCliArgs);
23
63
  (yield db_1.XenonDatabase.CLIArgs)
24
64
  .chain()
25
65
  .find()
@@ -111,7 +151,8 @@ describe('Device filter tests', () => {
111
151
  });
112
152
  });
113
153
  });
114
- describe('Pending sessions', () => __awaiter(void 0, void 0, void 0, function* () {
154
+ describe('Pending sessions', () => {
155
+ (0, loki_stores_1.useLokiStores)();
115
156
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
116
157
  (yield db_1.XenonDatabase.PendingSessionsModel).removeDataOnly();
117
158
  }));
@@ -131,4 +172,4 @@ describe('Pending sessions', () => __awaiter(void 0, void 0, void 0, function* (
131
172
  const pendingSessions = (yield db_1.XenonDatabase.PendingSessionsModel).chain().data();
132
173
  (0, chai_1.expect)(pendingSessions.length).to.equal(1);
133
174
  }));
134
- }));
175
+ });
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const net_1 = __importDefault(require("net"));
18
+ const sinon_1 = __importDefault(require("sinon"));
19
+ const PortAllocator_1 = require("../../src/services/PortAllocator");
20
+ const scratch_port_leases_1 = require("../helpers/scratch-port-leases");
21
+ const SHAPES = [
22
+ { host: '127.0.0.1', label: 'IPv4 loopback (127.0.0.1)' },
23
+ { host: '::1', label: 'IPv6 loopback (::1)' },
24
+ { host: '0.0.0.0', label: 'IPv4 wildcard (0.0.0.0)' },
25
+ { host: '::', label: 'IPv6 wildcard, dual-stack (::) — what iproxy does' },
26
+ { host: '::', ipv6Only: true, label: 'IPv6 wildcard, v6-only (::)' },
27
+ ];
28
+ function listen(port, host, ipv6Only = false) {
29
+ return new Promise((resolve, reject) => {
30
+ const server = net_1.default.createServer((socket) => socket.destroy());
31
+ server.once('error', reject);
32
+ server.listen({ port, host, ipv6Only }, () => resolve(server));
33
+ });
34
+ }
35
+ function close(server) {
36
+ return new Promise((resolve) => server.close(() => resolve()));
37
+ }
38
+ describe('PortAllocator OS probe and lease ownership (real sockets, scratch DB)', () => {
39
+ const scratch = (0, scratch_port_leases_1.useScratchPortLeases)();
40
+ let allocator;
41
+ let servers;
42
+ beforeEach(() => {
43
+ allocator = new PortAllocator_1.PortAllocator();
44
+ allocator.configure({
45
+ mjpeg: [scratch.base, scratch.base + 1],
46
+ wda: [scratch.base + 50, scratch.base + 51],
47
+ });
48
+ servers = [];
49
+ });
50
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
51
+ yield Promise.all(servers.map(close));
52
+ sinon_1.default.restore();
53
+ }));
54
+ function occupy(port, shape) {
55
+ return __awaiter(this, void 0, void 0, function* () {
56
+ servers.push(yield listen(port, shape.host, shape.ipv6Only));
57
+ });
58
+ }
59
+ function leaseOf(port) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ return scratch.db.portLease.findUnique({ where: { port } });
62
+ });
63
+ }
64
+ describe('acquire skips a port something is listening on', () => {
65
+ for (const shape of SHAPES) {
66
+ it(`a listener on ${shape.label}`, () => __awaiter(void 0, void 0, void 0, function* () {
67
+ var _a;
68
+ yield occupy(scratch.base, shape);
69
+ const port = yield allocator.acquire('mjpeg', 'android-s9');
70
+ (0, chai_1.expect)(port, `must not lease a port with a listener on ${shape.host}`).to.equal(scratch.base + 1);
71
+ (0, chai_1.expect)(yield leaseOf(scratch.base)).to.equal(null);
72
+ (0, chai_1.expect)((_a = (yield leaseOf(scratch.base + 1))) === null || _a === void 0 ? void 0 : _a.leasedToUdid).to.equal('android-s9');
73
+ }));
74
+ }
75
+ it("drops the udid's own stale lease when an iproxy-shaped listener holds the port", () => __awaiter(void 0, void 0, void 0, function* () {
76
+ // The reuse path: the S9+ still has a lease row on the port while the
77
+ // iPhone's iproxy is the one listening there.
78
+ const now = Date.now();
79
+ yield scratch.db.portLease.create({
80
+ data: {
81
+ port: scratch.base,
82
+ purpose: 'mjpeg',
83
+ leasedToUdid: 'android-s9',
84
+ leasedAt: now,
85
+ expiresAt: now + 60000,
86
+ },
87
+ });
88
+ yield occupy(scratch.base, SHAPES[3]);
89
+ const port = yield allocator.acquire('mjpeg', 'android-s9');
90
+ (0, chai_1.expect)(port).to.equal(scratch.base + 1);
91
+ (0, chai_1.expect)(yield leaseOf(scratch.base)).to.equal(null);
92
+ }));
93
+ });
94
+ it('still leases a port nothing is listening on', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ const port = yield allocator.acquire('mjpeg', 'android-s9');
96
+ (0, chai_1.expect)(port).to.equal(scratch.base);
97
+ const row = yield leaseOf(scratch.base);
98
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.leasedToUdid).to.equal('android-s9');
99
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.purpose).to.equal('mjpeg');
100
+ }));
101
+ describe('a host without IPv6', () => {
102
+ it('does not count an IPv6 address it cannot bind as "in use"', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ const probe = sinon_1.default.stub(allocator, 'bindProbe').callsFake((...args) => __awaiter(void 0, void 0, void 0, function* () {
104
+ const host = args[1];
105
+ if (host === '::')
106
+ return 'EAFNOSUPPORT';
107
+ if (host === '::1')
108
+ return 'EADDRNOTAVAIL';
109
+ return null;
110
+ }));
111
+ (0, chai_1.expect)(yield allocator.acquire('mjpeg', 'android-s9')).to.equal(scratch.base);
112
+ const hosts = probe.getCalls().map((c) => c.args[1]);
113
+ (0, chai_1.expect)(hosts).to.include.members(['127.0.0.1', '0.0.0.0', '::1', '::']);
114
+ }));
115
+ it('still counts EADDRINUSE on an IPv6 address as in use', () => __awaiter(void 0, void 0, void 0, function* () {
116
+ sinon_1.default.stub(allocator, 'bindProbe').callsFake((...args) => __awaiter(void 0, void 0, void 0, function* () {
117
+ const [port, host] = args;
118
+ return host === '::' && port === scratch.base ? 'EADDRINUSE' : null;
119
+ }));
120
+ (0, chai_1.expect)(yield allocator.acquire('mjpeg', 'android-s9')).to.equal(scratch.base + 1);
121
+ }));
122
+ });
123
+ describe('release is scoped to the lease holder', () => {
124
+ it("leaves another udid's lease on the same port alone", () => __awaiter(void 0, void 0, void 0, function* () {
125
+ var _a;
126
+ const port = yield allocator.acquire('mjpeg', 'android-s9');
127
+ yield allocator.release(port, 'iphone-17');
128
+ (0, chai_1.expect)((_a = (yield leaseOf(port))) === null || _a === void 0 ? void 0 : _a.leasedToUdid, 'a stale owner must not free a live lease').to.equal('android-s9');
129
+ }));
130
+ it('deletes the lease when the holder releases it', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ const port = yield allocator.acquire('mjpeg', 'android-s9');
132
+ yield allocator.release(port, 'android-s9');
133
+ (0, chai_1.expect)(yield leaseOf(port)).to.equal(null);
134
+ }));
135
+ });
136
+ describe('claim: leasing a port the udid is already serving on', () => {
137
+ it('leases a port with a live listener, which acquire would refuse', () => __awaiter(void 0, void 0, void 0, function* () {
138
+ yield occupy(scratch.base + 50, SHAPES[3]);
139
+ (0, chai_1.expect)(yield allocator.claim('wda', 'iphone-17', scratch.base + 50)).to.equal(true);
140
+ const row = yield leaseOf(scratch.base + 50);
141
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.leasedToUdid).to.equal('iphone-17');
142
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.purpose).to.equal('wda');
143
+ }));
144
+ it("refuses, and keeps the row, when another udid holds the port's lease", () => __awaiter(void 0, void 0, void 0, function* () {
145
+ var _a;
146
+ const held = yield allocator.acquire('wda', 'iphone-other');
147
+ (0, chai_1.expect)(yield allocator.claim('wda', 'iphone-17', held)).to.equal(false);
148
+ (0, chai_1.expect)((_a = (yield leaseOf(held))) === null || _a === void 0 ? void 0 : _a.leasedToUdid).to.equal('iphone-other');
149
+ }));
150
+ });
151
+ });
@@ -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 profile_1 = require("../../src/app/routers/profile");
21
21
  const typedi_1 = require("typedi");
22
22
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
@@ -34,13 +34,13 @@ describe('profile router', () => {
34
34
  it('GET /profile/access-key returns the caller accessKey', () => __awaiter(void 0, void 0, void 0, function* () {
35
35
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findById').resolves({ accessKey: 'xen_abc' });
36
36
  const app = appWithAuth({ userId: 'u1', role: 'ADMIN', scopes: 'devices,sessions,read' });
37
- const r = yield (0, supertest_1.default)(app).get('/profile/access-key');
37
+ const r = yield (0, loopbackRequest_1.default)(app).get('/profile/access-key');
38
38
  (0, chai_1.expect)(r.body.accessKey).to.equal('xen_abc');
39
39
  }));
40
40
  it('POST /profile/access-key/rotate returns a new accessKey', () => __awaiter(void 0, void 0, void 0, function* () {
41
41
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey').resolves({ id: 'u1', accessKey: 'xen_NEW' });
42
42
  const app = appWithAuth({ userId: 'u1', role: 'ADMIN', scopes: 'devices,sessions,read' });
43
- const r = yield (0, supertest_1.default)(app).post('/profile/access-key/rotate');
43
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
44
44
  (0, chai_1.expect)(r.body.accessKey).to.equal('xen_NEW');
45
45
  }));
46
46
  it('GET /profile/tokens lists caller tokens without secrets', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -48,7 +48,7 @@ describe('profile router', () => {
48
48
  { id: 'k1', name: 'CI', scopes: 'sessions,read', expiresAt: null, createdAt: new Date(), lastUsedAt: null },
49
49
  ]);
50
50
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
51
- const r = yield (0, supertest_1.default)(app).get('/profile/tokens');
51
+ const r = yield (0, loopbackRequest_1.default)(app).get('/profile/tokens');
52
52
  (0, chai_1.expect)(r.body[0].name).to.equal('CI');
53
53
  (0, chai_1.expect)(r.body[0].keyHash).to.be.undefined;
54
54
  }));
@@ -56,14 +56,14 @@ describe('profile router', () => {
56
56
  const create = sinon_1.default.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
57
57
  .resolves({ id: 'k2', raw: 'rawsecret' });
58
58
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
59
- const r = yield (0, supertest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
59
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
60
60
  (0, chai_1.expect)(r.status).to.equal(201);
61
61
  (0, chai_1.expect)(r.body.token).to.equal('rawsecret');
62
62
  (0, chai_1.expect)(create.firstCall.args[0].scopes).to.deep.equal(['sessions', 'read']);
63
63
  }));
64
64
  it('POST /profile/tokens rejects an attempt to widen scopes', () => __awaiter(void 0, void 0, void 0, function* () {
65
65
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
66
- const r = yield (0, supertest_1.default)(app).post('/profile/tokens').send({ name: 'CI', scopes: ['admin'] });
66
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI', scopes: ['admin'] });
67
67
  (0, chai_1.expect)(r.status).to.equal(400);
68
68
  (0, chai_1.expect)(r.body.error).to.match(/cannot widen/);
69
69
  }));
@@ -73,7 +73,7 @@ describe('profile router', () => {
73
73
  .resolves({ id: 'k3', raw: 'rawsecret' });
74
74
  const future = new Date(Date.now() + 30 * 24 * 60 * 60000).toISOString();
75
75
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
76
- const r = yield (0, supertest_1.default)(app).post('/profile/tokens').send({ name: 'CI', expiresAt: future });
76
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI', expiresAt: future });
77
77
  (0, chai_1.expect)(r.status).to.equal(201);
78
78
  (0, chai_1.expect)(r.body.expiresAt).to.equal(future);
79
79
  (0, chai_1.expect)((_a = create.firstCall.args[0].expiresAt) === null || _a === void 0 ? void 0 : _a.toISOString()).to.equal(future);
@@ -81,13 +81,13 @@ describe('profile router', () => {
81
81
  it('POST /profile/tokens rejects a past expiresAt with 400', () => __awaiter(void 0, void 0, void 0, function* () {
82
82
  const past = new Date(Date.now() - 60000).toISOString();
83
83
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
84
- const r = yield (0, supertest_1.default)(app).post('/profile/tokens').send({ name: 'CI', expiresAt: past });
84
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI', expiresAt: past });
85
85
  (0, chai_1.expect)(r.status).to.equal(400);
86
86
  (0, chai_1.expect)(r.body.error).to.match(/future/);
87
87
  }));
88
88
  it('POST /profile/tokens rejects an unparseable expiresAt with 400', () => __awaiter(void 0, void 0, void 0, function* () {
89
89
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
90
- const r = yield (0, supertest_1.default)(app).post('/profile/tokens').send({ name: 'CI', expiresAt: 'not-a-date' });
90
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI', expiresAt: 'not-a-date' });
91
91
  (0, chai_1.expect)(r.status).to.equal(400);
92
92
  (0, chai_1.expect)(r.body.error).to.match(/ISO-8601/);
93
93
  }));
@@ -95,14 +95,14 @@ describe('profile router', () => {
95
95
  sinon_1.default.stub(prisma_1.prisma.apiKey, 'findFirst').resolves({ id: 'k1', userId: 'u1' });
96
96
  const revoke = sinon_1.default.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'revoke').resolves(undefined);
97
97
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
98
- const r = yield (0, supertest_1.default)(app).delete('/profile/tokens/k1');
98
+ const r = yield (0, loopbackRequest_1.default)(app).delete('/profile/tokens/k1');
99
99
  (0, chai_1.expect)(r.status).to.equal(204);
100
100
  (0, chai_1.expect)(revoke.calledOnceWithExactly('k1')).to.be.true;
101
101
  }));
102
102
  it('DELETE /profile/tokens/:id 404s when the token belongs to someone else', () => __awaiter(void 0, void 0, void 0, function* () {
103
103
  sinon_1.default.stub(prisma_1.prisma.apiKey, 'findFirst').resolves(null);
104
104
  const app = appWithAuth({ userId: 'u1', role: 'MEMBER', scopes: 'sessions,read' });
105
- const r = yield (0, supertest_1.default)(app).delete('/profile/tokens/k1');
105
+ const r = yield (0, loopbackRequest_1.default)(app).delete('/profile/tokens/k1');
106
106
  (0, chai_1.expect)(r.status).to.equal(404);
107
107
  }));
108
108
  });
@@ -61,6 +61,7 @@ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
61
61
  const config_1 = require("../../src/config");
62
62
  const prisma_1 = require("../../src/prisma");
63
63
  const container_registration_1 = require("../helpers/container-registration");
64
+ const loki_stores_1 = require("../helpers/loki-stores");
64
65
  /**
65
66
  * createSession writes who asked onto the pending-session row, so the queue
66
67
  * can show a member her own and her team's requests. It goes on that row only:
@@ -79,6 +80,10 @@ const caps = (extra = {}) => ({
79
80
  });
80
81
  const withKey = (accessKey, extra = {}) => caps(Object.assign({ 'xe:options': { accessKey, token: 'tk' } }, extra));
81
82
  describe('createSession records who asked on the pending-session row', () => {
83
+ // The in-memory stores, also when this file runs on its own: without
84
+ // NODE_ENV=test the factory handed out the Prisma stores, which are the
85
+ // developer's ~/.cache/xenon/xenon.db.
86
+ (0, loki_stores_1.useLokiStores)();
82
87
  let svc;
83
88
  let device;
84
89
  let context;