@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,129 @@
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 OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
19
+ const AIService_1 = require("../../src/services/AIService");
20
+ /**
21
+ * `xenon: smartTap { text }` (and `omniClick`) compared the text with one OCR
22
+ * word at a time, so any text with a space in it ("Sign in", "Forgot
23
+ * password?") never matched: the answer was `clicked: false`. The
24
+ * `-custom:ai-text` locator had the same rule. Now the text is looked for
25
+ * across neighbouring words on one line, and the tap goes to the middle of
26
+ * the words it covers.
27
+ */
28
+ describe('Omni-Vision text taps match phrases', () => {
29
+ // Tesseract's words, in screenshot pixels. One line holds "Sign in" and,
30
+ // far to the right, "Help"; "Sign" and "in" also appear on two lines.
31
+ const word = (text, x0, y0, x1, y1) => ({
32
+ text,
33
+ confidence: 90,
34
+ bbox: { x0, y0, x1, y1 },
35
+ });
36
+ const WORDS = [
37
+ word('Sign', 100, 200, 150, 224),
38
+ word('in', 158, 200, 180, 224),
39
+ word('Help', 600, 200, 650, 224),
40
+ word('Forgot', 100, 300, 170, 324),
41
+ word('password?', 178, 300, 290, 324),
42
+ word('Sign', 100, 400, 150, 424),
43
+ word('in', 100, 440, 120, 464),
44
+ ];
45
+ let service;
46
+ let driver;
47
+ beforeEach(() => {
48
+ service = new OmniVisionService_1.OmniVisionService();
49
+ sinon_1.default.stub(service, 'performOcr').resolves({ text: '', words: WORDS });
50
+ driver = {
51
+ sessionId: 'session-1',
52
+ // An Android driver: its W3C coordinates are the screenshot's pixels.
53
+ caps: { platformName: 'Android' },
54
+ getScreenshot: sinon_1.default.stub().resolves('aGVsbG8='),
55
+ performActions: sinon_1.default.stub().resolves(),
56
+ releaseActions: sinon_1.default.stub().resolves(),
57
+ };
58
+ });
59
+ afterEach(() => sinon_1.default.restore());
60
+ const tappedAt = () => {
61
+ const move = driver.performActions.firstCall.args[0][0].actions[0];
62
+ return { x: move.x, y: move.y };
63
+ };
64
+ it('taps the middle of a two-word text', () => __awaiter(void 0, void 0, void 0, function* () {
65
+ const result = yield service.omniClickByText(driver, { text: 'Sign in' });
66
+ (0, chai_1.expect)(result.clicked, result.message).to.equal(true);
67
+ (0, chai_1.expect)(result.target).to.deep.include({
68
+ text: 'Sign in',
69
+ x: 140,
70
+ y: 212,
71
+ rect: { x: 100, y: 200, width: 80, height: 24 },
72
+ });
73
+ (0, chai_1.expect)(tappedAt()).to.deep.equal({ x: 140, y: 212 });
74
+ }));
75
+ it('matches a phrase whatever its case, and part of a word at either end', () => __awaiter(void 0, void 0, void 0, function* () {
76
+ var _a;
77
+ const result = yield service.omniClickByText(driver, { text: 'forgot PASSWORD' });
78
+ (0, chai_1.expect)(result.clicked, result.message).to.equal(true);
79
+ (0, chai_1.expect)((_a = result.target) === null || _a === void 0 ? void 0 : _a.text).to.equal('Forgot password?');
80
+ (0, chai_1.expect)(tappedAt()).to.deep.equal({ x: 195, y: 312 });
81
+ }));
82
+ it('does not join words on different lines, or far apart on one line', () => __awaiter(void 0, void 0, void 0, function* () {
83
+ for (const text of ['in Help', 'Sign in Help', 'password? Sign']) {
84
+ const result = yield service.omniClickByText(driver, { text });
85
+ (0, chai_1.expect)(result.clicked, text).to.equal(false);
86
+ }
87
+ (0, chai_1.expect)(driver.performActions.called).to.equal(false);
88
+ }));
89
+ it('still matches a single word, as before', () => __awaiter(void 0, void 0, void 0, function* () {
90
+ const result = yield service.omniClickByText(driver, { text: 'help' });
91
+ (0, chai_1.expect)(result.clicked, result.message).to.equal(true);
92
+ (0, chai_1.expect)(tappedAt()).to.deep.equal({ x: 625, y: 212 });
93
+ }));
94
+ it('finds a phrase with the -custom:ai-text locator too', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ const found = yield service.findByText(driver, 'Sign in');
96
+ (0, chai_1.expect)(found).to.have.lengthOf(1);
97
+ (0, chai_1.expect)(found[0].text).to.equal('Sign in');
98
+ (0, chai_1.expect)(found[0].rect).to.deep.equal({ x: 100, y: 200, width: 80, height: 24 });
99
+ }));
100
+ });
101
+ /**
102
+ * `xenon: smartTap { icon }` / `visualTap` answered `clicked: false`, "No visual
103
+ * match found", when it never looked: no AI provider, or the AI call failed.
104
+ * It now fails with the reason; `clicked: false` means the AI looked and found
105
+ * nothing.
106
+ */
107
+ describe('Omni-Vision icon taps that cannot look', () => {
108
+ const driver = {
109
+ sessionId: 'session-1',
110
+ caps: { platformName: 'Android' },
111
+ getScreenshot: () => __awaiter(void 0, void 0, void 0, function* () { return 'aGVsbG8='; }),
112
+ performActions: sinon_1.default.stub().resolves(),
113
+ };
114
+ beforeEach(() => {
115
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
116
+ });
117
+ afterEach(() => sinon_1.default.restore());
118
+ it('fails when no AI provider is configured', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value(null);
120
+ const err = yield new OmniVisionService_1.OmniVisionService().omniClickByIcon(driver, { icon: 'the gear' }).then(() => null, (e) => e);
121
+ (0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.match(/No AI provider is configured/);
122
+ }));
123
+ it('answers clicked: false when the AI looked and found nothing', () => __awaiter(void 0, void 0, void 0, function* () {
124
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves(null);
125
+ const result = yield new OmniVisionService_1.OmniVisionService().omniClickByIcon(driver, { icon: 'the gear' });
126
+ (0, chai_1.expect)(result.clicked).to.equal(false);
127
+ (0, chai_1.expect)(result.message).to.match(/No visual match/);
128
+ }));
129
+ });
@@ -70,16 +70,14 @@ describe('OmniVisionService Unit Tests', () => {
70
70
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.contain('omni_ai_');
71
71
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.rect).to.deep.equal({ x: 30, y: 30, width: 40, height: 40 });
72
72
  }));
