@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
@@ -187,11 +187,19 @@ paths:
187
187
  operationId: getLabSettings
188
188
  summary: Read the lab's settings and AI provider
189
189
  description: |
190
- The lab-wide settings stored in the database (health checks and build cleanup), merged
191
- with the AI settings currently in effect. API keys are never returned: `geminiSet`,
192
- `openaiSet` and `anthropicSet` say only whether one is configured. A setting never saved
193
- is absent. Needs the `ADMIN` role or above, as the dashboard's settings pages do; through
194
- 2.12 any signed-in user could read it.
190
+ The lab-wide settings in effect (health checks, build cleanup and the AI self-healing
191
+ switch), merged with the AI settings in effect (provider, models and base URL). Each
192
+ setting is the value saved in the dashboard, else the plugin option or environment
193
+ variable the server was started with, else the default, which is also what the server
194
+ runs with. A model or base URL set nowhere is absent: the provider's own applies. A user
195
+ name, password or query value in the base URL is sent as `***`. `defaults` holds the defaults themselves, which
196
+ is what "restore defaults" goes back to. Only `healthCheckSchedule` can be absent: no
197
+ schedule is set. Through 2.13 `enableSelfHealing` was neither stored nor sent, so the
198
+ Settings page always showed it on.
199
+ API keys are never returned: `geminiSet`, `openaiSet` and `anthropicSet` say only
200
+ whether one is configured. Needs the `ADMIN` role or above, as the dashboard's settings
201
+ pages do; through 2.12 any signed-in user could read it. Through 2.13 a setting never
202
+ saved was absent, and `defaults` was not sent.
195
203
  tags:
196
204
  - Configuration
197
205
  responses:
@@ -202,8 +210,19 @@ paths:
202
210
  schema:
203
211
  $ref: '#/components/schemas/PlatformSettings'
204
212
  example:
213
+ enableSelfHealing: true
205
214
  healthCheckIntervalMs: 300000
206
215
  buildCleanupDays: 30
216
+ buildCleanupMaxCount: 100
217
+ buildCleanupSchedule: 0 0 * * *
218
+ deleteBuildAssets: true
219
+ defaults:
220
+ enableSelfHealing: true
221
+ healthCheckIntervalMs: 300000
222
+ buildCleanupDays: 30
223
+ buildCleanupMaxCount: 100
224
+ buildCleanupSchedule: 0 0 * * *
225
+ deleteBuildAssets: true
207
226
  aiProvider: gemini
208
227
  aiModel: gemini-3-flash-preview
209
228
  geminiModel: gemini-3-flash-preview
@@ -233,11 +252,34 @@ paths:
233
252
  Saves the lab-wide settings and changes the AI provider settings. Every field is
234
253
  optional; fields not sent are left as they are, and unknown fields are ignored.
235
254
 
236
- - Health-check and build-cleanup fields are stored in the database.
237
- - AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) change the
238
- running server only. They are not stored, so a restart returns to the values the
239
- server was started with. API keys can't be set here; they come from the server's
240
- environment.
255
+ - Health-check and build-cleanup fields are stored in the database. A stored value
256
+ replaces the plugin option the server was started with, and applies without a
257
+ restart: the health check within a minute, the retention values at the next cleanup
258
+ run, and a new `buildCleanupSchedule` at once. The cleanup fields are checked first
259
+ and refused with `400` when they could not work: `buildCleanupDays` and
260
+ `buildCleanupMaxCount` must be whole numbers of at least 1, `buildCleanupSchedule` a
261
+ cron expression of five or six fields, `deleteBuildAssets` true or false. Nothing is
262
+ saved then. Send only the fields to change: a field sent is saved as the lab's own.
263
+ Through 2.13 only one setting could be saved: saving a second failed with a `500`.
264
+ - `enableSelfHealing` (true or false; anything else is refused with `400`) is the AI
265
+ self-healing switch. A stored value replaces the plugin option the server was started
266
+ with, and applies to the next command this server runs, with no restart. A session's
267
+ own `xe:options.healingTiers` still limits the tiers it may use. Through 2.13 the
268
+ switch was accepted and ignored. The value belongs to the server it is saved on:
269
+ saving it on a hub changes the hub's own sessions, not those on a node's phones,
270
+ which the node's own setting decides.
271
+ - AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) are stored
272
+ too. A stored value replaces the plugin option or environment variable the server was
273
+ started with, and applies to the next AI call, with no restart. An empty value removes
274
+ the stored one, and the server's own applies again. They are checked first and refused
275
+ with `400`: `aiProvider` must be `gemini`, `openai`, `anthropic` or `ollama`; a model
276
+ name has no spaces and at most 200 characters; `aiBaseUrl` is an http or https address
277
+ with no user name, password or query. Through 2.14 they changed the running server
278
+ only, and a restart undid them. Like the self-healing switch, they belong to the server
279
+ they are saved on.
280
+ - API keys can't be set here, and are never stored: they come from the server's
281
+ environment. `aiTemperature`, `aiMaxTokens` and `aiTopP`, which the AI engine page sent
282
+ through 2.14, were never used by any AI call and are ignored like any unknown field.
241
283
 
