@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
package/lib/src/plugin.js CHANGED
@@ -60,9 +60,12 @@ const AICommandService_1 = require("./services/AICommandService");
60
60
  const TracingService_1 = require("./services/TracingService");
61
61
  const CommandInterceptor_1 = require("./interceptors/CommandInterceptor");
62
62
  const SessionLifecycleService_1 = require("./services/SessionLifecycleService");
63
+ const createHandoff_1 = require("./gateway/createHandoff");
63
64
  const ServerManager_1 = require("./services/ServerManager");
64
65
  const IPluginArgs_1 = require("./interfaces/IPluginArgs");
65
66
  const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
67
+ const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
68
+ const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
66
69
  const config_1 = require("./config");
67
70
  const SessionManager_1 = require("./sessions/SessionManager");
68
71
  const event_manager_1 = require("./dashboard/event-manager");
@@ -94,6 +97,7 @@ class XenonPlugin extends base_plugin_1.default {
94
97
  return __awaiter(this, void 0, void 0, function* () {
95
98
  var _a;
96
99
  const sessionId = driver.sessionId;
100
+ typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
97
101
  const deviceFilter = {
98
102
  session_id: sessionId ? sessionId : undefined,
99
103
  udid: driver.caps && driver.caps.udid ? driver.caps.udid : undefined,
@@ -105,6 +109,10 @@ class XenonPlugin extends base_plugin_1.default {
105
109
  hubToken: config_1.config.hubToken,
106
110
  }).unblockDevice(deviceFilter);
107
111
  }
112
+ else if (sessionId) {
113
+ // Keyed on the session: its phone may be another session's by now.
114
+ yield (0, device_service_1.releaseSessionDevices)(sessionId);
115
+ }
108
116
  else {
109
117
  yield (0, device_service_1.unblockDeviceMatchingFilter)(deviceFilter);
110
118
  }
@@ -168,7 +176,25 @@ class XenonPlugin extends base_plugin_1.default {
168
176
  if (caps && (!Array.isArray(caps.firstMatch) || caps.firstMatch.length === 0)) {
169
177
  caps.firstMatch = [{}];
170
178
  }
171
- return yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).createSession(next, driver, caps);
179
+ // The umbrella: where a node answers the hub's "does this session exist"
180
+ // without sending the session a command (gateway/nodeSessionStatus.ts).
181
+ typedi_1.Container.get(appiumUmbrella_1.AppiumUmbrella).note(driver);
182
+ const lifecycle = typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService);
183
+ // The session gateway allocated this request's phone in front of Appium's
184
+ // route (gateway/sessionCreate.ts) and handed it here through the
185
+ // request's async context. It only ever hands over a phone this server
186
+ // drives: it creates a node's session itself and answers it, so Appium
187
+ // never has to hold a session it has no driver for.
188
+ const handoff = (0, createHandoff_1.currentCreateHandoff)();
189
+ if (handoff)
190
+ return yield lifecycle.completeLocalSession(handoff.take(), next, driver, caps);
191
+ // A create that did not come through the gateway (it could not be placed
192
+ // ahead of Appium's routes). A phone this server drives is still created
193
+ // here; one on another server is refused, since answering for Appium
194
+ // without calling next() is what broke its umbrella driver.
195
+ this.xenonLog.warn("createSession reached the plugin without the session gateway's allocation; allocating " +
196
+ 'here. Sessions on phones other servers drive need the session gateway, and are refused.');
197
+ return yield lifecycle.createSession(next, driver, caps, { localOnly: true });
172
198
  });
173
199
  }