73
- it('getVirtualElement should retrieve stored elements', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ it('getVirtualElement gives back an element kept for its session, to that session only', () => __awaiter(void 0, void 0, void 0, function* () {
74
74
  sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
75
75
  const created = yield omniService.findByIcon(mockDriver, 'back');
76
- if (created) {
77
- const retrieved = omniService.getVirtualElement(created.id);
78
- (0, chai_1.expect)(retrieved).to.deep.equal(created);
79
- }
80
- else {
76
+ if (!created)
81
77
  throw new Error('Failed to create mock virtual element');
82
- }
78
+ omniService.remember('session-1', created);
79
+ (0, chai_1.expect)(omniService.getVirtualElement(created.id, 'session-1')).to.deep.equal(created);
80
+ (0, chai_1.expect)(omniService.getVirtualElement(created.id, 'session-2')).to.equal(undefined);
83
81
  }));
84
82
  it('analyzeScreen should combine OCR and AI insights', () => __awaiter(void 0, void 0, void 0, function* () {
85
83
  const mockWorker = {
@@ -95,7 +93,7 @@ describe('OmniVisionService Unit Tests', () => {
95
93
  terminate: sinon_1.default.stub().resolves(),
96
94
  };
97
95
  sinon_1.default.stub(tesseract_js_1.default, 'createWorker').resolves(mockWorker);
98
- sinon_1.default.stub(AIService_1.AI_SERVICE, 'analyzeFailure').resolves('AI Insights text');
96
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'describeScreen').resolves('AI Insights text');
99
97
  const analysis = yield omniService.analyzeScreen(mockDriver);
100
98
  (0, chai_1.expect)(analysis.ocr.text).to.equal('Screen Text');
101
99
  (0, chai_1.expect)(analysis.ai_insights).to.equal('AI Insights text');
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const root = path_1.default.join(__dirname, '..', '..');
10
+ const readJson = (file) => JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
11
+ const pkg = readJson(path_1.default.join(root, 'package.json'));
12
+ const lock = readJson(path_1.default.join(root, 'package-lock.json'));
13
+ const appium = readJson(require.resolve('appium/package.json'));
14
+ /**
15
+ * The plugin runs inside Appium, so it supports the Node versions Appium 3
16
+ * does and no others. `engines` used to say `^14.17.0 || ^16.13.0 || >=18.0.0`
17
+ * while every Appium 3 release needs 20.19, 22.12 or 24 and later.
18
+ */
19
+ describe('package.json engines', () => {
20
+ it("are the installed Appium 3's, the Appium the plugin is built and tested against", () => {
21
+ (0, chai_1.expect)(appium.version).to.match(/^3\./);
22
+ (0, chai_1.expect)(pkg.engines).to.deep.equal({ node: appium.engines.node, npm: appium.engines.npm });
23
+ });
24
+ it('match the copy in package-lock.json', () => {
25
+ (0, chai_1.expect)(lock.packages[''].engines).to.deep.equal(pkg.engines);
26
+ });
27
+ });
@@ -73,6 +73,20 @@ describe('An Appium config file for Xenon', function () {
73
73
  (0, chai_1.expect)((_a = result.reason) !== null && _a !== void 0 ? _a : result.errors).to.be.empty;
74
74
  });
75
75
  });
76
+ it('is accepted with the network capture option Xenon Control writes', function () {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ var _a;
79
+ // The server's `interceptor` option is now each session's default
80
+ // (resolveInterceptorOptions); configs that set it must keep starting.
81
+ const result = yield load(`${README_SAMPLE} interceptor:\n enabled: true\n bufferSize: 50\n captureBodies: false\n`);
82
+ (0, chai_1.expect)((_a = result.reason) !== null && _a !== void 0 ? _a : result.errors).to.be.empty;
83
+ (0, chai_1.expect)(result.config.server.plugin.xenon.interceptor).to.deep.equal({
84
+ enabled: true,
85
+ bufferSize: 50,
86
+ captureBodies: false,
87
+ });
88
+ });
89
+ });
76
90
  it('is still refused when a value is invalid', function () {
77
91
  return __awaiter(this, void 0, void 0, function* () {
78
92
  const result = yield load(README_SAMPLE.replace('platform: both', 'platform: windows'));
@@ -3,8 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const chai_1 = require("chai");
4
4
  const passwordResetMode_1 = require("../../src/services/passwordResetMode");
5
5
  describe('passwordResetMode', () => {
6
- it('is email when an SMTP URL is configured', () => {
7
- (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://mail.example.com:587' })).to.equal('email');
6
+ it('is email when an SMTP URL and XENON_PUBLIC_URL are configured', () => {
7
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({
8
+ smtpUrl: 'smtp://mail.example.com:587',
9
+ publicUrl: 'https://xenon.example.com',
10
+ })).to.equal('email');
11
+ });
12
+ // The emailed link points at XENON_PUBLIC_URL, never at the request's Host:
13
+ // without it Xenon emails no link, so the form must not promise one.
14
+ it('is admin with SMTP but no XENON_PUBLIC_URL', () => {
15
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://mail.example.com:587' })).to.equal('admin');
8
16
  });
9
17
  it('is admin without SMTP, even though the log fallback defaults on', () => {
10
18
  // The fallback writes the link to the server log, which the person who
@@ -0,0 +1,64 @@
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 typedi_1 = require("typedi");
19
+ const ServerManager_1 = require("../../../src/services/ServerManager");
20
+ const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
21
+ const device_store_1 = require("../../../src/data-service/device-store");
22
+ const container_registration_1 = require("../../helpers/container-registration");
23
+ const ME = 'http://10.0.0.5:4723';
24
+ const NODE_ID = 'node-me';
25
+ /**
26
+ * At its start a server checks the network of the Android phones it drives:
27
+ * never a phone another server on the same machine drives (a hub keeps its
28
+ * nodes' rows), a cloud phone or an iPhone.
29
+ */
30
+ describe('the phone network check at boot', () => {
31
+ let restoreRegs;
32
+ let cleanUpAtBoot;
33
+ beforeEach(() => {
34
+ restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore);
35
+ cleanUpAtBoot = sinon_1.default.stub().resolves();
36
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { cleanUpAtBoot });
37
+ });
38
+ afterEach(() => {
39
+ sinon_1.default.restore();
40
+ restoreRegs();
41
+ });
42
+ it("checks this server's own Android phones, once each", () => __awaiter(void 0, void 0, void 0, function* () {
43
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
44
+ getAllDevices: () => __awaiter(void 0, void 0, void 0, function* () {
45
+ return [
46
+ { udid: 'MINE', platform: 'android', host: ME, nodeId: NODE_ID },
47
+ { udid: 'MINE', platform: 'android', host: ME, nodeId: NODE_ID },
48
+ { udid: 'NO-ID', platform: 'android', host: ME },
49
+ { udid: 'NODE', platform: 'android', host: 'http://10.0.0.5:4724', nodeId: 'node-b' },
50
+ { udid: 'CLOUD', platform: 'android', host: ME, nodeId: NODE_ID, cloud: { name: 'x' } },
51
+ { udid: 'IPHONE', platform: 'ios', host: ME, nodeId: NODE_ID },
52
+ ];
53
+ }),
54
+ });
55
+ yield typedi_1.Container.get(ServerManager_1.ServerManager).cleanUpPhoneNetworks({ origin: ME, hosts: new Set([ME]) }, NODE_ID);
56
+ (0, chai_1.expect)(cleanUpAtBoot.calledOnce).to.equal(true);
57
+ (0, chai_1.expect)(cleanUpAtBoot.firstCall.args[0]).to.deep.equal(['MINE', 'NO-ID']);
58
+ }));
59
+ it('never fails the boot', () => __awaiter(void 0, void 0, void 0, function* () {
60
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').throws(new Error('no store'));
61
+ yield typedi_1.Container.get(ServerManager_1.ServerManager).cleanUpPhoneNetworks({ origin: ME, hosts: new Set([ME]) }, NODE_ID);
62
+ (0, chai_1.expect)(cleanUpAtBoot.called).to.equal(false);
63
+ }));
64
+ });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("reflect-metadata");
4
+ const chai_1 = require("chai");
5
+ const XenonCapabilityManager_1 = require("../../../src/XenonCapabilityManager");
6
+ const interceptorOptions_1 = require("../../../src/services/interceptor/interceptorOptions");
7
+ const capsOf = (alwaysMatch) => (0, XenonCapabilityManager_1.getXenonCapabilities)({ alwaysMatch, firstMatch: [{}] });
8
+ /**
9
+ * The server's `interceptor` option was declared in schema.json and shown in
10
+ * Xenon Control, but nothing read it: turning it on did nothing. It is now the
11
+ * default for this server's sessions, and a session's own capability wins
12
+ * field by field.
13
+ */
14
+ describe('resolveInterceptorOptions: the server option is the default, the session decides', () => {
15
+ it('leaves capture off when neither the session nor the server turns it on', () => {
16
+ (0, chai_1.expect)((0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({}), undefined).enabled).to.equal(false);
17
+ (0, chai_1.expect)((0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({}), {}).enabled).to.equal(false);
18
+ });
19
+ it("turns capture on for a session that doesn't say, when the server's option is on", () => {
20
+ const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({}), { enabled: true });
21
+ (0, chai_1.expect)(opts.enabled).to.equal(true);
22
+ (0, chai_1.expect)(opts.captureBodies).to.equal(true);
23
+ (0, chai_1.expect)(opts.mocks).to.deep.equal([]);
24
+ });
25
+ it("lets a session turn capture off when the server's option is on", () => {
26
+ const object = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: false } }), {
27
+ enabled: true,
28
+ });
29
+ (0, chai_1.expect)(object.enabled).to.equal(false);
30
+ const flat = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptorEnabled': false }), {
31
+ enabled: true,
32
+ });
33
+ (0, chai_1.expect)(flat.enabled).to.equal(false);
34
+ });
35
+ it("lets a session turn capture on when the server's option is off", () => {
36
+ const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: true } }), {
37
+ enabled: false,
38
+ });
39
+ (0, chai_1.expect)(opts.enabled).to.equal(true);
40
+ });
41
+ it("takes bufferSize and captureBodies from the server when the session doesn't set them", () => {
42
+ const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: true } }), {
43
+ bufferSize: 50,
44
+ captureBodies: false,
45
+ });
46
+ (0, chai_1.expect)(opts.bufferSize).to.equal(50);
47
+ (0, chai_1.expect)(opts.captureBodies).to.equal(false);
48
+ });
49
+ it("keeps the session's own bufferSize and captureBodies over the server's", () => {
50
+ const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({ 'xe:interceptor': { enabled: true, bufferSize: 7, captureBodies: true } }), { bufferSize: 50, captureBodies: false });
51
+ (0, chai_1.expect)(opts.bufferSize).to.equal(7);
52
+ (0, chai_1.expect)(opts.captureBodies).to.equal(true);
53
+ });
54
+ it('keeps mocks and host filters from the session only', () => {
55
+ const mocks = [{ match: { url: '**/x' }, respondWith: { status: 204 } }];
56
+ const opts = (0, interceptorOptions_1.resolveInterceptorOptions)(capsOf({
57
+ 'xe:interceptor': { mocks, includeHosts: ['a.example'], excludeHosts: ['b.example'] },
58
+ }), { enabled: true });
59
+ (0, chai_1.expect)(opts.mocks).to.deep.equal(mocks);
60
+ (0, chai_1.expect)(opts.includeHosts).to.deep.equal(['a.example']);
61
+ (0, chai_1.expect)(opts.excludeHosts).to.deep.equal(['b.example']);
62
+ });
63
+ it('reads a session that sets nothing about capture as unset, not as off', () => {
64
+ const caps = capsOf({});
65
+ (0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(undefined);
66
+ (0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_CAPTURE_BODIES]).to.equal(undefined);
67
+ });
68
+ it('reads an interceptor object without `enabled` as unset', () => {
69
+ const caps = capsOf({ 'xe:interceptor': { bufferSize: 3 } });
70
+ (0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(undefined);
71
+ (0, chai_1.expect)((0, interceptorOptions_1.resolveInterceptorOptions)(caps, { enabled: true }).enabled).to.equal(true);
72
+ });
73
+ it("reads `enabled: 'false'` as off", () => {
74
+ const caps = capsOf({ 'xe:interceptor': { enabled: 'false' } });
75
+ (0, chai_1.expect)(caps[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(false);
76
+ });
77
+ });
@@ -0,0 +1,145 @@
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 fs_1 = __importDefault(require("fs"));
19
+ const net_1 = __importDefault(require("net"));
20
+ const os_1 = __importDefault(require("os"));
21
+ const path_1 = __importDefault(require("path"));
22
+ const typedi_1 = require("typedi");
23
+ const InterceptorService_1 = require("../../../src/services/InterceptorService");
24
+ const CertManager_1 = require("../../../src/services/interceptor/CertManager");
25
+ const MitmProxyHost_1 = require("../../../src/services/interceptor/MitmProxyHost");
26
+ const PortAllocator_1 = require("../../../src/services/PortAllocator");
27
+ const PhoneNetworkLedger_1 = require("../../../src/services/network/PhoneNetworkLedger");
28
+ const config_1 = require("../../../src/config");
29
+ const container_registration_1 = require("../../helpers/container-registration");
30
+ const scratch_database_1 = require("../../helpers/scratch-database");
31
+ const fake_adb_1 = require("../../helpers/fake-adb");
32
+ const PROXY_PORT = 11150;
33
+ const phone = { udid: 'R5CT', platform: 'android', deviceType: 'real', host: 'h' };
34
+ const ON = { enabled: true, mocks: [], includeHosts: [], excludeHosts: [] };
35
+ /**
36
+ * The interceptor points the whole phone at its proxy. When the session ends
37
+ * it puts back the proxy the phone had (Xenon used to write ":0" over a lab's
38
+ * own proxy), and records the change while it lasts so a restart can undo it.
39
+ */
40
+ describe('InterceptorService and the phone proxy', function () {
41
+ this.timeout(60000);
42
+ (0, scratch_database_1.useScratchDatabase)();
43
+ let adb;
44
+ (0, fake_adb_1.useFakeAdb)(() => adb);
45
+ let restoreRegs;
46
+ let assetsBefore;
47
+ let assetsDir;
48
+ let ledger;
49
+ let range;
50
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
51
+ adb = (0, fake_adb_1.fakeAdb)({ 'shell settings get global http_proxy': 'proxy.corp:3128' });
52
+ restoreRegs = (0, container_registration_1.saveRegistrations)(PortAllocator_1.PortAllocator);
53
+ range = [11100, 11199];
54
+ typedi_1.Container.set(PortAllocator_1.PortAllocator, {
55
+ acquire: () => __awaiter(this, void 0, void 0, function* () { return PROXY_PORT; }),
56
+ releaseForUdid: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
57
+ rangeOf: () => range,
58
+ });
59
+ sinon_1.default.stub(CertManager_1.CertManager.prototype, 'ensure').resolves({
60
+ certPath: '/nonexistent/ca.pem',
61
+ keyPath: '/nonexistent/ca.key',
62
+ subjectHash: 'abc',
63
+ });
64
+ sinon_1.default.stub(CertManager_1.CertManager.prototype, 'androidCertFilename').returns('abc.0');
65
+ sinon_1.default.stub(MitmProxyHost_1.MitmProxyHost.prototype, 'start').resolves();
66
+ sinon_1.default.stub(MitmProxyHost_1.MitmProxyHost.prototype, 'stop').resolves();
67
+ assetsBefore = config_1.config.sessionAssetsPath;
68
+ assetsDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-icp-'));
69
+ (0, config_1.updateConfig)({ sessionAssetsPath: assetsDir });
70
+ ledger = new PhoneNetworkLedger_1.PhoneNetworkLedger();
71
+ for (const entry of yield ledger.list())
72
+ yield ledger.forget(entry.sessionId);
73
+ }));
74
+ afterEach(() => {
75
+ sinon_1.default.restore();
76
+ restoreRegs();
77
+ (0, config_1.updateConfig)({ sessionAssetsPath: assetsBefore });
78
+ fs_1.default.rmSync(assetsDir, { recursive: true, force: true });
79
+ });
80
+ it('reads the proxy the phone had and writes its own down before setting it', () => __awaiter(this, void 0, void 0, function* () {
81
+ const svc = new InterceptorService_1.InterceptorService();
82
+ yield svc.start('s1', phone, ON);
83
+ const cmds = adb.commands('R5CT');
84
+ const read = cmds.indexOf('shell settings get global http_proxy');
85
+ const set = cmds.indexOf(`shell settings put global http_proxy 127.0.0.1:${PROXY_PORT}`);
86
+ (0, chai_1.expect)(read, cmds.join('\n')).to.be.greaterThan(-1);
87
+ (0, chai_1.expect)(set).to.be.greaterThan(read);
88
+ const [entry] = yield ledger.list();
89
+ (0, chai_1.expect)(entry).to.include({ sessionId: 's1', udid: 'R5CT' });
90
+ (0, chai_1.expect)(entry.proxy).to.deep.equal({
91
+ set: `127.0.0.1:${PROXY_PORT}`,
92
+ previous: 'proxy.corp:3128',
93
+ reversePort: PROXY_PORT,
94
+ });
95
+ yield svc.stop('s1');
96
+ }));
97
+ it("puts the phone's own proxy back at the end, and saves the capture", () => __awaiter(this, void 0, void 0, function* () {
98
+ const svc = new InterceptorService_1.InterceptorService();
99
+ yield svc.start('s1', phone, ON);
100
+ adb.calls.length = 0;
101
+ yield svc.stop('s1');
102
+ (0, chai_1.expect)(adb.commands('R5CT')).to.include('shell settings put global http_proxy proxy.corp:3128');
103
+ (0, chai_1.expect)(adb.commands('R5CT')).not.to.include('shell settings put global http_proxy :0');
104
+ (0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
105
+ (0, chai_1.expect)(fs_1.default.existsSync(path_1.default.join(assetsDir, 's1', 'interceptor', 'requests.json'))).to.equal(true);
106
+ }));
107
+ it('clears the proxy at the end when the phone had none', () => __awaiter(this, void 0, void 0, function* () {
108
+ adb.answers['shell settings get global http_proxy'] = ':0';
109
+ const svc = new InterceptorService_1.InterceptorService();
110
+ yield svc.start('s1', phone, ON);
111
+ adb.calls.length = 0;
112
+ yield svc.stop('s1');
113
+ (0, chai_1.expect)(adb.commands('R5CT')).to.include('shell settings put global http_proxy :0');
114
+ }));
115
+ it("doesn't put back a proxy an earlier run left pointing at a dead capture port", () => __awaiter(this, void 0, void 0, function* () {
116
+ const closed = net_1.default.createServer();
117
+ yield new Promise((r) => closed.listen(0, '127.0.0.1', () => r()));
118
+ const dead = closed.address().port;
119
+ yield new Promise((r) => closed.close(() => r()));
120
+ range = [Math.min(dead, PROXY_PORT), Math.max(dead, PROXY_PORT)];
121
+ adb.answers['shell settings get global http_proxy'] = `127.0.0.1:${dead}`;
122
+ const svc = new InterceptorService_1.InterceptorService();
123
+ yield svc.start('s1', phone, ON);
124
+ adb.calls.length = 0;
125
+ yield svc.stop('s1');
126
+ (0, chai_1.expect)(adb.commands('R5CT')).to.include('shell settings put global http_proxy :0');
127
+ (0, chai_1.expect)(adb.commands('R5CT')).not.to.include(`shell settings put global http_proxy 127.0.0.1:${dead}`);
128
+ }));
129
+ it('keeps the record when the proxy could not be put back', () => __awaiter(this, void 0, void 0, function* () {
130
+ const svc = new InterceptorService_1.InterceptorService();
131
+ yield svc.start('s1', phone, ON);
132
+ adb.answers['shell settings put global http_proxy proxy.corp:3128'] = new Error('offline');
133
+ yield svc.stop('s1');
134
+ (0, chai_1.expect)((yield ledger.list()).map((e) => e.sessionId)).to.deep.equal(['s1']);
135
+ (0, chai_1.expect)(svc.isActive('s1')).to.equal(false);
136
+ }));
137
+ it('stops once, however many times it is asked', () => __awaiter(this, void 0, void 0, function* () {
138
+ const svc = new InterceptorService_1.InterceptorService();
139
+ yield svc.start('s1', phone, ON);
140
+ adb.calls.length = 0;
141
+ yield Promise.all([svc.stop('s1'), svc.stop('s1')]);
142
+ const puts = adb.commands('R5CT').filter((c) => c.startsWith('shell settings put'));
143
+ (0, chai_1.expect)(puts).to.deep.equal(['shell settings put global http_proxy proxy.corp:3128']);
144
+ }));
145
+ });