242
284
  Needs the `SUPER_ADMIN` role and the `admin` scope; an `ADMIN` gets `403` with a message
243
285
  saying so (through 2.12 an `ADMIN` could). With the dashboard cookie the request must
@@ -254,6 +296,7 @@ paths:
254
296
  healthCheckIntervalMs: 300000
255
297
  buildCleanupDays: 14
256
298
  deleteBuildAssets: true
299
+ enableSelfHealing: false
257
300
  aiProvider: anthropic
258
301
  anthropicModel: claude-sonnet-4-6
259
302
  responses:
@@ -265,6 +308,16 @@ paths:
265
308
  $ref: '#/components/schemas/Success'
266
309
  example:
267
310
  success: true
311
+ '400':
312
+ description: A build-cleanup field, the self-healing switch or an AI field has a value that could not work. Nothing was saved.
313
+ content:
314
+ application/json:
315
+ schema:
316
+ $ref: '#/components/schemas/Error'
317
+ example:
318
+ error: invalid_setting
319
+ field: buildCleanupDays
320
+ message: buildCleanupDays must be a whole number of at least 1.
268
321
  '401':
269
322
  $ref: '#/components/responses/Unauthorized'
270
323
  '403':
@@ -279,7 +332,7 @@ paths:
279
332
  $ref: '#/components/schemas/Error'
280
333
  example:
281
334
  error: true
282
- message: Unique constraint failed on the fields (`id`)
335
+ message: Can't reach database server
283
336
  /api/config/reset-metrics:
284
337
  post:
285
338
  operationId: resetDeviceHealCounts
@@ -415,21 +468,39 @@ paths:
415
468
  description: |
416
469
  Adds a webhook, active at once. When one of its `events` happens Xenon POSTs to `url`:
417
470
 