174
200
  deleteSession(next, driver, sessionId) {
package/lib/src/prisma.js CHANGED
@@ -36,7 +36,8 @@ const MODEL_DELEGATES = new Set([
36
36
  'build', 'session', 'sessionLog', 'log', 'profiling', 'app', 'device',
37
37
  'pendingSession', 'cLIArgs', 'webhookConfig', 'webConfig', 'locatorEtalon',
38
38
  'lease', 'portLease', 'apiKey', 'selectorState', 'user', 'userSession',
39
- 'passwordResetToken', 'team', 'teamMember', 'eventLog', 'project',
39
+ 'passwordResetToken', 'team', 'teamMember', 'eventLog', 'project', 'recording', 'bookmark',
40
+ 'annotation',
40
41
  ]);
41
42
  /** Cache of plain-object wrappers, keyed by model name. */
42
43
  const _modelWrappers = new Map();
@@ -78,8 +78,11 @@ let DeviceReconciler = DeviceReconciler_1 = class DeviceReconciler {
78
78
  .join(', ')}`);
79
79
  for (const device of orphans) {
80
80
  try {
81
- yield (0, device_service_1.unblockDevice)(device.udid, device.host);
82
- this.orphansFreed++;
81
+ // Keyed on the session read above: if it ended meanwhile and the
82
+ // phone went to another session, that one keeps it.
83
+ if (yield (0, device_service_1.releaseSessionDevice)(device.udid, device.host, device.session_id)) {
84
+ this.orphansFreed++;
85
+ }
83
86
  }
84
87
  catch (err) {
85
88
  this.logger.error(`Failed to unblock ghost device ${device.udid}: ${err.message}`);
@@ -24,6 +24,7 @@ const prisma_1 = require("../prisma");
24
24
  const logger_1 = __importDefault(require("../logger"));
25
25
  const event_manager_1 = require("../dashboard/event-manager");
26
26
  const SessionStatus_1 = require("../types/SessionStatus");
27
+ const device_service_1 = require("../data-service/device-service");
27
28
  let OrphanSweeper = class OrphanSweeper {
28
29
  constructor() {
29
30
  this.log = logger_1.default.scope('OrphanSweeper');
@@ -60,25 +61,17 @@ let OrphanSweeper = class OrphanSweeper {
60
61
  this.log.info(`Sweeping ${stale.length} orphaned session(s)`);
61
62
  for (const s of stale) {
62
63
  try {
63
- yield prisma_1.prisma.$transaction([
64
- prisma_1.prisma.session.update({
65
- where: { id: s.id },
66
- data: {
67
- status: 'failed',
68
- failure_reason: 'Session heartbeat timeout',
69
- endTime: new Date(),
70
- },
71
- }),
72
- prisma_1.prisma.device.updateMany({
73
- where: { udid: s.device_udid },
74
- data: {
75
- busy: false,
76
- session_id: null,
77
- owningSessionId: null,
78
- lockedAt: null,
79
- },
80
- }),
81
- ]);
64
+ yield prisma_1.prisma.session.update({
65
+ where: { id: s.id },
66
+ data: {
67
+ status: 'failed',
68
+ failure_reason: 'Session heartbeat timeout',
69
+ endTime: new Date(),
70
+ },
71
+ });
72
+ // This session's claim only, not every row with its udid: the phone
73
+ // may be another session's by now (deviceClaims.ts).
74
+ yield (0, device_service_1.releaseSessionDevices)(s.id);
82
75
  // Event emit stays outside the transaction (side-effect with own error boundary)
83
76
  yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(s.id, SessionStatus_1.SessionStatus.FAILED, 'Session heartbeat timeout');
84
77
  }
@@ -32,6 +32,13 @@ const DEFAULT_RANGES = {
32
32
  system: [10100, 10199],
33
33
  proxy: [11100, 11199],
34
34
  };
35
+ /** Loopback and wildcard, in both families. See isOsFree. */
36
+ const PROBE_HOSTS = ['127.0.0.1', '::1', '0.0.0.0', '::'];
37
+ /**
38
+ * Bind errors meaning the host can't use that address at all (IPv6 disabled),
39
+ * not that something holds the port. Any other error still counts as in use.
40
+ */
41
+ const ADDRESS_UNAVAILABLE = new Set(['EADDRNOTAVAIL', 'EAFNOSUPPORT']);
35
42
  class PortRangeExhaustedError extends Error {
36
43
  constructor(purpose) {
37
44
  super(`Port range for purpose '${purpose}' is exhausted`);
@@ -166,9 +173,43 @@ let PortAllocator = class PortAllocator {
166
173
  .catch(() => undefined);
167
174
  });
168
175
  }
169
- release(port) {
176
+ /**
177
+ * Lease a port `udid` is already serving on. acquire() would refuse it,
178
+ * because its OS probe sees the listener, and that listener is the device's
179
+ * own: iOS attaching to the WDA an Appium session forwards is the case. The
180
+ * lease is what keeps the port from being handed to another device once the
181
+ * listener closes.
182
+ *
183
+ * Returns false, and leaves the row alone, when another udid holds an
184
+ * unexpired lease on the port.
185
+ */
186
+ claim(purpose_1, udid_1, port_1) {
187
+ return __awaiter(this, arguments, void 0, function* (purpose, udid, port, opts = {}) {
188
+ var _a;
189
+ const now = Date.now();
190
+ const ttlMs = (_a = opts.ttlMs) !== null && _a !== void 0 ? _a : 60 * 60 * 1000;
191
+ const held = yield prisma_1.prisma.portLease.findUnique({ where: { port } });
192
+ if (held && held.leasedToUdid !== udid && held.expiresAt >= now)
193
+ return false;
194
+ const lease = { purpose, leasedToUdid: udid, leasedAt: now, expiresAt: now + ttlMs };
195
+ yield prisma_1.prisma.portLease.upsert({
196
+ where: { port },
197
+ create: Object.assign({ port }, lease),
198
+ update: lease,
199
+ });
200
+ return true;
201
+ });
202
+ }
203
+ /**
204
+ * Release `port` if `udid` holds it. A port number alone doesn't say whose
205
+ * lease it is: a stale session releasing "its" port used to delete whatever
206
+ * lease held that number by then, including another device's live one.
207
+ */
208
+ release(port, udid) {
170
209
  return __awaiter(this, void 0, void 0, function* () {
171
- yield prisma_1.prisma.portLease.delete({ where: { port } }).catch(() => undefined);
210
+ yield prisma_1.prisma.portLease
211
+ .deleteMany({ where: { port, leasedToUdid: udid } })
212
+ .catch(() => undefined);
172
213
  });
173
214
  }
174
215
  /**
@@ -195,23 +236,41 @@ let PortAllocator = class PortAllocator {
195
236
  yield prisma_1.prisma.portLease.deleteMany({ where: { expiresAt: { lt: Date.now() } } });
196
237
  });
197
238
  }
239
+ /**
240
+ * Whether nothing listens on `port` on any local address.
241
+ *
242
+ * One bind can't tell. On macOS a bind to 127.0.0.1 succeeds beside a
243
+ * wildcard listener, and the more specific socket then takes every
244
+ * 127.0.0.1 connection. iproxy listens on the IPv6 wildcard, so a
245
+ * 127.0.0.1-only probe called an iPhone's 9100 free, the S9+'s MJPEG server
246
+ * bound 127.0.0.1:9100 over it, and the iPhone's recording showed the S9+.
247
+ *
248
+ * Each listener shape fails at least one of these binds (checked on macOS
249
+ * with Node's default SO_REUSEADDR): 127.0.0.1 → 127.0.0.1, ::1 → ::1,
250
+ * 0.0.0.0 → 0.0.0.0, dual-stack :: → 0.0.0.0 and ::, v6-only :: → ::.
251
+ */
198
252
  isOsFree(port) {
253
+ return __awaiter(this, void 0, void 0, function* () {
254
+ let probed = 0;
255
+ // One at a time: probes in flight together can fail on each other (a
256
+ // 0.0.0.0 bind fails beside a :: one).
257
+ for (const host of PROBE_HOSTS) {
258
+ const code = yield this.bindProbe(port, host);
259
+ if (code === null)
260
+ probed++;
261
+ else if (!ADDRESS_UNAVAILABLE.has(code))
262
+ return false;
263
+ }
264
+ return probed > 0;
265
+ });
266
+ }
267
+ /** Bind `host:port` and close it again. Resolves null on success, else the error code. */
268
+ bindProbe(port, host) {
199
269
  return new Promise((resolve) => {
200
270
  const server = net_1.default.createServer();
201
- let settled = false;
202
- const done = (ok) => {
203
- if (settled)
204
- return;
205
- settled = true;
206
- server.removeAllListeners();
207
- if (ok)
208
- server.close(() => resolve(true));
209
- else
210
- resolve(false);
211
- };
212
- server.once('error', () => done(false));
213
- server.once('listening', () => done(true));
214
- server.listen(port, '127.0.0.1');
271
+ server.once('error', (err) => { var _a; return resolve((_a = err.code) !== null && _a !== void 0 ? _a : 'UNKNOWN'); });
272
+ server.once('listening', () => server.close(() => resolve(null)));
273
+ server.listen(port, host);
215
274
  });
216
275
  }
217
276
  };
@@ -63,6 +63,7 @@ const networkAddresses_1 = require("../helpers/networkAddresses");
63
63
  const logger_1 = __importDefault(require("../logger"));
64
64
  const h264StreamWs_1 = require("../app/ws/h264StreamWs");
65
65
  const logcatWs_1 = require("../app/ws/logcatWs");
66
+ const upgradeRouter_1 = require("../app/ws/upgradeRouter");
66
67
  const StreamTicketService_1 = require("./token/StreamTicketService");
67
68
  const AndroidH264StreamService_1 = __importDefault(require("../device-managers/android/AndroidH264StreamService"));
68
69
  const LogcatStreamService_1 = require("../device-managers/android/LogcatStreamService");
@@ -77,16 +78,16 @@ const config_service_1 = require("../data-service/config-service");
77
78
  const PluginContext_1 = require("../PluginContext");
78
79
  const device_store_1 = require("../data-service/device-store");
79
80
  const device_utils_1 = require("../device-utils");
81
+ const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
80
82
  const app_1 = require("../app");
81
- const wd_command_proxy_1 = require("../proxy/wd-command-proxy");
82
83
  const registerCommandAuth_1 = require("../app/registerCommandAuth");
84
+ const defaultGateway_1 = require("../gateway/defaultGateway");
83
85
  const appium_adb_1 = require("appium-adb");
84
86
  const ChromeDriverManager_1 = __importDefault(require("../device-managers/ChromeDriverManager"));
85
87
  const AndroidDeviceManager_1 = __importDefault(require("../device-managers/AndroidDeviceManager"));
86
88
  const IOSDeviceManager_1 = __importDefault(require("../device-managers/IOSDeviceManager"));
87
89
  const device_managers_1 = require("../device-managers");
88
90
  const pluginArgs_1 = require("../data-service/pluginArgs");
89
- const NodeDevices_1 = __importDefault(require("../device-managers/NodeDevices"));
90
91
  const config_1 = require("../config");
91
92
  const SocketServer_1 = require("./SocketServer");
92
93
  const SocketClient_1 = require("./SocketClient");
@@ -128,13 +129,17 @@ let ServerManager = ServerManager_1 = class ServerManager {
128
129
  }
129
130
  yield this.syncDatabaseAndAIConfig(pluginArgs);
130
131
  yield this.initializeCoreSubsystems(pluginArgs, cliArgs.port);
131
- this.registerRoutes(expressApp, cliArgs, pluginArgs);
132
+ this.registerRoutes(expressApp, httpServer, cliArgs, pluginArgs);
132
133
  yield this.bootEmulators(pluginArgs);
133
134
  this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
134
- yield this.setupHubOrNode(pluginArgs, cliArgs, httpServer, nodeId);
135
- yield this.setupMaintenanceCrons(pluginArgs);
135
+ // The hosts this server files its own phones under. Every "is this phone
136
+ // mine?" below compares against these exactly: a node on this machine
137
+ // shares the IP and differs only by port.
138
+ const localHosts = (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, cliArgs.port);
139
+ yield this.setupHubOrNode(pluginArgs, cliArgs, httpServer, localHosts);
140
+ yield this.setupMaintenanceCrons(pluginArgs, localHosts);
136
141
  // Live H.264 preview WebSocket (Android; feature-flagged in the frontend).
137
- // Only claims /stream/h264 — socket.io keeps its own upgrades. Ticket-auth'd.
142
+ // A route of the upgrade router: only /stream/h264 upgrades reach it. Ticket-auth'd.
138
143
  if (httpServer) {
139
144
  (0, h264StreamWs_1.attachH264Ws)(httpServer, {
140
145
  redeem: (ticket, udid) => typedi_1.Container.get(StreamTicketService_1.StreamTicketService).redeem(ticket, udid),
@@ -144,8 +149,8 @@ let ServerManager = ServerManager_1 = class ServerManager {
144
149
  startStream: (udid) => typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid),
145
150
  });
146
151
  // Continuous logcat WebSocket (Android; replaces the 3s dump-poll).
147
- // Only claims /logcat — socket.io and the h264 WS keep their own
148
- // upgrades. Ticket-auth'd like h264, plus an ownership check h264
152
+ // A route of the upgrade router: only /logcat upgrades reach it.
153
+ // Ticket-auth'd like h264, plus an ownership check h264
149
154
  // doesn't need: logcat routinely carries auth tokens and PII from
150
155
  // whatever app is under test, so it's an ownership-checked read, not
151
156
  // an open one — see docs/superpowers/specs/2026-08-09-logcat-stream-design.md
@@ -191,7 +196,7 @@ let ServerManager = ServerManager_1 = class ServerManager {
191
196
  // Principal Cleaning: Attempt to recover remote sessions before marking as failed
192
197
  const { SessionManager } = yield Promise.resolve().then(() => __importStar(require('../sessions/SessionManager')));
193
198
  const sessionManager = typedi_1.Container.get(SessionManager);
194
- const recoveredCount = yield sessionManager.recoverActiveSessions(nodeId, plugin_1.XenonPlugin.nodeBasePath);
199
+ const recoveredCount = yield sessionManager.recoverActiveSessions(nodeId, plugin_1.XenonPlugin.nodeBasePath, (host) => (0, localDeviceHosts_1.isLocalDeviceHost)(localHosts, host));
195
200
  const recoveredSessionIds = sessionManager.getAllSessions().map((s) => s.getId());
196
201
  if (recoveredCount > 0) {
197
202
  logger_1.default.info(`🔄 Successfully recovered ${recoveredCount} remote sessions`);
@@ -232,9 +237,9 @@ let ServerManager = ServerManager_1 = class ServerManager {
232
237
  const { cleanupZombieSessions } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/session-service')));
233
238
  yield cleanupZombieSessions(recoveredSessionIds);
234
239
  // Initial device discovery poll to start managers and trackers
235
- yield (0, device_utils_1.updateDeviceList)(pluginArgs.bindHostOrIp, pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
240
+ yield (0, device_utils_1.updateDeviceList)(localHosts, pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
236
241
  // remove stale devices
237
- yield (0, device_utils_1.removeStaleDevices)(pluginArgs.bindHostOrIp, pluginArgs.tlsRejectUnauthorized);
242
+ yield (0, device_utils_1.removeStaleDevices)(localHosts, pluginArgs.tlsRejectUnauthorized);
238
243
  this.logger.info(`🚀 Xenon will be served at http://${pluginArgs.bindHostOrIp}:${cliArgs.port}/xenon with id ${nodeId}`);
239
244
  this.logger.info('You can provide the following URLs to access Xenon:');
240
245
  for (const url of (0, networkAddresses_1.listReachableBaseUrls)(cliArgs.port)) {
@@ -314,7 +319,8 @@ let ServerManager = ServerManager_1 = class ServerManager {
314
319
  yield (0, device_utils_1.initializeStorage)();
315
320
  const { runMigrations } = yield Promise.resolve().then(() => __importStar(require('../scripts/run-migrations')));
316
321
  yield runMigrations();
317
- yield device_store_1.DeviceStoreFactory.getStore().clearStorage();
322
+ // A hub keeps its nodes' phones: the sessions on them outlive its restart.
323
+ yield device_store_1.DeviceStoreFactory.getStore().clearStorage((0, localDeviceHosts_1.devicesClearedAtBoot)(pluginArgs, (0, localDeviceHosts_1.localDeviceHosts)(pluginArgs, port)));
318
324
  const { bootstrapIdentity } = yield Promise.resolve().then(() => __importStar(require('./identity/bootstrap')));
319
325
  yield bootstrapIdentity();
320
326
  typedi_1.Container.get(EmailService_1.EmailService).warnIfLogFallbackEnabled();
@@ -345,15 +351,33 @@ let ServerManager = ServerManager_1 = class ServerManager {
345
351
  typedi_1.Container.set(ARTIFACT_STORE, new FsArtifactStore(config_1.config.recordingsAssetsPath));
346
352
  });
347
353
  }
348
- registerRoutes(expressApp, cliArgs, pluginArgs) {
354
+ registerRoutes(expressApp, httpServer, cliArgs, pluginArgs) {
349
355
  expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs));
350
- // Per-command auth (XENON_REQUIRE_COMMAND_AUTH) goes in front of Appium's
351
- // routes, and is placed before the proxy middleware so that, where the
352
- // proxy is also spliced ahead of the routes, a hub checks a command before
353
- // forwarding it to a node. Throws when the setting is on and it cannot be
354
- // placed, rather than serve session commands unchecked.
355
- (0, registerCommandAuth_1.registerCommandAuth)(expressApp, cliArgs);
356
- (0, wd_command_proxy_1.registerProxyMiddlware)(expressApp, cliArgs);
356
+ // The session gateway goes in front of Appium's routes: Xenon's own
357
+ // /wd-internal calls (with the per-process secret), per-command auth
358
+ // (XENON_REQUIRE_COMMAND_AUTH), then, on a hub, forwarding of the sessions
359
+ // nodes run, so a hub checks a command before forwarding it and Appium's
360
+ // umbrella never sees a remote session. A node accepts the hub's session
361
+ // token in place of the client's credentials. With it go the filter on
362
+ // Appium's session listing and the guard on session WebSockets (BiDi,
363
+ // driver /ws/session/...), which upgrade outside the routes. One set of
364
+ // deps, so all three share a credential cache. Each throws when the
365
+ // setting is on and it cannot be installed, rather than serve sessions
366
+ // unchecked.
367
+ const commandAuth = (0, registerCommandAuth_1.commandAuthDeps)();
368
+ (0, registerCommandAuth_1.registerSessionGateway)(expressApp, cliArgs, commandAuth, (0, defaultGateway_1.gatewayOptionsFor)(pluginArgs, cliArgs, () => typedi_1.Container.get(PluginContext_1.PluginContext).nodeId));
369
+ (0, registerCommandAuth_1.registerSessionUpgradeGuard)(httpServer, expressApp, cliArgs, commandAuth);
370
+ // One owner per WebSocket upgrade (upgradeRouter.ts): Xenon's H.264,
371
+ // logcat and socket.io paths go to Xenon, everything else to Appium behind
372
+ // the session guard. Installed after the guard, so the guard never sees
373
+ // Xenon's sockets, and before socket.io and the H.264 / logcat sockets
374
+ // attach to it below. Without it, which side got an upgrade depended on
375
+ // the Node version, and each version broke one side.
376
+ if (httpServer) {
377
+ const router = (0, upgradeRouter_1.upgradeRouterFor)(httpServer);
378
+ this.logger.info("WebSocket upgrades: Xenon's own paths go to Xenon; every other path goes to " +
379
+ `${router.describeAppiumPath()}.`);
380
+ }
357
381
  }
