@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
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const jose = __importStar(require("jose"));
51
+ const typedi_1 = require("typedi");
52
+ const fs_1 = __importDefault(require("fs"));
53
+ const os_1 = __importDefault(require("os"));
54
+ const path_1 = __importDefault(require("path"));
55
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
56
+ const auth_1 = require("../../src/app/routers/auth");
57
+ const container_registration_1 = require("../helpers/container-registration");
58
+ /**
59
+ * A token POST /auth/token mints never carries more than the credential
60
+ * minting it: not more scopes, and not a longer life.
61
+ *
62
+ * - The `sessionToken` beside a xenon-mcp token creates Appium sessions, so
63
+ * it is minted only when the grant includes `appium:use` (which needs the
64
+ * `sessions` scope), and carries the scopes a session is judged by. Any
65
+ * credential used to get one, a read-only key included.
66
+ * - A token minted with a Bearer token, or an API key that expires, ends no
67
+ * later than that credential. A 1-hour token used to mint a fresh 1-hour
68
+ * token, for ever.
69
+ */
70
+ describe('POST /auth/token mints nothing its creator does not have', () => {
71
+ let dir;
72
+ let restoreContainer;
73
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
74
+ restoreContainer = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService);
75
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-mint-limits-'));
76
+ const svc = new JwtKeyService_1.JwtKeyService();
77
+ yield svc.init(dir);
78
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, svc);
79
+ }));
80
+ afterEach(() => {
81
+ restoreContainer();
82
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
83
+ });
84
+ const base = { kind: 'api-key', userId: 'u1', role: 'MEMBER', teamId: null, rateLimit: 300 };
85
+ const auth = (extra) => (Object.assign(Object.assign({}, base), extra));
86
+ describe('the session token', () => {
87
+ it('is not minted for a read-only credential', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ const out = yield (0, auth_1.issueToken)(auth({ scopes: 'read' }), { audience: 'xenon-mcp' });
89
+ (0, chai_1.expect)(out.token).to.be.a('string');
90
+ (0, chai_1.expect)(out.sessionToken).to.equal(undefined);
91
+ }));
92
+ it('is not minted when the grant asked for leaves out appium:use', () => __awaiter(void 0, void 0, void 0, function* () {
93
+ const out = yield (0, auth_1.issueToken)(auth({ scopes: 'devices,sessions,read' }), {
94
+ audience: 'xenon-mcp',
95
+ scopes: ['xenon:devices:read'],
96
+ });
97
+ (0, chai_1.expect)(out.sessionToken).to.equal(undefined);
98
+ }));
99
+ it('carries `sessions`, and `admin` only when its creator has it', () => __awaiter(void 0, void 0, void 0, function* () {
100
+ const member = yield (0, auth_1.issueToken)(auth({ scopes: 'devices,sessions,read' }), {
101
+ audience: 'xenon-mcp',
102
+ });
103
+ (0, chai_1.expect)(jose.decodeJwt(String(member.sessionToken)).scopes).to.equal('sessions');
104
+ const dashboardAdmin = yield (0, auth_1.issueToken)(auth({ kind: 'user-session', role: 'ADMIN', scopes: 'admin,devices,sessions,read' }), { audience: 'xenon-mcp' });
105
+ (0, chai_1.expect)(jose.decodeJwt(String(dashboardAdmin.sessionToken)).scopes).to.equal('admin,sessions');
106
+ }));
107
+ });
108
+ // `admin` lets a session take over a lease someone else made. A token asked
109
+ // for with narrower MCP scopes doesn't get it from an admin key's breadth.
110
+ describe("the session token's admin scope follows the grant", () => {
111
+ const adminKey = () => auth({ scopes: 'admin' });
112
+ const scopesOf = (t) => jose.decodeJwt(String(t)).scopes;
113
+ it('is left out when narrower scopes are asked for', () => __awaiter(void 0, void 0, void 0, function* () {
114
+ const out = yield (0, auth_1.issueToken)(adminKey(), { audience: 'xenon-mcp', scopes: ['appium:use'] });
115
+ (0, chai_1.expect)(scopesOf(out.sessionToken)).to.equal('sessions');
116
+ }));
117
+ it('is given for the default grant, and for a full-admin one', () => __awaiter(void 0, void 0, void 0, function* () {
118
+ const byDefault = yield (0, auth_1.issueToken)(adminKey(), { audience: 'xenon-mcp' });
119
+ (0, chai_1.expect)(scopesOf(byDefault.sessionToken)).to.equal('admin,sessions');
120
+ const full = yield (0, auth_1.issueToken)(adminKey(), {
121
+ audience: 'xenon-mcp',
122
+ scopes: [
123
+ 'appium:use',
124
+ 'xenon:analytics:read',
125
+ 'xenon:devices:lock',
126
+ 'xenon:devices:read',
127
+ 'xenon:recordings',
128
+ ],
129
+ });
130
+ (0, chai_1.expect)(scopesOf(full.sessionToken)).to.equal('admin,sessions');
131
+ }));
132
+ });
133
+ describe('its lifetime', () => {
134
+ const now = () => Math.floor(Date.now() / 1000);
135
+ for (const audience of ['xenon-rest', 'xenon-mcp']) {
136
+ it(`a ${audience} token ends no later than the credential that minted it`, () => __awaiter(void 0, void 0, void 0, function* () {
137
+ const creatorExp = now() + 600;
138
+ const out = yield (0, auth_1.issueToken)(auth({ kind: 'bearer', scopes: 'sessions,read', credentialExpiresAt: creatorExp * 1000 }), { audience });
139
+ const p = jose.decodeJwt(out.token);
140
+ (0, chai_1.expect)(p.exp).to.be.at.most(creatorExp);
141
+ (0, chai_1.expect)(out.expiresIn).to.be.at.most(600);
142
+ (0, chai_1.expect)(out.expiresIn).to.be.at.least(598);
143
+ if (out.sessionToken) {
144
+ (0, chai_1.expect)(jose.decodeJwt(out.sessionToken).exp).to.be.at.most(creatorExp);
145
+ }
146
+ }));
147
+ }
148
+ it('keeps its own lifetime when the credential outlives it', () => __awaiter(void 0, void 0, void 0, function* () {
149
+ const out = yield (0, auth_1.issueToken)(auth({ scopes: 'sessions', credentialExpiresAt: Date.now() + 30 * 86400000 }), { audience: 'xenon-rest' });
150
+ (0, chai_1.expect)(out.expiresIn).to.equal(3600);
151
+ }));
152
+ it('is refused when the credential has already expired', () => __awaiter(void 0, void 0, void 0, function* () {
153
+ let caught;
154
+ try {
155
+ yield (0, auth_1.issueToken)(auth({ scopes: 'sessions', credentialExpiresAt: Date.now() - 1000 }), {
156
+ audience: 'xenon-rest',
157
+ });
158
+ }
159
+ catch (err) {
160
+ caught = err;
161
+ }
162
+ (0, chai_1.expect)(caught === null || caught === void 0 ? void 0 : caught.message).to.match(/expired/);
163
+ }));
164
+ });
165
+ });
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const typedi_1 = require("typedi");
52
+ const plugin_1 = require("../../src/plugin");
53
+ const DeviceService = __importStar(require("../../src/data-service/device-service"));
54
+ const device_store_1 = require("../../src/data-service/device-store");
55
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
56
+ const SessionMetricsService_1 = require("../../src/services/metrics/SessionMetricsService");
57
+ const PhoneNetworkRestore_1 = require("../../src/services/network/PhoneNetworkRestore");
58
+ const SocketServer_1 = require("../../src/services/SocketServer");
59
+ const MetricsService_1 = require("../../src/services/MetricsService");
60
+ const NotificationService_1 = require("../../src/services/NotificationService");
61
+ const AIService_1 = require("../../src/services/AIService");
62
+ const shutdownReason_1 = require("../../src/services/session/shutdownReason");
63
+ const prisma_1 = require("../../src/prisma");
64
+ const container_registration_1 = require("../helpers/container-registration");
65
+ const scratch_database_1 = require("../helpers/scratch-database");
66
+ /**
67
+ * A session Appium ends by itself reaches `XenonPlugin.onUnexpectedShutdown`
68
+ * with a cause. For an idle session that is Appium's new-command timeout.
69
+ * The plugin threw the cause away and recorded "Driver shut down
70
+ * unexpectedly", which no failure pattern matches, so an idle session on this
71
+ * server's own phones was filed UNKNOWN and its "Why it failed" card opened
72
+ * the general runbook rather than the Timeout one.
73
+ */
74
+ // What @appium/base-driver 10.2 passes when a session sits idle too long.
75
+ const IDLE = "New Command Timeout of 60 seconds expired. Try customizing the timeout using the 'newCommandTimeout' desired capability";
76
+ describe('the reason of a session Appium ends by itself', () => {
77
+ it("is Appium's cause: its new-command timeout", () => {
78
+ (0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(new Error(IDLE))).to.equal(IDLE);
79
+ });
80
+ it("is a driver's own account of giving up, which still reads as one", () => {
81
+ (0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(new Error('The driver was unexpectedly shut down!'))).to.equal('The driver was unexpectedly shut down!');
82
+ });
83
+ it('is the old fixed reason when there is no cause to tell', () => {
84
+ for (const cause of [undefined, null, new Error(''), new Error(' '), {}, 42]) {
85
+ (0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(cause), String(cause)).to.equal(shutdownReason_1.UNEXPECTED_SHUTDOWN_REASON);
86
+ }
87
+ // The umbrella's placeholder when a driver gives none.
88
+ (0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(new Error('Unknown error'))).to.equal(shutdownReason_1.UNEXPECTED_SHUTDOWN_REASON);
89
+ });
90
+ it('is the message alone, never a stack', () => {
91
+ const err = new Error(IDLE);
92
+ (0, chai_1.expect)((0, shutdownReason_1.unexpectedShutdownReason)(err)).not.to.contain(' at ');
93
+ });
94
+ });
95
+ describe('a session Appium ends for being idle, on a hub that records sessions', function () {
96
+ this.timeout(30000);
97
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
98
+ let restoreRegs;
99
+ let wasHub;
100
+ const plugin = {
101
+ pluginArgs: { enableDashboard: true },
102
+ xenonLog: { withSession: () => ({ info() { } }) },
103
+ };
104
+ const seed = (id) => scratch.db.session.create({
105
+ data: {
106
+ id,
107
+ status: 'running',
108
+ desired_capabilities: '{}',
109
+ session_capabilities: '{}',
110
+ node_id: 'n-1',
111
+ has_live_video: false,
112
+ device_udid: 'R5CT',
113
+ device_platform: 'android',
114
+ device_version: '14',
115
+ },
116
+ });
117
+ const shutDown = (id, cause) => plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: id, caps: { udid: 'R5CT' } }, cause);
118
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
119
+ restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService, SocketServer_1.SocketServer, MetricsService_1.MetricsService, NotificationService_1.NotificationService);
120
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreSession: sinon_1.default.stub().resolves() });
121
+ typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
122
+ typedi_1.Container.set(SocketServer_1.SocketServer, {
123
+ emitToDashboard: sinon_1.default.stub(),
124
+ emitToDashboardForDevices: sinon_1.default.stub().resolves(),
125
+ hasScopedDashboard: sinon_1.default.stub().returns(false),
126
+ });
127
+ typedi_1.Container.set(MetricsService_1.MetricsService, {
128
+ incrementSessionSuccess: sinon_1.default.stub(),
129
+ incrementSessionFailure: sinon_1.default.stub(),
130
+ });
131
+ typedi_1.Container.set(NotificationService_1.NotificationService, { notifySessionFailed: sinon_1.default.stub().resolves() });
132
+ sinon_1.default.stub(DeviceService, 'releaseSessionDevices').resolves();
133
+ sinon_1.default
134
+ .stub(device_store_1.DeviceStoreFactory, 'getStore')
135
+ .returns({ getDevices: () => __awaiter(this, void 0, void 0, function* () { return []; }), findDevices: () => __awaiter(this, void 0, void 0, function* () { return []; }) });
136
+ sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
137
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(false);
138
+ wasHub = plugin_1.XenonPlugin.IS_HUB;
139
+ plugin_1.XenonPlugin.IS_HUB = true;
140
+ yield scratch.db.session.deleteMany({});
141
+ }));
142
+ afterEach(() => {
143
+ plugin_1.XenonPlugin.IS_HUB = wasHub;
144
+ sinon_1.default.restore();
145
+ restoreRegs();
146
+ });
147
+ it("is failed with Appium's reason and filed TIMEOUT", () => __awaiter(this, void 0, void 0, function* () {
148
+ yield seed('s-idle');
149
+ yield shutDown('s-idle', new Error(IDLE));
150
+ const row = yield prisma_1.prisma.session.findUnique({ where: { id: 's-idle' } });
151
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.status).to.equal('failed');
152
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_reason).to.equal(IDLE);
153
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_category).to.equal('TIMEOUT');
154
+ }));
155
+ it('keeps the old reason, filed UNKNOWN, when Appium gives no cause', () => __awaiter(this, void 0, void 0, function* () {
156
+ yield seed('s-crash');
157
+ yield shutDown('s-crash', undefined);
158
+ const row = yield prisma_1.prisma.session.findUnique({ where: { id: 's-crash' } });
159
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.status).to.equal('failed');
160
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_reason).to.equal(shutdownReason_1.UNEXPECTED_SHUTDOWN_REASON);
161
+ (0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.failure_category).to.equal('UNKNOWN');
162
+ }));
163
+ });
@@ -24,6 +24,7 @@ const UserService_1 = require("../../src/services/UserService");
24
24
  const ApiKeyService_1 = require("../../src/services/ApiKeyService");