418
- - with `payloadTemplate`: the template, with each `{{key}}` (or `{{a.b}}`) replaced from
419
- the event (`eventType` plus the event's fields), sent as JSON if the result parses as
420
- JSON, otherwise as `{ "text": "<result>" }`;
471
+ - with `payloadTemplate`: the template, with each `{{name}}` (or `{{a.b}}`, `{{list.0.b}}`)
472
+ replaced from the event (`eventType` plus the event's fields, below). A template that
473
+ is JSON as written is filled in string by string, so a value holding a quote or a line
474
+ break can't break it, and is sent as that JSON. Any other template is filled in as
475
+ text and sent as JSON if the result parses, otherwise as `{ "text": "<result>" }`. A
476
+ `{{name}}` the event doesn't have is left as written;
421
477
  - otherwise, `type: slack`: a Slack message with an attachment;
422
478
  - otherwise: `{ "event": "<event>", "payload": { ... } }`.
423
479
 
424
- Each delivery is one POST, never retried. A template whose result isn't JSON is sent
425
- once, as text. A webhook that refuses an event or can't be reached doesn't fail the
426
- event: the failure is logged on the server and the other webhooks still get it. Use
427
- `POST /api/webhook/test` to see whether a webhook accepts deliveries.
480
+ Each delivery is one POST, never retried. A webhook that refuses an event or can't be
481
+ reached doesn't fail the event: the failure is logged on the server and the other
482
+ webhooks still get it. Use `POST /api/webhook/test` to see whether a webhook accepts
483
+ deliveries.
428
484
 
429
- Events are `device_offline`, `session_failed`, `device_new` and
430
- `selector_health_digest`. Neither `url` nor the event names are checked. Needs the
431
- `ADMIN` role and the `admin` scope. With the dashboard cookie the request must come from
432
- the same host.
485
+ What each event carries (a device event also has the device's other fields, which may
486
+ change; only the names listed are promised):
487
+
488
+ - `device_offline`, `device_new`: `udid`, `name`, `host`, `platform`.
489
+ Sent when a device stops being reported, or is first detected.
490
+
491
+ - `session_failed`: `sessionId`, `sessionName`, `failureReason`, `udid`, `deviceName`,
492
+ `platform`, `osVersion`, `startTime`, `endTime` (ISO 8601; unknown values are empty
493
+ text). Sent once per session when it ends as failed: the test marked it failed, a command
494
+ failed, it timed out for inactivity, its driver crashed, or its heartbeat stopped. Not
495
+ when the server itself shuts down, not for a session already failed when the server
496
+ started, and not when the dashboard is off, since the session's record is what it is
497
+ built from.
498
+
499
+ - `selector_health_digest`: `windowDays`, `totalHeals`, `distinctSelectors` and
500
+ `hotspots`, a list of `healCount`, `originalSelector` and `suggestedRewrite`.
501
+
502
+ Neither `url` nor the event names are checked. Needs the `ADMIN` role and the `admin`
503
+ scope. With the dashboard cookie the request must come from the same host.
433
504
  tags:
434
505
  - Webhooks
435
506
  requestBody:
@@ -536,11 +607,12 @@ paths:
536
607
  operationId: testWebhook
537
608
  summary: Send a test message to a URL
538
609
  description: |
539
- Sends a sample `device_new` event (a device named `Test Device`, udid
540
- `test-device-udid`, host `127.0.0.1`) to `url` the way a webhook with this `type` and
541
- `payloadTemplate` gets its real events (see `POST /api/webhook`): the filled-in
542
- template if there is one, else a Slack message for `type: slack` (the default), else
543
- `{ "event": "device_new", "payload": { ... } }`. No stored webhook is involved.
610
+ Sends a sample of `event` (`device_new` unless named; a device named `Test Device`, udid
611
+ `test-device-udid`, host `127.0.0.1`, or for `session_failed` a failed session on it) to
612
+ `url` the way a webhook with this `type` and `payloadTemplate` gets its real events (see
613
+ `POST /api/webhook`, which lists what each event carries): the filled-in template if
614
+ there is one, else a Slack message for `type: slack` (the default), else
615
+ `{ "event": "<event>", "payload": { ... } }`. No stored webhook is involved.
544
616
 
545
617
  Answers `200` when `url` accepted the delivery (a 2xx), and `502 delivery_failed`, with
546
618
  the reason, when it refused it or couldn't be reached. Needs the `ADMIN` role and the
@@ -575,6 +647,16 @@ paths:
575
647
  The webhook's template, filled in from the sample event as a real event's
576
648
  would be.
577
649
  example: '{"text": "{{eventType}}: {{name}} ({{udid}})"}'
650
+ event:
651
+ type: string
652
+ default: device_new
653
+ enum:
654
+ - device_offline
655
+ - session_failed
656
+ - device_new
657
+ - selector_health_digest
658
+ description: Which event to send a sample of.
659
+ example: session_failed
578
660
  example:
579
661
  url: https://hooks.slack.com/services/T000/B000/XXXXXXXX
580
662
  type: slack
@@ -588,13 +670,18 @@ paths:
588
670
  example:
589
671
  success: true
590
672
  '400':
591
- description: '`url` is missing or not a string.'
673
+ description: '`url` is missing or not a string, or `event` is not one of the four.'
592
674
  content:
593
675
  application/json:
594
676
  schema:
595
677
  $ref: '#/components/schemas/Error'
596
- example:
597
- error: url is required
678
+ examples:
679
+ noUrl:
680
+ value:
681
+ error: url is required
682
+ badEvent:
683
+ value:
684
+ error: 'event must be one of: device_offline, session_failed, device_new, selector_health_digest'
598
685
  '401':
599
686
  $ref: '#/components/responses/Unauthorized'
600
687
  '403':
@@ -2264,8 +2351,12 @@ components:
2264
2351
  example: 734120
2265
2352
  PlatformSettings:
2266
2353
  type: object
2267
- description: The lab's stored settings, merged with the AI settings in effect. A setting never saved is absent.
2354
+ description: The lab's settings in effect (saved, else the startup option, else the default), merged with the AI settings in effect. Only `healthCheckSchedule` can be absent.
2268
2355
  properties:
2356
+ enableSelfHealing:
2357
+ type: boolean
2358
+ description: Whether a failed findElement goes on to the self-healing tiers.
2359
+ example: true
2269
2360
  healthCheckIntervalMs:
2270
2361
  type: integer
2271
2362
  example: 300000
@@ -2283,8 +2374,30 @@ components:
2283
2374
  example: 0 3 * * *
2284
2375
  deleteBuildAssets:
2285
2376
  type: boolean
2377
+ defaults:
2378
+ type: object
2379
+ description: The server's own defaults for the settings above, which is what "restore defaults" goes back to.
2380
+ properties:
2381
+ enableSelfHealing:
2382
+ type: boolean
2383
+ example: true
2384
+ healthCheckIntervalMs:
2385
+ type: integer
2386
+ example: 300000
2387
+ buildCleanupDays:
2388
+ type: integer
2389
+ example: 30
2390
+ buildCleanupMaxCount:
2391
+ type: integer
2392
+ example: 100
2393
+ buildCleanupSchedule:
2394
+ type: string
2395
+ example: 0 0 * * *
2396
+ deleteBuildAssets:
2397
+ type: boolean
2286
2398
  aiProvider:
2287
2399
  type: string
2400
+ description: The provider in effect, saved on the AI engine page, else the server's option or environment, else gemini.
2288
2401
  example: gemini
2289
2402
  aiModel:
2290
2403
  type: string
@@ -2311,6 +2424,10 @@ components:
2311
2424
  type: object
2312
2425
  description: Any subset. Unknown fields are ignored.
2313
2426
  properties:
2427
+ enableSelfHealing:
2428
+ type: boolean
2429
+ description: The AI self-healing switch. Takes effect at the next command, with no restart.
2430
+ example: false
2314
2431
  healthCheckIntervalMs:
2315
2432
  type: integer
2316
2433
  example: 300000
@@ -2332,30 +2449,37 @@ components:
2332
2449
  type: boolean
2333
2450
  aiProvider:
2334
2451
  type: string
2335
- description: Not stored; until the server restarts.
2452
+ description: The provider every AI call uses. Stored; empty goes back to the server's own.
2336
2453
  enum:
2337
2454
  - gemini
2338
2455
  - openai
2339
2456
  - anthropic
2340
2457
  - ollama
2458
+ - ''
2341
2459
  aiModel:
2342
2460
  type: string
2343
- description: Not stored; until the server restarts.
2461
+ description: The model for a provider with no model of its own below. Stored; empty goes back to the server's own.
2462
+ maxLength: 200
2344
2463
  aiBaseUrl:
2345
2464
  type: string
2346
- description: Not stored; until the server restarts.
2465
+ description: Where OpenAI-compatible and Ollama calls go. An http or https address with no user name, password or query. Stored; empty goes back to the server's own.
2466
+ example: http://gpu-box.lab:11434
2347
2467
  geminiModel:
2348
2468
  type: string
2349
- description: Not stored; until the server restarts.
2469
+ description: Stored; empty goes back to the server's own.
2470
+ maxLength: 200
2350
2471
  openaiModel:
2351
2472
  type: string
2352
- description: Not stored; until the server restarts.
2473
+ description: Stored; empty goes back to the server's own.
2474
+ maxLength: 200
2353
2475
  anthropicModel:
2354
2476
  type: string
2355
- description: Not stored; until the server restarts.
2477
+ description: Stored; empty goes back to the server's own.
2478
+ maxLength: 200
2356
2479
  ollamaModel:
2357
2480
  type: string
2358
- description: Not stored; until the server restarts.
2481
+ description: Stored; empty goes back to the server's own.
2482
+ maxLength: 200
2359
2483
  PlatformAiTestRequest:
2360
2484
  type: object
2361
2485
  description: Every field is optional and falls back to the server's current setting.
@@ -2448,7 +2572,9 @@ components:
2448
2572
  example: slack
2449
2573
  payloadTemplate:
2450
2574
  type: string
2451
- description: A body template with `{{key}}` placeholders, used in place of the default body.
2575
+ description: >-
2576
+ A body template with `{{name}}` placeholders, used in place of the default body (see
2577
+ `POST /api/webhook` for the names each event has).
2452
2578
  example: '{"text":"{{eventType}} on {{udid}}"}'
2453
2579
  PlatformApp:
2454
2580
  type: object
@@ -108,13 +108,13 @@ paths:
108
108
  name: Checkout with saved card
109
109
  status: failed
110
110
  node_id: 3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d
111
- has_live_video: true
111
+ has_live_video: false
112
112
  video_recording_enabled: true
113
113
  video_recording: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.mp4
114
114
  startTime: '2026-10-03T14:20:11.004Z'
115
115
  endTime: '2026-10-03T14:22:05.120Z'
116
116
  failure_reason: 'NoSuchElementError: An element could not be located'
117
- failure_category: element_not_found
117
+ failure_category: ELEMENT_NOT_FOUND
118
118
  device_udid: emulator-5554
119
119
  device_platform: android
120
120
  device_version: '14'
@@ -923,7 +923,7 @@ paths:
923
923
  type: string
924
924
  example: |
925
925
  id,build_id,status,failure_category,failure_reason,device_platform,device_version,device_name,node_id,startTime,endTime,name
926
- 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,element_not_found,"NoSuchElementError: An element could not be located",android,14,Pixel 7,3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d,2026-10-03T14:20:11.004Z,2026-10-03T14:22:05.120Z,Checkout with saved card
926
+ 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,ELEMENT_NOT_FOUND,"NoSuchElementError: An element could not be located",android,14,Pixel 7,3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d,2026-10-03T14:20:11.004Z,2026-10-03T14:22:05.120Z,Checkout with saved card
927
927
  '400':
928
928
  description: '`format` is neither `json` nor `csv`.'
929
929
  content:
@@ -2688,6 +2688,13 @@ components:
2688
2688
  failure_category:
2689
2689
  type: string
2690
2690
  nullable: true
2691
+ description: >-
2692
+ What kind of failure it was: `ELEMENT_NOT_FOUND`, `APP_CRASH`, `TIMEOUT`,
2693
+ `PERMISSION_BLOCKED`, `WDA_FAILURE`, `XENON_COMMAND_FAILURE`, `SYSTEM_OVERLOAD` or
2694
+ `UNKNOWN`, read from the failure reason and the last failed commands when the
2695
+ session fails; or `HUB_RESTART` for a session the server's restart ended. Null for a
2696
+ session that hasn't failed, and for a failure recorded without that analysis.
2697
+ example: ELEMENT_NOT_FOUND
2691
2698
  is_profiling_available:
2692
2699
  type: boolean
2693
2700
  device_info:
@@ -15,6 +15,8 @@ const typedi_1 = require("typedi");
15
15
  const ApiKeyService_1 = require("../../services/ApiKeyService");
16
16
  const scopeGuard_1 = require("../../middleware/scopeGuard");
17
17
  const roleGuard_1 = require("../../middleware/roleGuard");
18
+ const mintedLifetime_1 = require("../../services/token/mintedLifetime");
19
+ const KNOWN_SCOPES = ['read', 'sessions', 'devices', 'admin'];
18
20
  function apiKeysRouter() {
19
21
  const r = (0, express_1.Router)();
20
22
  r.use((0, roleGuard_1.roleGuard)('ADMIN'));
@@ -23,11 +25,16 @@ function apiKeysRouter() {
23
25
  res.json(yield svc.list());
24
26
  }));
25
27
  r.post('/', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
26
- var _a;
28
+ var _a, _b;
27
29
  const { name, scopes, rateLimit, teamId, expiresAt } = req.body;
28
30
  if (!name || !Array.isArray(scopes) || scopes.length === 0) {
29
31
  return res.status(400).json({ error: 'name and scopes required' });
30
32
  }
33
+ // Only the scopes that exist. Any string was stored, so a typo made a key
34
+ // with none of the scopes its maker meant.
35
+ if (!scopes.every((s) => KNOWN_SCOPES.includes(s))) {
36
+ return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
37
+ }
31
38
  let expiresAtDate;
32
39
  if (expiresAt !== undefined && expiresAt !== null) {
33
40
  const d = new Date(expiresAt);
@@ -39,6 +46,11 @@ function apiKeysRouter() {
39
46
  }
40
47
  expiresAtDate = d;
41
48
  }
49
+ // Made with a credential that expires, a key ends no later than it.
50
+ const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.credentialExpiresAt);
51
+ if ('error' in within)
52
+ return res.status(400).json({ error: within.error });
53
+ expiresAtDate = within.expiresAt;
42
54
  const { id, raw } = yield svc.create({
43
55
  name,
44
56
  scopes,
@@ -47,7 +59,7 @@ function apiKeysRouter() {
47
59
  userId: req.auth.userId,
48
60
  expiresAt: expiresAtDate,
49
61
  });
50
- res.json({ id, key: raw, expiresAt: (_a = expiresAtDate === null || expiresAtDate === void 0 ? void 0 : expiresAtDate.toISOString()) !== null && _a !== void 0 ? _a : null });
62
+ res.json({ id, key: raw, expiresAt: (_b = expiresAtDate === null || expiresAtDate === void 0 ? void 0 : expiresAtDate.toISOString()) !== null && _b !== void 0 ? _b : null });
51
63
  }));
