@xenon-device-management/xenon 2.14.0 → 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 (259) hide show
  1. package/README.md +7 -4
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.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-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
  40. package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +5 -5
  77. package/lib/src/app/dashboardPluginLink.js +117 -0
  78. package/lib/src/app/index.js +13 -34
  79. package/lib/src/app/openapi/control.yaml +6 -4
  80. package/lib/src/app/openapi/grid.yaml +8 -2
  81. package/lib/src/app/openapi/identity.yaml +103 -31
  82. package/lib/src/app/openapi/platform.yaml +33 -15
  83. package/lib/src/app/openapi/sessions.yaml +10 -3
  84. package/lib/src/app/routers/apikeys.js +14 -2
  85. package/lib/src/app/routers/auth.js +29 -5
  86. package/lib/src/app/routers/control.js +13 -5
  87. package/lib/src/app/routers/dashboard.js +42 -27
  88. package/lib/src/app/routers/grid.js +5 -3
  89. package/lib/src/app/routers/profile.js +21 -0
  90. package/lib/src/app/routers/users.js +10 -5
  91. package/lib/src/app/swagger.js +2 -0
  92. package/lib/src/app/ws/h264StreamWs.js +10 -1
  93. package/lib/src/config.js +1 -0
  94. package/lib/src/dashboard/commands.js +5 -5
  95. package/lib/src/dashboard/event-manager.js +49 -61
  96. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  97. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  98. package/lib/src/data-service/device-store.js +14 -1
  99. package/lib/src/data-service/prisma-store.js +19 -7
  100. package/lib/src/data-service/web-config-service.js +7 -0
  101. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  102. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  103. package/lib/src/device-utils.js +20 -2
  104. package/lib/src/gateway/defaultGateway.js +1 -1
  105. package/lib/src/gateway/forwardToNode.js +2 -0
  106. package/lib/src/gateway/healReport.js +87 -0
  107. package/lib/src/gateway/sessionGateway.js +12 -2
  108. package/lib/src/generated/client/edge.js +4 -3
  109. package/lib/src/generated/client/index-browser.js +1 -0
  110. package/lib/src/generated/client/index.d.ts +32 -0
  111. package/lib/src/generated/client/index.js +4 -3
  112. package/lib/src/generated/client/package.json +1 -1
  113. package/lib/src/generated/client/schema.prisma +3 -0
  114. package/lib/src/generated/client/wasm.js +1 -0
  115. package/lib/src/interceptors/CommandInterceptor.js +243 -181
  116. package/lib/src/middleware/authMiddleware.js +15 -3
  117. package/lib/src/middleware/commandCaller.js +3 -3
  118. package/lib/src/middleware/verifyCredential.js +58 -1
  119. package/lib/src/plugin.js +8 -3
  120. package/lib/src/services/AICommandService.js +9 -2
  121. package/lib/src/services/AIService.js +192 -42
  122. package/lib/src/services/EmailService.js +29 -0
  123. package/lib/src/services/OrphanSweeper.js +2 -0
  124. package/lib/src/services/SelectorVerificationJob.js +15 -4
  125. package/lib/src/services/ServerManager.js +11 -1
  126. package/lib/src/services/SessionLifecycleService.js +86 -26
  127. package/lib/src/services/SocketServer.js +19 -3
  128. package/lib/src/services/bug-report/video-slice.js +4 -1
  129. package/lib/src/services/device-access/leaseOverride.js +7 -5
  130. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  131. package/lib/src/services/healing/HealEtalonService.js +3 -2
  132. package/lib/src/services/healing/HealingOrchestrator.js +55 -72
  133. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  134. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  135. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  136. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  137. package/lib/src/services/healing/resilioPath.js +274 -0
  138. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  139. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  140. package/lib/src/services/ocr/ocrData.js +118 -0
  141. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  142. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  143. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  144. package/lib/src/services/passwordResetLink.js +50 -9
  145. package/lib/src/services/passwordResetMode.js +3 -1
  146. package/lib/src/services/publicUrl.js +45 -0
  147. package/lib/src/services/session/sessionIdentity.js +5 -3
  148. package/lib/src/services/session/shutdownReason.js +26 -0
  149. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  150. package/lib/src/services/token/mintedLifetime.js +51 -0
  151. package/lib/src/sessions/SessionManager.js +8 -2
  152. package/lib/src/sessions/sessionMemory.js +25 -0
  153. package/lib/test/helpers/fake-ai-provider.js +188 -0
  154. package/lib/test/helpers/loki-stores.js +33 -0
  155. package/lib/test/helpers/page-sources.js +164 -0
  156. package/lib/test/helpers/scratch-database.js +57 -13
  157. package/lib/test/integration/audit-ingest.spec.js +10 -10
  158. package/lib/test/integration/auth-flow.spec.js +9 -14
  159. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  160. package/lib/test/integration/bug-report-route.spec.js +22 -2
  161. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  162. package/lib/test/integration/forgot-password.spec.js +17 -18
  163. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  164. package/lib/test/integration/migration.spec.js +6 -1
  165. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  166. package/lib/test/integration/ports-router.spec.js +4 -4
  167. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  168. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  169. package/lib/test/integration/profile-tokens.spec.js +10 -16
  170. package/lib/test/integration/recordings-router.spec.js +16 -16
  171. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  172. package/lib/test/integration/role-matrix.spec.js +4 -9
  173. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  174. package/lib/test/integration/sdk-version.spec.js +2 -2
  175. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  176. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  177. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  178. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  179. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  180. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  181. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  182. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  183. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  184. package/lib/test/integration/users-flow.spec.js +7 -12
  185. package/lib/test/integration/users-lockout.spec.js +5 -3
  186. package/lib/test/unit/EmailService.test.js +66 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  188. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  189. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  190. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  191. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  192. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  193. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  194. package/lib/test/unit/apikeys-router.spec.js +84 -0
  195. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  196. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  197. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  198. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  199. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  200. package/lib/test/unit/device-log-book.spec.js +180 -0
  201. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  202. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  203. package/lib/test/unit/failure-analysis.spec.js +182 -0
  204. package/lib/test/unit/failure-categories.spec.js +128 -0
  205. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  206. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  207. package/lib/test/unit/heal-report.spec.js +71 -0
  208. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  209. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  210. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  211. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
  212. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  213. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  214. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  215. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  216. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  217. package/lib/test/unit/ocr-offline.spec.js +213 -0
  218. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  219. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  220. package/lib/test/unit/omni-vision.spec.js +5 -7
  221. package/lib/test/unit/package-engines.spec.js +27 -0
  222. package/lib/test/unit/passwordResetMode.test.js +10 -2
  223. package/lib/test/unit/profile-router.test.js +122 -1
  224. package/lib/test/unit/queue-requester.spec.js +1 -1
  225. package/lib/test/unit/reset-link-address.spec.js +266 -0
  226. package/lib/test/unit/reset-link.test.js +10 -4
  227. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  228. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  229. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  230. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  231. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  232. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  233. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  234. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  235. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  236. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  237. package/lib/test/unit/session-device-logs.spec.js +347 -0
  238. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  239. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  240. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  241. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  242. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  243. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  244. package/lib/test/unit/verify-credential.spec.js +33 -0
  245. package/lib/test/unit/vision-assertion.spec.js +3 -2
  246. package/lib/test/unit/web-config-service.spec.js +19 -0
  247. package/lib/tsconfig.tsbuildinfo +1 -1
  248. package/package.json +5 -5
  249. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  250. package/prisma/schema.prisma +3 -0
  251. package/schema.json +5 -5
  252. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  253. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  254. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  255. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  256. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  257. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  258. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  259. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -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) {
@@ -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(', ')}` });
@@ -141,8 +141,11 @@ function usersRouter() {
141
141
  });
142
142
  }));
143
143
  // 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.
144
+ // log: with SMTP and XENON_PUBLIC_URL the link is emailed to the user;
145
+ // otherwise it is returned once, to the admin who asked, and never logged.
146
+ // An emailed link points at XENON_PUBLIC_URL only, never the request's
147
+ // Host: the admin's request could name any host, and the user would get a
148
+ // genuine Xenon email sending their token there.
146
149
  r.post('/:id/reset-link', (req, res) => __awaiter(this, void 0, void 0, function* () {
147
150
  const auth = getAuth(req);
148
151
  const targetId = req.params.id;
@@ -168,10 +171,11 @@ function usersRouter() {
168
171
  const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
169
172
  const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
170
173
  const { raw } = yield resetSvc.createToken(target.id);
171
- const link = (0, passwordResetLink_1.buildResetLink)(req, raw);
174
+ const publicBase = (0, passwordResetLink_1.resetLinkBase)();
172
175
  const expiresAt = new Date(Date.now() + resetSvc.ttlMs()).toISOString();
173
- if (emailSvc.hasSmtp()) {
174
- yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link));
176
+ if (emailSvc.hasSmtp() && publicBase) {
177
+ const link = (0, passwordResetLink_1.buildResetLink)(publicBase, raw);
178
+ yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link, resetSvc.ttlMs()));
175
179
  auditLog.info(`${auth.userId} emailed a password-reset link to user ${target.id}`);
176
180
  return res.json({ emailed: true, expiresAt });
177
181
  }
@@ -179,6 +183,7 @@ function usersRouter() {
179
183
  auditLog.warn(`${auth.userId} issued a password-reset link for user ${target.id} (returned to the admin)`);
180
184
  // The body is a credential: keep it out of browser and proxy caches.
181
185
  res.set('Cache-Control', 'no-store');
186
+ const link = (0, passwordResetLink_1.buildResetLink)(publicBase !== null && publicBase !== void 0 ? publicBase : (0, passwordResetLink_1.requestBase)(req), raw);
182
187
  return res.json({ emailed: false, link, expiresAt });
183
188
  }));
184
189
  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
  }
package/lib/src/config.js CHANGED
@@ -87,6 +87,7 @@ exports.config = {
87
87
  userSessionTtlMs: Number(process.env.XENON_USER_SESSION_TTL_MS) || 24 * 60 * 60 * 1000,
88
88
  smtpUrl: process.env.XENON_SMTP_URL,
89
89
  smtpFrom: process.env.XENON_SMTP_FROM,
90
+ publicUrl: process.env.XENON_PUBLIC_URL,
90
91
  resetTokenTtlMs: Number(process.env.XENON_RESET_TOKEN_TTL_MS) || 60 * 60 * 1000,
91
92
  // Opt-in since 1.20.7: it writes the raw reset link (a credential) to the
92
93
  // server log. Without SMTP, admins issue links from the Users page instead.
@@ -59,10 +59,10 @@ const xenonScripts_1 = require("../interceptors/xenonScripts");
59
59
  *
60
60
  * Each answers `{ recorded: true }` when it wrote, or `{ recorded: false,
61
61
  * message }` saying why nothing was written: no record of the session on this
62
- * server (its dashboard is off, or it is a node, which keeps none for the
63
- * hub's sessions), an argument it can't use, or a failed write. None of them
64
- * fails the test's command. They are bookkeeping, and a test shouldn't die
65
- * because the server it ran on keeps no dashboard. Through 2.13.2
62
+ * server (it runs without `enableDashboard`, or it is a node, which keeps
63
+ * none for the hub's sessions), an argument it can't use, or a failed write.
64
+ * None of them fails the test's command. They are bookkeeping, and a test
65
+ * shouldn't die because the server it ran on records no sessions. Through 2.13.2
66
66
  * setSessionName, setSessionStatus and debug failed the command there
67
67
  * (Prisma's P2025, a foreign-key error), and addTag and captureEvidence
68
68
  * answered `null` as if they had worked.
@@ -132,7 +132,7 @@ class DashboardCommands {
132
132
  });
133
133
  if (!row) {
134
134
  answer = notRecorded(`This server keeps no record of session ${sessionId}, so nothing was saved. ` +
135
- 'Sessions are recorded on a hub with its dashboard turned on.');
135
+ 'Sessions are recorded by a server started with enableDashboard, never by a node.');
136
136
  }
137
137
  else {
138
138
  switch (command) {