25
25
  const verifyCredential_1 = require("../../src/middleware/verifyCredential");
26
26
  const container_registration_1 = require("../helpers/container-registration");
27
+ const commandCaller_1 = require("../../src/middleware/commandCaller");
27
28
  /**
28
29
  * The credential checks authMiddleware applies to the header pair and to a
29
30
  * Bearer JWT, pulled out so the WebDriver per-command check verifies exactly
@@ -80,6 +81,38 @@ describe('verifyCredential', () => {
80
81
  const token = yield keys.sign({ sub: 'u1' }, { audience: 'xenon-rest', ttlSeconds: 60 });
81
82
  (0, chai_1.expect)(yield (0, verifyCredential_1.verifyBearerCredential)(token)).to.equal(null);
82
83
  }));
84
+ // A token's scopes are fixed when it is minted, for up to a day. An
85
+ // admin demoted to member since keeps no `admin` from it: REST
86
+ // (resolveActor, scopeGuard) and per-command auth's override read these.
87
+ describe("the scopes, by the user's role now", () => {
88
+ const mint = (scopes, audience = 'xenon-rest') => keys.sign({ sub: 'u1', scopes }, { audience, ttlSeconds: 60 });
89
+ const withRole = (role) => typedi_1.Container.get(UserService_1.UserService).findById.resolves(Object.assign(Object.assign({}, active), { role }));
90
+ it('drops admin from the token of a user who is now a member', () => __awaiter(void 0, void 0, void 0, function* () {
91
+ withRole('MEMBER');
92
+ for (const audience of ['xenon-rest', 'xenon-mcp']) {
93
+ const out = yield (0, verifyCredential_1.verifyBearerCredential)(yield mint('admin,devices,sessions,read', audience));
94
+ (0, chai_1.expect)(out === null || out === void 0 ? void 0 : out.payload.scopes, audience).to.equal('devices,sessions,read');
95
+ }
96
+ }));
97
+ it('keeps admin for an ADMIN or SUPER_ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
98
+ for (const role of ['ADMIN', 'SUPER_ADMIN']) {
99
+ withRole(role);
100
+ const out = yield (0, verifyCredential_1.verifyBearerCredential)(yield mint('admin,sessions'));
101
+ (0, chai_1.expect)(out === null || out === void 0 ? void 0 : out.payload.scopes, role).to.equal('admin,sessions');
102
+ }
103
+ }));
104
+ it('gives a demoted admin no override in per-command auth', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ withRole('MEMBER');
106
+ const verdict = yield new commandCaller_1.CommandCallerVerifier().verify({
107
+ kind: 'bearer',
108
+ token: yield mint('admin,sessions'),
109
+ });
110
+ (0, chai_1.expect)(verdict).to.deep.equal({
111
+ valid: true,
112
+ caller: { userId: 'u1', overrideAdmin: false },
113
+ });
114
+ }));
115
+ });
83
116
  it('throws when the signing key is not available, rather than calling the token invalid', () => __awaiter(void 0, void 0, void 0, function* () {
84
117
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, new JwtKeyService_1.JwtKeyService()); // never initialised
85
118
  const token = yield keys.sign({ sub: 'u1' }, { audience: 'xenon-rest', ttlSeconds: 60 });
@@ -0,0 +1,119 @@
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
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const VisionAssertionService_1 = require("../../src/services/omni-vision/VisionAssertionService");
19
+ const AIService_1 = require("../../src/services/AIService");
20
+ /**
21
+ * `xenon: assertVisualState` answered `{ result: true, message: 'Assertion
22
+ * placeholder' }` whenever the AI step gave it nothing usable: with no AI
23
+ * provider, when the provider failed, and usually even with one, because the
24
+ * condition was wrapped in a "find the centre coordinates, answer {x, y}"
25
+ * prompt. A test asserting a screen state passed without anything being
26
+ * checked.
27
+ *
28
+ * Now it asks the provider a yes/no question of the screenshot and answers
29
+ * with the provider's verdict. Whenever there is no verdict (no provider, a
30
+ * failed call, an answer that isn't true or false, no screenshot, no
31
+ * condition) it fails with an error the test sees, never `true` and never
32
+ * `false`: a `false` would pass a test that asserts a state is absent.
33
+ */
34
+ describe('xenon: assertVisualState', () => {
35
+ const SCREEN = 'c2NyZWVuc2hvdA==';
36
+ let service;
37
+ let driver;
38
+ beforeEach(() => {
39
+ service = new VisionAssertionService_1.VisionAssertionService();
40
+ driver = { getScreenshot: sinon_1.default.stub().resolves(SCREEN) };
41
+ // The real provider set-up reads the server's AI settings; these tests set the provider themselves.
42
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
43
+ });
44
+ afterEach(() => sinon_1.default.restore());
45
+ const failureOf = (p) => p.then((value) => {
46
+ throw new Error(`expected a failure, got ${JSON.stringify(value)}`);
47
+ }, (e) => e);
48
+ function withProvider(answer) {
49
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value({ analyze: () => __awaiter(this, void 0, void 0, function* () { return ''; }) });
50
+ const call = sinon_1.default.stub(AIService_1.AI_SERVICE, 'callProvider');
51
+ if (answer instanceof Error)
52
+ call.rejects(answer);
53
+ else
54
+ call.resolves(answer);
55
+ return call;
56
+ }
57
+ it('fails, rather than passing, when no AI provider is configured', () => __awaiter(void 0, void 0, void 0, function* () {
58
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value(null);
59
+ const err = yield failureOf(service.assertState(driver, 'The Login button is visible'));
60
+ (0, chai_1.expect)(err.message).to.match(/No AI provider is configured/);
61
+ (0, chai_1.expect)(err.message).to.match(/not checked/);
62
+ }));
63
+ it('asks a yes/no question about the condition, with the screenshot', () => __awaiter(void 0, void 0, void 0, function* () {
64
+ const call = withProvider('{"result": true, "reason": "A Login button is shown."}');
65
+ const answer = yield service.assertState(driver, 'The Login button is visible');
66
+ (0, chai_1.expect)(answer).to.deep.equal({ result: true, message: 'A Login button is shown.' });
67
+ (0, chai_1.expect)(call.calledOnce).to.equal(true);
68
+ const [prompt, image] = call.firstCall.args;
69
+ (0, chai_1.expect)(image).to.equal(SCREEN);
70
+ (0, chai_1.expect)(prompt).to.include('The Login button is visible');
71
+ (0, chai_1.expect)(prompt).to.match(/true or false/i);
72
+ (0, chai_1.expect)(prompt).to.not.match(/coordinates/i);
73
+ }));
74
+ it("answers false when the provider says the condition doesn't hold", () => __awaiter(void 0, void 0, void 0, function* () {
75
+ withProvider('```json\n{"result": false, "reason": "The cart shows 2 items."}\n```');
76
+ (0, chai_1.expect)(yield service.assertState(driver, 'The cart is empty')).to.deep.equal({
77
+ result: false,
78
+ message: 'The cart shows 2 items.',
79
+ });
80
+ }));
81
+ it("fails when the provider's answer is not a true/false verdict", () => __awaiter(void 0, void 0, void 0, function* () {
82
+ for (const answer of ['{"x": 120, "y": 340}', 'Yes, I can see it.', '{"result": "maybe"}']) {
83
+ sinon_1.default.restore();
84
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
85
+ driver = { getScreenshot: sinon_1.default.stub().resolves(SCREEN) };
86
+ withProvider(answer);
87
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
88
+ (0, chai_1.expect)(err.message, answer).to.match(/could not read/i);
89
+ }
90
+ }));
91
+ it('fails when the provider call fails', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ withProvider(new Error('429 quota exceeded'));
93
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
94
+ (0, chai_1.expect)(err.message).to.match(/429 quota exceeded/);
95
+ }));
96
+ it('fails when the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
97
+ // Any provider's 429 (ai-rate-limit.spec.ts).
98
+ withProvider(new AIService_1.AIRateLimitedError('429 Too Many Requests'));
99
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
100
+ (0, chai_1.expect)(err.message).to.match(/rate-limited/i);
101
+ (0, chai_1.expect)(err.message).to.match(/not checked/);
102
+ }));
103
+ it('fails without asking when there is no condition', () => __awaiter(void 0, void 0, void 0, function* () {
104
+ const call = withProvider('{"result": true, "reason": "ok"}');
105
+ for (const empty of ['', ' ', undefined]) {
106
+ const err = yield failureOf(service.assertState(driver, empty));
107
+ (0, chai_1.expect)(err.message).to.match(/needs a condition/);
108
+ }
109
+ (0, chai_1.expect)(call.called).to.equal(false);
110
+ (0, chai_1.expect)(driver.getScreenshot.called).to.equal(false);
111
+ }));
112
+ it('fails when no screenshot can be taken', () => __awaiter(void 0, void 0, void 0, function* () {
113
+ const call = withProvider('{"result": true, "reason": "ok"}');
114
+ driver.getScreenshot.rejects(new Error('WDA is not running'));
115
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
116
+ (0, chai_1.expect)(err.message).to.match(/WDA is not running/);
117
+ (0, chai_1.expect)(call.called).to.equal(false);
118
+ }));
119
+ });
@@ -0,0 +1,133 @@
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
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const os_1 = __importDefault(require("os"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const sinon_1 = __importDefault(require("sinon"));
21
+ const typedi_1 = require("typedi");
22
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
23
+ const IOSTunnels_1 = require("../../src/device-managers/ios/IOSTunnels");
24
+ const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
25
+ const device_store_1 = require("../../src/data-service/device-store");
26
+ /**
27
+ * The Shell tab on iOS: simctl commands and a few generic ones on simulators,
28
+ * go-ios commands on real iPhones. Through 2.13.2 a real iPhone's command could
29
+ * carry its own `--udid` (another phone on the Mac), `list` named every device
30
+ * attached to the Mac, simulator commands took any path on this Mac, and the
31
+ * go-ios call ran without the phone's tunnel, so iOS 17+ phones never answered.
32
+ *
33
+ * `xcrun` and `ios` here are scripts that print the arguments they were given.
34
+ */
35
+ const SIMULATOR = 'TEST-SIM-0000-AAAA';
36
+ const IPHONE = '00008110-00084CE80E51401E';
37
+ const ECHO = `#!/bin/sh
38
+ printf '%s|' "$@"
39
+ printf 'port=%s' "$GO_IOS_AGENT_PORT"
40
+ `;
41
+ describe('WDAClient.executeShell', () => {
42
+ let dir;
43
+ let savedPath;
44
+ let realDevice;
45
+ const borrowed = [];
46
+ before(() => {
47
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-fake-shell-'));
48
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'xcrun'), ECHO, { mode: 0o755 });
49
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'ios'), ECHO, { mode: 0o755 });
50
+ });
51
+ after(() => fs_1.default.rmSync(dir, { recursive: true, force: true }));
52
+ beforeEach(() => {
53
+ savedPath = process.env.PATH;
54
+ process.env.PATH = `${dir}:${process.env.PATH}`;
55
+ borrowed.length = 0;
56
+ const stream = { isGoIOSAvailable: () => __awaiter(void 0, void 0, void 0, function* () { return true; }), goIOSPath: path_1.default.join(dir, 'ios') };
57
+ const tunnels = {
58
+ borrow(udid) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ borrowed.push(udid);
61
+ return 12100;
62
+ });
63
+ },
64
+ envFor: () => (Object.assign(Object.assign({}, process.env), { GO_IOS_AGENT_PORT: '12100' })),
65
+ };
66
+ const real = typedi_1.Container.get.bind(typedi_1.Container);
67
+ sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
68
+ if (token === IOSStreamService_1.default)
69
+ return stream;
70
+ if (token === IOSTunnels_1.IOSTunnels)
71
+ return tunnels;
72
+ return real(token);
73
+ });
74
+ sinon_1.default
75
+ .stub(device_store_1.DeviceStoreFactory, 'getStore')
76
+ .returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ realDevice }); }) });
77
+ });
78
+ afterEach(() => {
79
+ process.env.PATH = savedPath;
80
+ sinon_1.default.restore();
81
+ });
82
+ const run = (udid, command) => new WDAClient_1.WDAClient().executeShell(udid, command).then((out) => ({ out }), (err) => ({ err: err.message }));
83
+ describe('on a simulator', () => {
84
+ beforeEach(() => {
85
+ realDevice = false;
86
+ });
87
+ it('runs a simctl command for this simulator', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ const { out } = (yield run(SIMULATOR, 'getenv HOME'));
89
+ (0, chai_1.expect)(out).to.match(new RegExp(`^simctl\\|getenv\\|${SIMULATOR}\\|HOME\\|`));
90
+ }));
91
+ it('spawns a generic command in this simulator, without arguments', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const { out } = (yield run(SIMULATOR, 'ls'));
93
+ (0, chai_1.expect)(out).to.match(new RegExp(`^simctl\\|spawn\\|${SIMULATOR}\\|ls\\|`));
94
+ (0, chai_1.expect)(yield run(SIMULATOR, 'ls /Users'))
95
+ .to.have.property('err')
96
+ .that.match(/not allowed/);
97
+ }));
98
+ it('refuses list, chaining and look-alikes', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ for (const cmd of ['list', 'ls; id', 'lsof', 'getenv $HOME']) {
100
+ (0, chai_1.expect)(yield run(SIMULATOR, cmd), cmd)
101
+ .to.have.property('err')
102
+ .that.match(/not allowed/);
103
+ }
104
+ }));
105
+ });
106
+ describe('on a real iPhone', () => {
107
+ beforeEach(() => {
108
+ realDevice = true;
109
+ });
110
+ it('runs a go-ios command for this phone, through its tunnel', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ const { out } = (yield run(IPHONE, 'apps --list'));
112
+ (0, chai_1.expect)(out).to.equal(`apps|--list|--udid|${IPHONE}|port=12100`);
113
+ (0, chai_1.expect)(borrowed).to.deep.equal([IPHONE]);
114
+ }));
115
+ it('refuses a command that names another device, and list', () => __awaiter(void 0, void 0, void 0, function* () {
116
+ for (const cmd of [
117
+ 'apps --udid 00008101-OTHER',
118
+ 'apps --udid=00008101-OTHER',
119
+ 'list',
120
+ 'info --json',
121
+ ]) {
122
+ (0, chai_1.expect)(yield run(IPHONE, cmd), cmd)
123
+ .to.have.property('err')
124
+ .that.match(/not allowed/);
125
+ }
126
+ }));
127
+ it('says a generic command needs a simulator', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ (0, chai_1.expect)(yield run(IPHONE, 'ls'))
129
+ .to.have.property('err')
130
+ .that.match(/real/i);
131
+ }));
132
+ });
133
+ });