@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
@@ -50,6 +50,7 @@ const axios_1 = __importDefault(require("axios"));
50
50
  const http_1 = __importDefault(require("http"));
51
51
  const https_1 = __importDefault(require("https"));
52
52
  const logger_1 = __importDefault(require("./logger"));
53
+ const outboundProxy_1 = require("./helpers/outboundProxy");
53
54
  /**
54
55
  * InternalHttpClient - Centralized HTTP client for all internal communication.
55
56
  *
@@ -62,9 +63,10 @@ const logger_1 = __importDefault(require("./logger"));
62
63
  */
63
64
  class InternalHttpClient {
64
65
  constructor(tlsRejectUnauthorized, timeoutMs) {
66
+ this.rejectUnauthorized = InternalHttpClient.resolveRejectUnauthorized(tlsRejectUnauthorized);
65
67
  this.axiosInstance = axios_1.default.create({
66
68
  httpAgent: this.getHttpAgent(),
67
- httpsAgent: this.getHttpsAgent(tlsRejectUnauthorized),
69
+ httpsAgent: this.getHttpsAgent(),
68
70
  timeout: InternalHttpClient.resolveTimeoutMs(timeoutMs),
69
71
  maxContentLength: Infinity,
70
72
  maxBodyLength: Infinity,
@@ -92,14 +94,16 @@ class InternalHttpClient {
92
94
  keepAliveMsecs: 120000,
93
95
  });
94
96
  }
95
- getHttpsAgent(tlsRejectUnauthorized) {
96
- // Principal Decoupling: Prioritize constructor arg, fall back to env var
97
- const rejectUnauthorized = tlsRejectUnauthorized !== undefined
97
+ // Principal Decoupling: Prioritize constructor arg, fall back to env var
98
+ static resolveRejectUnauthorized(tlsRejectUnauthorized) {
99
+ return tlsRejectUnauthorized !== undefined
98
100
  ? tlsRejectUnauthorized
99
101
  : process.env.XENON_TLS_REJECT_UNAUTHORIZED !== 'false';
102
+ }
103
+ getHttpsAgent() {
100
104
  return new https_1.default.Agent({
101
105
  // Hardened TLS Security: rejectUnauthorized defaults to true (production-safe).
102
- rejectUnauthorized,
106
+ rejectUnauthorized: this.rejectUnauthorized,
103
107
  keepAlive: true,
104
108
  keepAliveMsecs: 120000,
105
109
  });
@@ -108,6 +112,15 @@ class InternalHttpClient {
108
112
  // Request interceptor - adds timing and logging
109
113
  this.axiosInstance.interceptors.request.use((config) => {
110
114
  var _a;
115
+ // The proxy, by Xenon's one rule (helpers/outboundProxy.ts): the
116
+ // `proxy` option, else the environment's. A request that set its own
117
+ // `proxy` keeps it.
118
+ if (config.proxy === undefined) {
119
+ const target = InternalHttpClient.targetOf(config);
120
+ if (target) {
121
+ Object.assign(config, (0, outboundProxy_1.axiosProxyConfig)(target, { rejectUnauthorized: this.rejectUnauthorized }));
122
+ }
123
+ }
111
124
  // Add request start time for duration calculation
112
125
  config.metadata = { startTime: Date.now() };
113
126
  // Log outgoing request
@@ -193,6 +206,15 @@ class InternalHttpClient {
193
206
  return this.axiosInstance(config);
194
207
  }));
195
208
  }
209
+ static targetOf(config) {
210
+ var _a;
211
+ try {
212
+ return new URL((_a = config.url) !== null && _a !== void 0 ? _a : '', config.baseURL);
213
+ }
214
+ catch (_b) {
215
+ return undefined;
216
+ }
217
+ }
196
218
  static classifyRetry(error) {
197
219
  const response = error.response;
198
220
  if (response) {
@@ -148,7 +148,12 @@ function iOSCapabilities(caps, freeDevice) {
148
148
  const streamService = typedi_1.Container.get(IOSStreamService);
149
149
  const streamStatus = streamService.getStreamStatus(freeDevice.udid);
150
150
  logger_1.default.info(`[Xenon] 🔍 Checking Stream Status for ${freeDevice.udid}: ${(streamStatus === null || streamStatus === void 0 ? void 0 : streamStatus.status) || 'None'}`);
151
- if (streamStatus && (streamStatus.status === 'running' || streamStatus.status === 'starting')) {
151
+ // An iPhone only. A simulator's WDA is the driver's: a stream entry for
152
+ // one is at most an attach to an earlier session's WDA (a preview left
153
+ // open), which this session's driver neither started nor can restart.
154
+ if (freeDevice.realDevice &&
155
+ streamStatus &&
156
+ (streamStatus.status === 'running' || streamStatus.status === 'starting')) {
152
157
  const wdaUrl = `http://127.0.0.1:${streamStatus.wdaPort}`;
153
158
  fm['appium:webDriverAgentUrl'] = wdaUrl;
154
159
  reusingStreamWda = true;
@@ -289,9 +294,11 @@ function getXenonCapabilities(caps) {
289
294
  // 3. Screenshot on Every Command
290
295
  const screenCapEvery = getAnyCap(XENON_CAPABILITIES.SCREENSHOT_ON_EVERY_COMMAND, XENON_CAPABILITIES.SCREENSHOT_EVERY_COMMAND);
291
296
  capabilities[XENON_CAPABILITIES.SCREENSHOT_ON_EVERY_COMMAND] = String(screenCapEvery) === 'true';
292
- // 4. Save Logs
297
+ // 4. Save Logs: the session's Device logs, a switch on by default like
298
+ // video. Through 2.15 it was read nowhere, and documented as off by default.
293
299
  const logsCap = getAnyCap(XENON_CAPABILITIES.SAVE_LOGS, XENON_CAPABILITIES.SAVE_DEVICE_LOGS);
294
- capabilities[XENON_CAPABILITIES.SAVE_DEVICE_LOGS] = String(logsCap) === 'true';
300
+ capabilities[XENON_CAPABILITIES.SAVE_DEVICE_LOGS] =
301
+ logsCap !== undefined ? String(logsCap) === 'true' : true;
295
302
  // 5. Build and Session Names
296
303
  capabilities[XENON_CAPABILITIES.BUILD_NAME] = getAnyCap(XENON_CAPABILITIES.BUILD_NAME, 'buildName');
297
304
  capabilities[XENON_CAPABILITIES.SESSION_NAME] = getAnyCap(XENON_CAPABILITIES.SESSION_NAME, 'sessionName');
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DASHBOARD_PING_TIMEOUT_MS = exports.DASHBOARD_RETRY_MAX_MS = void 0;
16
+ exports.ownServerOrigin = ownServerOrigin;
17
+ exports.dashboardPluginMiddlewareFor = dashboardPluginMiddlewareFor;
18
+ exports.dashboardPluginMiddleware = dashboardPluginMiddleware;
19
+ const InternalHttpClient_1 = require("../InternalHttpClient");
20
+ const logger_1 = __importDefault(require("../logger"));
21
+ const publicUrl_1 = require("../services/publicUrl");
22
+ /**
23
+ * Where the appium-dashboard-plugin is, when it runs on this Appium server
24
+ * (at `/dashboard`), for the device list: `request['dashboard-plugin-url']`
25
+ * is where Xenon itself fetches the plugin's sessions, and
26
+ * `request['dashboard-plugin-link']` the base of each device's
27
+ * `dashboard_link`, for people to open.
28
+ *
29
+ * Both come from configuration, never from a request. Through 2.14 they came
30
+ * from the Host header of the first /xenon/api request after startup, before
31
+ * the login: Xenon sent a GET to `<that host>/dashboard/api/ping`, kept
32
+ * `<that host>/dashboard` for the life of the process if the answer had
33
+ * `pong`, fetched `<that host>/dashboard/api/sessions` on every device list,
34
+ * and gave every user a link to that host. A forged Host made Xenon call a
35
+ * server of the sender's choosing and point everyone at it.
36
+ *
37
+ * - Xenon reaches the plugin on this server's own address and port
38
+ * (`ownServerOrigin`, a wildcard bind address on 127.0.0.1), over HTTPS when
39
+ * Appium serves it (`--ssl-certificate-path` and `--ssl-key-path`).
40
+ * - People get `<XENON_PUBLIC_URL>/dashboard`, or `/dashboard` on the server
41
+ * their browser already has open when XENON_PUBLIC_URL isn't set.
42
+ *
43
+ * The ping is sent once, with a short time limit, and no request waits for
44
+ * it: a request that arrives while it is out gets no link. A plain-HTTP ping
45
+ * to an HTTPS server got ECONNRESET and was retried three times, about 4 s,
46
+ * while every signed-in request waited on it. Once it succeeds it isn't sent
47
+ * again; after a failure it is retried with an exponential back-off (1 s
48
+ * doubling to 30 s), so a server without the plugin doesn't ping on every
49
+ * request.
50
+ */
51
+ exports.DASHBOARD_RETRY_MAX_MS = 30000;
52
+ /** How long the ping may take. It goes to this same server. */
53
+ exports.DASHBOARD_PING_TIMEOUT_MS = 2000;
54
+ /**
55
+ * A server's own origin from its bind address and port; a wildcard is reached
56
+ * on 127.0.0.1. `tls` when Appium serves HTTPS.
57
+ */
58
+ function ownServerOrigin(address, port, tls = false) {
59
+ const wildcard = !address || address === '0.0.0.0' || address === '::';
60
+ const host = wildcard ? '127.0.0.1' : address.includes(':') ? `[${address}]` : address;
61
+ return `${tls ? 'https' : 'http'}://${host}:${port}`;
62
+ }
63
+ /**
64
+ * The middleware for this server: its bind address, port and whether Appium
65
+ * serves HTTPS, from Appium's CLI arguments. The ping is sent once
66
+ * (`retry: false`) with DASHBOARD_PING_TIMEOUT_MS.
67
+ */
68
+ function dashboardPluginMiddlewareFor(ownServer) {
69
+ const config = {
70
+ silent: true,
71
+ retry: false,
72
+ timeout: exports.DASHBOARD_PING_TIMEOUT_MS,
73
+ };
74
+ return dashboardPluginMiddleware({
75
+ ownServerOrigin: ownServerOrigin(ownServer.address, ownServer.port, ownServer.tls),
76
+ get: (url) => InternalHttpClient_1.InternalHttpClient.get(url, config),
77
+ warn: (message) => logger_1.default.warn(message),
78
+ });
79
+ }
80
+ function dashboardPluginMiddleware(deps) {
81
+ var _a, _b;
82
+ const now = (_a = deps.now) !== null && _a !== void 0 ? _a : Date.now;
83
+ const publicBase = (_b = deps.publicBase) !== null && _b !== void 0 ? _b : (() => (0, publicUrl_1.publicServerBase)());
84
+ const pluginUrl = `${deps.ownServerOrigin}/dashboard`;
85
+ let present = false;
86
+ let pinging = false;
87
+ let nextRetryAt = 0;
88
+ let retryDelayMs = 1000;
89
+ const ping = () => __awaiter(this, void 0, void 0, function* () {
90
+ var _a;
91
+ try {
92
+ const response = yield deps.get(`${pluginUrl}/api/ping`);
93
+ if (response && response['pong'])
94
+ return true;
95
+ }
96
+ catch (err) {
97
+ (_a = deps.warn) === null || _a === void 0 ? void 0 : _a.call(deps, `[Xenon] Dashboard ping failed, retrying in ${retryDelayMs}ms: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
98
+ }
99
+ nextRetryAt = now() + retryDelayMs;
100
+ retryDelayMs = Math.min(retryDelayMs * 2, exports.DASHBOARD_RETRY_MAX_MS);
101
+ return false;
102
+ });
103
+ return (req, _res, next) => {
104
+ var _a;
105
+ if (!present && !pinging && now() >= nextRetryAt) {
106
+ // One ping at a time, and nobody waits for it.
107
+ pinging = true;
108
+ void ping().then((ok) => {
109
+ present = ok;
110
+ pinging = false;
111
+ });
112
+ }
113
+ req['dashboard-plugin-url'] = present ? pluginUrl : '';
114
+ req['dashboard-plugin-link'] = present ? `${(_a = publicBase()) !== null && _a !== void 0 ? _a : ''}/dashboard` : '';
115
+ next();
116
+ };
117
+ }
@@ -54,7 +54,6 @@ const pluginArgs_1 = require("../data-service/pluginArgs");
54
54
  const cors_1 = __importDefault(require("cors"));
55
55
  const async_lock_1 = __importDefault(require("async-lock"));
56
56
  const crypto_1 = __importDefault(require("crypto"));
57
- const InternalHttpClient_1 = require("../InternalHttpClient");
58
57
  const logger_1 = __importStar(require("../logger"));
59
58
  const sessionContext_1 = require("../logging/sessionContext");
60
59
  const dashboard_1 = __importDefault(require("./routers/dashboard"));
@@ -91,7 +90,7 @@ const PluginContext_1 = require("../PluginContext");
91
90
  const nodeWebDriverUrl_1 = require("../gateway/nodeWebDriverUrl");
92
91
  const nodeSessionStatus_1 = require("../gateway/nodeSessionStatus");
93
92
  const nodeSessionMetrics_1 = require("../gateway/nodeSessionMetrics");
94
- const dashboardPluginUrl = null;
93
+ const dashboardPluginLink_1 = require("./dashboardPluginLink");
95
94
  const ASYNC_LOCK = new async_lock_1.default();
96
95
  // An async handler's error reaches the error handlers, as on Express 5.
97
96
  (0, apiErrors_1.forwardAsyncErrors)();
@@ -148,37 +147,6 @@ apiRouter.use((req, res, next) => {
148
147
  res.setHeader('Expires', '0');
149
148
  next();
150
149
  });
151
- // Dashboard state cache - runs once and persists on success.
152
- // On failure we back off exponentially (1s → 2s → 4s → … capped at 30s)
153
- // so a down dashboard doesn't turn every API request into a fresh ping.
154
- let dashboardPluginPromise = null;
155
- let dashboardNextRetryAt = 0;
156
- let dashboardRetryDelayMs = 1000;
157
- const DASHBOARD_RETRY_MAX_MS = 30000;
158
- apiRouter.use((req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
159
- if (dashboardPluginPromise === null && Date.now() >= dashboardNextRetryAt) {
160
- dashboardPluginPromise = (() => __awaiter(void 0, void 0, void 0, function* () {
161
- const pingurl = `${req.protocol}://${req.get('host')}/dashboard/api/ping`;
162
- try {
163
- const response = yield InternalHttpClient_1.InternalHttpClient.get(pingurl, { silent: true });
164
- if (response && response['pong']) {
165
- dashboardRetryDelayMs = 1000;
166
- dashboardNextRetryAt = 0;
167
- return `${req.protocol}://${req.get('host')}/dashboard`;
168
- }
169
- }
170
- catch (err) {
171
- logger_1.default.warn(`[Xenon] Dashboard ping failed, retrying in ${dashboardRetryDelayMs}ms: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
172
- }
173
- dashboardNextRetryAt = Date.now() + dashboardRetryDelayMs;
174
- dashboardRetryDelayMs = Math.min(dashboardRetryDelayMs * 2, DASHBOARD_RETRY_MAX_MS);
175
- dashboardPluginPromise = null;
176
- return '';
177
- }))();
178
- }
179
- req['dashboard-plugin-url'] = dashboardPluginPromise ? yield dashboardPluginPromise : '';
180
- return next();
181
- }));
182
150
  const findPublicPath = () => {
183
151
  const rootDir = path_1.default.resolve(__dirname, '..', '..');
184
152
  const searchPaths = [
@@ -227,7 +195,13 @@ router.use('/api', apiRouter);
227
195
  // express.static mount here, outside the login, which also served every
228
196
  // recording and interceptor capture stored under the same folder.
229
197
  router.use(staticFilesRouter);
230
- function createRouter(pluginArgs) {
198
+ /**
199
+ * `ownServer` is this Appium server's bind address and port, and whether it
200
+ * serves HTTPS (from its CLI arguments). With it, the device list links to an appium-dashboard-plugin on
201
+ * this server (dashboardPluginLink.ts); without it, as in specs that build a
202
+ * router alone, there is none.
203
+ */
204
+ function createRouter(pluginArgs, ownServer) {
231
205
  // CSRF defense (runs before /health so even a hostile GET->POST confused-
232
206
  // deputy has no soft target). Pass-through for GETs, for header-authed
233
207
  // callers, and when authDisabled=true. Returns 403 for cookie-authed
@@ -254,6 +228,11 @@ function createRouter(pluginArgs) {
254
228
  // provisioned.
255
229
  apiRouter.use(authMiddleware_1.authMiddleware);
256
230
  apiRouter.use((0, rateLimitMiddleware_1.rateLimitMiddleware)());
231
+ // The appium-dashboard-plugin's address, for the device list, from this
232
+ // server's own address and XENON_PUBLIC_URL. Never from a request: it was
233
+ // built from the Host header of the first request, before the login.
234
+ if (ownServer)
235
+ apiRouter.use((0, dashboardPluginLink_1.dashboardPluginMiddlewareFor)(ownServer));
257
236
  // Authenticated auth endpoints: /me, /change-password, /dashboard-session
258
237
  apiRouter.use('/auth', (0, auth_1.authAuthedRouter)());
259
238
  // Admin: API key management
@@ -1965,10 +1965,12 @@ paths:
1965
1965
  its own rules; on success the hub also marks the phone busy and held by you at once. A cloud
1966
1966
  provider's phone answers `501`.
1967
1967
 
1968
- One capture runs per Android device. A page that shows MJPEG (a browser that cannot decode
1969
- H.264, which on plain http it cannot, or a page whose H.264 player failed) sends
1970
- `player: mjpeg`: the server then starts the MJPEG capture, and first ends an H.264 capture
1971
- already running for the device, so the two never run together.
1968
+ One capture per Android device where it can be. A page that shows MJPEG (a browser that
1969
+ cannot decode H.264, which on plain http it cannot, or a page whose H.264 player failed)
1970
+ sends `player: mjpeg`: the server then starts the MJPEG capture, and ends an H.264 capture
1971
+ running for the device once nobody plays it: first, if nobody does, else when its last
1972
+ viewer leaves. Another page may still be playing it, and keeps it until then. A node on
1973
+ 2.13 or older ignores `player`.
1972
1974
  requestBody:
1973
1975
  required: false
1974
1976
  content:
@@ -16,7 +16,11 @@ paths:
16
16
  `session_id` is that value, as an object, or an empty `200` body
17
17
  when there is none. When the Appium dashboard plugin is installed,
18
18
  each row in the list also gets `dashboard_link` and
19
- `total_session_count`.
19
+ `total_session_count`. Xenon reaches the plugin on this server's own
20
+ address and port, and the link is `<XENON_PUBLIC_URL>/dashboard?…`,
21
+ or `/dashboard?…` on the server the caller has open when
22
+ `XENON_PUBLIC_URL` isn't set. Through 2.14 both were built from the
23
+ `Host` header of the first request after startup, signed in or not.
20
24
 
21
25
  `GET /api/device` is the same endpoint.
22
26
  tags:
@@ -2065,7 +2069,9 @@ components:
2065
2069
  description: On a hub, the preview hold the node last reported (`manual_<userId>_<udid>`).
2066
2070
  dashboard_link:
2067
2071
  type: string
2068
- description: Only with the Appium dashboard plugin installed.
2072
+ description: >-
2073
+ Only with the Appium dashboard plugin installed: the plugin's page for this device,
2074
+ under `XENON_PUBLIC_URL`, or a path on this server when it isn't set.
2069
2075
  total_session_count:
2070
2076
  type: integer
2071
2077
  description: Only with the Appium dashboard plugin installed.
@@ -10,9 +10,10 @@ paths:
10
10
  Public. Tells the sign-in page how a user who forgot their password can get back in, before
11
11
  anyone is signed in.
12
12
 
13
- `passwordReset` is `email` when the server has SMTP configured (the forgot-password form
14
- really emails a link), otherwise `admin` (the user must ask an administrator, who issues a
15
- link with `POST /api/users/{id}/reset-link`). Nothing else about the mail setup is revealed.
13
+ `passwordReset` is `email` when the server has SMTP configured and `XENON_PUBLIC_URL` set to
14
+ its address (the forgot-password form really emails a link, which points there),
15
+ otherwise `admin` (the user must ask an administrator, who issues a link with
16
+ `POST /api/users/{id}/reset-link`). Nothing else about the mail setup is revealed.
16
17
  tags:
17
18
  - Authentication
18
19
  security: []
@@ -34,8 +35,8 @@ paths:
34
35
  Public. Checks the email (case-insensitive) and password of an `ACTIVE` user and, on success,
35
36
  sets the `xenon_dashboard_session` cookie (HttpOnly, `SameSite=Strict`, `Secure` when the
36
37
  request came over HTTPS or with `X-Forwarded-Proto: https`). The cookie lives for the
37
- server's user-session lifetime (24 hours by default, `XENON_USER_SESSION_TTL_MS`) and is
38
- renewed on every authenticated request. Updates the user's last sign-in time.
38
+ server's user-session lifetime (24 hours by default, `XENON_USER_SESSION_TTL_MS`, which may be
39
+ longer or shorter) and is renewed for that long on every authenticated request. Updates the user's last sign-in time.
39
40
 
40
41
  A wrong password, an unknown email and an inactive user all answer the same `401`.
41
42
 
@@ -116,8 +117,11 @@ paths:
116
117
  addresses have accounts. Then, in the background, if the email belongs to an `ACTIVE` user and
117
118
  the server can deliver mail (SMTP configured, or the opt-in log fallback
118
119
  `XENON_PASSWORD_RESET_LOG_FALLBACK=true`), it creates a single-use reset token (valid 1 hour by
119
- default) and sends a link to `/xenon/reset-password#<token>`. Without either, no token is created;
120
- `GET /api/auth/options` says `admin` in that case.
120
+ default, `XENON_RESET_TOKEN_TTL_MS`) and sends a link to
121
+ `<server address>/xenon/reset-password#<token>`. The server address is `XENON_PUBLIC_URL`
122
+ (such as `https://xenon.example.com`; a trailing `/xenon/` is dropped), never the request's
123
+ `Host`, which whoever asks chooses. Without a way to deliver mail, or without a usable
124
+ `XENON_PUBLIC_URL`, no token is created; `GET /api/auth/options` says `admin` then.
121
125
 
122
126
  **Rate limit.** 3 requests per 15 minutes per client IP by default, then `429` with
123
127
  `Retry-After`.
@@ -387,16 +391,28 @@ paths:
387
391
  (`XENON_MCP_TOKEN_TTL_SEC`). It carries granular `scopes` (by default `appium:use` and
388
392
  `xenon:devices:read`, as far as the caller's scopes allow) and, as a REST bearer token, only
389
393
  the flat scopes those need (`appium:use` gives `sessions`, `xenon:devices:lock` gives
390
- `devices`). The answer also has a `sessionToken` (audience `xenon-session`, same lifetime)
391
- to put in an Appium session's `xe:options.sessionToken`.
394
+ `devices`). When the grant includes `appium:use` the answer also has a `sessionToken`
395
+ (audience `xenon-session`, same lifetime) to put in an Appium session's
396
+ `xe:options.sessionToken`. It carries the `sessions` scope, and `admin` only when the
397
+ credential minting it has `admin` and the grant is the default one (no `scopes` asked for) or
398
+ a full-admin one. With `admin` a session may use a lease someone else made, and only while
399
+ its user is still an `ADMIN` (a `SUPER_ADMIN`'s session may, `admin` or not). A credential
400
+ without `sessions` or `admin`, a read-only one for instance, gets no `sessionToken`.
401
+
402
+ **Never longer than the credential.** A token minted with a Bearer token, or with an API key
403
+ that expires, ends no later than that credential: `expiresIn` is cut to what is left of it. A
404
+ credential with no time left answers `400`. A dashboard sign-in renews itself, so its tokens get
405
+ the full lifetime.
406
+
407
+ **Scopes follow the role.** A token's scopes are fixed when it is minted, but its `admin` scope
408
+ stops counting once its user is a `MEMBER`: the API and per-command auth read the token's scopes
409
+ without it.
392
410
 
393
411
  Granular scopes a credential may grant: `read` gives `xenon:devices:read` and
394
412
  `xenon:analytics:read`; `sessions` gives `appium:use` and `xenon:recordings`; `devices` gives
395
413
  `xenon:devices:read`, `xenon:devices:lock` and `xenon:recordings`; `admin` gives all of them.
396
414
 
397
- Cookie callers need a same-host `Origin` or `Referer`. So, as the server is written, does a
398
- caller using only `Authorization: Bearer`: the same-origin check exempts only requests with an
399
- `x-xenon-access-key` header.
415
+ Cookie callers need a same-host `Origin` or `Referer`.
400
416
  tags:
401
417
  - Authentication
402
418
  requestBody:
@@ -441,7 +457,9 @@ paths:
441
457
  - xenon:devices:read
442
458
  sessionToken: eyJhbGciOiJSUzI1NiIsImtpZCI6IjNmMmE5YzRlN2QxYiJ9.eyJzdWIiOiI3YzFlMmQ0YSIsImF1ZCI6Inhlbm9uLXNlc3Npb24ifQ.c2lnbmF0dXJl
443
459
  '400':
444
- description: An unsupported audience, `scopes` that isn't an array, or an unknown granular scope.
460
+ description: |
461
+ An unsupported audience, `scopes` that isn't an array, an unknown granular scope, or a
462
+ credential with no time left to give the token.
445
463
  content:
446
464
  application/json:
447
465
  schema:
@@ -451,6 +469,10 @@ paths:
451
469
  value:
452
470
  error: bad_request
453
471
  details: 'unsupported audience: xenon-node'
472
+ expired:
473
+ value:
474
+ error: bad_request
475
+ details: the credential used to mint this token has expired
454
476
  unknownScope:
455
477
  value:
456
478
  error: unknown_scope
@@ -584,8 +606,12 @@ paths:
584
606
  description: |
585
607
  Gives the caller a new access key. The old one stops working at once, for every one of their
586
608
  API tokens: scripts and CI using the key pair must be updated. The tokens themselves are kept.
587
- Any signed-in user may call it; with authentication disabled it rotates the first
588
- `SUPER_ADMIN`'s key. Cookie callers need a same-host `Origin` or `Referer`.
609
+
610
+ Since that cuts off every key pair the user has, whatever its scopes, it takes a dashboard
611
+ sign-in (the cookie) or a credential with the `admin` scope. Any other API token or Bearer
612
+ token, a `read`-only one for instance, gets `403`. Through 2.14 any credential could. With
613
+ authentication disabled it rotates the first `SUPER_ADMIN`'s key. Cookie callers need a
614
+ same-host `Origin` or `Referer`.
589
615
  tags:
590
616
  - Profile
591
617
  responses:
@@ -600,7 +626,13 @@ paths:
600
626
  '401':
601
627
  $ref: '#/components/responses/Unauthorized'
602
628
  '403':
603
- $ref: '#/components/responses/Forbidden'
629
+ description: A credential other than a dashboard sign-in, without the `admin` scope.
630
+ content:
631
+ application/json:
632
+ schema:
633
+ $ref: '#/components/schemas/Error'
634
+ example:
635
+ error: rotating the access key needs a dashboard sign-in or a credential with the admin scope
604
636
  '404':
605
637
  $ref: '#/components/responses/IdentityUserNotFound'
606
638
  '429':
@@ -653,6 +685,11 @@ paths:
653
685
  role was checked, so an admin's `read`-only key could mint a wider token. The token has the
654
686
  default rate limit (300 a minute) and no team binding.
655
687
 
688
+ Made with a credential that expires (a Bearer token, or an API key with an `expiresAt`), the
689
+ token expires no later than it: without `expiresAt` it gets that credential's, and a later
690
+ `expiresAt` answers `400`. Through 2.14 a 1-hour Bearer token could make a token that never
691
+ expired. From the dashboard, `expiresAt` may be left out for a token that never expires.
692
+
656
693
  Any signed-in user may call it. Cookie callers need a same-host `Origin` or `Referer`.
657
694
  tags:
658
695
  - Profile
@@ -681,8 +718,8 @@ paths:
681
718
  expiresAt: '2027-01-01T00:00:00.000Z'
682
719
  '400':
683
720
  description: >-
684
- `name` is missing, `expiresAt` is invalid or past, a scope is unknown, or a scope is
685
- beyond your role or the credential you are using.
721
+ `name` is missing, `expiresAt` is invalid, past or later than the credential you are
722
+ using, a scope is unknown, or a scope is beyond your role or the credential you are using.
686
723
  content:
687
724
  application/json:
688
725
  schema:
@@ -694,6 +731,11 @@ paths:
694
731
  expiry:
695
732
  value:
696
733
  error: expiresAt must be in the future
734
+ outlives:
735
+ value:
736
+ error: >-
737
+ expiresAt can be no later than the credential you are using, which expires at
738
+ 2026-10-04T13:00:00.000Z
697
739
  unknown:
698
740
  value:
699
741
  error: scopes must be some of read, sessions, devices, admin
@@ -872,7 +914,9 @@ paths:
872
914
  - You can't change your own role.
873
915
  - The last active `SUPER_ADMIN` can't be demoted or deactivated.
874
916
  - Setting `status` to `INACTIVE` signs the user out of every dashboard session. Their API
875
- tokens and bearer tokens stop working too, since an inactive user's credentials are refused.
917
+ tokens, bearer tokens and session tokens stop working too, since an inactive user's
918
+ credentials are refused: on this API at once, for a new Appium session at once, and for the
919
+ commands of a session already running within 30 seconds with `XENON_REQUIRE_COMMAND_AUTH`.
876
920
 
877
921
  Cookie callers need a same-host `Origin` or `Referer`.
878
922
  tags:
@@ -973,9 +1017,11 @@ paths:
973
1017
  Needs role `ADMIN` or above. An `ADMIN` may issue links only for `MEMBER` accounts. Creates a
974
1018
  single-use reset link for an `ACTIVE` user, valid 1 hour by default.
975
1019
 
976
- With SMTP configured the link is emailed to the user and the answer is `emailed: true`.
977
- Without it the link is returned once, in this answer only (`Cache-Control: no-store`), for the
978
- admin to pass on; it is never written to the server log. Your own password goes through
1020
+ With SMTP and `XENON_PUBLIC_URL` configured the link is emailed to the user and the answer is
1021
+ `emailed: true`; the emailed link points at `XENON_PUBLIC_URL`, never at the request's `Host`.
1022
+ Without either the link is returned once, in this answer only (`Cache-Control: no-store`), for
1023
+ the admin to pass on; it is never written to the server log. A returned link points at
1024
+ `XENON_PUBLIC_URL` when it is set, else at the address the admin reached the server on. Your own password goes through
979
1025
  `POST /api/auth/change-password` instead. Cookie callers need a same-host `Origin` or
980
1026
  `Referer`.
981
1027
  tags:
@@ -1322,10 +1368,20 @@ paths:
1322
1368
  summary: Create an API key
1323
1369
  description: |
1324
1370
  Creates an API token owned by the caller and returns its value **once** as `key`; it is used as
1325
- `x-xenon-token` with the caller's access key. Unlike `POST /api/profile/tokens`, it can bind the
1326
- key to a team (the key then sees that team's devices only, within its owner's teams), set its
1327
- rate limit and grant any scope, `admin` included. Needs role `ADMIN` or above and the `admin`
1328
- scope. Cookie callers need a same-host `Origin` or `Referer`.
1371
+ `x-xenon-token` with the caller's access key. Unlike `POST /api/profile/tokens`, it can name a
1372
+ team, set its rate limit and grant any scope, `admin` included. `scopes` must be some of `read`,
1373
+ `sessions`, `devices` and `admin`; any other string answers `400` (through 2.14 it was stored,
1374
+ and gave the key nothing). Needs role `ADMIN` or above and the `admin` scope. Cookie callers
1375
+ need a same-host `Origin` or `Referer`.
1376
+
1377
+ **The team doesn't narrow the key.** The key belongs to the caller, an administrator, and an
1378
+ administrator's credentials reach every team's devices, so the key does too whichever team it
1379
+ names. The team is recorded on the leases the key takes, and limits the key only if its owner
1380
+ is later made a `MEMBER`. A test session keeps to one team with `xe:options.team`.
1381
+
1382
+ Made with a credential that expires (a Bearer token, or an API key with an `expiresAt`), the
1383
+ key expires no later than it: without `expiresAt` it gets that credential's, and a later
1384
+ `expiresAt` answers `400`.
1329
1385
  tags:
1330
1386
  - API Keys
1331
1387
  requestBody:
@@ -1365,7 +1421,9 @@ paths:
1365
1421
  key: 5d2e8f1a9c4b7e3d6a0f2c8b1e5d9a4c7f3b6e0d2a8c5f1e9b4d7a3c6f0e2b8d
1366
1422
  expiresAt: '2027-04-01T00:00:00.000Z'
1367
1423
  '400':
1368
- description: '`name` or `scopes` is missing, or `expiresAt` is invalid or past.'
1424
+ description: >-
1425
+ `name` or `scopes` is missing, a scope is unknown, or `expiresAt` is invalid, past or later
1426
+ than the credential making the request.
1369
1427
  content:
1370
1428
  application/json:
1371
1429
  schema:
@@ -1374,6 +1432,9 @@ paths:
1374
1432
  missing:
1375
1433
  value:
1376
1434
  error: name and scopes required
1435
+ unknown:
1436
+ value:
1437
+ error: scopes must be some of read, sessions, devices, admin
1377
1438
  expiry:
1378
1439
  value:
1379
1440
  error: expiresAt must be an ISO-8601 datetime
@@ -1835,7 +1896,9 @@ components:
1835
1896
  type: string
1836
1897
  sessionToken:
1837
1898
  type: string
1838
- description: '`xenon-mcp` only: a session token for `xe:options.sessionToken`, with the same lifetime.'
1899
+ description: >-
1900
+ `xenon-mcp` only, and only when the grant includes `appium:use`: a session token for
1901
+ `xe:options.sessionToken`, with the same lifetime.
1839
1902
  IdentityAccessKey:
1840
1903
  type: object
1841
1904
  properties:
@@ -1881,7 +1944,10 @@ components:
1881
1944
  type: string
1882
1945
  format: date-time
1883
1946
  nullable: true
1884
- description: When the token stops working; must be in the future. Null or absent for never.
1947
+ description: >-
1948
+ When the token stops working; must be in the future, and no later than the credential
1949
+ making the request when that one expires. Null or absent for never, or, with a credential
1950
+ that expires, for when that credential does.
1885
1951
  IdentityCreatedToken:
1886
1952
  type: object
1887
1953
  properties:
@@ -2099,12 +2165,18 @@ components:
2099
2165
  teamId:
2100
2166
  type: string
2101
2167
  nullable: true
2102
- description: Bind the key to this team.
2168
+ description: >-
2169
+ A team to note on the key. The key belongs to the caller, an administrator, so like them it
2170
+ reaches every team's devices whichever team this names: the team is recorded on the leases
2171
+ the key takes, and limits the key only if its owner is later made a `MEMBER`.
2103
2172
  expiresAt:
2104
2173
  type: string
2105
2174
  format: date-time
2106
2175
  nullable: true
2107
- description: When the key stops working; must be in the future. Null or absent for never.
2176
+ description: >-
2177
+ When the key stops working; must be in the future, and no later than the credential
2178
+ making the request when that one expires. Null or absent for never, or, with a credential
2179
+ that expires, for when that credential does.
2108
2180
  IdentityProject:
2109
2181
  type: object
2110
2182
  properties: