@xenon-device-management/xenon 2.13.2 → 2.15.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 (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -69,7 +69,6 @@ const device_utils_1 = require("../device-utils");
69
69
  const TracingService_1 = require("./TracingService");
70
70
  const PortAllocator_1 = require("./PortAllocator");
71
71
  const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
72
- const JwtKeyService_1 = require("./token/JwtKeyService");
73
72
  const sessionIdentity_1 = require("./session/sessionIdentity");
74
73
  const leaseSessionCaps_1 = require("./lease/leaseSessionCaps");
75
74
  const sessionCredentials_1 = require("./session/sessionCredentials");
@@ -80,6 +79,7 @@ const callerTeamIds_1 = require("./device-access/callerTeamIds");
80
79
  const queueVisibility_1 = require("./device-access/queueVisibility");
81
80
  const appVisibility_1 = require("./device-access/appVisibility");
82
81
  const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
82
+ const sessionMemory_1 = require("../sessions/sessionMemory");
83
83
  const SessionMetricsService_1 = require("./metrics/SessionMetricsService");
84
84
  const appCapability_1 = require("./session/appCapability");
85
85
  const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
@@ -98,6 +98,23 @@ const SessionStatus_1 = require("../types/SessionStatus");
98
98
  const SessionType_1 = __importDefault(require("../enums/SessionType"));
99
99
  const async_lock_1 = __importDefault(require("async-lock"));
100
100
  const base_driver_1 = require("@appium/base-driver");
101
+ const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
102
+ const interceptorOptions_1 = require("./interceptor/interceptorOptions");
103
+ /**
104
+ * A session token admits a session only with the `sessions` scope, as a key
105
+ * pair does. Tokens minted before this check carry no `scopes` claim, and
106
+ * POST /auth/token gave one to any credential, a read-only one included:
107
+ * they are refused too.
108
+ */
109
+ function sessionTokenGrantsSessions(scopes) {
110
+ if (typeof scopes !== 'string')
111
+ return false;
112
+ const owned = scopes.split(',').map((s) => s.trim());
113
+ return owned.includes('sessions') || owned.includes('admin');
114
+ }
115
+ const SESSION_TOKEN_LACKS_SCOPE = 'session rejected: xe:options.sessionToken carries no `sessions` scope; tokens minted by ' +
116
+ 'Xenon 2.14 or earlier carry none — mint a new one with POST /xenon/api/auth/token ' +
117
+ '({"audience":"xenon-mcp"}) using a credential that has the `sessions` scope.';
101
118
  /**
102
119
  * How long the hub waits for a node (or cloud provider) to create a session.
103
120
  * A create is sent once (InternalRequestConfig `retry: false`): the node may
@@ -261,6 +278,7 @@ let SessionLifecycleService = class SessionLifecycleService {
261
278
  apiKeyId: authResult.apiKeyId,
262
279
  userId: authResult.userId,
263
280
  appDownload,
281
+ allocatedAt: Date.now(),
264
282
  };
265
283
  // The grant opens one phone, on this node. A token taken to another node
266
284
  // (an emulator's udid repeats across machines), or naming a phone this
@@ -389,7 +407,7 @@ let SessionLifecycleService = class SessionLifecycleService {
389
407
  this.logger.debug(`📱 Removing pending session with capability_id: ${allocation.pendingSessionId}`);
390
408
  yield (0, pending_sessions_service_1.removePendingSession)(allocation.pendingSessionId);
391
409
  if (this.isCreateSessionResponseInternal(session)) {
392
- yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId);
410
+ yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId, allocation.allocatedAt);
393
411
  }
394
412
  else {
395
413
  yield this.handleSessionFailure(session, device, remote);
@@ -503,9 +521,11 @@ let SessionLifecycleService = class SessionLifecycleService {
503
521
  // took out of `xe:options` (or its `xenon:options` alias): an access-key +
504
522
  // token pair, or a session token. Today this is best-effort: if none is
505
523
  // supplied we log a warning and let the request through to avoid breaking
506
- // pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on. A
507
- // supplied key that is invalid / revoked / has insufficient scope is
508
- // rejected. Respects the global `authDisabled` flag.
524
+ // pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on.
525
+ // Credentials that don't verify — a wrong, revoked or expired key or token,
526
+ // or one whose user is deleted or not ACTIVE — count as none. Credentials
527
+ // that verify without the `sessions` scope are rejected. Respects the
528
+ // global `authDisabled` flag.
509
529
  //
510
530
  // Returns the caller's { apiKeyId, callerTeamIds } so allocation can filter
511
531
  // devices, and app resolution uploaded apps, by team. `apiKeyId` is null
@@ -547,23 +567,47 @@ let SessionLifecycleService = class SessionLifecycleService {
547
567
  }
548
568
  const { ApiKeyService } = yield Promise.resolve().then(() => __importStar(require('./ApiKeyService')));
549
569
  const svc = typedi_1.Container.get(ApiKeyService);
570
+ const { verifyKeyPairCredential, verifySessionTokenCredential, isWrongSessionToken } = yield Promise.resolve().then(() => __importStar(require('../middleware/verifyCredential')));
550
571
  // xe:options.{accessKey, token} — the only key-pair credential shape.
572
+ // Checked by REST's rule (verifyKeyPairCredential): a live key whose owner
573
+ // exists and is ACTIVE. The key alone (verifyPair) let a user set to
574
+ // Inactive, or deleted, keep creating sessions as themselves. The owner
575
+ // found here is the one lookup the session's scope and lease use.
551
576
  const pair = credentials.pair;
552
- const row = pair ? yield svc.verifyPair(pair.accessKey, pair.token) : null;
577
+ const verifiedPair = pair ? yield verifyKeyPairCredential(pair.accessKey, pair.token) : null;
578
+ // The session token, verified once for both the gate and attribution, by
579
+ // REST's rule too: a live signature and an ACTIVE subject, looked up now.
580
+ let tokenCheck;
581
+ const verifyToken = (t) => {
582
+ if (!tokenCheck)
583
+ tokenCheck = verifySessionTokenCredential(t);
584
+ return tokenCheck;
585
+ };
586
+ // Both readers below treat any failure as a token that doesn't check out:
587
+ // the gate refuses it as "invalid or expired", attribution ignores it. A
588
+ // check that could not run (the database or the signing key unavailable)
589
+ // is not that: it refuses the create here, as a key whose owner can't be
590
+ // looked up does, rather than run the session with no owner.
591
+ if (!verifiedPair && credentials.sessionToken) {
592
+ yield verifyToken(credentials.sessionToken).catch((err) => {
593
+ if (!isWrongSessionToken(err))
594
+ throw err;
595
+ });
596
+ }
553
597
  const { assertSessionTokenGate, sessionTokenGateEnabled } = yield Promise.resolve().then(() => __importStar(require('./sessionTokenGate')));
554
598
  try {
555
599
  yield assertSessionTokenGate({
556
600
  enabled: sessionTokenGateEnabled(),
557
- hasValidKeyPair: !!row,
601
+ hasValidKeyPair: !!verifiedPair,
558
602
  token: credentials.sessionToken,
559
- verify: (t) => typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(t, { audience: 'xenon-session' }),
603
+ verify: verifyToken,
560
604
  });
561
605
  }
562
606
  catch (err) {
563
607
  this.logger.error(`❌ ${err.message}`);
564
608
  throw new base_driver_1.errors.InvalidArgumentError(err.message);
565
609
  }
566
- if (!row) {
610
+ if (!verifiedPair) {
567
611
  // No key pair. An xe:options.sessionToken still identifies the caller,
568
612
  // so read it for attribution even when the gate is off — otherwise the
569
613
  // ownership guard fails closed on a session whose owner we actually know.
@@ -571,23 +615,36 @@ let SessionLifecycleService = class SessionLifecycleService {
571
615
  // The token's teamId claim narrows it the way a key's team does; kept
572
616
  // from the one verify rather than verifying again.
573
617
  let tokenTeamId = null;
618
+ let tokenScopes;
619
+ let tokenOwner;
574
620
  const identity = yield (0, sessionIdentity_1.resolveSessionIdentity)({
575
621
  row: null,
576
622
  sessionToken: credentials.sessionToken,
577
623
  verify: (t) => __awaiter(this, void 0, void 0, function* () {
578
- const payload = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(t, {
579
- audience: 'xenon-session',
580
- });
624
+ const { payload, user } = yield verifyToken(t);
581
625
  tokenTeamId = typeof (payload === null || payload === void 0 ? void 0 : payload.teamId) === 'string' ? payload.teamId : null;
626
+ tokenScopes = payload === null || payload === void 0 ? void 0 : payload.scopes;
627
+ tokenOwner = user;
582
628
  return payload;
583
629
  }),
584
630
  });
631
+ // A token that names its owner may create sessions only with the
632
+ // `sessions` scope, as a key pair may: refused, like a key without it.
633
+ if (identity.userId && !sessionTokenGrantsSessions(tokenScopes)) {
634
+ this.logger.error('Rejecting session: xe:options.sessionToken lacks the `sessions` scope');
635
+ throw new base_driver_1.errors.InvalidArgumentError(SESSION_TOKEN_LACKS_SCOPE);
636
+ }
585
637
  if (!identity.userId) {
586
638
  this.logger.warn('Session created without valid credentials. Pass `xe:options.accessKey` + ' +
587
639
  '`xe:options.token`, or `xe:options.sessionToken`.');
588
640
  }
589
- const leaseAccess = this.leaseAccessFor(identity.userId
590
- ? { kind: 'session-token', userId: identity.userId, teamId: tokenTeamId }
641
+ const leaseAccess = this.leaseAccessFor(identity.userId && tokenOwner
642
+ ? {
643
+ kind: 'session-token',
644
+ scopes: String(tokenScopes),
645
+ user: tokenOwner,
646
+ teamId: tokenTeamId,
647
+ }
591
648
  : { kind: 'none' });
592
649
  // A token's user is scoped as REST scopes them. No credentials at all
593
650
  // stays unscoped (the back-compat path), with no lookup.
@@ -597,6 +654,7 @@ let SessionLifecycleService = class SessionLifecycleService {
597
654
  teamId: identity.userId ? tokenTeamId : null,
598
655
  } });
599
656
  }
657
+ const { row, user: owner } = verifiedPair;
600
658
  if (!svc.hasScope(row, ['sessions'])) {
601
659
  this.logger.error('Rejecting session: credentials lack the `sessions` scope');
602
660
  throw new base_driver_1.errors.InvalidArgumentError('credentials are invalid, revoked, or lack the `sessions` scope');
@@ -605,7 +663,7 @@ let SessionLifecycleService = class SessionLifecycleService {
605
663
  const leaseAccess = this.leaseAccessFor({
606
664
  kind: 'api-key',
607
665
  scopes: row.scopes,
608
- userId: row.userId,
666
+ user: owner,
609
667
  teamId: (_a = row.teamId) !== null && _a !== void 0 ? _a : null,
610
668
  });
611
669
  // The phones and apps this session may use: REST's rule
@@ -640,7 +698,8 @@ let SessionLifecycleService = class SessionLifecycleService {
640
698
  // How a session that names a lease is judged, looked up at most once and
641
699
  // only when asked. Who may override follows canOverrideLease; which phones
642
700
  // it can see follows REST's computeTeamIds. Both need the owner's live role,
643
- // as authMiddleware reads it for every REST request.
701
+ // as authMiddleware reads it for every REST request: the owner the
702
+ // credential check looked up a moment ago, ACTIVE then.
644
703
  leaseAccessFor(credential) {
645
704
  let looked;
646
705
  return () => {
@@ -657,24 +716,24 @@ let SessionLifecycleService = class SessionLifecycleService {
657
716
  if (credential.kind === 'none') {
658
717
  return { canOverride: (0, leaseOverride_1.canOverrideLease)({ kind: 'none' }), teamIds: undefined };
659
718
  }
719
+ const { user } = credential;
660
720
  try {
661
- const { UserService } = yield Promise.resolve().then(() => __importStar(require('./UserService')));
662
- const found = yield typedi_1.Container.get(UserService).findById(credential.userId);
663
- const user = found && found.status === 'ACTIVE' ? found : null;
664
- const canOverride = (0, leaseOverride_1.canOverrideLease)(credential.kind === 'api-key'
665
- ? { kind: 'api-key', scopes: credential.scopes, user }
666
- : { kind: 'session-token', user });
667
- const role = (user === null || user === void 0 ? void 0 : user.role) === 'SUPER_ADMIN' || (user === null || user === void 0 ? void 0 : user.role) === 'ADMIN' ? user.role : 'MEMBER';
721
+ const canOverride = (0, leaseOverride_1.canOverrideLease)({
722
+ kind: credential.kind,
723
+ scopes: credential.scopes,
724
+ user,
725
+ });
726
+ const role = user.role === 'SUPER_ADMIN' || user.role === 'ADMIN' ? user.role : 'MEMBER';
668
727
  const teamIds = yield (0, callerTeamIds_1.computeTeamIds)({
669
728
  role,
670
- userId: credential.userId,
729
+ userId: user.id,
671
730
  apiKeyTeamId: credential.teamId,
672
731
  });
673
732
  return { canOverride, teamIds };
674
733
  }
675
734
  catch (err) {
676
735
  // Fail closed: no override, and the shared pool only.
677
- this.logger.warn(`Could not look up how user ${credential.userId} may use a lease: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
736
+ this.logger.warn(`Could not look up how user ${user.id} may use a lease: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
678
737
  return { canOverride: false, teamIds: [] };
679
738
  }
680
739
  });
@@ -788,7 +847,7 @@ let SessionLifecycleService = class SessionLifecycleService {
788
847
  }
789
848
  }
790
849
  finalizeSession(session_1, device_1, caps_1, driver_1, isRemote_1) {
791
- return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null) {
850
+ return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null, allocatedAt) {
792
851
  var _a;
793
852
  const sessionId = session.value[0];
794
853
  const sessionResponse = session.value[1];
@@ -824,6 +883,7 @@ let SessionLifecycleService = class SessionLifecycleService {
824
883
  const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver, webDriverUrl);
825
884
  sessionInstance.apiKeyId = apiKeyId;
826
885
  sessionInstance.userId = userId;
886
+ sessionInstance.allocatedAt = allocatedAt;
827
887
  // A session this server drives: its owner is known while it runs, row or
828
888
  // not (a node writes none for the hub's sessions; LiveSessionOwners).
829
889
  if (sessionInstance instanceof LocalSession_1.LocalSession) {
@@ -877,26 +937,11 @@ let SessionLifecycleService = class SessionLifecycleService {
877
937
  applyPostSessionLogic(session, caps, device) {
878
938
  return __awaiter(this, void 0, void 0, function* () {
879
939
  const context = typedi_1.Container.get(PluginContext_1.PluginContext);
880
- const networkProfile = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE];
881
- if (networkProfile) {
882
- const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
883
- yield typedi_1.Container.get(NetworkConditioningService).applyProfile(session.getId(), device, networkProfile);
884
- }
885
- if (caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]) {
886
- try {
887
- const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
888
- yield typedi_1.Container.get(InterceptorService).start(session.getId(), device, {
889
- enabled: true,
890
- bufferSize: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_BUFFER_SIZE],
891
- captureBodies: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES] !== false,
892
- mocks: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS] || [],
893
- includeHosts: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS] || [],
894
- excludeHosts: caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS] || [],
895
- });
896
- }
897
- catch (err) {
898
- this.logger.warn(`🕸️ Failed to start interceptor for ${session.getId()}: ${err.message}`);
899
- }
940
+ // Only the server that drives the phone changes its network: a node
941
+ // applies its own sessions' profile and interceptor, and a hub that did
942
+ // it too ran its own adb against the node's phone.
943
+ if (session.getType() === SessionType_1.default.LOCAL) {
944
+ yield this.applySessionNetwork(session.getId(), caps, device, context.pluginArgs);
900
945
  }
901
946
  const isDashboardEnabled = !!context.pluginArgs.enableDashboard;
902
947
  const shouldSaveLogs = session.getType() !== SessionType_1.default.CLOUD;
@@ -930,6 +975,36 @@ let SessionLifecycleService = class SessionLifecycleService {
930
975
  }
931
976
  });
932
977
  }
978
+ /**
979
+ * The session's network profile and network capture, on a phone this
980
+ * server drives. First what an earlier session left on the phone and
981
+ * couldn't put back is undone, so this session starts from the phone's own
982
+ * settings and records those as the ones to put back. Each ending undoes
983
+ * these through PhoneNetworkRestore.
984
+ */
985
+ applySessionNetwork(sessionId, caps, device, pluginArgs) {
986
+ return __awaiter(this, void 0, void 0, function* () {
987
+ var _a;
988
+ if (((_a = device.platform) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'android') {
989
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreLeftovers({ udid: device.udid });
990
+ }
991
+ const networkProfile = caps[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE];
992
+ if (networkProfile) {
993
+ const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
994
+ yield typedi_1.Container.get(NetworkConditioningService).applyProfile(sessionId, device, networkProfile);
995
+ }
996
+ const interceptor = (0, interceptorOptions_1.resolveInterceptorOptions)(caps, pluginArgs.interceptor);
997
+ if (interceptor.enabled) {
998
+ try {
999
+ const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
1000
+ yield typedi_1.Container.get(InterceptorService).start(sessionId, device, interceptor);
1001
+ }
1002
+ catch (err) {
1003
+ this.logger.warn(`🕸️ Failed to start interceptor for ${sessionId}: ${err.message}`);
1004
+ }
1005
+ }
1006
+ });
1007
+ }
933
1008
  /**
934
1009
  * The row a hub keeps for a session it routes to a node or a cloud when the
935
1010
  * dashboard's own (onSessionStarted) isn't written: with the dashboard off,
@@ -1097,6 +1172,10 @@ let SessionLifecycleService = class SessionLifecycleService {
1097
1172
  // which releases ports and archives video twice.
1098
1173
  if (sessionId) {
1099
1174
  yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
1175
+ // The phone's network (profile, interceptor proxy) and the capture,
1176
+ // before the phone is released to the next session. Whether or not
1177
+ // SESSION_MANAGER holds the session.
1178
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, 'session deleted');
1100
1179
  yield (0, device_service_1.releaseSessionDevices)(sessionId);
1101
1180
  this.logger.info(`📱 Unblocking the device that is blocked for session ${sessionId}`);
1102
1181
  const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
@@ -1127,6 +1206,7 @@ let SessionLifecycleService = class SessionLifecycleService {
1127
1206
  finally {
1128
1207
  if (sessionId) {
1129
1208
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
1209
+ (0, sessionMemory_1.forgetSessionMemory)(sessionId);
1130
1210
  // Before the lock's "still in memory?" check, which a dashboard-off
1131
1211
  // node fails. On a node this ends the figures it holds for its hub;
1132
1212
  // on a hub it collects a node session's last ones. Idempotent.
@@ -1137,37 +1217,10 @@ let SessionLifecycleService = class SessionLifecycleService {
1137
1217
  // Another concurrent deleteSession already cleaned up.
1138
1218
  return;
1139
1219
  }
1140
- const device = session.getDevice();
1141
- try {
1142
- const { NetworkConditioningService } = yield Promise.resolve().then(() => __importStar(require('./NetworkConditioningService')));
1143
- yield typedi_1.Container.get(NetworkConditioningService).reset(sessionId, device);
1144
- }
1145
- catch (resetErr) {
1146
- this.logger.warn(`⚠️ NetworkConditioningService.reset failed for session ${sessionId}: ${resetErr.message}`);
1147
- }
1148
- try {
1149
- const { InterceptorService } = yield Promise.resolve().then(() => __importStar(require('./InterceptorService')));
1150
- const interceptor = typedi_1.Container.get(InterceptorService);
1151
- if (interceptor.isActive(sessionId))
1152
- yield interceptor.stop(sessionId);
1153
- }
1154
- catch (interceptorErr) {
1155
- this.logger.warn(`⚠️ InterceptorService.stop failed for session ${sessionId}: ${interceptorErr.message}`);
1156
- }
1220
+ // The phone's network was put back before it was released (above).
1157
1221
  yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, status, reason);
1158
1222
  SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
1159
1223
  typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).forget(sessionId);
1160
- try {
1161
- const { getSessionById } = yield Promise.resolve().then(() => __importStar(require('../dashboard/services/session-service')));
1162
- const sessionData = yield getSessionById(sessionId);
1163
- if (sessionData && (sessionData.status === 'failed' || sessionData.failure_reason)) {
1164
- const { NotificationService } = yield Promise.resolve().then(() => __importStar(require('./NotificationService')));
1165
- yield typedi_1.Container.get(NotificationService).dispatchEvent('session_failed', sessionData);
1166
- }
1167
- }
1168
- catch (err) {
1169
- /* ignore notification errors */
1170
- }
1171
1224
  }));
