@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,126 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ require("reflect-metadata");
13
+ const chai_1 = require("chai");
14
+ const AIService_1 = require("../../src/services/AIService");
15
+ const CircuitBreaker_1 = require("../../src/services/CircuitBreaker");
16
+ const fake_ai_provider_1 = require("../helpers/fake-ai-provider");
17
+ /**
18
+ * A rate-limited provider (HTTP 429) is a failed call, whichever provider it
19
+ * is.
20
+ *
21
+ * Gemini's provider answered a 429 with the string
22
+ * 'CONNECTION_OK_RATE_LIMITED' instead of failing, so a failed session's AI
23
+ * analysis was saved as that string and shown on the session page, in the
24
+ * copied report and in bug reports. The circuit breaker counted it as a
25
+ * success, so it never stopped calling a provider that kept refusing. Only the
26
+ * visual assertion and the screen description checked for the string.
27
+ */
28
+ describe('AI provider rate limits', () => {
29
+ const ai = (0, fake_ai_provider_1.useFakeAiProviders)();
30
+ const FAILURE = {
31
+ sessionId: 'sess-rate-limit',
32
+ failureReason: 'An element could not be located on the page',
33
+ commandLogs: [],
34
+ deviceLogs: [],
35
+ };
36
+ const SCREEN = 'c2NyZWVuc2hvdA==';
37
+ const failureOf = (p) => p.then((value) => {
38
+ throw new Error(`expected a failure, got ${JSON.stringify(value)}`);
39
+ }, (e) => e);
40
+ for (const provider of fake_ai_provider_1.PROVIDERS) {
41
+ describe(provider, () => {
42
+ let service;
43
+ let model;
44
+ beforeEach(() => {
45
+ ({ model } = ai.use(provider));
46
+ ai.answer({ status: 429 });
47
+ service = new AIService_1.AIService();
48
+ });
49
+ it('gives no failure analysis', () => __awaiter(void 0, void 0, void 0, function* () {
50
+ (0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal(null);
51
+ (0, chai_1.expect)(ai.calls).to.have.length(1);
52
+ }));
53
+ it('counts against the circuit breaker, which stops calling after five', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ for (let i = 0; i < 6; i++)
55
+ yield service.analyzeFailure(FAILURE);
56
+ (0, chai_1.expect)(ai.calls).to.have.length(5);
57
+ const breaker = CircuitBreaker_1.CIRCUIT_BREAKERS.snapshot().find((b) => b.key === `ai:${provider}:${model}`);
58
+ (0, chai_1.expect)(breaker === null || breaker === void 0 ? void 0 : breaker.state).to.equal('open');
59
+ }));
60
+ it('fails a visual assertion, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
61
+ const err = yield failureOf(service.assertVisual(SCREEN, 'The cart is empty'));
62
+ (0, chai_1.expect)(err.message).to.match(/rate-limited/);
63
+ (0, chai_1.expect)(err.message).to.match(/not checked/);
64
+ }));
65
+ it('gives no screen description, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
66
+ const err = yield failureOf(service.describeScreen(SCREEN));
67
+ (0, chai_1.expect)(err.message).to.match(/rate-limited/);
68
+ }));
69
+ it('fails a locator test, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ const err = yield failureOf(service.visualFind(SCREEN, 'the cart icon', { throwOnError: true }));
71
+ (0, chai_1.expect)(err.message).to.match(/rate-limited/);
72
+ }));
73
+ it('finds nothing for a healing or an ai-icon search', () => __awaiter(void 0, void 0, void 0, function* () {
74
+ (0, chai_1.expect)(yield service.visualFind(SCREEN, 'the cart icon')).to.equal(null);
75
+ (0, chai_1.expect)(yield service.healLocator({ selector: '//x', strategy: 'xpath', xml: '<hierarchy/>' })).to.equal(null);
76
+ }));
77
+ it('says in plain words, once the breaker has opened, that it is waiting', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ for (let i = 0; i < 5; i++)
79
+ yield failureOf(service.describeScreen(SCREEN));
80
+ // The sixth isn't sent. Testers see this (Omni-Scan, Test locator).
81
+ const described = yield failureOf(service.describeScreen(SCREEN));
82
+ (0, chai_1.expect)(described.message).to.match(/has been failing/);
83
+ (0, chai_1.expect)(described.message).to.match(/again in \d+ s/);
84
+ (0, chai_1.expect)(described.message).not.to.match(/circuit|ai:/i);
85
+ const asserted = yield failureOf(service.assertVisual(SCREEN, 'The cart is empty'));
86
+ (0, chai_1.expect)(asserted.message).to.match(/has been failing/);
87
+ (0, chai_1.expect)(asserted.message).to.match(/not checked/);
88
+ (0, chai_1.expect)(asserted.message).not.to.match(/circuit|ai:/i);
89
+ (0, chai_1.expect)(ai.calls).to.have.length(5);
90
+ }));
91
+ it('fails the connection test, saying the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const result = yield service.testConnection({});
93
+ (0, chai_1.expect)(result.success).to.equal(false);
94
+ (0, chai_1.expect)(result.message).to.match(/rate-limited or out of quota/);
95
+ }));
96
+ });
97
+ }
98
+ it('gemini: an error that only mentions 429 is not a rate limit', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ ai.use('gemini');
100
+ ai.answer({
101
+ status: 400,
102
+ message: 'The input token count (1429384) exceeds the maximum number of tokens allowed.',
103
+ });
104
+ const service = new AIService_1.AIService();
105
+ const err = yield failureOf(service.describeScreen(SCREEN));
106
+ (0, chai_1.expect)(err.message).not.to.match(/rate-limited/);
107
+ // A 400 is the request's fault: the breaker doesn't count it, so all six are sent.
108
+ for (let i = 0; i < 5; i++)
109
+ yield service.analyzeFailure(FAILURE);
110
+ (0, chai_1.expect)(ai.calls).to.have.length(6);
111
+ }));
112
+ it('ollama: is asked again as soon as it is back', () => __awaiter(void 0, void 0, void 0, function* () {
113
+ ai.use('ollama');
114
+ ai.ollamaUp(false);
115
+ const service = new AIService_1.AIService();
116
+ (0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal(null);
117
+ ai.ollamaUp(true);
118
+ ai.answer({ text: 'Root Cause: the app crashed.' });
119
+ (0, chai_1.expect)(yield service.analyzeFailure(FAILURE)).to.equal('Root Cause: the app crashed.');
120
+ }));
121
+ it("still gives a provider's real answer", () => __awaiter(void 0, void 0, void 0, function* () {
122
+ ai.use('gemini');
123
+ ai.answer({ text: 'Root Cause: the Login button was covered by a dialog.' });
124
+ (0, chai_1.expect)(yield new AIService_1.AIService().analyzeFailure(FAILURE)).to.equal('Root Cause: the Login button was covered by a dialog.');
125
+ }));
126
+ });
@@ -0,0 +1,272 @@
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 http_1 = __importDefault(require("http"));
17
+ const chai_1 = require("chai");
18
+ const typedi_1 = require("typedi");
19
+ const ws_1 = require("ws");
20
+ const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
21
+ const h264StreamWs_1 = require("../../src/app/ws/h264StreamWs");
22
+ const PluginContext_1 = require("../../src/PluginContext");
23
+ const container_registration_1 = require("../helpers/container-registration");
24
+ const loopbackServer_1 = require("../helpers/loopbackServer");
25
+ /**
26
+ * Which viewers an H.264 capture outlives once a page has asked for MJPEG.
27
+ *
28
+ * A page whose H.264 player fails (a device control page, or a Live devices
29
+ * tile) asks for MJPEG (`stream/start` with `player: 'mjpeg'`). The phone may
30
+ * have other viewers still playing its H.264 preview: another tile, another
31
+ * tab, another admin. Ending the capture under them froze their picture on
32
+ * its last frame. So the capture ends when nobody watches it: at once if
33
+ * nobody does, else the moment the last one leaves, and never after the
34
+ * ten-minute idle wait an unwatched H.264 capture otherwise gets.
35
+ */
36
+ const UDID = 'R58M123';
37
+ const key = (n) => ({ type: 'key', data: Buffer.from([n]), ptsMs: n });
38
+ // The capture seam (openCapture) is stubbed, as in android-h264-service.spec.ts.
39
+ function make() {
40
+ const svc = Object.create(AndroidH264StreamService_1.default.prototype);
41
+ svc.sessions = new Map();
42
+ svc.startPromises = new Map();
43
+ svc.scrcpyIncompatible = new Set();
44
+ return svc;
45
+ }
46
+ /** A service whose next capture is under the test's control. */
47
+ function withCapture() {
48
+ const svc = make();
49
+ const capture = {
50
+ kills: 0,
51
+ push: () => undefined,
52
+ };
53
+ let release = () => undefined;
54
+ let held = false;
55
+ svc.openCapture = (_udid, onPacket) => __awaiter(this, void 0, void 0, function* () {
56
+ capture.push = onPacket;
57
+ if (held)
58
+ yield new Promise((r) => (release = r));
59
+ onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
60
+ return {
61
+ kill: () => {
62
+ capture.kills += 1;
63
+ },
64
+ };
65
+ });
66
+ return {
67
+ svc,
68
+ capture,
69
+ /** Keep the next capture starting until `finishStart()`. */
70
+ holdStart: () => (held = true),
71
+ finishStart: () => release(),
72
+ };
73
+ }
74
+ /** A viewer's socket: what it received, and how it leaves. */
75
+ function watch(mux) {
76
+ const got = [];
77
+ const leave = mux.addClient((p) => got.push(p.type));
78
+ return { got, leave };
79
+ }
80
+ /** Waits for `done()`, a few milliseconds at a time: a socket's close lands on the server later. */
81
+ function until(done, what) {
82
+ return __awaiter(this, void 0, void 0, function* () {
83
+ const by = Date.now() + 2000;
84
+ while (!done()) {
85
+ if (Date.now() > by)
86
+ throw new Error(`timed out waiting until ${what}`);
87
+ yield new Promise((r) => setTimeout(r, 5));
88
+ }
89
+ });
90
+ }
91
+ describe('AndroidH264StreamService: a page that asks for MJPEG', () => {
92
+ const loopback = (0, loopbackServer_1.loopbackServers)();
93
+ let restoreContainer;
94
+ beforeEach(() => {
95
+ restoreContainer = (0, container_registration_1.saveRegistrations)(PluginContext_1.PluginContext);
96
+ typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: {} });
97
+ });
98
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
99
+ restoreContainer();
100
+ yield loopback.closeAll();
101
+ }));
102
+ it('ends the H.264 capture at once when nobody watches it', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ const { svc, capture } = withCapture();
104
+ yield svc.start(UDID);
105
+ yield svc.endWhenUnwatched(UDID);
106
+ (0, chai_1.expect)(capture.kills).to.equal(1);
107
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
108
+ }));
109
+ it('keeps it for a viewer still playing it', () => __awaiter(void 0, void 0, void 0, function* () {
110
+ const { svc, capture } = withCapture();
111
+ const viewer = watch(yield svc.start(UDID));
112
+ yield svc.endWhenUnwatched(UDID);
113
+ (0, chai_1.expect)(capture.kills).to.equal(0);
114
+ capture.push(key(1));
115
+ (0, chai_1.expect)(viewer.got).to.deep.equal(['config', 'key']);
116
+ }));
117
+ it('ends it the moment its last viewer leaves, not after the idle wait', () => __awaiter(void 0, void 0, void 0, function* () {
118
+ const { svc, capture } = withCapture();
119
+ const mux = yield svc.start(UDID);
120
+ const a = watch(mux);
121
+ const b = watch(mux);
122
+ yield svc.endWhenUnwatched(UDID);
123
+ a.leave();
124
+ (0, chai_1.expect)(capture.kills, 'b still plays it').to.equal(0);
125
+ b.leave();
126
+ (0, chai_1.expect)(capture.kills).to.equal(1);
127
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
128
+ }));
129
+ it('serves a viewer who joins before then, and ends when they leave too', () => __awaiter(void 0, void 0, void 0, function* () {
130
+ const { svc, capture } = withCapture();
131
+ const a = watch(yield svc.start(UDID));
132
+ yield svc.endWhenUnwatched(UDID);
133
+ const late = watch(yield svc.start(UDID));
134
+ a.leave();
135
+ (0, chai_1.expect)(capture.kills).to.equal(0);
136
+ capture.push(key(2));
137
+ (0, chai_1.expect)(late.got).to.include('key');
138
+ late.leave();
139
+ (0, chai_1.expect)(capture.kills).to.equal(1);
140
+ }));
141
+ it('does nothing when no H.264 capture runs for the phone', () => __awaiter(void 0, void 0, void 0, function* () {
142
+ const { svc, capture } = withCapture();
143
+ yield svc.endWhenUnwatched(UDID);
144
+ (0, chai_1.expect)(capture.kills).to.equal(0);
145
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
146
+ }));
147
+ it('leaves a capture that is still starting to the viewer starting it', () => __awaiter(void 0, void 0, void 0, function* () {
148
+ // A tile's socket starts the capture it is about to watch: ending it
149
+ // mid-start would leave that capture running with nothing to stop it.
150
+ const { svc, capture, holdStart, finishStart } = withCapture();
151
+ holdStart();
152
+ const starting = svc.start(UDID);
153
+ yield Promise.resolve();
154
+ yield svc.endWhenUnwatched(UDID);
155
+ finishStart();
156
+ const viewer = watch(yield starting);
157
+ (0, chai_1.expect)(capture.kills).to.equal(0);
158
+ viewer.leave();
159
+ (0, chai_1.expect)(capture.kills).to.equal(1);
160
+ }));
161
+ it('ends a capture whose viewer never came at the watchdog’s next look', () => __awaiter(void 0, void 0, void 0, function* () {
162
+ const { svc, capture, holdStart, finishStart } = withCapture();
163
+ holdStart();
164
+ const starting = svc.start(UDID);
165
+ yield Promise.resolve();
166
+ yield svc.endWhenUnwatched(UDID);
167
+ finishStart();
168
+ yield starting;
169
+ svc.sweep(Date.now());
170
+ (0, chai_1.expect)(capture.kills).to.equal(1);
171
+ }));
172
+ it('keeps the idle wait for a capture no page asked to end', () => __awaiter(void 0, void 0, void 0, function* () {
173
+ const { svc, capture } = withCapture();
174
+ yield svc.start(UDID);
175
+ svc.sweep(Date.now());
176
+ svc.sweep(Date.now() + 60000);
177
+ (0, chai_1.expect)(capture.kills).to.equal(0);
178
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.not.equal(undefined);
179
+ }));
180
+ it('does not end a newer capture when an older one’s last viewer leaves', () => __awaiter(void 0, void 0, void 0, function* () {
181
+ const { svc, capture } = withCapture();
182
+ const old = watch(yield svc.start(UDID));
183
+ yield svc.endWhenUnwatched(UDID);
184
+ yield svc.stop(UDID); // a recording took the phone, say
185
+ (0, chai_1.expect)(capture.kills).to.equal(1);
186
+ yield svc.start(UDID);
187
+ old.leave();
188
+ (0, chai_1.expect)(capture.kills).to.equal(1);
189
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.not.equal(undefined);
190
+ }));
191
+ it('ends it when the last viewer’s H.264 socket closes', () => __awaiter(void 0, void 0, void 0, function* () {
192
+ // The socket route (h264StreamWs.ts), as a tile's player opens and closes it.
193
+ const { svc, capture } = withCapture();
194
+ const server = yield loopback.serve(http_1.default.createServer());
195
+ (0, h264StreamWs_1.attachH264Ws)(server, {
196
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'usr_alice' }); }),
197
+ startStream: (udid) => svc.start(udid),
198
+ });
199
+ const { port } = server.address();
200
+ // The config packet is sent the moment the socket joins the multiplexer.
201
+ const viewer = () => new Promise((resolve, reject) => {
202
+ const ws = new ws_1.WebSocket(`ws://127.0.0.1:${port}/xenon/api/control/${UDID}/stream/h264?ticket=t`);
203
+ ws.once('message', () => resolve(ws));
204
+ ws.once('error', reject);
205
+ });
206
+ const tile = yield viewer();
207
+ const otherTab = yield viewer();
208
+ const viewers = () => { var _a, _b; return (_b = (_a = svc.getMultiplexer(UDID)) === null || _a === void 0 ? void 0 : _a.clientCount) !== null && _b !== void 0 ? _b : 0; };
209
+ yield svc.endWhenUnwatched(UDID);
210
+ tile.close();
211
+ yield until(() => viewers() === 1, 'the tile’s socket has left');
212
+ (0, chai_1.expect)(capture.kills, 'the other tab still plays it').to.equal(0);
213
+ otherTab.close();
214
+ yield until(() => capture.kills === 1, 'the capture has ended');
215
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
216
+ }));
217
+ });
218
+ /**
219
+ * A capture stopped while viewers still play it: a recording starting on the
220
+ * phone (ensureMjpegForRecording), or a stream/stop. Their sockets stayed open
221
+ * with no frames, so the picture froze on its last frame and the page never
222
+ * fell back to MJPEG.
223
+ */
224
+ describe('AndroidH264StreamService: a capture stopped under its viewers', () => {
225
+ const loopback = (0, loopbackServer_1.loopbackServers)();
226
+ let restoreContainer;
227
+ beforeEach(() => {
228
+ restoreContainer = (0, container_registration_1.saveRegistrations)(PluginContext_1.PluginContext);
229
+ typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: {} });
230
+ });
231
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
232
+ restoreContainer();
233
+ yield loopback.closeAll();
234
+ }));
235
+ it('closes its viewers’ sockets with 1012 when the capture is stopped under them', () => __awaiter(void 0, void 0, void 0, function* () {
236
+ // 1012 says the stream ended: the player shows MJPEG, without asking the
237
+ // server to end an H.264 capture that is already gone.
238
+ const { svc, capture } = withCapture();
239
+ const server = yield loopback.serve(http_1.default.createServer());
240
+ (0, h264StreamWs_1.attachH264Ws)(server, {
241
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'usr_alice' }); }),
242
+ startStream: (udid) => svc.start(udid),
243
+ });
244
+ const { port } = server.address();
245
+ const sockets = [];
246
+ // How the socket ended, or 'still open' if it hadn't within 2 s.
247
+ const viewer = () => new Promise((resolve, reject) => {
248
+ const ws = new ws_1.WebSocket(`ws://127.0.0.1:${port}/xenon/api/control/${UDID}/stream/h264?ticket=t`);
249
+ sockets.push(ws);
250
+ const closed = new Promise((r) => {
251
+ const timer = setTimeout(() => r('still open'), 2000);
252
+ ws.once('close', (code, reason) => {
253
+ clearTimeout(timer);
254
+ r([code, reason.toString()]);
255
+ });
256
+ });
257
+ ws.once('message', () => resolve({ closed }));
258
+ ws.once('error', reject);
259
+ });
260
+ try {
261
+ const tile = yield viewer();
262
+ const otherTab = yield viewer();
263
+ yield svc.stop(UDID);
264
+ (0, chai_1.expect)(yield tile.closed).to.deep.equal([1012, 'stream ended']);
265
+ (0, chai_1.expect)(yield otherTab.closed).to.deep.equal([1012, 'stream ended']);
266
+ }
267
+ finally {
268
+ sockets.forEach((ws) => ws.terminate());
269
+ }
270
+ (0, chai_1.expect)(capture.kills).to.equal(1);
271
+ }));
272
+ });
@@ -54,6 +54,7 @@ const device_store_1 = require("../../src/data-service/device-store");
54
54
  const recording_store_1 = require("../../src/services/recording/recording-store");
