@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
@@ -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:
@@ -188,9 +188,11 @@ paths:
188
188
  summary: Read the lab's settings and AI provider
189
189
  description: |
190
190
  The lab-wide settings in effect (health checks, build cleanup and the AI self-healing
191
- switch), merged with the AI settings currently in effect. Each setting is the value saved
192
- in the dashboard, else the plugin option the server was started with, else the default,
193
- which is also what the server runs with. `defaults` holds the defaults themselves, which
191
+ switch), merged with the AI settings in effect (provider, models and base URL). Each
192
+ setting is the value saved in the dashboard, else the plugin option or environment
193
+ variable the server was started with, else the default, which is also what the server
194
+ runs with. A model or base URL set nowhere is absent: the provider's own applies. A user
195
+ name, password or query value in the base URL is sent as `***`. `defaults` holds the defaults themselves, which
194
196
  is what "restore defaults" goes back to. Only `healthCheckSchedule` can be absent: no
195
197
  schedule is set. Through 2.13 `enableSelfHealing` was neither stored nor sent, so the
196
198
  Settings page always showed it on.
@@ -266,10 +268,18 @@ paths:
266
268
  switch was accepted and ignored. The value belongs to the server it is saved on:
267
269
  saving it on a hub changes the hub's own sessions, not those on a node's phones,
268
270
  which the node's own setting decides.
269
- - AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) change the
270
- running server only. They are not stored, so a restart returns to the values the
271
- server was started with. API keys can't be set here; they come from the server's
272
- environment.
271
+ - AI fields (`aiProvider`, `aiModel`, `aiBaseUrl` and the per-provider models) are stored
272
+ too. A stored value replaces the plugin option or environment variable the server was
273
+ started with, and applies to the next AI call, with no restart. An empty value removes
274
+ the stored one, and the server's own applies again. They are checked first and refused
275
+ with `400`: `aiProvider` must be `gemini`, `openai`, `anthropic` or `ollama`; a model
276
+ name has no spaces and at most 200 characters; `aiBaseUrl` is an http or https address
277
+ with no user name, password or query. Through 2.14 they changed the running server
278
+ only, and a restart undid them. Like the self-healing switch, they belong to the server
279
+ they are saved on.
280
+ - API keys can't be set here, and are never stored: they come from the server's
281
+ environment. `aiTemperature`, `aiMaxTokens` and `aiTopP`, which the AI engine page sent
282
+ through 2.14, were never used by any AI call and are ignored like any unknown field.
273
283
 
274
284
  Needs the `SUPER_ADMIN` role and the `admin` scope; an `ADMIN` gets `403` with a message
275
285
  saying so (through 2.12 an `ADMIN` could). With the dashboard cookie the request must
@@ -299,7 +309,7 @@ paths:
299
309
  example:
300
310
  success: true
301
311
  '400':
302
- description: A build-cleanup field or the self-healing switch has a value that could not work. Nothing was saved.
312
+ description: A build-cleanup field, the self-healing switch or an AI field has a value that could not work. Nothing was saved.
303
313
  content:
304
314
  application/json:
305
315
  schema:
@@ -2387,6 +2397,7 @@ components:
2387
2397
  type: boolean
2388
2398
  aiProvider:
2389
2399
  type: string
2400
+ description: The provider in effect, saved on the AI engine page, else the server's option or environment, else gemini.
2390
2401
  example: gemini
2391
2402
  aiModel:
2392
2403
  type: string
@@ -2438,30 +2449,37 @@ components:
2438
2449
  type: boolean
2439
2450
  aiProvider:
2440
2451
  type: string
2441
- description: Not stored; until the server restarts.
2452
+ description: The provider every AI call uses. Stored; empty goes back to the server's own.
2442
2453
  enum:
2443
2454
  - gemini
2444
2455
  - openai
2445
2456
  - anthropic
2446
2457
  - ollama
2458
+ - ''
2447
2459
  aiModel:
2448
2460
  type: string
2449
- description: Not stored; until the server restarts.
2461
+ description: The model for a provider with no model of its own below. Stored; empty goes back to the server's own.
2462
+ maxLength: 200
2450
2463
  aiBaseUrl:
2451
2464
  type: string
2452
- description: Not stored; until the server restarts.
2465
+ description: Where OpenAI-compatible and Ollama calls go. An http or https address with no user name, password or query. Stored; empty goes back to the server's own.
2466
+ example: http://gpu-box.lab:11434
2453
2467
  geminiModel:
2454
2468
  type: string
2455
- description: Not stored; until the server restarts.
2469
+ description: Stored; empty goes back to the server's own.
2470
+ maxLength: 200
2456
2471
  openaiModel:
2457
2472
  type: string
2458
- description: Not stored; until the server restarts.
2473
+ description: Stored; empty goes back to the server's own.
2474
+ maxLength: 200
2459
2475
  anthropicModel:
2460
2476
  type: string
2461
- description: Not stored; until the server restarts.
2477
+ description: Stored; empty goes back to the server's own.
2478
+ maxLength: 200
2462
2479
  ollamaModel:
2463
2480
  type: string
2464
- description: Not stored; until the server restarts.
2481
+ description: Stored; empty goes back to the server's own.
2482
+ maxLength: 200
2465
2483
  PlatformAiTestRequest:
2466
2484
  type: object
2467
2485
  description: Every field is optional and falls back to the server's current setting.