358
382
  bootEmulators(pluginArgs) {
359
383
  return __awaiter(this, void 0, void 0, function* () {
@@ -384,22 +408,18 @@ let ServerManager = ServerManager_1 = class ServerManager {
384
408
  }
385
409
  (0, pluginArgs_1.addCLIArgs)(cliArgs);
386
410
  }
387
- setupHubOrNode(pluginArgs, cliArgs, httpServer, nodeId) {
411
+ setupHubOrNode(pluginArgs, cliArgs, httpServer, localHosts) {
388
412
  return __awaiter(this, void 0, void 0, function* () {
389
413
  const hubArgument = pluginArgs.hub;
390
414
  if (hubArgument !== undefined) {
391
415
  this.logger.info(`📡 I'm a node and my hub is ${hubArgument}`);
392
- yield (0, device_utils_1.setupCronUpdateDeviceList)(pluginArgs.bindHostOrIp, hubArgument, pluginArgs.sendNodeDevicesToHubIntervalMs, pluginArgs.tlsRejectUnauthorized);
416
+ yield (0, device_utils_1.setupCronUpdateDeviceList)(localHosts, hubArgument, pluginArgs.sendNodeDevicesToHubIntervalMs, pluginArgs.tlsRejectUnauthorized);
393
417
  // Handle graceful shutdown
394
418
  ['SIGINT', 'SIGTERM'].forEach((signal) => {
395
419
  process.once(signal, () => __awaiter(this, void 0, void 0, function* () {
396
420
  logger_1.default.info(`Received ${signal}, unregistering node from hub...`);
397
421
  try {
398
- yield new NodeDevices_1.default(hubArgument, {
399
- tlsRejectUnauthorized: pluginArgs.tlsRejectUnauthorized,
400
- hubAccessKey: config_1.config.hubAccessKey,
401
- hubToken: config_1.config.hubToken,
402
- }).unRegisterNode(pluginArgs.bindHostOrIp);
422
+ yield (0, device_utils_1.unregisterNodeFromHub)(hubArgument, localHosts, pluginArgs.tlsRejectUnauthorized, typedi_1.Container.get(PluginContext_1.PluginContext).nodeId);
403
423
  }
404
424
  catch (err) {
405
425
  logger_1.default.error(`Error during node unregistration: ${err}`);
@@ -417,7 +437,7 @@ let ServerManager = ServerManager_1 = class ServerManager {
417
437
  // Principal discovery: Background poll to prune stale/offline devices on the Hub itself
418
438
  (() => __awaiter(this, void 0, void 0, function* () {
419
439
  const { setupCronLocalDiscovery } = yield Promise.resolve().then(() => __importStar(require('../device-utils')));
420
- yield setupCronLocalDiscovery(pluginArgs.bindHostOrIp, pluginArgs.sendNodeDevicesToHubIntervalMs);
440
+ yield setupCronLocalDiscovery(localHosts, pluginArgs.sendNodeDevicesToHubIntervalMs);
421
441
  }))();
422
442
  const socketServer = typedi_1.Container.get(SocketServer_1.SocketServer);
423
443
  socketServer.initialize(httpServer);
@@ -426,12 +446,12 @@ let ServerManager = ServerManager_1 = class ServerManager {
426
446
  }
427
447
  });
428
448
  }
429
- setupMaintenanceCrons(pluginArgs) {
449
+ setupMaintenanceCrons(pluginArgs, localHosts) {
430
450
  return __awaiter(this, void 0, void 0, function* () {
431
451
  var _a;
432
452
  if (!((_a = pluginArgs.cloud) === null || _a === void 0 ? void 0 : _a.cloudName)) {
433
453
  // 1. Check for stale nodes
434
- yield (0, device_utils_1.setupCronCheckStaleDevices)(pluginArgs.checkStaleDevicesIntervalMs, pluginArgs.bindHostOrIp, pluginArgs.tlsRejectUnauthorized);
454
+ yield (0, device_utils_1.setupCronCheckStaleDevices)(pluginArgs.checkStaleDevicesIntervalMs, localHosts, pluginArgs.tlsRejectUnauthorized);
435
455
  // 2. Release blocked devices
436
456
  yield (0, device_utils_1.setupCronReleaseBlockedDevices)(pluginArgs.checkBlockedDevicesIntervalMs, pluginArgs.newCommandTimeoutSec);
437
457
  // 3. Clean up pending sessions