52
64
  r.delete('/:id', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
53
65
  yield svc.revoke(req.params.id);
@@ -28,11 +28,15 @@ const JwtKeyService_1 = require("../../services/token/JwtKeyService");
28
28
  const mcpScopes_1 = require("../../services/token/mcpScopes");
29
29
  const passwordResetMode_1 = require("../../services/passwordResetMode");
30
30
  const passwordResetLink_1 = require("../../services/passwordResetLink");
31
+ const mintedLifetime_1 = require("../../services/token/mintedLifetime");
31
32
  const SESSION_COOKIE = 'xenon_dashboard_session';
32
33
  const isSecureFromReq = (req) => req.secure || req.headers['x-forwarded-proto'] === 'https';
33
34
  const MCP_TTL_SEC = Number(process.env.XENON_MCP_TOKEN_TTL_SEC || 86400);
34
35
  const REST_TTL_SEC = 3600;
35
36
  const MINTABLE_AUDIENCES = ['xenon-rest', 'xenon-mcp'];
37
+ function grantsScope(scopesCsv, scope) {
38
+ return scopesCsv.split(',').some((s) => s.trim() === scope);
39
+ }
36
40
  function issueToken(auth, body) {
37
41
  return __awaiter(this, void 0, void 0, function* () {
38
42
  var _a, _b, _c, _d;
@@ -40,7 +44,7 @@ function issueToken(auth, body) {
40
44
  if (!MINTABLE_AUDIENCES.includes(audience)) {
41
45
  throw new Error(`unsupported audience: ${audience}`);
42
46
  }
43
- const expiresIn = audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC;
47
+ const expiresIn = (0, mintedLifetime_1.mintedLifetimeSec)(audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC, auth.credentialExpiresAt);
44
48
  const svc = typedi_1.Container.get(JwtKeyService_1.JwtKeyService);
45
49
  if (audience === 'xenon-mcp') {
46
50
  // Validate the untrusted `scopes` body field before it reaches the mapper:
@@ -66,7 +70,23 @@ function issueToken(auth, body) {
66
70
  // Session-token capability (spec §3 item 6 / R9): a sibling credential the
67
71
  // client injects as `xe:options.sessionToken` so the Appium createSession
68
72
  // interceptor can refuse tokenless direct-connect sessions when the gate is on.
69
- const sessionToken = yield svc.sign({ sub: auth.userId, teamId: (_c = auth.teamId) !== null && _c !== void 0 ? _c : null }, { audience: 'xenon-session', ttlSeconds: expiresIn });
73
+ //
74
+ // It creates Appium sessions, so it comes only with a grant that includes
75
+ // `appium:use`, which needs the `sessions` scope. Through 2.14 every
76
+ // credential got one, a read-only key included. It carries the scopes a
77
+ // session is judged by: `sessions`, and `admin` (a lease override) only
78
+ // when the minting credential has it and the grant isn't narrower than
79
+ // it: the default grant (no scopes asked for), or a full-admin one.
80
+ if (!grant.granular.includes('appium:use')) {
81
+ return { token, expiresIn, audience, scopes: grant.granular };
82
+ }
83
+ const askedForScopes = Array.isArray(body.scopes) && body.scopes.length > 0;
84
+ const sessionAdmin = grantsScope(auth.scopes, 'admin') && (!askedForScopes || grant.roles.includes('admin'));
85
+ const sessionToken = yield svc.sign({
86
+ sub: auth.userId,
87
+ teamId: (_c = auth.teamId) !== null && _c !== void 0 ? _c : null,
88
+ scopes: sessionAdmin ? 'admin,sessions' : 'sessions',
89
+ }, { audience: 'xenon-session', ttlSeconds: expiresIn });
70
90
  return { token, expiresIn, audience, scopes: grant.granular, sessionToken };
71
91
  }
72
92
  const token = yield svc.sign({ sub: auth.userId, role: auth.role, scopes: auth.scopes, teamId: (_d = auth.teamId) !== null && _d !== void 0 ? _d : null }, { audience, ttlSeconds: expiresIn });
@@ -139,11 +159,15 @@ function authPublicRouter() {
139
159
  const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
140
160
  // No SMTP and no opt-in log fallback: nothing can reach the user, so
141
161
  // don't mint a live credential. Admins issue links from the Users page.
142
- if (user && user.status === 'ACTIVE' && emailSvc.canDeliver()) {
162
+ // No XENON_PUBLIC_URL: nothing says where the link may point. The
163
+ // request's Host is the asker's to choose, and the asker may not be
164
+ // the account's owner, so it is never used here.
165
+ const base = (0, passwordResetLink_1.resetLinkBase)();
166
+ if (user && user.status === 'ACTIVE' && emailSvc.canDeliver() && base) {
143
167
  const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
144
168
  const { raw } = yield resetSvc.createToken(user.id);
145
- const link = (0, passwordResetLink_1.buildResetLink)(req, raw);
146
- yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link));
169
+ const link = (0, passwordResetLink_1.buildResetLink)(base, raw);
170
+ yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link, resetSvc.ttlMs()));
147
171
  }
148
172
  }
149
173
  catch (e) {
@@ -625,7 +625,7 @@ const MJPEG_PROXY_CACHE = new Map();
625
625
  * Start stream endpoint - Initiates WDA and MJPEG streaming for iOS devices
626
626
  */
627
627
  router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
628
- var _a, _b, _c, _d, _e;
628
+ var _a, _b, _c, _d, _e, _f;
629
629
  const { udid } = req.params;
630
630
  // Someone is about to watch: a page that just left (a reload, or another tab
631
631
  // closing) must not stop the stream this page is starting.
@@ -709,7 +709,11 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
709
709
  // A recording device keeps MJPEG preview so we never run screenrecord and
710
710
  // the recording's screencap pipeline against the same device at once.
711
711
  const recording = yield typedi_1.Container.get(recording_store_1.RecordingStore).isRecording(udid);
712
- const streamType = (0, streamType_1.resolveStreamType)(device.platform, flagOn, recording);
712
+ // A page that shows MJPEG says so (`player: 'mjpeg'`): device control, a
713
+ // browser with no WebCodecs, or a page whose H.264 player just failed.
714
+ // Anything else is the server's choice.
715
+ const clientShowsMjpeg = ((_f = req.body) === null || _f === void 0 ? void 0 : _f.player) === 'mjpeg';
716
+ const streamType = (0, streamType_1.resolveStreamType)(device.platform, flagOn, recording, !clientShowsMjpeg);
713
717
  let mjpegPort;
714
718
  if (device.platform === 'ios' || device.platform === 'tvos') {
715
719
  mjpegPort = (yield typedi_1.Container.get(IOSStreamService_1.default).startStream(udid)).mjpegPort;
@@ -720,6 +724,20 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
720
724
  yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid, { source: h264Cfg.source });
721
725
  }
722
726
  else {
727
+ // One capture pipeline per device where possible.
728
+ const h264 = typedi_1.Container.get(AndroidH264StreamService_1.default);
729
+ if (recording) {
730
+ // A recording reads the MJPEG capture, and H.264 never runs beside it.
731
+ if (h264.getMultiplexer(udid))
732
+ yield h264.stop(udid);
733
+ }
734
+ else {
735
+ // This page shows MJPEG. An H.264 capture still running for the phone
736
+ // (this page's own before its player failed, or a tile's) ends before
737
+ // the screencap loop starts if nobody watches it, else when its last
738
+ // viewer leaves: another tile or tab may still be playing it.
739
+ yield h264.endWhenUnwatched(udid);
740
+ }
723
741
  mjpegPort = (yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(udid)).mjpegPort;
724
742
  }
725
743
  // Lazy-load the screen size if missing: tiles need it to accept taps.