@xenon-device-management/xenon 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +3 -4
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
  5. package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
  69. package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
  70. package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +11 -104
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/routers/nodePhoneControl.js +301 -0
  84. package/lib/src/app/ws/h264StreamWs.js +43 -12
  85. package/lib/src/app/ws/logcatWs.js +48 -14
  86. package/lib/src/app/ws/nodeSocketRelay.js +167 -0
  87. package/lib/src/app/ws/upgradeRouter.js +215 -0
  88. package/lib/src/dashboard/event-manager.js +7 -6
  89. package/lib/src/data-service/device-service.js +152 -49
  90. package/lib/src/data-service/device-store.js +92 -10
  91. package/lib/src/data-service/deviceClaims.js +116 -0
  92. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  93. package/lib/src/data-service/prisma-store.js +102 -31
  94. package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
  95. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  96. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  97. package/lib/src/device-managers/NodeDevices.js +7 -2
  98. package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
  99. package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
  100. package/lib/src/device-managers/android/previewHold.js +2 -2
  101. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  102. package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
  103. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  104. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  105. package/lib/src/device-utils.js +78 -24
  106. package/lib/src/gateway/createHandoff.js +73 -0
  107. package/lib/src/gateway/defaultGateway.js +80 -0
  108. package/lib/src/gateway/forwardToNode.js +147 -0
  109. package/lib/src/gateway/hubSessionToken.js +356 -0
  110. package/lib/src/gateway/internalCall.js +90 -0
  111. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  112. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  113. package/lib/src/gateway/sessionCreate.js +136 -0
  114. package/lib/src/gateway/sessionGateway.js +201 -0
  115. package/lib/src/gateway/sessionLocator.js +100 -0
  116. package/lib/src/generated/client/edge.js +7 -4
  117. package/lib/src/generated/client/index-browser.js +4 -1
  118. package/lib/src/generated/client/index.d.ts +112 -1
  119. package/lib/src/generated/client/index.js +7 -4
  120. package/lib/src/generated/client/package.json +1 -1
  121. package/lib/src/generated/client/schema.prisma +9 -0
  122. package/lib/src/generated/client/wasm.js +4 -1
  123. package/lib/src/helpers/index.js +5 -1
  124. package/lib/src/logger.js +5 -0
  125. package/lib/src/middleware/authMiddleware.js +80 -0
  126. package/lib/src/middleware/commandAuth.js +31 -17
  127. package/lib/src/middleware/csrfMiddleware.js +3 -2
  128. package/lib/src/middleware/deviceAccessGuard.js +4 -1
  129. package/lib/src/middleware/sessionListingFilter.js +153 -0
  130. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  131. package/lib/src/plugin.js +27 -1
  132. package/lib/src/prisma.js +2 -1
  133. package/lib/src/services/DeviceReconciler.js +5 -2
  134. package/lib/src/services/OrphanSweeper.js +12 -19
  135. package/lib/src/services/PortAllocator.js +75 -16
  136. package/lib/src/services/ServerManager.js +63 -30
  137. package/lib/src/services/SessionLifecycleService.js +335 -76
  138. package/lib/src/services/ShutdownCoordinator.js +7 -1
  139. package/lib/src/services/SocketServer.js +11 -2
  140. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  141. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  142. package/lib/src/services/device-access/ticketActorAccess.js +3 -1
  143. package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
  144. package/lib/src/services/lease/LeaseService.js +19 -5
  145. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  146. package/lib/src/services/session/sessionCredentials.js +7 -59
  147. package/lib/src/sessions/CloudSession.js +12 -0
  148. package/lib/src/sessions/LocalSession.js +52 -26
  149. package/lib/src/sessions/RemoteSession.js +116 -9
  150. package/lib/src/sessions/SessionManager.js +49 -6
  151. package/lib/src/sessions/appiumUmbrella.js +32 -0
  152. package/lib/test/helpers/appium-umbrella.js +84 -0
  153. package/lib/test/helpers/fake-device-table.js +97 -0
  154. package/lib/test/helpers/loki-stores.js +41 -0
  155. package/lib/test/helpers/loopbackRequest.js +77 -0
  156. package/lib/test/helpers/own-node-id.js +29 -0
  157. package/lib/test/helpers/scratch-database.js +100 -0
  158. package/lib/test/helpers/scratch-port-leases.js +119 -0
  159. package/lib/test/helpers/test-container.js +35 -27
  160. package/lib/test/integration/androidDevices.spec.js +5 -4
  161. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  162. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  163. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  164. package/lib/test/setup/scratch-default-database.js +32 -0
  165. package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
  166. package/lib/test/unit/DeviceModel.spec.js +9 -9
  167. package/lib/test/unit/HealingCascade.test.js +5 -1
  168. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  169. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  170. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  171. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  172. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  173. package/lib/test/unit/android-h264-service.spec.js +24 -0
  174. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  175. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  176. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  177. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  178. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  179. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  180. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  181. package/lib/test/unit/authMiddleware.test.js +3 -0
  182. package/lib/test/unit/bug-report/route.spec.js +9 -9
  183. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  184. package/lib/test/unit/clipboard-route.spec.js +22 -8
  185. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  186. package/lib/test/unit/command-auth.spec.js +31 -31
  187. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  188. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  189. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  190. package/lib/test/unit/create-error-message.spec.js +43 -0
  191. package/lib/test/unit/create-handoff.spec.js +86 -0
  192. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  193. package/lib/test/unit/device-access-guard.spec.js +66 -28
  194. package/lib/test/unit/device-service.spec.js +4 -0
  195. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  196. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  197. package/lib/test/unit/device-team-guard.spec.js +20 -20
  198. package/lib/test/unit/device-utils.spec.js +8 -0
  199. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  200. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  201. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  202. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  203. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  204. package/lib/test/unit/helpers.spec.js +27 -0
  205. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  206. package/lib/test/unit/hub-create-once.spec.js +190 -0
  207. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  208. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  209. package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
  210. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  211. package/lib/test/unit/hub-node-race.spec.js +426 -0
  212. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  213. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  214. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  215. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  216. package/lib/test/unit/hub-session-token.spec.js +265 -0
  217. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  218. package/lib/test/unit/install-repository-app-team.spec.js +6 -2
  219. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  220. package/lib/test/unit/internal-call.spec.js +181 -0
  221. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  222. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  223. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  224. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  225. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  226. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  227. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  228. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  229. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
  230. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  231. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  232. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  233. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  234. package/lib/test/unit/logcat-ws.spec.js +12 -1
  235. package/lib/test/unit/loopback-request.spec.js +81 -0
  236. package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
  237. package/lib/test/unit/node-create-caps.spec.js +54 -0
  238. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  239. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  240. package/lib/test/unit/node-phone-control.spec.js +131 -0
  241. package/lib/test/unit/node-socket-relay.spec.js +357 -0
  242. package/lib/test/unit/omni-vision.spec.js +7 -3
  243. package/lib/test/unit/plugin.spec.js +45 -4
  244. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  245. package/lib/test/unit/profile-router.test.js +11 -11
  246. package/lib/test/unit/queue-requester.spec.js +5 -0
  247. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  248. package/lib/test/unit/queue-visibility.spec.js +1 -1
  249. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  250. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  251. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  252. package/lib/test/unit/recording-store.spec.js +9 -3
  253. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  254. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  255. package/lib/test/unit/remote-session-create.spec.js +130 -0
  256. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  257. package/lib/test/unit/reset-link.test.js +14 -14
  258. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  259. package/lib/test/unit/session-assets.spec.js +9 -9
  260. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  261. package/lib/test/unit/session-create-layer.spec.js +255 -0
  262. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  263. package/lib/test/unit/session-credentials.spec.js +0 -47
  264. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  265. package/lib/test/unit/session-locator.spec.js +250 -0
  266. package/lib/test/unit/session-node-calls.spec.js +174 -0
  267. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  268. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  269. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  270. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  271. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  272. package/lib/test/unit/stream-leave-route.spec.js +19 -7
  273. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  274. package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
  275. package/lib/test/unit/stream-stop-recording.spec.js +17 -5
  276. package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
  277. package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
  278. package/lib/test/unit/upgrade-router.spec.js +446 -0
  279. package/lib/test/unit/users-router.test.js +11 -8
  280. package/lib/test/unit/verify-credential.spec.js +4 -1
  281. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  282. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  283. package/lib/tsconfig.tsbuildinfo +1 -1
  284. package/package.json +3 -4
  285. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  286. package/prisma/schema.prisma +9 -0
  287. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  288. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  289. package/lib/src/proxy/wd-command-proxy.js +0 -221
