@xenon-device-management/xenon 2.14.0 → 2.16.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 (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -188,9 +188,11 @@ paths:
188
188
  summary: Read the lab's settings and AI provider
189
189
  description: |
190
190
  The lab-wide settings in effect (health checks, build cleanup and the AI self-healing
191
- switch), merged with the AI settings currently in effect. Each setting is the value saved
192
- in the dashboard, else the plugin option the server was started with, else the default,
193
- which is also what the server runs with. `defaults` holds the defaults themselves, which
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
194
196
  is what "restore defaults" goes back to. Only `healthCheckSchedule` can be absent: no
195
197
  schedule is set. Through 2.13 `enableSelfHealing` was neither stored nor sent, so the
196
198
  Settings page always showed it on.
@@ -266,10 +268,18 @@ paths:
266
268
  switch was accepted and ignored. The value belongs to the server it is saved on:
267
269
  saving it on a hub changes the hub's own sessions, not those on a node's phones,
268
270
  which the node's own setting decides.
269
- - AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) change the
270
- running server only. They are not stored, so a restart returns to the values the
271
- server was started with. API keys can't be set here; they come from the server's
272
- environment.
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.
273
283
 
274
284
  Needs the `SUPER_ADMIN` role and the `admin` scope; an `ADMIN` gets `403` with a message
275
285
  saying so (through 2.12 an `ADMIN` could). With the dashboard cookie the request must
@@ -299,7 +309,7 @@ paths:
299
309
  example:
300
310
  success: true
301
311
  '400':
302
- description: A build-cleanup field or the self-healing switch has a value that could not work. Nothing was saved.
312
+ description: A build-cleanup field, the self-healing switch or an AI field has a value that could not work. Nothing was saved.
303
313
  content:
304
314
  application/json:
305
315
  schema:
@@ -2387,6 +2397,7 @@ components:
2387
2397
  type: boolean
2388
2398
  aiProvider:
2389
2399
  type: string
2400
+ description: The provider in effect, saved on the AI engine page, else the server's option or environment, else gemini.
2390
2401
  example: gemini
2391
2402
  aiModel:
2392
2403
  type: string
@@ -2438,30 +2449,37 @@ components:
2438
2449
  type: boolean
2439
2450
  aiProvider:
2440
2451
  type: string
2441
- description: Not stored; until the server restarts.
2452
+ description: The provider every AI call uses. Stored; empty goes back to the server's own.
2442
2453
  enum:
2443
2454
  - gemini
2444
2455
  - openai
2445
2456
  - anthropic
2446
2457
  - ollama
2458
+ - ''
2447
2459
  aiModel:
2448
2460
  type: string
2449
- 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
2450
2463
  aiBaseUrl:
2451
2464
  type: string
2452
- 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
2453
2467
  geminiModel:
2454
2468
  type: string
2455
- description: Not stored; until the server restarts.
2469
+ description: Stored; empty goes back to the server's own.
2470
+ maxLength: 200
2456
2471
  openaiModel:
2457
2472
  type: string
2458
- description: Not stored; until the server restarts.
2473
+ description: Stored; empty goes back to the server's own.
2474
+ maxLength: 200
2459
2475
  anthropicModel:
2460
2476
  type: string
2461
- description: Not stored; until the server restarts.
2477
+ description: Stored; empty goes back to the server's own.
2478
+ maxLength: 200
2462
2479
  ollamaModel:
2463
2480
  type: string
2464
- description: Not stored; until the server restarts.
2481
+ description: Stored; empty goes back to the server's own.
2482
+ maxLength: 200
2465
2483
  PlatformAiTestRequest:
2466
2484
  type: object
2467
2485
  description: Every field is optional and falls back to the server's current setting.
@@ -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,15 @@ components:
2688
2688
  failure_category:
2689
2689
  type: string
2690
2690
  nullable: true
2691
+ description: >-
2692
+ What kind of failure it was, read from the failure reason and the last failed
2693
+ commands' errors when the session fails: `SESSION_LOST`, `APP_CRASH`,
2694
+ `SYSTEM_OVERLOAD`, `STALE_ELEMENT`, `TIMEOUT`, `ELEMENT_NOT_FOUND`,
2695
+ `PERMISSION_BLOCKED` or `UNKNOWN`; or `HUB_RESTART` for a session the server's
2696
+ restart or shutdown ended. Sessions filed through 2.15 may also hold `WDA_FAILURE` or
2697
+ `XENON_COMMAND_FAILURE`, no longer written. Null for a session that hasn't failed,
2698
+ and for a failure recorded without that analysis.
2699
+ example: ELEMENT_NOT_FOUND
2691
2700
  is_profiling_available:
2692
2701
  type: boolean
2693
2702
  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,16 @@ 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");
32
+ const mcpTokenTtl_1 = require("../../services/token/mcpTokenTtl");
31
33
  const SESSION_COOKIE = 'xenon_dashboard_session';
32
34
  const isSecureFromReq = (req) => req.secure || req.headers['x-forwarded-proto'] === 'https';
33
- const MCP_TTL_SEC = Number(process.env.XENON_MCP_TOKEN_TTL_SEC || 86400);
35
+ const MCP_TTL_SEC = (0, mcpTokenTtl_1.mcpTokenTtlSec)();
34
36
  const REST_TTL_SEC = 3600;
35
37
  const MINTABLE_AUDIENCES = ['xenon-rest', 'xenon-mcp'];
38
+ function grantsScope(scopesCsv, scope) {
39
+ return scopesCsv.split(',').some((s) => s.trim() === scope);
40
+ }
36
41
  function issueToken(auth, body) {
37
42
  return __awaiter(this, void 0, void 0, function* () {
38
43
  var _a, _b, _c, _d;
@@ -40,7 +45,7 @@ function issueToken(auth, body) {
40
45
  if (!MINTABLE_AUDIENCES.includes(audience)) {
41
46
  throw new Error(`unsupported audience: ${audience}`);
42
47
  }
43
- const expiresIn = audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC;
48
+ const expiresIn = (0, mintedLifetime_1.mintedLifetimeSec)(audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC, auth.credentialExpiresAt);
44
49
  const svc = typedi_1.Container.get(JwtKeyService_1.JwtKeyService);
45
50
  if (audience === 'xenon-mcp') {
46
51
  // Validate the untrusted `scopes` body field before it reaches the mapper:
@@ -66,7 +71,23 @@ function issueToken(auth, body) {
66
71
  // Session-token capability (spec §3 item 6 / R9): a sibling credential the
67
72
  // client injects as `xe:options.sessionToken` so the Appium createSession
68
73
  // 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 });
74
+ //
75
+ // It creates Appium sessions, so it comes only with a grant that includes
76
+ // `appium:use`, which needs the `sessions` scope. Through 2.14 every
77
+ // credential got one, a read-only key included. It carries the scopes a
78
+ // session is judged by: `sessions`, and `admin` (a lease override) only
79
+ // when the minting credential has it and the grant isn't narrower than
80
+ // it: the default grant (no scopes asked for), or a full-admin one.
81
+ if (!grant.granular.includes('appium:use')) {
82
+ return { token, expiresIn, audience, scopes: grant.granular };
83
+ }
84
+ const askedForScopes = Array.isArray(body.scopes) && body.scopes.length > 0;
85
+ const sessionAdmin = grantsScope(auth.scopes, 'admin') && (!askedForScopes || grant.roles.includes('admin'));
86
+ const sessionToken = yield svc.sign({
87
+ sub: auth.userId,
88
+ teamId: (_c = auth.teamId) !== null && _c !== void 0 ? _c : null,
89
+ scopes: sessionAdmin ? 'admin,sessions' : 'sessions',
90
+ }, { audience: 'xenon-session', ttlSeconds: expiresIn });
70
91
  return { token, expiresIn, audience, scopes: grant.granular, sessionToken };
71
92
  }
72
93
  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 +160,15 @@ function authPublicRouter() {
139
160
  const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
140
161
  // No SMTP and no opt-in log fallback: nothing can reach the user, so
141
162
  // don't mint a live credential. Admins issue links from the Users page.
142
- if (user && user.status === 'ACTIVE' && emailSvc.canDeliver()) {
163
+ // No XENON_PUBLIC_URL: nothing says where the link may point. The
164
+ // request's Host is the asker's to choose, and the asker may not be
165
+ // the account's owner, so it is never used here.
166
+ const base = (0, passwordResetLink_1.resetLinkBase)();
167
+ if (user && user.status === 'ACTIVE' && emailSvc.canDeliver() && base) {
143
168
  const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
144
169
  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));
170
+ const link = (0, passwordResetLink_1.buildResetLink)(base, raw);
171
+ yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link, resetSvc.ttlMs()));
147
172
  }
148
173
  }
149
174
  catch (e) {
@@ -724,12 +724,20 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
724
724
  yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid, { source: h264Cfg.source });
725
725
  }
726
726
  else {
727
- // One capture pipeline per device: an H.264 capture still running for it
728
- // (a tile's, or this page's own before its player failed) ends before the
729
- // screencap loop starts.
727
+ // One capture pipeline per device where possible.
730
728
  const h264 = typedi_1.Container.get(AndroidH264StreamService_1.default);
731
- if (h264.getMultiplexer(udid))
732
- yield h264.stop(udid);
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
+ }
733
741
  mjpegPort = (yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(udid)).mjpegPort;
734
742
  }