55
55
  const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
56
56
  const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
57
+ const H264Multiplexer_1 = require("../../src/device-managers/android/H264Multiplexer");
57
58
  const previewHold_1 = require("../../src/device-managers/android/previewHold");
58
59
  const UDID = 'android-1';
59
60
  const HOLD = `manual_user-1_${UDID}`;
@@ -126,7 +127,7 @@ describe('Android live-preview hold', () => {
126
127
  it('releases the hold once nobody has watched for 10 minutes', () => __awaiter(void 0, void 0, void 0, function* () {
127
128
  const svc = new AndroidH264StreamService_1.default();
128
129
  const kill = sinon_1.default.spy();
129
- svc.sessions.set(UDID, { status: 'running', mux: { clientCount: 0 }, capture: { kill } });
130
+ svc.sessions.set(UDID, { status: 'running', mux: new H264Multiplexer_1.H264Multiplexer(), capture: { kill } });
130
131
  yield clock.tickAsync(60000); // first sweep notes it is empty
131
132
  yield clock.tickAsync(11 * 60000);
132
133
  (0, chai_1.expect)(kill.called, 'capture stopped').to.equal(true);
@@ -137,7 +138,7 @@ describe('Android live-preview hold', () => {
137
138
  const svc = new AndroidH264StreamService_1.default();
138
139
  svc.sessions.set(UDID, {
139
140
  status: 'running',
140
- mux: { clientCount: 0 },
141
+ mux: new H264Multiplexer_1.H264Multiplexer(),
141
142
  capture: { kill() { } },
142
143
  });
143
144
  yield clock.tickAsync(12 * 60000);
@@ -0,0 +1,84 @@
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 express_1 = __importDefault(require("express"));
19
+ const typedi_1 = require("typedi");
20
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
21
+ const apikeys_1 = require("../../src/app/routers/apikeys");
22
+ const ApiKeyService_1 = require("../../src/services/ApiKeyService");
23
+ /**
24
+ * POST /apikeys stores the scopes it is given. It stored any string, so a
25
+ * typo (`session`, `Admin`) made a key that silently had none of the scopes
26
+ * its maker meant, and a list shown back on the API keys page that no check
27
+ * reads. It takes the four scopes POST /profile/tokens takes.
28
+ */
29
+ describe('POST /apikeys takes only scopes that exist', () => {
30
+ let create;
31
+ function app(auth = {}) {
32
+ const a = (0, express_1.default)();
33
+ a.use(express_1.default.json());
34
+ a.use((req, _res, next) => {
35
+ req.auth = Object.assign({ kind: 'user-session', userId: 'admin1', role: 'SUPER_ADMIN', scopes: 'admin,devices,sessions,read' }, auth);
36
+ next();
37
+ });
38
+ a.use('/apikeys', (0, apikeys_1.apiKeysRouter)());
39
+ return a;
40
+ }
41
+ beforeEach(() => {
42
+ create = sinon_1.default.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create').resolves({ id: 'k1', raw: 'raw' });
43
+ });
44
+ afterEach(() => sinon_1.default.restore());
45
+ for (const scopes of [['root'], ['sessions', 'Admin'], ['session'], [42]]) {
46
+ it(`refuses ${JSON.stringify(scopes)}`, () => __awaiter(void 0, void 0, void 0, function* () {
47
+ const r = yield (0, loopbackRequest_1.default)(app()).post('/apikeys').send({ name: 'CI', scopes });
48
+ (0, chai_1.expect)(r.status).to.equal(400);
49
+ (0, chai_1.expect)(r.body.error).to.equal('scopes must be some of read, sessions, devices, admin');
50
+ (0, chai_1.expect)(create.called).to.equal(false);
51
+ }));
52
+ }
53
+ it('creates a key with known scopes', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const r = yield (0, loopbackRequest_1.default)(app())
55
+ .post('/apikeys')
56
+ .send({ name: 'CI', scopes: ['sessions', 'devices'] });
57
+ (0, chai_1.expect)(r.status).to.equal(200);
58
+ (0, chai_1.expect)(create.firstCall.args[0].scopes).to.deep.equal(['sessions', 'devices']);
59
+ }));
60
+ // Made with a credential that expires, a key ends no later than it: an
61
+ // admin's 1-hour Bearer token made itself a key that never expired.
62
+ describe('from a credential that expires', () => {
63
+ const inAnHour = new Date(Date.now() + 3600000);
64
+ const bearer = () => app({ kind: 'bearer', scopes: 'admin', credentialExpiresAt: inAnHour.getTime() });
65
+ it('expires with that credential when no expiresAt is asked for', () => __awaiter(void 0, void 0, void 0, function* () {
66
+ var _a;
67
+ const r = yield (0, loopbackRequest_1.default)(bearer())
68
+ .post('/apikeys')
69
+ .send({ name: 'CI', scopes: ['sessions'] });
70
+ (0, chai_1.expect)(r.status).to.equal(200);
71
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(inAnHour.toISOString());
72
+ (0, chai_1.expect)((_a = create.firstCall.args[0].expiresAt) === null || _a === void 0 ? void 0 : _a.getTime()).to.equal(inAnHour.getTime());
73
+ }));
74
+ it('refuses an expiresAt later than that credential', () => __awaiter(void 0, void 0, void 0, function* () {
75
+ const later = new Date(inAnHour.getTime() + 60000).toISOString();
76
+ const r = yield (0, loopbackRequest_1.default)(bearer())
77
+ .post('/apikeys')
78
+ .send({ name: 'CI', scopes: ['sessions'], expiresAt: later });
79
+ (0, chai_1.expect)(r.status).to.equal(400);
80
+ (0, chai_1.expect)(r.body.error).to.match(/later than the credential/);
81
+ (0, chai_1.expect)(create.called).to.equal(false);
82
+ }));
83
+ });
84
+ });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const appleFamily_1 = require("../../src/data-service/appleFamily");
5
+ /**
6
+ * Whether a phone is an iPhone or an iPad, for `appium:iPhoneOnly` and
7
+ * `appium:iPadOnly`. Its model and form factor are read before its name: a
8
+ * real phone is named by its owner.
9
+ */
10
+ describe('appleFamilyOf', () => {
11
+ const ios = (over) => (Object.assign({ platform: 'ios', name: 'unknown' }, over));
12
+ it('reads the family from the model first', () => {
13
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ model: 'iPhone17,3', name: 'My iPad' }))).to.equal('iphone');
14
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ model: 'iPad13,4', name: 'iPhone spare' }))).to.equal('ipad');
15
+ });
16
+ it('reads it from the form factor when there is no model (a simulator)', () => {
17
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ formFactor: 'phone', name: 'Test device 1' }))).to.equal('iphone');
18
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ formFactor: 'tablet', name: 'Test device 2' }))).to.equal('ipad');
19
+ });
20
+ it('puts an Apple TV in neither family', () => {
21
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ formFactor: 'tv', name: 'iPad-like TV' }))).to.equal(undefined);
22
+ });
23
+ it('falls back to the marketing name, then the name, for an old row', () => {
24
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ marketingName: 'iPad Air (5th generation)', name: 'x' }))).to.equal('ipad');
25
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ name: 'iPhone 13' }))).to.equal('iphone');
26
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ name: 'iPad (9th generation)' }))).to.equal('ipad');
27
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ name: 'unknown' }))).to.equal(undefined);
28
+ });
29
+ it('is case-insensitive about a name', () => {
30
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)(ios({ name: 'IPHONE 12' }))).to.equal('iphone');
31
+ });
32
+ it('is nothing for a device that is not iOS', () => {
33
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)({ platform: 'android', name: 'Pixel Tablet', formFactor: 'tablet' })).to.equal(undefined);
34
+ (0, chai_1.expect)((0, appleFamily_1.appleFamilyOf)({ platform: 'android', name: 'iPhone clone', model: 'iPhone1,1' })).to.equal(undefined);
35
+ });
36
+ });
@@ -65,6 +65,15 @@ describe('authMiddleware — Bearer branch', () => {
65
65
  (0, chai_1.expect)(req.auth.userId).to.equal('u1');
66
66
  (0, chai_1.expect)(req.auth.scopes).to.equal('devices,read');
67
67
  }));
68
+ it("drops admin from a demoted user's token: req.auth.scopes follows the role now", () => __awaiter(void 0, void 0, void 0, function* () {
69
+ // `user` is a MEMBER: the token was minted while they were an admin.
70
+ const token = yield keySvc.sign({ sub: 'u1', scopes: 'admin,devices,sessions,read' }, { audience: 'xenon-rest', ttlSeconds: 60 });
71
+ const req = { headers: { authorization: `Bearer ${token}` }, query: {} };
72
+ const next = sinon_1.default.spy();
73
+ yield (0, authMiddleware_1.authMiddleware)(req, fakeRes(), next);
74
+ (0, chai_1.expect)(next.calledOnce).to.equal(true);
75
+ (0, chai_1.expect)(req.auth.scopes).to.equal('devices,sessions,read');
76
+ }));
68
77
  // Widened by Phase 2a Task 1: the MCP plugin's tools call this same REST
69
78
  // surface with a gateway-injected xenon-mcp-audience token, so it must be
70
79
  // accepted here too (see authMiddleware.mcpAudience.spec.ts for the full