@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -56,6 +56,10 @@ const prisma_1 = require("../../prisma");
56
56
  const SessionManager_1 = require("../../sessions/SessionManager");
57
57
  const UniversalMjpegProxy_1 = require("../../helpers/UniversalMjpegProxy");
58
58
  const web_config_service_1 = require("../../data-service/web-config-service");
59
+ const PluginContext_1 = require("../../PluginContext");
60
+ const labSettings_1 = require("../../services/settings/labSettings");
61
+ const SelfHealingSwitch_1 = require("../../services/settings/SelfHealingSwitch");
62
+ const aiEngineSettings_1 = require("../../services/settings/aiEngineSettings");
59
63
  const typedi_1 = require("typedi");
60
64
  const scopeGuard_1 = require("../../middleware/scopeGuard");
61
65
  const roleGuard_1 = require("../../middleware/roleGuard");
@@ -1099,17 +1103,49 @@ function streamLiveSessionVideo(request, response) {
1099
1103
  }
1100
1104
  });
1101
1105
  }
1106
+ /**
1107
+ * The base URL as the dashboard may see it: a user name, password or query
1108
+ * value it carries is masked. Saving one is refused (aiEngineSettings.ts), but
1109
+ * the server's environment can still hold one, and every admin can read this.
1110
+ */
1111
+ function maskedBaseUrl(url) {
1112
+ if (!url)
1113
+ return url;
1114
+ let parsed;
1115
+ try {
1116
+ parsed = new URL(url);
1117
+ }
1118
+ catch (_a) {
1119
+ return url;
1120
+ }
1121
+ // Shown as given unless something needs masking: toString() would add a '/'.
1122
+ if (!parsed.username && !parsed.password && !parsed.search && !parsed.hash)
1123
+ return url;
1124
+ if (parsed.username || parsed.password) {
1125
+ parsed.username = '***';
1126
+ parsed.password = '';
1127
+ }
1128
+ for (const name of [...new Set(parsed.searchParams.keys())]) {
1129
+ parsed.searchParams.set(name, '***');
1130
+ }
1131
+ parsed.hash = '';
1132
+ return parsed.toString();
1133
+ }
1102
1134
  function getGlobalConfig(request, response) {
1103
1135
  return __awaiter(this, void 0, void 0, function* () {
1104
1136
  try {
1105
1137
  const dbConfig = yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig();
1106
- // Merge with Environment Config (AI Settings)
1138
+ // What the server runs with: a saved value, else the startup option, else
1139
+ // the default. The pages show this (a saved value alone left them to guess
1140
+ // at what a setting never saved was), and `defaults` for "restore defaults".
1141
+ const effective = (0, labSettings_1.effectiveSettings)(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs, dbConfig);
1142
+ // The AI engine settings in effect (aiEngineSettings.ts keeps `config` at
1143
+ // them). Never a key: only whether one is set.
1107
1144
  const { config } = yield Promise.resolve().then(() => __importStar(require('../../config')));
1108
- // Sanitize keys - return boolean existence only
1109
1145
  const aiConfig = {
1110
1146
  aiProvider: config.aiProvider,
1111
1147
  aiModel: config.aiModel,
1112
- aiBaseUrl: config.aiBaseUrl,
1148
+ aiBaseUrl: maskedBaseUrl(config.aiBaseUrl),
1113
1149
  geminiModel: config.geminiModel,
1114
1150
  openaiModel: config.openaiModel,
1115
1151
  anthropicModel: config.anthropicModel,
@@ -1118,7 +1154,9 @@ function getGlobalConfig(request, response) {
1118
1154
  openaiSet: !!config.openaiApiKey,
1119
1155
  anthropicSet: !!config.anthropicApiKey,
1120
1156
  };
1121
- return response.status(200).json(Object.assign(Object.assign({}, dbConfig), aiConfig));
1157
+ return response
1158
+ .status(200)
1159
+ .json(Object.assign(Object.assign(Object.assign(Object.assign({}, dbConfig), effective), { defaults: (0, labSettings_1.settingsDefaults)() }), aiConfig));
1122
1160
  }
1123
1161
  catch (err) {
1124
1162
  return response.status(500).json({ error: true, message: err.message });
@@ -1127,31 +1165,36 @@ function getGlobalConfig(request, response) {
1127
1165
  }
1128
1166
  function updateGlobalConfig(request, response) {
1129
1167
  return __awaiter(this, void 0, void 0, function* () {
1168
+ var _a;
1130
1169
  try {
1131
1170
  const payload = request.body;
1132
- // Handle Runtime AI Config Overrides (Memory only)
1133
- // Only pass defined values to avoid overwriting env vars (e.g. aiBaseUrl, ollamaModel) with undefined
1134
- const runtimeOverrides = {};
1135
- if (payload.aiProvider !== undefined)
1136
- runtimeOverrides.aiProvider = payload.aiProvider;
1137
- if (payload.aiModel !== undefined)
1138
- runtimeOverrides.aiModel = payload.aiModel;
1139
- if (payload.aiBaseUrl !== undefined)
1140
- runtimeOverrides.aiBaseUrl = payload.aiBaseUrl;
1141
- if (payload.geminiModel !== undefined)
1142
- runtimeOverrides.geminiModel = payload.geminiModel;
1143
- if (payload.openaiModel !== undefined)
1144
- runtimeOverrides.openaiModel = payload.openaiModel;
1145
- if (payload.anthropicModel !== undefined)
1146
- runtimeOverrides.anthropicModel = payload.anthropicModel;
1147
- if (payload.ollamaModel !== undefined)
1148
- runtimeOverrides.ollamaModel = payload.ollamaModel;
1149
- if (Object.keys(runtimeOverrides).length > 0) {
1150
- const { updateConfig } = yield Promise.resolve().then(() => __importStar(require('../../config')));
1151
- updateConfig(runtimeOverrides);
1171
+ // Before anything is saved: a value the cleanup job would act on, such as a
1172
+ // retention window of 0, is refused rather than stored, and so is an AI
1173
+ // provider or address that could not work, now that it outlives a restart.
1174
+ const problem = (_a = (0, labSettings_1.validateSettingsUpdate)(payload !== null && payload !== void 0 ? payload : {})) !== null && _a !== void 0 ? _a : (0, aiEngineSettings_1.validateAiEngineUpdate)(payload !== null && payload !== void 0 ? payload : {});
1175
+ if (problem) {
1176
+ return response
1177
+ .status(400)
1178
+ .json({ error: 'invalid_setting', field: problem.field, message: problem.message });
1179
+ }
1180
+ // Saved, then in force for the next AI call. Through 2.14 the AI fields
1181
+ // changed the running server only, and a restart undid them.
1182
+ const aiEngine = (0, aiEngineSettings_1.aiEngineUpdateOf)(payload !== null && payload !== void 0 ? payload : {});
1183
+ yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(Object.assign(Object.assign({}, payload), aiEngine));
1184
+ if (Object.keys(aiEngine).length > 0) {
1185
+ typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).noteSaved(aiEngine);
1186
+ }
1187
+ // The command interceptor asks at every command and never reads the
1188
+ // database: hand it the saved value, in force from the next command.
1189
+ if (payload.enableSelfHealing !== undefined) {
1190
+ typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).set(payload.enableSelfHealing);
1191
+ }
1192
+ // The retention values are read at each cleanup run, so they apply by
1193
+ // themselves. A schedule is a timer: replace it now, not at the next restart.
1194
+ if (payload.buildCleanupSchedule !== undefined) {
1195
+ const { rescheduleCleanupBuilds } = yield Promise.resolve().then(() => __importStar(require('../../device-utils')));
1196
+ yield rescheduleCleanupBuilds(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs);
1152
1197
  }
1153
- // Persist Web Configs to DB
1154
- yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(payload);
1155
1198
  return response.status(200).json({ success: true });
1156
1199
  }
1157
1200
  catch (err) {
@@ -89,10 +89,12 @@ function getDevices(request, response) {
89
89
  const [enriched] = yield (0, device_service_1.enrichDevicesWithTeamNames)([match]);
90
90
  return response.json(enriched);
91
91
  }
92
- /* dashboard-plugin-url is the base url for opening the appium-dashboard-plugin
93
- * This value will be attached to all express request via middleware
92
+ /* dashboard-plugin-url is where this server reaches the appium-dashboard-plugin,
93
+ * and dashboard-plugin-link the base of the link people open; both from
94
+ * configuration, never from the request (app/dashboardPluginLink.ts).
94
95
  */
95
96
  const dashboardPluginUrl = request['dashboard-plugin-url'];
97
+ const dashboardPluginLink = request['dashboard-plugin-link'];
96
98
  if (dashboardPluginUrl) {
97
99
  const response = yield InternalHttpClient_1.InternalHttpClient.get(`${dashboardPluginUrl}/api/sessions?start_time=${SERVER_UP_TIME}`);
98
100
  const sessions = ((_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.rows) || [];
@@ -105,7 +107,7 @@ function getDevices(request, response) {
105
107
  });
106
108
  devices = devices.map((d) => {
107
109
  var _a;
108
- d.dashboard_link = `${dashboardPluginUrl}?device_udid=${d.udid}&start_time=${SERVER_UP_TIME}`;
110
+ d.dashboard_link = `${dashboardPluginLink}?device_udid=${d.udid}&start_time=${SERVER_UP_TIME}`;
109
111
  d.total_session_count = ((_a = deviceSessionMap[d.udid]) === null || _a === void 0 ? void 0 : _a.length) || 0;
110
112
  return d;
111
113
  });
@@ -402,10 +404,33 @@ function updateTags(request, response) {
402
404
  if (!udid || !host || !Array.isArray(tags)) {
403
405
  return response.status(400).json({ error: 'Missing udid, host, or tags array' });
404
406
  }
407
+ // The tags are saved for the phone (deviceSettings.ts): only for one that
408
+ // is here. Through 2.13 any udid and host answered 200 and wrote nothing.
409
+ if (!(yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid, host }))) {
410
+ return response.status(404).json({ error: 'Device not found' });
411
+ }
405
412
  yield (0, device_service_1.updateDeviceTags)(udid, host, tags);
406
413
  response.status(200).json({ success: true });
407
414
  });
408
415
  }
416
+ /**
417
+ * Every phone of this udid that this server knows: those connected now, and
418
+ * those whose settings are saved while they are away (deviceSettings.ts), so
419
+ * a phone that is not connected can still be moved, and its team deleted.
420
+ */
421
+ function phonesOfUdid(udid) {
422
+ return __awaiter(this, void 0, void 0, function* () {
423
+ const store = device_store_1.DeviceStoreFactory.getStore();
424
+ const phones = new Map();
425
+ for (const phone of [
426
+ ...(yield store.findDevices({ udid })),
427
+ ...(yield store.findSavedPhones(udid)),
428
+ ]) {
429
+ phones.set(`${phone.udid}\u0000${phone.host}`, { udid: phone.udid, host: phone.host });
430
+ }
431
+ return [...phones.values()];
432
+ });
433
+ }
409
434
  function assignDeviceToTeam(request, response) {
410
435
  return __awaiter(this, void 0, void 0, function* () {
411
436
  const { udid } = request.params;
@@ -415,20 +440,19 @@ function assignDeviceToTeam(request, response) {
415
440
  if (!team)
416
441
  return response.status(404).json({ error: 'team not found' });
417
442
  }
418
- const result = yield prisma_1.prisma.device.updateMany({
419
- where: { udid },
420
- data: { teamId: teamId || null },
421
- });
422
- if (result.count === 0)
443
+ const phones = yield phonesOfUdid(udid);
444
+ if (phones.length === 0)
423
445
  return response.status(404).json({ error: 'device not found' });
424
- // Refresh in-memory store so subsequent allocation sees the change immediately.
425
- const rows = yield prisma_1.prisma.device.findMany({ where: { udid } });
426
- for (const row of rows) {
427
- yield store.updateDevice(row.udid, row.host, { teamId: row.teamId });
446
+ // Through the store, which saves the team for the phone as it writes the
447
+ // row: a phone keeps it when it disconnects and comes back.
448
+ for (const phone of phones) {
449
+ yield device_store_1.DeviceStoreFactory.getStore().updateDevice(phone.udid, phone.host, {
450
+ teamId: teamId || null,
451
+ });
428
452
  }
429
453
  // The live dashboard events follow the new team now, not after the cache's TTL.
430
454
  typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver).note(udid, teamId || null);
431
- response.json({ ok: true, updated: result.count });
455
+ response.json({ ok: true, updated: phones.length });
432
456
  });
433
457
  }
434
458
  /**
@@ -17,6 +17,7 @@ const ApiKeyService_1 = require("../../services/ApiKeyService");
17
17
  const UserService_1 = require("../../services/UserService");
18
18
  const prisma_1 = require("../../prisma");
19
19
  const profileIdentity_1 = require("./profileIdentity");
20
+ const mintedLifetime_1 = require("../../services/token/mintedLifetime");
20
21
  const ROLE_SCOPES = {
21
22
  SUPER_ADMIN: ['admin'],
22
23
  ADMIN: ['devices', 'sessions', 'read'],
@@ -79,10 +80,23 @@ function profileRouter() {
79
80
  return res.status(404).json({ error: 'user not found' });
80
81
  return res.json({ accessKey: user.accessKey });
81
82
  }));
83
+ // Rotating the access key ends every key pair its owner has, whatever their
84
+ // scopes, so it takes the person themselves (a dashboard sign-in) or a
85
+ // credential with the admin scope. Through 2.14 any credential could, a
86
+ // read-only token included, and cut off every one of its owner's clients.
82
87
  r.post('/access-key/rotate', (req, res) => __awaiter(this, void 0, void 0, function* () {
88
+ var _a;
83
89
  const auth = requireAuth(req, res);
84
90
  if (!auth)
85
91
  return;
92
+ const credentialScopes = String((_a = auth.scopes) !== null && _a !== void 0 ? _a : '')
93
+ .split(',')
94
+ .map((s) => s.trim());
95
+ if (auth.kind !== 'user-session' && !credentialScopes.includes('admin')) {
96
+ return res.status(403).json({
97
+ error: 'rotating the access key needs a dashboard sign-in or a credential with the admin scope',
98
+ });
99
+ }
86
100
  const userId = yield effectiveUserId(auth.userId);
87
101
  if (!userId)
88
102
  return res.status(404).json({ error: 'user not found' });
@@ -132,6 +146,13 @@ function profileRouter() {
132
146
  }
133
147
  expiresAtDate = d;
134
148
  }
149
+ // A token made with a credential that expires (a Bearer token, an API key
150
+ // with an expiry) ends no later than it. Through 2.14 a 1-hour Bearer
151
+ // token could make itself a key that never expired.
152
+ const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, auth.credentialExpiresAt);
153
+ if ('error' in within)
154
+ return res.status(400).json({ error: within.error });
155
+ expiresAtDate = within.expiresAt;
135
156
  const allowed = (_a = ROLE_SCOPES[auth.role]) !== null && _a !== void 0 ? _a : ROLE_SCOPES.MEMBER;
136
157
  if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(isScope))) {
137
158
  return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
@@ -9,10 +9,15 @@ exports.resolveStreamType = resolveStreamType;
9
9
  * MJPEG path so the recording pipeline (which reads the MJPEG server) is
10
10
  * unaffected (spec phase-1 rule). iOS/tvOS always use MJPEG (WDA emits it
11
11
  * directly).
12
+ *
13
+ * `clientCanPlayH264` is false when the page that starts the stream shows
14
+ * MJPEG: device control, or a browser without WebCodecs (exposed only on
15
+ * https and localhost). Starting an H.264 capture for it would run a second
16
+ * capture beside the screencap loop its `<img>` starts.
12
17
  */
13
- function resolveStreamType(platform, flagOn, recording) {
18
+ function resolveStreamType(platform, flagOn, recording, clientCanPlayH264 = true) {
14
19
  const isAndroid = platform === 'android' || platform === 'androidtv' || platform === 'android-tv';
15
- if (!isAndroid || !flagOn || recording)
20
+ if (!isAndroid || !flagOn || recording || !clientCanPlayH264)
16
21
  return 'mjpeg';
17
22
  return 'h264';
18
23
  }
@@ -141,8 +141,11 @@ function usersRouter() {
141
141
  });
142
142
  }));
143
143
  // Admin-issued password reset. Replaces relaying a link out of the server
144
- // log: with SMTP the link is emailed to the user as before; without it the
145
- // link is returned once, to the admin who asked, and never logged.
144
+ // log: with SMTP and XENON_PUBLIC_URL the link is emailed to the user;
145
+ // otherwise it is returned once, to the admin who asked, and never logged.
146
+ // An emailed link points at XENON_PUBLIC_URL only, never the request's
147
+ // Host: the admin's request could name any host, and the user would get a
148
+ // genuine Xenon email sending their token there.
146
149
  r.post('/:id/reset-link', (req, res) => __awaiter(this, void 0, void 0, function* () {
147
150
  const auth = getAuth(req);
148
151
  const targetId = req.params.id;
@@ -168,10 +171,11 @@ function usersRouter() {
168
171
  const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
169
172
  const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
170
173
  const { raw } = yield resetSvc.createToken(target.id);
171
- const link = (0, passwordResetLink_1.buildResetLink)(req, raw);
174
+ const publicBase = (0, passwordResetLink_1.resetLinkBase)();
172
175
  const expiresAt = new Date(Date.now() + resetSvc.ttlMs()).toISOString();
173
- if (emailSvc.hasSmtp()) {
174
- yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link));
176
+ if (emailSvc.hasSmtp() && publicBase) {
177
+ const link = (0, passwordResetLink_1.buildResetLink)(publicBase, raw);
178
+ yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link, resetSvc.ttlMs()));
175
179
  auditLog.info(`${auth.userId} emailed a password-reset link to user ${target.id}`);
176
180
  return res.json({ emailed: true, expiresAt });
177
181
  }
@@ -179,6 +183,7 @@ function usersRouter() {
179
183
  auditLog.warn(`${auth.userId} issued a password-reset link for user ${target.id} (returned to the admin)`);
180
184
  // The body is a credential: keep it out of browser and proxy caches.
181
185
  res.set('Cache-Control', 'no-store');
186
+ const link = (0, passwordResetLink_1.buildResetLink)(publicBase !== null && publicBase !== void 0 ? publicBase : (0, passwordResetLink_1.requestBase)(req), raw);
182
187
  return res.json({ emailed: false, link, expiresAt });
183
188
  }));
184
189
  r.delete('/:id', (req, res) => __awaiter(this, void 0, void 0, function* () {
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const express_1 = require("express");
16
16
  const NotificationService_1 = require("../../services/NotificationService");
17
+ const webhookEvents_1 = require("../../services/webhookEvents");
17
18
  const typedi_1 = require("typedi");
18
19
  const logger_1 = __importDefault(require("../../logger"));
19
20
  const scopeGuard_1 = require("../../middleware/scopeGuard");
@@ -63,17 +64,20 @@ function deleteConfig(req, res) {
63
64
  }
64
65
  });
65
66
  }
66
- // Sends a sample event the way the webhook's real events go out (its type
67
- // and template), and says whether it was delivered.
67
+ // Sends a sample of an event the way the webhook's real events go out (its
68
+ // type and template), and says whether it was delivered.
68
69
  function testWebhook(req, res) {
69
70
  return __awaiter(this, void 0, void 0, function* () {
70
71
  var _a, _b;
71
- const { url, type, payloadTemplate } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
72
+ const { url, type, payloadTemplate, event } = (_a = req.body) !== null && _a !== void 0 ? _a : {};
72
73
  if (!url || typeof url !== 'string') {
73
74
  return res.status(400).json({ error: 'url is required' });
74
75
  }
76
+ if (event !== undefined && !(0, webhookEvents_1.isEventType)(event)) {
77
+ return res.status(400).json({ error: `event must be one of: ${webhookEvents_1.EVENT_TYPES.join(', ')}` });
78
+ }
75
79
  try {
76
- yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate);
80
+ yield typedi_1.Container.get(NotificationService_1.NotificationService).sendTest(url, type || 'slack', payloadTemplate, event);
77
81
  res.json({ success: true });
78
82
  }
79
83
  catch (err) {
@@ -75,6 +75,8 @@ Appium's WebDriver API (\`POST /session\` and the session's commands, under Appi
75
75
  - \`accessKey\` and \`token\`;
76
76
  - \`sessionToken\`, minted by \`POST /api/auth/token\`.
77
77
 
78
+ Either names the session's owner only while that user exists and is active, and a session token needs the \`sessions\` scope, as a key does. A key or token that doesn't check out counts as none: with \`XENON_REQUIRE_SESSION_TOKEN\` the session is refused, without it the session runs with no owner.
79
+
78
80
  A leased device also takes \`leaseId\` and \`leaseToken\`, which the lease's \`appiumCapabilities\` already carry. Xenon removes these credentials before the driver or any stored record sees them.
79
81
 
80
82
  ## Live streams
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.STREAM_ENDED = void 0;
15
16
  exports.encodeWsFrame = encodeWsFrame;
16
17
  exports.parseH264WsPath = parseH264WsPath;
17
18
  exports.attachH264Ws = attachH264Ws;
@@ -29,6 +30,12 @@ const nodeSocketRelay_1 = require("./nodeSocketRelay");
29
30
  * so it is not put on the wire).
30
31
  */
31
32
  const TYPE_CODE = { config: 0, key: 1, delta: 2 };
33
+ /**
34
+ * The close code for a stream whose capture stopped under its viewer, as the
35
+ * logcat socket uses it for an upstream that died. The player then shows MJPEG
36
+ * without asking stream/start: there is no H.264 capture left to end.
37
+ */
38
+ exports.STREAM_ENDED = 1012;
32
39
  function encodeWsFrame(p) {
33
40
  return Buffer.concat([Buffer.from([TYPE_CODE[p.type]]), p.data]);
34
41
  }
@@ -139,7 +146,9 @@ function attachH264Ws(server, deps) {
139
146
  if (p.type === 'delta' && ws.bufferedAmount > maxBuffered)
140
147
  return;
141
148
  ws.send(encodeWsFrame(p));
142
- });
149
+ },
150
+ // The capture stopped under this viewer (a recording, a stream/stop).
151
+ () => ws.close(exports.STREAM_ENDED, 'stream ended'));
143
152
  logger_1.default.info(`[${parsed.udid}] H.264 WS client connected`);
144
153
  }));
145
154
  }
package/lib/src/config.js CHANGED
@@ -32,14 +32,20 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var _a;
36
35
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.config = void 0;
36
+ exports.config = exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = void 0;
38
37
  exports.updateConfig = updateConfig;
39
38
  exports.resolveAuthDisabled = resolveAuthDisabled;
39
+ exports.resolveMaxConcurrentRecordings = resolveMaxConcurrentRecordings;
40
+ exports.resolveRecordingsAssetsPath = resolveRecordingsAssetsPath;
41
+ exports.recordingConfigFrom = recordingConfigFrom;
40
42
  const os = __importStar(require("os"));
41
43
  const path = __importStar(require("path"));
42
44
  const basePath = path.join(os.homedir(), '.cache', 'xenon');
45
+ /** Where free-form recordings go unless the option or the environment says otherwise. */
46
+ const DEFAULT_RECORDINGS_ASSETS_PATH = path.join(basePath, 'assets', 'sessions', 'recordings');
47
+ /** The server-wide cap on simultaneous free-form recordings, unless set. */
48
+ exports.DEFAULT_MAX_CONCURRENT_RECORDINGS = 4;
43
49
  exports.config = {
44
50
  cacheDir: basePath,
45
51
  databaseProvider: process.env.XENON_DB_PROVIDER || 'sqlite',
@@ -81,15 +87,15 @@ exports.config = {
81
87
  userSessionTtlMs: Number(process.env.XENON_USER_SESSION_TTL_MS) || 24 * 60 * 60 * 1000,
82
88
  smtpUrl: process.env.XENON_SMTP_URL,
83
89
  smtpFrom: process.env.XENON_SMTP_FROM,
90
+ publicUrl: process.env.XENON_PUBLIC_URL,
84
91
  resetTokenTtlMs: Number(process.env.XENON_RESET_TOKEN_TTL_MS) || 60 * 60 * 1000,
85
92
  // Opt-in since 1.20.7: it writes the raw reset link (a credential) to the
86
93
  // server log. Without SMTP, admins issue links from the Users page instead.
87
94
  passwordResetLogFallback: process.env.XENON_PASSWORD_RESET_LOG_FALLBACK === 'true',
88
95
  resetRateLimitAttempts: Number(process.env.XENON_RESET_RATE_LIMIT_ATTEMPTS) || 3,
89
96
  resetRateLimitWindowMs: Number(process.env.XENON_RESET_RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
90
- recordingsAssetsPath: process.env.XENON_RECORDINGS_ASSETS_PATH ||
91
- path.join(basePath, 'assets', 'sessions', 'recordings'),
92
- maxConcurrentRecordings: Number((_a = process.env.XENON_MAX_CONCURRENT_RECORDINGS) !== null && _a !== void 0 ? _a : 4),
97
+ recordingsAssetsPath: resolveRecordingsAssetsPath(undefined, process.env.XENON_RECORDINGS_ASSETS_PATH),
98
+ maxConcurrentRecordings: resolveMaxConcurrentRecordings(undefined, process.env.XENON_MAX_CONCURRENT_RECORDINGS),
93
99
  };
94
100
  function updateConfig(newConfig) {
95
101
  Object.assign(exports.config, newConfig);
@@ -113,3 +119,43 @@ function updateConfig(newConfig) {
113
119
  function resolveAuthDisabled(pluginArgValue, envDisabled) {
114
120
  return pluginArgValue === true || envDisabled === true;
115
121
  }
122
+ function wholeNumberAtLeastOne(value) {
123
+ return typeof value === 'number' && Number.isInteger(value) && value >= 1 ? value : undefined;
124
+ }
125
+ function nonBlankString(value) {
126
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
127
+ }
128
+ /**
129
+ * The cap on simultaneous free-form recordings: the `maxConcurrentRecordings`
130
+ * plugin option when it is set, else XENON_MAX_CONCURRENT_RECORDINGS, else 4.
131
+ * Both options are `schema.json` options Appium accepts, but only the variable
132
+ * was ever read. A value that is not a whole number of at least 1 is ignored,
133
+ * so the next source answers: the variable used to be taken as `Number(env)`,
134
+ * and `many` made that NaN, which no recording count exceeds (no cap at all).
135
+ * Pure, so the precedence is testable without booting a server.
136
+ */
137
+ function resolveMaxConcurrentRecordings(optionValue, envValue) {
138
+ var _a, _b;
139
+ const fromEnv = (envValue === null || envValue === void 0 ? void 0 : envValue.trim()) ? Number(envValue) : undefined;
140
+ return ((_b = (_a = wholeNumberAtLeastOne(optionValue)) !== null && _a !== void 0 ? _a : wholeNumberAtLeastOne(fromEnv)) !== null && _b !== void 0 ? _b : exports.DEFAULT_MAX_CONCURRENT_RECORDINGS);
141
+ }
142
+ /**
143
+ * Where free-form recordings are stored: the `recordingsAssetsPath` plugin
144
+ * option when it is set, else XENON_RECORDINGS_ASSETS_PATH, else
145
+ * `~/.cache/xenon/assets/sessions/recordings`. A blank value counts as unset.
146
+ */
147
+ function resolveRecordingsAssetsPath(optionValue, envValue) {
148
+ var _a, _b;
149
+ return (_b = (_a = nonBlankString(optionValue)) !== null && _a !== void 0 ? _a : nonBlankString(envValue)) !== null && _b !== void 0 ? _b : DEFAULT_RECORDINGS_ASSETS_PATH;
150
+ }
151
+ /**
152
+ * The part of `config` the recording plugin options set, from the options the
153
+ * server started with and the environment as it is now. Applied at boot,
154
+ * before the artifact store and the concurrency gate read them.
155
+ */
156
+ function recordingConfigFrom(pluginArgs, env = process.env) {
157
+ return {
158
+ maxConcurrentRecordings: resolveMaxConcurrentRecordings(pluginArgs.maxConcurrentRecordings, env.XENON_MAX_CONCURRENT_RECORDINGS),
159
+ recordingsAssetsPath: resolveRecordingsAssetsPath(pluginArgs.recordingsAssetsPath, env.XENON_RECORDINGS_ASSETS_PATH),
160
+ };
161
+ }