@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
@@ -15,18 +15,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
- const prisma_1 = require("../../src/prisma");
19
18
  const test_container_1 = require("../helpers/test-container");
19
+ const scratch_database_1 = require("../helpers/scratch-database");
20
20
  const SessionManager_1 = require("../../src/sessions/SessionManager");
21
21
  const SessionHeartbeatService_1 = require("../../src/services/SessionHeartbeatService");
22
22
  describe('SessionHeartbeatService write path', () => {
23
+ // Its own migrated database. It used the server's client directly, so it
24
+ // wrote this row to the developer's ~/.cache/xenon/xenon.db.
25
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
23
26
  const TEST_SESSION_ID = 'test-heartbeat-sess-001';
24
- let prismaClient;
25
27
  before(() => __awaiter(void 0, void 0, void 0, function* () {
26
28
  (0, test_container_1.setupTestContainer)();
27
- prismaClient = (0, prisma_1.getPrismaClient)();
28
29
  // Create a minimal session row for the test
29
- yield prismaClient.session.upsert({
30
+ yield scratch.db.session.upsert({
30
31
  where: { id: TEST_SESSION_ID },
31
32
  create: {
32
33
  id: TEST_SESSION_ID,
@@ -50,8 +51,6 @@ describe('SessionHeartbeatService write path', () => {
50
51
  });
51
52
  }));
52
53
  after(() => __awaiter(void 0, void 0, void 0, function* () {
53
- // Cleanup test session
54
- yield prismaClient.session.deleteMany({ where: { id: TEST_SESSION_ID } });
55
54
  yield (0, test_container_1.resetTestContainer)();
56
55
  }));
57
56
  afterEach(() => {
@@ -72,7 +71,7 @@ describe('SessionHeartbeatService write path', () => {
72
71
  const svc = new SessionHeartbeatService_1.SessionHeartbeatService();
73
72
  yield svc.checkAllSessions();
74
73
  // Verify the session was updated with heartbeat fields in the DB
75
- const updated = yield prismaClient.session.findUnique({
74
+ const updated = yield scratch.db.session.findUnique({
76
75
  where: { id: TEST_SESSION_ID },
77
76
  select: { last_heartbeat_at: true, heartbeat_pid: true, heartbeat_host: true },
78
77
  });
@@ -22,10 +22,13 @@ const crypto_1 = require("crypto");
22
22
  const typedi_1 = require("typedi");
23
23
  const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
24
24
  const StreamTicketService_1 = require("../../src/services/token/StreamTicketService");
25
+ const container_registration_1 = require("../helpers/container-registration");
25
26
  describe('StreamTicketService', () => {
26
27
  let dir;
27
28
  let svc;
29
+ let restoreContainer;
28
30
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
31
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService);
29
32
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-ticket-'));
30
33
  const keys = new JwtKeyService_1.JwtKeyService();
31
34
  yield keys.init(dir);
@@ -33,7 +36,7 @@ describe('StreamTicketService', () => {
33
36
  svc = new StreamTicketService_1.StreamTicketService();
34
37
  }));
35
38
  afterEach(() => {
36
- typedi_1.Container.reset();
39
+ restoreContainer();
37
40
  fs_1.default.rmSync(dir, { recursive: true, force: true });
38
41
  });
39
42
  it('mints and redeems a ticket once for the bound udid', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -55,9 +55,12 @@ const path_1 = __importDefault(require("path"));
55
55
  const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
56
56
  const auth_1 = require("../../src/app/routers/auth");
57
57
  const mcpScopes_1 = require("../../src/services/token/mcpScopes");
58
+ const container_registration_1 = require("../helpers/container-registration");
58
59
  describe('POST /auth/token handler (issueToken)', () => {
59
60
  let dir;
61
+ let restoreContainer;
60
62
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
63
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService);
61
64
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-tok-'));
62
65
  const svc = new JwtKeyService_1.JwtKeyService();
63
66
  yield svc.init(dir);
@@ -65,7 +68,7 @@ describe('POST /auth/token handler (issueToken)', () => {
65
68
  }));
66
69
  afterEach(() => {
67
70
  fs_1.default.rmSync(dir, { recursive: true, force: true });
68
- typedi_1.Container.reset();
71
+ restoreContainer();
69
72
  });
70
73
  const auth = {
71
74
  kind: 'api-key',
@@ -103,10 +106,21 @@ describe('POST /auth/token handler (issueToken)', () => {
103
106
  });
104
107
  describe('issueToken (xenon-mcp granular claims)', () => {
105
108
  const auth = { userId: 'u1', role: 'MEMBER', scopes: 'devices,sessions,read', teamId: 't1' };
109
+ let dir;
110
+ let restoreContainer;
111
+ // Its own key service, not the process-wide one: init() would give the
112
+ // shared singleton this suite's temporary keys for every later spec.
106
113
  before(() => __awaiter(void 0, void 0, void 0, function* () {
107
- const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'jwtkeys-'));
108
- yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).init(dir);
114
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService);
115
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'jwtkeys-'));
116
+ const svc = new JwtKeyService_1.JwtKeyService();
117
+ yield svc.init(dir);
118
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, svc);
109
119
  }));
120
+ after(() => {
121
+ restoreContainer();
122
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
123
+ });
110
124
  function decode(token) {
111
125
  return __awaiter(this, void 0, void 0, function* () {
112
126
  // decodeJwt skips signature verification — fine for claim-shape assertions
@@ -49,7 +49,7 @@ require("reflect-metadata");
49
49
  const chai_1 = require("chai");
50
50
  const sinon_1 = __importDefault(require("sinon"));
51
51
  const express_1 = __importDefault(require("express"));
52
- const supertest_1 = __importDefault(require("supertest"));
52
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
53
53
  const prisma_1 = require("../../src/prisma");
54
54
  const dashboard_1 = __importStar(require("../../src/app/routers/dashboard"));
55
55
  /**
@@ -144,7 +144,7 @@ describe('Selector Health reads — scoped to the caller’s sessions', () => {
144
144
  ];
145
145
  for (const url of ROUTES) {
146
146
  it(`${url}: a member counts only the sessions they may see`, () => __awaiter(void 0, void 0, void 0, function* () {
147
- const r = yield (0, supertest_1.default)(app(member)).get(url);
147
+ const r = yield (0, loopbackRequest_1.default)(app(member)).get(url);
148
148
  (0, chai_1.expect)(r.status).to.equal(200);
149
149
  const session = findMany.firstCall.args[0].where.session;
150
150
  if (url.includes('build=')) {
@@ -155,7 +155,7 @@ describe('Selector Health reads — scoped to the caller’s sessions', () => {
155
155
  }
156
156
  }));
157
157
  it(`${url}: an admin counts every session`, () => __awaiter(void 0, void 0, void 0, function* () {
158
- const r = yield (0, supertest_1.default)(app(admin)).get(url);
158
+ const r = yield (0, loopbackRequest_1.default)(app(admin)).get(url);
159
159
  (0, chai_1.expect)(r.status).to.equal(200);
160
160
  const session = findMany.firstCall.args[0].where.session;
161
161
  if (url.includes('build=')) {
@@ -183,7 +183,7 @@ describe('Selector Health reads — scoped to the caller’s sessions', () => {
183
183
  findFirst = sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findFirst').resolves(null);
184
184
  });
185
185
  it('a member: the last heal among the sessions they may see', () => __awaiter(void 0, void 0, void 0, function* () {
186
- const r = yield (0, supertest_1.default)(app(member)).get('/healing/state/muted');
186
+ const r = yield (0, loopbackRequest_1.default)(app(member)).get('/healing/state/muted');
187
187
  (0, chai_1.expect)(r.status).to.equal(200);
188
188
  (0, chai_1.expect)(r.body.muted).to.have.length(1);
189
189
  (0, chai_1.expect)(findFirst.firstCall.args[0].where).to.deep.equal({
@@ -194,7 +194,7 @@ describe('Selector Health reads — scoped to the caller’s sessions', () => {
194
194
  });
195
195
  }));
196
196
  it('an admin: the last heal on any session', () => __awaiter(void 0, void 0, void 0, function* () {
197
- const r = yield (0, supertest_1.default)(app(admin)).get('/healing/state/muted');
197
+ const r = yield (0, loopbackRequest_1.default)(app(admin)).get('/healing/state/muted');
198
198
  (0, chai_1.expect)(r.status).to.equal(200);
199
199
  (0, chai_1.expect)(findFirst.firstCall.args[0].where).to.not.have.property('session');
200
200
  }));
@@ -17,6 +17,7 @@ const chai_1 = require("chai");
17
17
  const typedi_1 = require("typedi");
18
18
  const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
19
19
  const PluginContext_1 = require("../../src/PluginContext");
20
+ const container_registration_1 = require("../helpers/container-registration");
20
21
  // Lifecycle/state only — the capture seam (openCapture) is stubbed so the test
21
22
  // drives packet flow without a real device or screenrecord process.
22
23
  function make() {
@@ -31,9 +32,14 @@ describe('AndroidH264StreamService', () => {
31
32
  // known default before each test (streaming undefined → source 'scrcpy'); the
32
33
  // WS-source tests override it in-test. Set (not remove) so TypeDI never hits a
33
34
  // ServiceNotFoundError on the next Container.get.
35
+ // The context is put back after each test: left behind, the plain object
36
+ // was every later spec's PluginContext, with no setContext().
37
+ let restoreContainer;
34
38
  beforeEach(() => {
39
+ restoreContainer = (0, container_registration_1.saveRegistrations)(PluginContext_1.PluginContext);
35
40
  typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: {} });
36
41
  });
42
+ afterEach(() => restoreContainer());
37
43
  it('start() creates a multiplexer, seeds it with the config packet, and resolves', () => __awaiter(void 0, void 0, void 0, function* () {
38
44
  const svc = make();
39
45
  svc.openCapture = (_udid, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
@@ -31,8 +31,15 @@ function makeService() {
31
31
  }
32
32
  describe('Android MJPEG server: a stalled capture ends client responses', () => {
33
33
  let server;
34
+ // Stopped after each test. close() leaves an open response streaming, and a
35
+ // test whose client never ends left its writeFrame loop writing a frame
36
+ // every 60ms for the rest of the run.
37
+ const sessions = [];
34
38
  afterEach(() => {
39
+ for (const session of sessions.splice(0))
40
+ session.status = 'stopped';
35
41
  try {
42
+ server === null || server === void 0 ? void 0 : server.closeAllConnections();
36
43
  server === null || server === void 0 ? void 0 : server.close();
37
44
  }
38
45
  catch (_a) {
@@ -64,6 +71,7 @@ describe('Android MJPEG server: a stalled capture ends client responses', () =>
64
71
  viewerCount: 0,
65
72
  captureHealth: new androidStreamReuse_1.CaptureHealth(),
66
73
  };
74
+ sessions.push(session);
67
75
  const now = Date.now();
68
76
  session.captureHealth.recordFailure(now - androidStreamReuse_1.CAPTURE_STALL_MS * 2, now);
69
77
  server = yield svc.createAndListenMjpegServer(session, 0);
@@ -93,6 +101,7 @@ describe('Android MJPEG server: a stalled capture ends client responses', () =>
93
101
  viewerCount: 0,
94
102
  captureHealth: new androidStreamReuse_1.CaptureHealth(),
95
103
  };
104
+ sessions.push(session);
96
105
  // Port 0 → the OS picks a free one, so this never collides with a real lease.
97
106
  server = yield svc.createAndListenMjpegServer(session, 0);
98
107
  const port = server.address().port;
@@ -133,6 +142,7 @@ describe('Android MJPEG server: a stalled capture ends client responses', () =>
133
142
  viewerCount: 0,
134
143
  captureHealth: new androidStreamReuse_1.CaptureHealth(),
135
144
  };
145
+ sessions.push(session);
136
146
  server = yield svc.createAndListenMjpegServer(session, 0);
137
147
  const port = server.address().port;
138
148
  let ended = false;
@@ -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 fs_1 = __importDefault(require("fs"));
21
21
  const os_1 = __importDefault(require("os"));
22
22
  const path_1 = __importDefault(require("path"));
@@ -101,20 +101,20 @@ describe('apps follow the device team rule', () => {
101
101
  findMany = sinon_1.default.stub(prisma_1.prisma.app, 'findMany').resolves([rows['app-shared']]);
102
102
  });
103
103
  it("asks the database for a member's shared and own-team apps only", () => __awaiter(void 0, void 0, void 0, function* () {
104
- const r = yield (0, supertest_1.default)(server('member-a')).get('/apps');
104
+ const r = yield (0, loopbackRequest_1.default)(server('member-a')).get('/apps');
105
105
  (0, chai_1.expect)(r.status).to.equal(200);
106
106
  const args = findMany.firstCall.args[0];
107
107
  (0, chai_1.expect)(args.where).to.deep.equal({ OR: [{ teamId: null }, { teamId: { in: ['team-a'] } }] });
108
108
  (0, chai_1.expect)(args.include).to.deep.equal({ team: { select: { id: true, name: true } } });
109
109
  }));
110
110
  it('a member in no team gets the shared apps only', () => __awaiter(void 0, void 0, void 0, function* () {
111
- yield (0, supertest_1.default)(server('member-none')).get('/apps');
111
+ yield (0, loopbackRequest_1.default)(server('member-none')).get('/apps');
112
112
  (0, chai_1.expect)(findMany.firstCall.args[0].where).to.deep.equal({
113
113
  OR: [{ teamId: null }, { teamId: { in: [] } }],
114
114
  });
115
115
  }));
116
116
  it('an admin is not filtered, and still gets team names', () => __awaiter(void 0, void 0, void 0, function* () {
117
- yield (0, supertest_1.default)(server('admin')).get('/apps');
117
+ yield (0, loopbackRequest_1.default)(server('admin')).get('/apps');
118
118
  const args = findMany.firstCall.args[0];
119
119
  (0, chai_1.expect)(args.where).to.equal(undefined);
120
120
  (0, chai_1.expect)(args.include).to.deep.equal({ team: { select: { id: true, name: true } } });
@@ -123,26 +123,26 @@ describe('apps follow the device team rule', () => {
123
123
  describe('GET /apps/:id/download', () => {
124
124
  it("serves a member their team's app and a shared one", () => __awaiter(void 0, void 0, void 0, function* () {
125
125
  for (const id of ['app-a', 'app-shared']) {
126
- const r = yield (0, supertest_1.default)(server('member-a')).get(`/apps/${id}/download`);
126
+ const r = yield (0, loopbackRequest_1.default)(server('member-a')).get(`/apps/${id}/download`);
127
127
  (0, chai_1.expect)(r.status, id).to.equal(200);
128
128
  }
129
129
  }));
130
130
  it("answers another team's app exactly as an unknown id", () => __awaiter(void 0, void 0, void 0, function* () {
131
- const hidden = yield (0, supertest_1.default)(server('member-a')).get('/apps/app-b/download');
132
- const unknown = yield (0, supertest_1.default)(server('member-a')).get('/apps/no-such-app/download');
131
+ const hidden = yield (0, loopbackRequest_1.default)(server('member-a')).get('/apps/app-b/download');
132
+ const unknown = yield (0, loopbackRequest_1.default)(server('member-a')).get('/apps/no-such-app/download');
133
133
  (0, chai_1.expect)(hidden.status).to.equal(404);
134
134
  (0, chai_1.expect)(hidden.body).to.deep.equal(UNKNOWN);
135
135
  (0, chai_1.expect)(unknown.status).to.equal(hidden.status);
136
136
  (0, chai_1.expect)(unknown.body).to.deep.equal(hidden.body);
137
137
  }));
138
138
  it('serves an admin any team’s app', () => __awaiter(void 0, void 0, void 0, function* () {
139
- const r = yield (0, supertest_1.default)(server('admin')).get('/apps/app-b/download');
139
+ const r = yield (0, loopbackRequest_1.default)(server('admin')).get('/apps/app-b/download');
140
140
  (0, chai_1.expect)(r.status).to.equal(200);
141
141
  }));
142
142
  it("a download ticket's request sees its own app only, though it carries no teams", () => __awaiter(void 0, void 0, void 0, function* () {
143
- (0, chai_1.expect)((yield (0, supertest_1.default)(server('ticket-for-app-a')).get('/apps/app-a/download')).status).to.equal(200);
143
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(server('ticket-for-app-a')).get('/apps/app-a/download')).status).to.equal(200);
144
144
  for (const id of ['app-b', 'app-shared']) {
145
- const r = yield (0, supertest_1.default)(server('ticket-for-app-a')).get(`/apps/${id}/download`);
145
+ const r = yield (0, loopbackRequest_1.default)(server('ticket-for-app-a')).get(`/apps/${id}/download`);
146
146
  (0, chai_1.expect)(r.status, id).to.equal(404);
147
147
  (0, chai_1.expect)(r.body, id).to.deep.equal(UNKNOWN);
148
148
  }
@@ -154,20 +154,20 @@ describe('apps follow the device team rule', () => {
154
154
  deleteApp = sinon_1.default.stub(app_service_1.APP_SERVICE, 'deleteApp').resolves(undefined);
155
155
  });
156
156
  it('is refused to a member', () => __awaiter(void 0, void 0, void 0, function* () {
157
- const r = yield (0, supertest_1.default)(server('member-a'))
157
+ const r = yield (0, loopbackRequest_1.default)(server('member-a'))
158
158
  .delete('/apps/app-a')
159
159
  .set('Origin', 'http://127.0.0.1');
160
160
  (0, chai_1.expect)(r.status).to.equal(403);
161
161
  (0, chai_1.expect)(deleteApp.called).to.equal(false);
162
162
  }));
163
163
  it('lets an admin delete any team’s app', () => __awaiter(void 0, void 0, void 0, function* () {
164
- const r = yield (0, supertest_1.default)(server('admin')).delete('/apps/app-b');
164
+ const r = yield (0, loopbackRequest_1.default)(server('admin')).delete('/apps/app-b');
165
165
  (0, chai_1.expect)(r.status).to.equal(204);
166
166
  (0, chai_1.expect)(deleteApp.calledOnceWith('app-b')).to.equal(true);
167
167
  }));
168
168
  it('answers an unknown id 404, and a hidden app the same way, deleting nothing', () => __awaiter(void 0, void 0, void 0, function* () {
169
- const unknown = yield (0, supertest_1.default)(server('admin')).delete('/apps/no-such-app');
170
- const hidden = yield (0, supertest_1.default)(server('team-scoped-admin')).delete('/apps/app-b');
169
+ const unknown = yield (0, loopbackRequest_1.default)(server('admin')).delete('/apps/no-such-app');
170
+ const hidden = yield (0, loopbackRequest_1.default)(server('team-scoped-admin')).delete('/apps/app-b');
171
171
  (0, chai_1.expect)(unknown.status).to.equal(404);
172
172
  (0, chai_1.expect)(unknown.body).to.deep.equal(UNKNOWN);
173
173
  (0, chai_1.expect)(hidden.status).to.equal(unknown.status);
@@ -187,7 +187,7 @@ describe('apps follow the device team rule', () => {
187
187
  .callsFake(((args) => __awaiter(void 0, void 0, void 0, function* () { return args.where.id === 'team-a' ? { id: 'team-a' } : null; })));
188
188
  });
189
189
  const post = (caller, teamId) => {
190
- const r = (0, supertest_1.default)(server(caller))
190
+ const r = (0, loopbackRequest_1.default)(server(caller))
191
191
  .post('/apps/upload')
192
192
  .attach('app', Buffer.from('ipa bytes'), 'app.ipa');
193
193
  return teamId === undefined ? r : r.field('teamId', teamId);
@@ -229,7 +229,7 @@ describe('apps follow the device team rule', () => {
229
229
  .stub(prisma_1.prisma.team, 'findUnique')
230
230
  .callsFake(((args) => __awaiter(void 0, void 0, void 0, function* () { return ['team-a', 'team-b'].includes(args.where.id) ? { id: args.where.id } : null; })));
231
231
  });
232
- const put = (caller, id, body) => (0, supertest_1.default)(server(caller))
232
+ const put = (caller, id, body) => (0, loopbackRequest_1.default)(server(caller))
233
233
  .put(`/apps/${id}/team`)
234
234
  .send(body);
235
235
  it('moves an app to a team, as PUT /device/:udid/team does a phone', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -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 auth_1 = require("../../src/app/routers/auth");
22
22
  const UserService_1 = require("../../src/services/UserService");
@@ -58,7 +58,7 @@ describe('GET /auth/me with authDisabled', () => {
58
58
  config_1.config.authDisabled = true;
59
59
  // No User row exists for the synthetic id — this is the real-world state.
60
60
  const findById = sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findById').resolves(null);
61
- const res = yield (0, supertest_1.default)(appWithSyntheticAuth()).get('/auth/me');
61
+ const res = yield (0, loopbackRequest_1.default)(appWithSyntheticAuth()).get('/auth/me');
62
62
  (0, chai_1.expect)(res.status).to.equal(200);
63
63
  (0, chai_1.expect)(res.body.userId).to.equal('auth-disabled');
64
64
  (0, chai_1.expect)(res.body.role).to.equal('SUPER_ADMIN');
@@ -74,14 +74,17 @@ describe('GET /auth/me with authDisabled', () => {
74
74
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findById').resolves({
75
75
  id: 'auth-disabled', email: 'u@x.local', name: 'U', role: 'SUPER_ADMIN', accessKey: 'ak',
76
76
  });
77
- const res = yield (0, supertest_1.default)(appWithSyntheticAuth()).get('/auth/me');
77
+ const res = yield (0, loopbackRequest_1.default)(appWithSyntheticAuth()).get('/auth/me');
78
78
  (0, chai_1.expect)(res.status).to.equal(200);
79
79
  (0, chai_1.expect)(res.body.authDisabled).to.equal(false);
80
+ // The caller's own key, unredacted: the logger's accessKey redaction
81
+ // never touches a response.
82
+ (0, chai_1.expect)(res.body.accessKey).to.equal('ak');
80
83
  }));
81
84
  it('still 401s when auth is enabled and the user does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
82
85
  config_1.config.authDisabled = false;
83
86
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findById').resolves(null);
84
- const res = yield (0, supertest_1.default)(appWithSyntheticAuth()).get('/auth/me');
87
+ const res = yield (0, loopbackRequest_1.default)(appWithSyntheticAuth()).get('/auth/me');
85
88
  (0, chai_1.expect)(res.status).to.equal(401);
86
89
  (0, chai_1.expect)(res.body.error).to.equal('unauthenticated');
87
90
  }));
@@ -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 fs_1 = __importDefault(require("fs"));
22
22
  const os_1 = __importDefault(require("os"));
@@ -110,7 +110,7 @@ describe('authMiddleware — app download ticket', () => {
110
110
  res.on('end', () => done(null, Buffer.concat(chunks)));
111
111
  };
112
112
  const download = (id, ticket) => {
113
- const r = (0, supertest_1.default)(app).get(`/xenon/api/apps/${id}/download`);
113
+ const r = (0, loopbackRequest_1.default)(app).get(`/xenon/api/apps/${id}/download`);
114
114
  return ticket === undefined ? r : r.query({ ticket });
115
115
  };
116
116
  const INVALID = { error: 'invalid ticket' };
@@ -158,16 +158,16 @@ describe('authMiddleware — app download ticket', () => {
158
158
  }));
159
159
  it('an app ticket is refused on the stream route', () => __awaiter(void 0, void 0, void 0, function* () {
160
160
  const t = yield tickets.mint('UDID-1');
161
- const r = yield (0, supertest_1.default)(app).get('/xenon/api/control/UDID-1/stream').query({ ticket: t });
161
+ const r = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/control/UDID-1/stream').query({ ticket: t });
162
162
  (0, chai_1.expect)(r.status).to.equal(401);
163
163
  (0, chai_1.expect)(r.body).to.deep.equal(INVALID);
164
164
  }));
165
165
  it('opens nothing but the download: not the list, nor another method', () => __awaiter(void 0, void 0, void 0, function* () {
166
166
  const t = yield tickets.mint('app-A');
167
- const list = yield (0, supertest_1.default)(app).get('/xenon/api/apps').query({ ticket: t });
167
+ const list = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/apps').query({ ticket: t });
168
168
  (0, chai_1.expect)(list.status).to.equal(401);
169
169
  (0, chai_1.expect)(list.body).to.deep.equal({ error: 'unauthenticated' });
170
- const del = yield (0, supertest_1.default)(app).delete('/xenon/api/apps/app-A/download').query({ ticket: t });
170
+ const del = yield (0, loopbackRequest_1.default)(app).delete('/xenon/api/apps/app-A/download').query({ ticket: t });
171
171
  (0, chai_1.expect)(del.status).to.equal(401);
172
172
  (0, chai_1.expect)(del.body).to.deep.equal({ error: 'unauthenticated' });
173
173
  // Neither attempt spent it.
@@ -25,6 +25,7 @@ const UserService_1 = require("../../src/services/UserService");
25
25
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
26
26
  const UserSessionService_1 = require("../../src/services/UserSessionService");
27
27
  const prisma_1 = require("../../src/prisma");
28
+ const container_registration_1 = require("../helpers/container-registration");
28
29
  function fakeRes() {
29
30
  const res = { statusCode: 200 };
30
31
  res.status = (c) => { res.statusCode = c; return res; };
@@ -36,7 +37,9 @@ describe('authMiddleware — Bearer branch', () => {
36
37
  let dir;
37
38
  let keySvc;
38
39
  const user = { id: 'u1', role: 'MEMBER', status: 'ACTIVE' };
40
+ let restoreContainer;
39
41
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
42
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
40
43
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-bearer-'));
41
44
  keySvc = new JwtKeyService_1.JwtKeyService();
42
45
  yield keySvc.init(dir);
@@ -48,7 +51,7 @@ describe('authMiddleware — Bearer branch', () => {
48
51
  }));
49
52
  afterEach(() => {
50
53
  sinon_1.default.restore();
51
- typedi_1.Container.reset();
54
+ restoreContainer();
52
55
  fs_1.default.rmSync(dir, { recursive: true, force: true });
53
56
  });
54
57
  it('accepts a valid xenon-rest Bearer token and sets req.auth', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -25,6 +25,7 @@ const UserService_1 = require("../../src/services/UserService");
25
25
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
26
26
  const UserSessionService_1 = require("../../src/services/UserSessionService");
27
27
  const prisma_1 = require("../../src/prisma");
28
+ const container_registration_1 = require("../helpers/container-registration");
28
29
  function fakeRes() {
29
30
  const res = { statusCode: 200 };
30
31
  res.status = (c) => { res.statusCode = c; return res; };
@@ -39,7 +40,9 @@ describe('authMiddleware — Bearer branch accepts xenon-mcp audience', () => {
39
40
  let dir;
40
41
  let keySvc;
41
42
  const user = { id: 'u1', role: 'MEMBER', status: 'ACTIVE' };
43
+ let restoreContainer;
42
44
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
45
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
43
46
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-mcp-aud-'));
44
47
  keySvc = new JwtKeyService_1.JwtKeyService();
45
48
  yield keySvc.init(dir);
@@ -51,7 +54,7 @@ describe('authMiddleware — Bearer branch accepts xenon-mcp audience', () => {
51
54
  }));
52
55
  afterEach(() => {
53
56
  sinon_1.default.restore();
54
- typedi_1.Container.reset();
57
+ restoreContainer();
55
58
  fs_1.default.rmSync(dir, { recursive: true, force: true });
56
59
  });
57
60
  it('accepts a valid xenon-mcp Bearer token (ACTIVE user) and sets req.auth.kind=bearer', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -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 fs_1 = __importDefault(require("fs"));
22
22
  const os_1 = __importDefault(require("os"));
@@ -29,6 +29,8 @@ const device_store_1 = require("../../src/data-service/device-store");
29
29
  const UserService_1 = require("../../src/services/UserService");
30
30
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
31
31
  const UserSessionService_1 = require("../../src/services/UserSessionService");
32
+ const container_registration_1 = require("../helpers/container-registration");
33
+ const loopbackServer_1 = require("../helpers/loopbackServer");
32
34
  function fakeRes() {
33
35
  const res = { statusCode: 200 };
34
36
  res.status = (c) => {
@@ -46,7 +48,9 @@ describe('authMiddleware — stream-ticket branch', () => {
46
48
  let dir;
47
49
  let keySvc;
48
50
  let ticketSvc;
51
+ let restoreContainer;
49
52
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
53
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, StreamTicketService_1.StreamTicketService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
50
54
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-authmw-ticket-'));
51
55
  keySvc = new JwtKeyService_1.JwtKeyService();
52
56
  yield keySvc.init(dir);
@@ -62,7 +66,7 @@ describe('authMiddleware — stream-ticket branch', () => {
62
66
  }));
63
67
  afterEach(() => {
64
68
  sinon_1.default.restore();
65
- typedi_1.Container.reset();
69
+ restoreContainer();
66
70
  fs_1.default.rmSync(dir, { recursive: true, force: true });
67
71
  });
68
72
  // req.path here mirrors what authMiddleware actually sees at runtime:
@@ -194,7 +198,12 @@ describe('authMiddleware — stream-ticket branch (real mount-order, supertest)'
194
198
  let dir;
195
199
  let app;
196
200
  let ticketSvc;
201
+ let restoreContainer;
202
+ // Served on 127.0.0.1: request(app) listens on every address and connects
203
+ // to 127.0.0.1, where another process's listener on that port can answer.
204
+ const loopback = (0, loopbackServer_1.loopbackServers)();
197
205
  beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
206
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, StreamTicketService_1.StreamTicketService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
198
207
  dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-authmw-mount-'));
199
208
  const keySvc = new JwtKeyService_1.JwtKeyService();
200
209
  yield keySvc.init(dir);
@@ -222,21 +231,24 @@ describe('authMiddleware — stream-ticket branch (real mount-order, supertest)'
222
231
  app = (0, express_1.default)();
223
232
  app.use('/xenon/api', apiRouter);
224
233
  }));
225
- afterEach(() => {
234
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
226
235
  sinon_1.default.restore();
227
- typedi_1.Container.reset();
236
+ restoreContainer();
237
+ yield loopback.closeAll();
228
238
  fs_1.default.rmSync(dir, { recursive: true, force: true });
229
- });
239
+ }));
230
240
  it('a valid ticket on GET /xenon/api/control/:udid/stream reaches the route (past 401)', () => __awaiter(void 0, void 0, void 0, function* () {
231
241
  const ticket = yield ticketSvc.mint('UDID-1', 'actor-1');
232
- const res = yield (0, supertest_1.default)(app).get('/xenon/api/control/UDID-1/stream').query({ ticket });
242
+ const res = yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
243
+ .get('/xenon/api/control/UDID-1/stream')
244
+ .query({ ticket });
233
245
  // The ticket branch fired: request cleared auth and hit the handler's
234
246
  // device lookup (404), rather than being rejected at the middleware (401).
235
247
  (0, chai_1.expect)(res.status).to.not.equal(401);
236
248
  (0, chai_1.expect)(res.status).to.equal(404); // 'Device not found' from the stubbed store
237
249
  }));
238
250
  it('the SAME request without a ticket is rejected 401 (proves the guard is non-vacuous)', () => __awaiter(void 0, void 0, void 0, function* () {
239
- const res = yield (0, supertest_1.default)(app).get('/xenon/api/control/UDID-1/stream');
251
+ const res = yield (0, loopbackRequest_1.default)(yield loopback.serve(app)).get('/xenon/api/control/UDID-1/stream');
240
252
  (0, chai_1.expect)(res.status).to.equal(401);
241
253
  (0, chai_1.expect)(res.body).to.deep.equal({ error: 'unauthenticated' });
242
254
  }));
@@ -92,6 +92,9 @@ describe('authMiddleware', () => {
92
92
  role: 'MEMBER',
93
93
  status: 'ACTIVE',
94
94
  });
95
+ // A member's teams are looked up; without this the lookup read the
96
+ // developer's own database.
97
+ sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
95
98
  const req = { headers: { cookie: 'xenon_dashboard_session=legacy-key-id' } };
96
99
  let called = false;
97
100
  yield (0, authMiddleware_1.authMiddleware)(req, mkRes(), () => {
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
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 sinon_1 = __importDefault(require("sinon"));
20
20
  const BugReportService_1 = require("../../../src/services/bug-report/BugReportService");
21
21
  const bug_report_1 = __importDefault(require("../../../src/app/routers/bug-report"));
@@ -63,8 +63,8 @@ describe('bug-report route', () => {
63
63
  throw new Error(`Session ${opts.sessionId} not found`);
64
64
  throw new Error('built a report');
65
65
  }));
66
- const hidden = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-b/bug-report?mode=full');
67
- const unknown = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-nope/bug-report?mode=full');
66
+ const hidden = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-b/bug-report?mode=full');
67
+ const unknown = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-nope/bug-report?mode=full');
68
68
  (0, chai_1.expect)(hidden.status).to.equal(404);
69
69
  (0, chai_1.expect)(unknown.status).to.equal(404);
70
70
  (0, chai_1.expect)(hidden.body).to.deep.equal({ error: 'Session sess-b not found' });
@@ -78,26 +78,26 @@ describe('bug-report route', () => {
78
78
  entries: [{ name: 'a.txt', source: { kind: 'buffer', data: Buffer.from('hi') } }],
79
79
  cleanup: () => __awaiter(void 0, void 0, void 0, function* () { }),
80
80
  });
81
- const res = yield (0, supertest_1.default)(makeApp({ admin: true })).post('/sessions/sess-b/bug-report?mode=full');
81
+ const res = yield (0, loopbackRequest_1.default)(makeApp({ admin: true })).post('/sessions/sess-b/bug-report?mode=full');
82
82
  (0, chai_1.expect)(res.status).to.equal(200);
83
83
  }));
84
84
  it('401 when the caller is not signed in', () => __awaiter(void 0, void 0, void 0, function* () {
85
- const res = yield (0, supertest_1.default)(makeApp({ signedIn: false })).post('/sessions/sess-1/bug-report?mode=full');
85
+ const res = yield (0, loopbackRequest_1.default)(makeApp({ signedIn: false })).post('/sessions/sess-1/bug-report?mode=full');
86
86
  (0, chai_1.expect)(res.status).to.equal(401);
87
87
  }));
88
88
  it('400 on invalid mode', () => __awaiter(void 0, void 0, void 0, function* () {
89
- const res = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=bogus');
89
+ const res = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=bogus');
90
90
  (0, chai_1.expect)(res.status).to.equal(400);
91
91
  }));
92
92
  it('400 on out-of-range windowSec', () => __awaiter(void 0, void 0, void 0, function* () {
93
- const res = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=slice&windowSec=9999');
93
+ const res = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=slice&windowSec=9999');
94
94
  (0, chai_1.expect)(res.status).to.equal(400);
95
95
  }));
96
96
  it('404 when service throws not-found', () => __awaiter(void 0, void 0, void 0, function* () {
97
97
  sinon_1.default
98
98
  .stub(BugReportService_1.BugReportService.prototype, 'assemble')
99
99
  .rejects(new Error('Session sess-1 not found'));
100
- const res = yield (0, supertest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=full');
100
+ const res = yield (0, loopbackRequest_1.default)(makeApp()).post('/sessions/sess-1/bug-report?mode=full');
101
101
  (0, chai_1.expect)(res.status).to.equal(404);
102
102
  }));
103
103
  it('200 streams zip on success', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -107,7 +107,7 @@ describe('bug-report route', () => {
107
107
  entries: [{ name: 'a.txt', source: { kind: 'buffer', data: Buffer.from('hi') } }],
108
108
  cleanup: () => __awaiter(void 0, void 0, void 0, function* () { }),
109
109
  });
110
- const res = yield (0, supertest_1.default)(makeApp())
110
+ const res = yield (0, loopbackRequest_1.default)(makeApp())
111
111
  .post('/sessions/sess-1/bug-report?mode=full')
112
112
  .buffer(true)
113
113
  .parse((r, cb) => {