1172
1225
  }
1173
1226
  }
@@ -1240,6 +1293,7 @@ let SessionLifecycleService = class SessionLifecycleService {
1240
1293
  stopSessionForShutdown(sessionId, reason) {
1241
1294
  return __awaiter(this, void 0, void 0, function* () {
1242
1295
  yield sessionCleanupLock.acquire(sessionId, () => __awaiter(this, void 0, void 0, function* () {
1296
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, reason);
1243
1297
  try {
1244
1298
  yield (0, device_service_1.releaseSessionDevices)(sessionId);
1245
1299
  }
@@ -1258,13 +1312,17 @@ let SessionLifecycleService = class SessionLifecycleService {
1258
1312
  }
1259
1313
  }
1260
1314
  try {
1261
- yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, SessionStatus_1.SessionStatus.FAILED, reason);
1315
+ // A shutdown fails the sessions it drains; no test caused it, so no webhook.
1316
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStopped(sessionId, SessionStatus_1.SessionStatus.FAILED, reason, {
1317
+ notify: false,
1318
+ });
1262
1319
  }
1263
1320
  catch (err) {
1264
1321
  this.logger.warn(`[shutdown] onSessionStopped failed for ${sessionId}: ${err.message}`);
1265
1322
  }
1266
1323
  SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
1267
1324
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
1325
+ (0, sessionMemory_1.forgetSessionMemory)(sessionId);
1268
1326
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
1269
1327
  }));
1270
1328
  });