735
743
  // Lazy-load the screen size if missing: tiles need it to accept taps.
@@ -59,6 +59,7 @@ const web_config_service_1 = require("../../data-service/web-config-service");
59
59
  const PluginContext_1 = require("../../PluginContext");
60
60
  const labSettings_1 = require("../../services/settings/labSettings");
61
61
  const SelfHealingSwitch_1 = require("../../services/settings/SelfHealingSwitch");
62
+ const aiEngineSettings_1 = require("../../services/settings/aiEngineSettings");
62
63
  const typedi_1 = require("typedi");
63
64
  const scopeGuard_1 = require("../../middleware/scopeGuard");
64
65
  const roleGuard_1 = require("../../middleware/roleGuard");
@@ -1102,6 +1103,34 @@ function streamLiveSessionVideo(request, response) {
1102
1103
  }
1103
1104
  });
1104
1105
  }
1106
+ /**
1107
+ * The base URL as the dashboard may see it: a user name, password or query
1108
+ * value it carries is masked. Saving one is refused (aiEngineSettings.ts), but
1109
+ * the server's environment can still hold one, and every admin can read this.
1110
+ */
1111
+ function maskedBaseUrl(url) {
1112
+ if (!url)
1113
+ return url;
1114
+ let parsed;
1115
+ try {
1116
+ parsed = new URL(url);
1117
+ }
1118
+ catch (_a) {
1119
+ return url;
1120
+ }
1121
+ // Shown as given unless something needs masking: toString() would add a '/'.
1122
+ if (!parsed.username && !parsed.password && !parsed.search && !parsed.hash)
1123
+ return url;
1124
+ if (parsed.username || parsed.password) {
1125
+ parsed.username = '***';
1126
+ parsed.password = '';
1127
+ }
1128
+ for (const name of [...new Set(parsed.searchParams.keys())]) {
1129
+ parsed.searchParams.set(name, '***');
1130
+ }
1131
+ parsed.hash = '';
1132
+ return parsed.toString();
1133
+ }
1105
1134
  function getGlobalConfig(request, response) {
1106
1135
  return __awaiter(this, void 0, void 0, function* () {
1107
1136
  try {
@@ -1110,13 +1139,13 @@ function getGlobalConfig(request, response) {
1110
1139
  // the default. The pages show this (a saved value alone left them to guess
1111
1140
  // at what a setting never saved was), and `defaults` for "restore defaults".
1112
1141
  const effective = (0, labSettings_1.effectiveSettings)(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs, dbConfig);
1113
- // Merge with Environment Config (AI Settings)
1142
+ // The AI engine settings in effect (aiEngineSettings.ts keeps `config` at
1143
+ // them). Never a key: only whether one is set.
1114
1144
  const { config } = yield Promise.resolve().then(() => __importStar(require('../../config')));
1115
- // Sanitize keys - return boolean existence only
1116
1145
  const aiConfig = {
1117
1146
  aiProvider: config.aiProvider,
1118
1147
  aiModel: config.aiModel,
1119
- aiBaseUrl: config.aiBaseUrl,
1148
+ aiBaseUrl: maskedBaseUrl(config.aiBaseUrl),
1120
1149
  geminiModel: config.geminiModel,
1121
1150
  openaiModel: config.openaiModel,
1122
1151
  anthropicModel: config.anthropicModel,
@@ -1136,39 +1165,25 @@ function getGlobalConfig(request, response) {
1136
1165
  }
1137
1166
  function updateGlobalConfig(request, response) {
1138
1167
  return __awaiter(this, void 0, void 0, function* () {
1168
+ var _a;
1139
1169
  try {
1140
1170
  const payload = request.body;
1141
1171
  // Before anything is saved: a value the cleanup job would act on, such as a
1142
- // retention window of 0, is refused rather than stored.
1143
- const problem = (0, labSettings_1.validateSettingsUpdate)(payload !== null && payload !== void 0 ? payload : {});
1172
+ // retention window of 0, is refused rather than stored, and so is an AI
1173
+ // provider or address that could not work, now that it outlives a restart.
1174
+ const problem = (_a = (0, labSettings_1.validateSettingsUpdate)(payload !== null && payload !== void 0 ? payload : {})) !== null && _a !== void 0 ? _a : (0, aiEngineSettings_1.validateAiEngineUpdate)(payload !== null && payload !== void 0 ? payload : {});
1144
1175
  if (problem) {
1145
1176
  return response
1146
1177
  .status(400)
1147
1178
  .json({ error: 'invalid_setting', field: problem.field, message: problem.message });
1148
1179
  }
1149
- // Handle Runtime AI Config Overrides (Memory only)
1150
- // Only pass defined values to avoid overwriting env vars (e.g. aiBaseUrl, ollamaModel) with undefined
1151
- const runtimeOverrides = {};
1152
- if (payload.aiProvider !== undefined)
1153
- runtimeOverrides.aiProvider = payload.aiProvider;
1154
- if (payload.aiModel !== undefined)
1155
- runtimeOverrides.aiModel = payload.aiModel;
1156
- if (payload.aiBaseUrl !== undefined)
1157
- runtimeOverrides.aiBaseUrl = payload.aiBaseUrl;
1158
- if (payload.geminiModel !== undefined)
1159
- runtimeOverrides.geminiModel = payload.geminiModel;
1160
- if (payload.openaiModel !== undefined)
1161
- runtimeOverrides.openaiModel = payload.openaiModel;
1162
- if (payload.anthropicModel !== undefined)
1163
- runtimeOverrides.anthropicModel = payload.anthropicModel;
1164
- if (payload.ollamaModel !== undefined)
1165
- runtimeOverrides.ollamaModel = payload.ollamaModel;
1166
- if (Object.keys(runtimeOverrides).length > 0) {
1167
- const { updateConfig } = yield Promise.resolve().then(() => __importStar(require('../../config')));
1168
- updateConfig(runtimeOverrides);
1180
+ // Saved, then in force for the next AI call. Through 2.14 the AI fields
1181
+ // changed the running server only, and a restart undid them.
1182
+ const aiEngine = (0, aiEngineSettings_1.aiEngineUpdateOf)(payload !== null && payload !== void 0 ? payload : {});
1183
+ yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(Object.assign(Object.assign({}, payload), aiEngine));
1184
+ if (Object.keys(aiEngine).length > 0) {
1185
+ typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).noteSaved(aiEngine);
1169
1186
  }
1170
- // Persist Web Configs to DB
1171
- yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(payload);
1172
1187
  // The command interceptor asks at every command and never reads the
1173
1188
  // database: hand it the saved value, in force from the next command.
1174
1189
  if (payload.enableSelfHealing !== undefined) {
@@ -89,10 +89,12 @@ function getDevices(request, response) {
89
89
  const [enriched] = yield (0, device_service_1.enrichDevicesWithTeamNames)([match]);
90
90
  return response.json(enriched);
91
91
  }
92
- /* dashboard-plugin-url is the base url for opening the appium-dashboard-plugin
93
- * This value will be attached to all express request via middleware
92
+ /* dashboard-plugin-url is where this server reaches the appium-dashboard-plugin,
93
+ * and dashboard-plugin-link the base of the link people open; both from
94
+ * configuration, never from the request (app/dashboardPluginLink.ts).
94
95
  */
95
96
  const dashboardPluginUrl = request['dashboard-plugin-url'];
97
+ const dashboardPluginLink = request['dashboard-plugin-link'];
96
98
  if (dashboardPluginUrl) {
97
99
  const response = yield InternalHttpClient_1.InternalHttpClient.get(`${dashboardPluginUrl}/api/sessions?start_time=${SERVER_UP_TIME}`);
98
100
  const sessions = ((_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.rows) || [];
@@ -105,7 +107,7 @@ function getDevices(request, response) {
105
107
  });
106
108
  devices = devices.map((d) => {
107
109
  var _a;
108
- d.dashboard_link = `${dashboardPluginUrl}?device_udid=${d.udid}&start_time=${SERVER_UP_TIME}`;
110
+ d.dashboard_link = `${dashboardPluginLink}?device_udid=${d.udid}&start_time=${SERVER_UP_TIME}`;
109
111
  d.total_session_count = ((_a = deviceSessionMap[d.udid]) === null || _a === void 0 ? void 0 : _a.length) || 0;
110
112
  return d;
111
113
  });
@@ -17,6 +17,7 @@ const ApiKeyService_1 = require("../../services/ApiKeyService");
17
17
  const UserService_1 = require("../../services/UserService");
18
18
  const prisma_1 = require("../../prisma");
19
19
  const profileIdentity_1 = require("./profileIdentity");
20
+ const mintedLifetime_1 = require("../../services/token/mintedLifetime");
20
21
  const ROLE_SCOPES = {
21
22
  SUPER_ADMIN: ['admin'],
22
23
  ADMIN: ['devices', 'sessions', 'read'],
@@ -79,10 +80,23 @@ function profileRouter() {
79
80
  return res.status(404).json({ error: 'user not found' });
80
81
  return res.json({ accessKey: user.accessKey });
81
82
  }));
83
+ // Rotating the access key ends every key pair its owner has, whatever their
84
+ // scopes, so it takes the person themselves (a dashboard sign-in) or a
85
+ // credential with the admin scope. Through 2.14 any credential could, a
86
+ // read-only token included, and cut off every one of its owner's clients.
82
87
  r.post('/access-key/rotate', (req, res) => __awaiter(this, void 0, void 0, function* () {
88
+ var _a;
83
89
  const auth = requireAuth(req, res);
84
90
  if (!auth)
85
91
  return;
92
+ const credentialScopes = String((_a = auth.scopes) !== null && _a !== void 0 ? _a : '')
93
+ .split(',')
94
+ .map((s) => s.trim());
95
+ if (auth.kind !== 'user-session' && !credentialScopes.includes('admin')) {
96
+ return res.status(403).json({
97
+ error: 'rotating the access key needs a dashboard sign-in or a credential with the admin scope',
98
+ });
99
+ }
86
100
  const userId = yield effectiveUserId(auth.userId);
87
101
  if (!userId)
88
102
  return res.status(404).json({ error: 'user not found' });
@@ -132,6 +146,13 @@ function profileRouter() {
132
146
  }
133
147
  expiresAtDate = d;
134
148
  }
149
+ // A token made with a credential that expires (a Bearer token, an API key
150
+ // with an expiry) ends no later than it. Through 2.14 a 1-hour Bearer
151
+ // token could make itself a key that never expired.
152
+ const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, auth.credentialExpiresAt);
153
+ if ('error' in within)
154
+ return res.status(400).json({ error: within.error });
155
+ expiresAtDate = within.expiresAt;
135
156
  const allowed = (_a = ROLE_SCOPES[auth.role]) !== null && _a !== void 0 ? _a : ROLE_SCOPES.MEMBER;
136
157
  if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(isScope))) {
137
158
  return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
@@ -18,6 +18,7 @@ const express_1 = require("express");
18
18
  const typedi_1 = require("typedi");
19
19
  const crypto_1 = __importDefault(require("crypto"));
20
20
  const UserService_1 = require("../../services/UserService");
21
+ const UserSessionService_1 = require("../../services/UserSessionService");
21
22
  const roleGuard_1 = require("../../middleware/roleGuard");
22
23
  const scopeGuard_1 = require("../../middleware/scopeGuard");
23
24
  const userAuthorization_1 = require("../../services/identity/userAuthorization");
@@ -130,7 +131,7 @@ function usersRouter() {
130
131
  });
131
132
  if (status === 'INACTIVE') {
132
133
  // updateUser sets status; we additionally revoke sessions.
133
- yield prisma_1.prisma.userSession.deleteMany({ where: { userId: targetId } });
134
+ yield typedi_1.Container.get(UserSessionService_1.UserSessionService).revokeAllForUser(targetId);
134
135
  }
135
136
  res.json({
136
137
  id: updated.id,
@@ -141,8 +142,11 @@ function usersRouter() {
141
142
  });
142
143
  }));
143
144
  // Admin-issued password reset. Replaces relaying a link out of the server
144
- // log: with SMTP the link is emailed to the user as before; without it the
145
- // link is returned once, to the admin who asked, and never logged.
145
+ // log: with SMTP and XENON_PUBLIC_URL the link is emailed to the user;
146
+ // otherwise it is returned once, to the admin who asked, and never logged.
147
+ // An emailed link points at XENON_PUBLIC_URL only, never the request's
148
+ // Host: the admin's request could name any host, and the user would get a
149
+ // genuine Xenon email sending their token there.
146
150
  r.post('/:id/reset-link', (req, res) => __awaiter(this, void 0, void 0, function* () {
147
151
  const auth = getAuth(req);
148
152
  const targetId = req.params.id;
@@ -168,10 +172,11 @@ function usersRouter() {
168
172
  const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
169
173
  const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
170
174
  const { raw } = yield resetSvc.createToken(target.id);
171
- const link = (0, passwordResetLink_1.buildResetLink)(req, raw);
175
+ const publicBase = (0, passwordResetLink_1.resetLinkBase)();
172
176
  const expiresAt = new Date(Date.now() + resetSvc.ttlMs()).toISOString();
173
- if (emailSvc.hasSmtp()) {
174
- yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link));
177
+ if (emailSvc.hasSmtp() && publicBase) {
178
+ const link = (0, passwordResetLink_1.buildResetLink)(publicBase, raw);
179
+ yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link, resetSvc.ttlMs()));
175
180
  auditLog.info(`${auth.userId} emailed a password-reset link to user ${target.id}`);
176
181
  return res.json({ emailed: true, expiresAt });
177
182
  }
@@ -179,6 +184,7 @@ function usersRouter() {
179
184
  auditLog.warn(`${auth.userId} issued a password-reset link for user ${target.id} (returned to the admin)`);
180
185
  // The body is a credential: keep it out of browser and proxy caches.
181
186
  res.set('Cache-Control', 'no-store');
187
+ const link = (0, passwordResetLink_1.buildResetLink)(publicBase !== null && publicBase !== void 0 ? publicBase : (0, passwordResetLink_1.requestBase)(req), raw);
182
188
  return res.json({ emailed: false, link, expiresAt });
183
189
  }));
184
190
  r.delete('/:id', (req, res) => __awaiter(this, void 0, void 0, function* () {
@@ -75,6 +75,8 @@ Appium's WebDriver API (\`POST /session\` and the session's commands, under Appi
75
75
  - \`accessKey\` and \`token\`;
76
76
  - \`sessionToken\`, minted by \`POST /api/auth/token\`.
77
77
 
78
+ Either names the session's owner only while that user exists and is active, and a session token needs the \`sessions\` scope, as a key does. A key or token that doesn't check out counts as none: with \`XENON_REQUIRE_SESSION_TOKEN\` the session is refused, without it the session runs with no owner.
79
+
78
80
  A leased device also takes \`leaseId\` and \`leaseToken\`, which the lease's \`appiumCapabilities\` already carry. Xenon removes these credentials before the driver or any stored record sees them.
79
81
 
80
82
  ## Live streams
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.STREAM_ENDED = void 0;
15
16
  exports.encodeWsFrame = encodeWsFrame;
16
17
  exports.parseH264WsPath = parseH264WsPath;
17
18
  exports.attachH264Ws = attachH264Ws;
@@ -29,6 +30,12 @@ const nodeSocketRelay_1 = require("./nodeSocketRelay");
29
30
  * so it is not put on the wire).
30
31
  */
31
32
  const TYPE_CODE = { config: 0, key: 1, delta: 2 };
33
+ /**
34
+ * The close code for a stream whose capture stopped under its viewer, as the
35
+ * logcat socket uses it for an upstream that died. The player then shows MJPEG
36
+ * without asking stream/start: there is no H.264 capture left to end.
37
+ */
38
+ exports.STREAM_ENDED = 1012;
32
39
  function encodeWsFrame(p) {
33
40
  return Buffer.concat([Buffer.from([TYPE_CODE[p.type]]), p.data]);
34
41
  }
@@ -139,7 +146,9 @@ function attachH264Ws(server, deps) {
139
146
  if (p.type === 'delta' && ws.bufferedAmount > maxBuffered)
140
147
  return;
141
148
  ws.send(encodeWsFrame(p));
142
- });
149
+ },
150
+ // The capture stopped under this viewer (a recording, a stream/stop).
151
+ () => ws.close(exports.STREAM_ENDED, 'stream ended'));
143
152
  logger_1.default.info(`[${parsed.udid}] H.264 WS client connected`);
144
153
  }));
145
154
  }