@@ -48,22 +48,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
48
48
  require("reflect-metadata");
49
49
  const chai_1 = require("chai");
50
50
  const sinon_1 = __importDefault(require("sinon"));
51
- const prisma_1 = require("../../src/prisma");
52
51
  const test_container_1 = require("../helpers/test-container");
52
+ const scratch_database_1 = require("../helpers/scratch-database");
53
+ const device_store_1 = require("../../src/data-service/device-store");
54
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
53
55
  const event_manager_1 = require("../../src/dashboard/event-manager");
54
56
  const SessionStatus_1 = require("../../src/types/SessionStatus");
55
57
  describe('OrphanSweeper', () => {
56
- let prismaClient;
58
+ // Its own migrated database, never the developer's ~/.cache/xenon/xenon.db:
59
+ // that file's schema is whatever the last local migrate left, so a new
60
+ // column failed this spec there and nowhere else.
61
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
62
+ let savedStore;
57
63
  const STALE_SESSION_ID = 'orphan-stale-sess-001';
58
64
  const STALE_UDID = 'udid-orphan-1';
59
65
  const NODE_ID = 'node-a';
60
66
  const FRESH_SESSION_ID = 'orphan-fresh-sess-002';
61
67
  before(() => __awaiter(void 0, void 0, void 0, function* () {
62
68
  (0, test_container_1.setupTestContainer)();
63
- prismaClient = (0, prisma_1.getPrismaClient)();
69
+ savedStore = device_store_1.DeviceStoreFactory._deviceStore;
70
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
64
71
  // Create a stale session (last_heartbeat_at is old / null with old updatedAt)
65
72
  const oldDate = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes ago
66
- yield prismaClient.session.upsert({
73
+ yield scratch.db.session.upsert({
67
74
  where: { id: STALE_SESSION_ID },
68
75
  create: {
69
76
  id: STALE_SESSION_ID,
@@ -87,7 +94,7 @@ describe('OrphanSweeper', () => {
87
94
  },
88
95
  });
89
96
  // Create a device for the stale session
90
- yield prismaClient.device.upsert({
97
+ yield scratch.db.device.upsert({
91
98
  where: { udid_host: { udid: STALE_UDID, host: 'http://localhost:4723' } },
92
99
  create: {
93
100
  udid: STALE_UDID,
@@ -105,10 +112,7 @@ describe('OrphanSweeper', () => {
105
112
  });
106
113
  }));
107
114
  after(() => __awaiter(void 0, void 0, void 0, function* () {
108
- yield prismaClient.session.deleteMany({
109
- where: { id: { in: [STALE_SESSION_ID, FRESH_SESSION_ID] } },
110
- });
111
- yield prismaClient.device.deleteMany({ where: { udid: STALE_UDID } });
115
+ device_store_1.DeviceStoreFactory._deviceStore = savedStore;
112
116
  yield (0, test_container_1.resetTestContainer)();
113
117
  }));
114
118
  afterEach(() => {
@@ -122,13 +126,13 @@ describe('OrphanSweeper', () => {
122
126
  const { OrphanSweeper } = yield Promise.resolve().then(() => __importStar(require('../../src/services/OrphanSweeper')));
123
127
  const sweeper = new OrphanSweeper();
124
128
  yield sweeper.sweep({ heartbeatIntervalMs: 30000 });
125
- const session = yield prismaClient.session.findUnique({
129
+ const session = yield scratch.db.session.findUnique({
126
130
  where: { id: STALE_SESSION_ID },
127
131
  select: { status: true, failure_reason: true },
128
132
  });
129
133
  (0, chai_1.expect)(session.status).to.equal('failed');
130
134
  (0, chai_1.expect)(session.failure_reason).to.match(/heartbeat timeout/i);
131
- const device = yield prismaClient.device.findFirst({
135
+ const device = yield scratch.db.device.findFirst({
132
136
  where: { udid: STALE_UDID },
133
137
  select: { busy: true, owningSessionId: true },
134
138
  });
@@ -138,7 +142,7 @@ describe('OrphanSweeper', () => {
138
142
  }));
139
143
  it('leaves fresh sessions alone', () => __awaiter(void 0, void 0, void 0, function* () {
140
144
  // Create a fresh session (heartbeat just now)
141
- yield prismaClient.session.upsert({
145
+ yield scratch.db.session.upsert({
142
146
  where: { id: FRESH_SESSION_ID },
143
147
  create: {
144
148
  id: FRESH_SESSION_ID,
@@ -164,7 +168,7 @@ describe('OrphanSweeper', () => {
164
168
  const { OrphanSweeper } = yield Promise.resolve().then(() => __importStar(require('../../src/services/OrphanSweeper')));
165
169
  const sweeper = new OrphanSweeper();
166
170
  yield sweeper.sweep({ heartbeatIntervalMs: 30000 });
167
- const session = yield prismaClient.session.findUnique({
171
+ const session = yield scratch.db.session.findUnique({
168
172
  where: { id: FRESH_SESSION_ID },
169
173
  select: { status: true },
170
174
  });
@@ -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
  }));
@@ -14,9 +14,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
17
18
  const typedi_1 = require("typedi");
18
19
  const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
19
20
  const PluginContext_1 = require("../../src/PluginContext");
21
+ const container_registration_1 = require("../helpers/container-registration");
20
22
  // Lifecycle/state only — the capture seam (openCapture) is stubbed so the test
21
23
  // drives packet flow without a real device or screenrecord process.
22
24
  function make() {
@@ -31,9 +33,14 @@ describe('AndroidH264StreamService', () => {
31
33
  // known default before each test (streaming undefined → source 'scrcpy'); the
32
34
  // WS-source tests override it in-test. Set (not remove) so TypeDI never hits a
33
35
  // ServiceNotFoundError on the next Container.get.
36
+ // The context is put back after each test: left behind, the plain object
37
+ // was every later spec's PluginContext, with no setContext().
38
+ let restoreContainer;
34
39
  beforeEach(() => {
40
+ restoreContainer = (0, container_registration_1.saveRegistrations)(PluginContext_1.PluginContext);
35
41
  typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: {} });
36
42
  });
43
+ afterEach(() => restoreContainer());
37
44
  it('start() creates a multiplexer, seeds it with the config packet, and resolves', () => __awaiter(void 0, void 0, void 0, function* () {
38
45
  const svc = make();
39
46
  svc.openCapture = (_udid, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
@@ -44,6 +51,23 @@ describe('AndroidH264StreamService', () => {
44
51
  (0, chai_1.expect)(mux).to.equal(svc.getMultiplexer('udid-x'));
45
52
  (0, chai_1.expect)(mux.clientCount).to.equal(0);
46
53
  }));
54
+ it('start() leaves no timer behind once the config packet arrives', () => __awaiter(void 0, void 0, void 0, function* () {
55
+ const clock = sinon_1.default.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
56
+ try {
57
+ const svc = make();
58
+ svc.openCapture = (_udid, onPacket) => __awaiter(void 0, void 0, void 0, function* () {
59
+ onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
60
+ return { kill: () => undefined };
61
+ });
62
+ yield svc.start('udid-t');
63
+ // The 3 s wait is only a cap: a start that got its config in time must
64
+ // not leave it pending for the process to wait out.
65
+ (0, chai_1.expect)(clock.countTimers()).to.equal(0);
66
+ }
67
+ finally {
68
+ clock.restore();
69
+ }
70
+ }));
47
71
  it('start() is idempotent per udid (concurrent starts share one capture)', () => __awaiter(void 0, void 0, void 0, function* () {
48
72
  const svc = make();
49
73
  let opens = 0;
@@ -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* () {