@@ -24,6 +24,7 @@ const logger_1 = __importDefault(require("../logger"));
24
24
  const SessionManager_1 = require("../sessions/SessionManager");
25
25
  const SessionLifecycleService_1 = require("./SessionLifecycleService");
26
26
  const SessionType_1 = __importDefault(require("../enums/SessionType"));
27
+ const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
27
28
  // Graceful shutdown phase that runs BEFORE the existing infrastructure
28
29
  // teardown in index.ts:cleanup(). Active sessions get a bounded chance to
29
30
  // archive their video + release ports + mark themselves failed, so we don't
@@ -57,6 +58,7 @@ let ShutdownCoordinator = class ShutdownCoordinator {
57
58
  const sessions = SessionManager_1.SESSION_MANAGER.getAllSessions().filter((session) => session.getType() === SessionType_1.default.LOCAL);
58
59
  if (sessions.length === 0) {
59
60
  this.logger.info('No active sessions to drain');
61
+ yield this.restorePhoneNetworks(timeoutMs);
60
62
  return { attempted: 0, completed: 0 };
61
63
  }
62
64
  this.logger.info(`Draining ${sessions.length} active session(s) (overall=${timeoutMs}ms, per-session=${perSessionTimeoutMs}ms)`);
@@ -84,10 +86,41 @@ let ShutdownCoordinator = class ShutdownCoordinator {
84
86
  }));
85
87
  const results = yield Promise.allSettled(tasks);
86
88
  const completed = results.filter((r) => r.status === 'fulfilled' && r.value).length;
89
+ yield this.restorePhoneNetworks(Math.max(0, deadline - Date.now()));
87
90
  this.logger.info(`Drain complete: ${completed}/${sessions.length} session(s) finalized`);
88
91
  return { attempted: sessions.length, completed };
89
92
  });
90
93
  }
94
+ /**
95
+ * The network of every phone a session of this server still has changed
96
+ * (profile, interceptor proxy), sessions SESSION_MANAGER doesn't hold
97
+ * included, within what is left of the budget. A shutdown that never gets
98
+ * here (Appium exits on SIGTERM first) leaves it to the next start, from
99
+ * the ledger (PhoneNetworkRestore.cleanUpAtBoot).
100
+ */
101
+ restorePhoneNetworks(budgetMs) {
102
+ return __awaiter(this, void 0, void 0, function* () {
103
+ var _a;
104
+ if (budgetMs <= 0)
105
+ return;
106
+ let timer;
107
+ try {
108
+ yield Promise.race([
109
+ typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreAll('Xenon is shutting down'),
110
+ new Promise((resolve) => {
111
+ timer = setTimeout(resolve, budgetMs);
112
+ }),
113
+ ]);
114
+ }
115
+ catch (err) {
116
+ this.logger.warn(`[shutdown] Putting phone networks back failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
117
+ }
118
+ finally {
119
+ if (timer)
120
+ clearTimeout(timer);
121
+ }
122
+ });
123
+ }
91
124
  };
92
125
  exports.ShutdownCoordinator = ShutdownCoordinator;
93
126
  exports.ShutdownCoordinator = ShutdownCoordinator = __decorate([
@@ -33,9 +33,14 @@ const SocketEvents_1 = require("../enums/SocketEvents");
33
33
  const callerTeamIds_1 = require("./device-access/callerTeamIds");
34
34
  const DeviceTeamResolver_1 = require("./device-access/DeviceTeamResolver");
35
35
  const upgradeRouter_1 = require("../app/ws/upgradeRouter");
36
+ const sessionCommandSummary_1 = require("../dashboard/sessionCommandSummary");
36
37
  /** socket.io's default path, which the dashboard and nodes connect to. */
37
38
  const SOCKET_IO_PATH = '/socket.io';
38
39
  const SESSION_COOKIE = 'xenon_dashboard_session';
40
+ const EVENT_LOG_KEEPS = new Map([
41
+ [SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, null],
42
+ [SocketEvents_1.SocketEvents.SESSION_COMMAND, sessionCommandSummary_1.sessionCommandSummary],
43
+ ]);
39
44
  function readCookie(cookieHeader, name) {
40
45
  if (!cookieHeader)
41
46
  return undefined;
@@ -242,12 +247,23 @@ let SocketServer = SocketServer_1 = class SocketServer {
242
247
  if (this.io) {
243
248
  this.io.to('dashboard').emit(event, data);
244
249
  }
245
- typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({ type: event, payload: data });
250
+ this.logEvent(event, data);
251
+ }
252
+ /** Once per event, unscoped: whole, or what EVENT_LOG_KEEPS keeps of it. */
253
+ logEvent(event, data) {
254
+ const keep = EVENT_LOG_KEEPS.get(event);
255
+ if (keep === null)
256
+ return;
257
+ typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({
258
+ type: event,
259
+ payload: keep ? keep(data) : data,
260
+ });
246
261
  }
247
262
  /**
248
263
  * A dashboard event about one or more phones: each dashboard socket gets it
249
264
  * only if its caller can see the phone ({@link DeviceEventScope}), by the
250
- * same team rule as REST. The event log records it once, unscoped.
265
+ * same team rule as REST. The event log records it once, unscoped, as
266
+ * EVENT_LOG_KEEPS says.
251
267
  *
252
268
  * Each phone's events are delivered through one chain, in the order they
253
269
  * were emitted, whatever their scope shape: one that waits on a team lookup
@@ -262,7 +278,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
262
278
  */
263
279
  emitToDashboardForDevices(event, data, scope) {
264
280
  var _a, _b;
265
- typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({ type: event, payload: data });
281
+ this.logEvent(event, data);
266
282
  const udids = 'udids' in scope ? scope.udids : [(_a = scope.udid) !== null && _a !== void 0 ? _a : ''];
267
283
  if (!this.hasScopedDashboard() && udids.every((udid) => !this.deliveries.has(udid))) {
268
284
  this.noteRow(scope);
@@ -52,20 +52,37 @@ let TeamService = class TeamService {
52
52
  delete(id) {
53
53
  return __awaiter(this, void 0, void 0, function* () {
54
54
  // Apps too: App.teamId is onDelete SetNull, so deleting a team that owns
55
- // apps would quietly share them with every member.
56
- const [devices, members, apps] = yield Promise.all([
55
+ // apps would quietly share them with every member. So is a phone's saved
56
+ // team (DeviceSetting): a phone of the team that is not connected now
57
+ // would come back in the shared pool.
58
+ const [connected, saved, members, apps] = yield Promise.all([
57
59
  prisma_1.prisma.device.count({ where: { teamId: id } }),
60
+ prisma_1.prisma.deviceSetting.findMany({ where: { teamId: id }, select: { udid: true, host: true } }),
58
61
  prisma_1.prisma.teamMember.count({ where: { teamId: id } }),
59
62
  prisma_1.prisma.app.count({ where: { teamId: id } }),
60
63
  ]);
64
+ const away = yield this.notConnected(saved);
65
+ const devices = connected + away;
61
66
  if (devices > 0 || members > 0 || apps > 0) {
62
- throw new Error(`Team still has ${devices} device(s), ${members} member(s) and ${apps} app(s). Reassign them before deleting.`);
67
+ const phones = away > 0 ? `${devices} device(s) (${away} not connected now)` : `${devices} device(s)`;
68
+ throw new Error(`Team still has ${phones}, ${members} member(s) and ${apps} app(s). Reassign them before deleting.`);
63
69
  }
64
70
  // Clear teamId on any (revoked) ApiKey rows so the FK doesn't block delete.
65
71
  yield prisma_1.prisma.apiKey.updateMany({ where: { teamId: id }, data: { teamId: null } });
66
72
  yield prisma_1.prisma.team.delete({ where: { id } });
67
73
  });
68
74
  }
75
+ /** How many of these saved phones have no Device row: not connected now. */
76
+ notConnected(saved) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ if (saved.length === 0)
79
+ return 0;
80
+ const here = yield prisma_1.prisma.device.count({
81
+ where: { OR: saved.map(({ udid, host }) => ({ udid, host })) },
82
+ });
83
+ return saved.length - here;
84
+ });
85
+ }
69
86
  listMembers(teamId) {
70
87
  return __awaiter(this, void 0, void 0, function* () {
71
88
  const rows = yield prisma_1.prisma.teamMember.findMany({
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.sliceVideo = sliceVideo;
7
7
  const child_process_1 = require("child_process");
8
+ const ffmpegPath_1 = require("../../helpers/ffmpegPath");
8
9
  const logger_1 = __importDefault(require("../../logger"));
9
10
  function sliceVideo(inputPath, startSec, endSec, outPath, spawnFn = child_process_1.spawn) {
10
11
  return new Promise((resolve) => {
@@ -16,7 +17,9 @@ function sliceVideo(inputPath, startSec, endSec, outPath, spawnFn = child_proces
16
17
  '-c', 'copy',
17
18
  outPath,
18
19
  ];
19
- const proc = spawnFn('ffmpeg', args);
20
+ // Xenon's own ffmpeg, as everywhere else: a bare name isn't found by a
21
+ // server the Mac app started, and finds a system ffmpeg first otherwise.
22
+ const proc = spawnFn((0, ffmpegPath_1.resolveFfmpegPath)(), args);
20
23
  let stderr = '';
21
24
  if (proc.stderr) {
22
25
  proc.stderr.on('data', (chunk) => {
@@ -2,18 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.canOverrideLease = canOverrideLease;
4
4
  function canOverrideLease(credential) {
5
- var _a, _b;
6
5
  switch (credential.kind) {
7
6
  case 'auth-disabled':
8
7
  return true;
9
- case 'api-key': {
8
+ case 'api-key':
9
+ case 'session-token': {
10
10
  if (!credential.user)
11
11
  return false;
12
12
  const scopes = new Set(credential.scopes.split(',').map((s) => s.trim()));
13
- return scopes.has('admin') || credential.user.role === 'SUPER_ADMIN';
13
+ const { role } = credential.user;
14
+ if (credential.kind === 'session-token') {
15
+ return role === 'SUPER_ADMIN' || (role === 'ADMIN' && scopes.has('admin'));
16
+ }
17
+ return scopes.has('admin') || role === 'SUPER_ADMIN';
14
18
  }
15
- case 'session-token':
16
- return ((_a = credential.user) === null || _a === void 0 ? void 0 : _a.role) === 'SUPER_ADMIN' || ((_b = credential.user) === null || _b === void 0 ? void 0 : _b.role) === 'ADMIN';
17
19
  default:
18
20
  return false;
19
21
  }