@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
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const grid_1 = __importDefault(require("../../src/app/routers/grid"));
21
21
  const typedi_1 = require("typedi");
@@ -23,8 +23,11 @@ const TeamService_1 = require("../../src/services/TeamService");
23
23
  const prisma_1 = require("../../src/prisma");
24
24
  const seedUser_1 = require("../helpers/seedUser");
25
25
  const device_store_1 = require("../../src/data-service/device-store");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
27
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
26
28
  describe('team visibility on /grid/devices (integration)', function () {
27
29
  this.timeout(60000);
30
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
28
31
  let sa;
29
32
  let aliceMember;
30
33
  let bobUnaffiliated;
@@ -43,6 +46,9 @@ describe('team visibility on /grid/devices (integration)', function () {
43
46
  teamB = yield typedi_1.Container.get(TeamService_1.TeamService).create(`p4a-team-b-${stamp}`);
44
47
  // Alice on team A only.
45
48
  yield typedi_1.Container.get(TeamService_1.TeamService).addMember(teamA.id, aliceMember.user.id);
49
+ // /devices lists the store grid.ts took when it was imported: Prisma under
50
+ // `npx mocha <file>`, Loki under `test:all` (NODE_ENV=test). So each phone
51
+ // is a Device row and is added to that store.
46
52
  store = device_store_1.DeviceStoreFactory.getStore();
47
53
  // Three test devices. Use minimal field set; the schema may have
48
54
  // additional optional fields we don't need to populate for this test.
@@ -77,18 +83,13 @@ describe('team visibility on /grid/devices (integration)', function () {
77
83
  });
78
84
  yield store.addDevices([teamBDev]);
79
85
  }));
86
+ // Loki is one collection for the whole process: take the phones back out.
87
+ // A Prisma store's rows went with the scratch database.
80
88
  after(() => __awaiter(this, void 0, void 0, function* () {
81
- yield prisma_1.prisma.device.deleteMany({
82
- where: { udid: { in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID] } },
83
- });
84
- yield prisma_1.prisma.teamMember.deleteMany({
85
- where: { teamId: { in: [teamA.id, teamB.id] } },
86
- });
87
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
88
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
89
- yield sa.cleanup();
90
- yield aliceMember.cleanup();
91
- yield bobUnaffiliated.cleanup();
89
+ if (store instanceof prisma_store_1.PrismaDeviceStore)
90
+ return;
91
+ for (const udid of [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID])
92
+ yield store.removeDevices({ udid });
92
93
  }));
93
94
  function buildApp() {
94
95
  const app = (0, express_1.default)();
@@ -101,7 +102,7 @@ describe('team visibility on /grid/devices (integration)', function () {
101
102
  return rows.map((r) => r.udid);
102
103
  }
103
104
  it('SUPER_ADMIN sees all three test devices', () => __awaiter(this, void 0, void 0, function* () {
104
- const r = yield (0, supertest_1.default)(buildApp()).get('/devices').set('Cookie', sa.cookie);
105
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/devices').set('Cookie', sa.cookie);
105
106
  (0, chai_1.expect)(r.status).to.equal(200);
106
107
  const got = udids(r.body);
107
108
  (0, chai_1.expect)(got).to.include(SHARED_UDID);
@@ -109,7 +110,7 @@ describe('team visibility on /grid/devices (integration)', function () {
109
110
  (0, chai_1.expect)(got).to.include(TEAM_B_UDID);
110
111
  }));
111
112
  it('Alice (team A member) sees shared + team A; not team B', () => __awaiter(this, void 0, void 0, function* () {
112
- const r = yield (0, supertest_1.default)(buildApp()).get('/devices').set('Cookie', aliceMember.cookie);
113
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/devices').set('Cookie', aliceMember.cookie);
113
114
  (0, chai_1.expect)(r.status).to.equal(200);
114
115
  const got = udids(r.body);
115
116
  (0, chai_1.expect)(got).to.include(SHARED_UDID);
@@ -117,7 +118,7 @@ describe('team visibility on /grid/devices (integration)', function () {
117
118
  (0, chai_1.expect)(got).to.not.include(TEAM_B_UDID);
118
119
  }));
119
120
  it('Bob (no team) sees only the shared device', () => __awaiter(this, void 0, void 0, function* () {
120
- const r = yield (0, supertest_1.default)(buildApp()).get('/devices').set('Cookie', bobUnaffiliated.cookie);
121
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/devices').set('Cookie', bobUnaffiliated.cookie);
121
122
  (0, chai_1.expect)(r.status).to.equal(200);
122
123
  const got = udids(r.body);
123
124
  (0, chai_1.expect)(got).to.include(SHARED_UDID);
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const typedi_1 = require("typedi");
20
20
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
21
21
  const sdk_leases_1 = require("../../src/app/routers/sdk-leases");
@@ -24,6 +24,8 @@ const TeamService_1 = require("../../src/services/TeamService");
24
24
  const device_store_1 = require("../../src/data-service/device-store");
25
25
  const prisma_1 = require("../../src/prisma");
26
26
  const seedUser_1 = require("../helpers/seedUser");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ const loki_stores_1 = require("../helpers/loki-stores");
27
29
  /**
28
30
  * SDK leases against the production store: the real auth middleware computes
29
31
  * the member's teams, and the Prisma store's match (a SQL where clause, not
@@ -32,6 +34,10 @@ const seedUser_1 = require("../helpers/seedUser");
32
34
  */
33
35
  describe('team boundary on SDK leases (integration)', function () {
34
36
  this.timeout(60000);
37
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
38
+ // The Prisma store this suite is about, under `test:all` too, whose
39
+ // NODE_ENV would hand the factory a Loki one.
40
+ (0, loki_stores_1.usePrismaStores)();
35
41
  let sa;
36
42
  let alice;
37
43
  let teamA;
@@ -63,15 +69,6 @@ describe('team boundary on SDK leases (integration)', function () {
63
69
  yield prisma_1.prisma.lease.deleteMany({ where: { deviceUdid: { in: UDIDS } } });
64
70
  yield prisma_1.prisma.device.updateMany({ where: { udid: { in: UDIDS } }, data: { busy: false } });
65
71
  }));
66
- after(() => __awaiter(this, void 0, void 0, function* () {
67
- yield prisma_1.prisma.lease.deleteMany({ where: { deviceUdid: { in: UDIDS } } });
68
- yield prisma_1.prisma.device.deleteMany({ where: { udid: { in: UDIDS } } });
69
- yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
70
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
71
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
72
- yield sa.cleanup();
73
- yield alice.cleanup();
74
- }));
75
72
  function lease(who, filters) {
76
73
  const svc = new LeaseService_1.LeaseService(prisma_1.prisma, device_store_1.DeviceStoreFactory.getStore(), {
77
74
  allocate: () => __awaiter(this, void 0, void 0, function* () { return ({ systemPort: 9001, chromedriverPort: 9002, mjpegServerPort: 9003 }); }),
@@ -80,7 +77,7 @@ describe('team boundary on SDK leases (integration)', function () {
80
77
  app.use(express_1.default.json());
81
78
  app.use(authMiddleware_1.authMiddleware);
82
79
  app.use('/sdk/leases', (0, sdk_leases_1.makeRouter)({ leaseService: svc }));
83
- return (0, supertest_1.default)(app)
80
+ return (0, loopbackRequest_1.default)(app)
84
81
  .post('/sdk/leases')
85
82
  .set('Cookie', who.cookie)
86
83
  .send({ filters: Object.assign({ platform: 'android', filterByHost: HOST }, filters) });
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const recordings_1 = __importDefault(require("../../src/app/routers/recordings"));
21
21
  const typedi_1 = require("typedi");
@@ -23,8 +23,10 @@ const TeamService_1 = require("../../src/services/TeamService");
23
23
  const prisma_1 = require("../../src/prisma");
24
24
  const seedUser_1 = require("../helpers/seedUser");
25
25
  const artifact_store_1 = require("../helpers/artifact-store");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
26
27
  describe('team visibility on /recordings reads (integration)', function () {
27
28
  this.timeout(60000);
29
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
28
30
  // GET /recordings/:groupId resolves ARTIFACT_STORE from the container, which
29
31
  // ServerManager registers at boot. Without it every allowed read is a 500.
30
32
  (0, artifact_store_1.useArtifactStore)();
@@ -90,21 +92,6 @@ describe('team visibility on /recordings reads (integration)', function () {
90
92
  data: Object.assign({ id: `rec-b-${stamp}`, group_id: GROUP_TEAM_B, device_udid: TEAM_B_UDID }, baseRecording),
91
93
  });
92
94
  }));
93
- after(() => __awaiter(this, void 0, void 0, function* () {
94
- yield prisma_1.prisma.recording.deleteMany({
95
- where: { group_id: { in: [GROUP_SHARED, GROUP_TEAM_A, GROUP_TEAM_B] } },
96
- });
97
- yield prisma_1.prisma.device.deleteMany({
98
- where: { udid: { in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID] } },
99
- });
100
- yield prisma_1.prisma.teamMember.deleteMany({
101
- where: { teamId: { in: [teamA.id, teamB.id] } },
102
- });
103
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
104
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
105
- yield sa.cleanup();
106
- yield aliceMember.cleanup();
107
- }));
108
95
  function buildApp() {
109
96
  const app = (0, express_1.default)();
110
97
  app.use(express_1.default.json());
@@ -113,19 +100,19 @@ describe('team visibility on /recordings reads (integration)', function () {
113
100
  return app;
114
101
  }
115
102
  it('Alice GET /recordings/:teamBGroup → 404', () => __awaiter(this, void 0, void 0, function* () {
116
- const r = yield (0, supertest_1.default)(buildApp())
103
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
117
104
  .get(`/recordings/${GROUP_TEAM_B}`)
118
105
  .set('Cookie', aliceMember.cookie);
119
106
  (0, chai_1.expect)(r.status).to.equal(404);
120
107
  }));
121
108
  it('SA GET /recordings/:teamBGroup → 200 (admin sees all)', () => __awaiter(this, void 0, void 0, function* () {
122
- const r = yield (0, supertest_1.default)(buildApp())
109
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
123
110
  .get(`/recordings/${GROUP_TEAM_B}`)
124
111
  .set('Cookie', sa.cookie);
125
112
  (0, chai_1.expect)(r.status).to.equal(200);
126
113
  }));
127
114
  it('Alice GET /recordings/:teamAGroup → 200 (member of team A)', () => __awaiter(this, void 0, void 0, function* () {
128
- const r = yield (0, supertest_1.default)(buildApp())
115
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
129
116
  .get(`/recordings/${GROUP_TEAM_A}`)
130
117
  .set('Cookie', aliceMember.cookie);
131
118
  (0, chai_1.expect)(r.status).to.equal(200);
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const typedi_1 = require("typedi");
20
20
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
21
21
  const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
@@ -23,6 +23,7 @@ const bug_report_1 = __importDefault(require("../../src/app/routers/bug-report")
23
23
  const TeamService_1 = require("../../src/services/TeamService");
24
24
  const prisma_1 = require("../../src/prisma");
25
25
  const seedUser_1 = require("../helpers/seedUser");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
26
27
  /**
27
28
  * A session's data follows its phone's team, run as real queries (the session
28
29
  * list, builds and healing reads build Prisma filters that a stub would only
@@ -32,6 +33,7 @@ const seedUser_1 = require("../helpers/seedUser");
32
33
  */
33
34
  describe('team visibility on session data (integration)', function () {
34
35
  this.timeout(60000);
36
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
35
37
  let sa;
36
38
  let alice;
37
39
  let teamA;
@@ -123,21 +125,6 @@ describe('team visibility on session data (integration)', function () {
123
125
  });
124
126
  }
125
127
  }));
126
- after(() => __awaiter(this, void 0, void 0, function* () {
127
- const ids = [...Object.values(S), `${S.b}-2`];
128
- yield prisma_1.prisma.sessionLog.deleteMany({ where: { session_id: { in: ids } } });
129
- yield prisma_1.prisma.selectorState.deleteMany({
130
- where: { original_selector: { in: [MUTED, MUTED_B] } },
131
- });
132
- yield prisma_1.prisma.session.deleteMany({ where: { id: { in: ids } } });
133
- yield prisma_1.prisma.build.deleteMany({ where: { id: { in: [mixedBuild.id, teamBBuild.id] } } });
134
- yield prisma_1.prisma.device.deleteMany({ where: { udid: { in: Object.values(U) } } });
135
- yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
136
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
137
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
138
- yield sa.cleanup();
139
- yield alice.cleanup();
140
- }));
141
128
  function app() {
142
129
  const a = (0, express_1.default)();
143
130
  a.use(express_1.default.json());
@@ -146,8 +133,8 @@ describe('team visibility on session data (integration)', function () {
146
133
  bug_report_1.default.register(a);
147
134
  return a;
148
135
  }
149
- const asAlice = (url) => (0, supertest_1.default)(app()).get(url).set('Cookie', alice.cookie);
150
- const asSa = (url) => (0, supertest_1.default)(app()).get(url).set('Cookie', sa.cookie);
136
+ const asAlice = (url) => (0, loopbackRequest_1.default)(app()).get(url).set('Cookie', alice.cookie);
137
+ const asSa = (url) => (0, loopbackRequest_1.default)(app()).get(url).set('Cookie', sa.cookie);
151
138
  const unknownBody = (id) => ({ error: true, message: `Session with id ${id} not found` });
152
139
  it("lists a member's sessions: shared, their team's, and their own on a phone that is gone", () => __awaiter(this, void 0, void 0, function* () {
153
140
  const ids = (yield asAlice('/session')).body.map((s) => s.id);
@@ -265,7 +252,7 @@ describe('team visibility on session data (integration)', function () {
265
252
  }));
266
253
  });
267
254
  it("refuses a bug report for another team's session as for an unknown one", () => __awaiter(this, void 0, void 0, function* () {
268
- const r = yield (0, supertest_1.default)(app())
255
+ const r = yield (0, loopbackRequest_1.default)(app())
269
256
  .post(`/sessions/${S.b}/bug-report?mode=full`)
270
257
  .set('Cookie', alice.cookie)
271
258
  .set('Origin', 'http://127.0.0.1');
@@ -15,25 +15,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const users_1 = require("../../src/app/routers/users");
21
- const prisma_1 = require("../../src/prisma");
21
+ const scratch_database_1 = require("../helpers/scratch-database");
22
22
  const seedUser_1 = require("../helpers/seedUser");
23
23
  describe('users CRUD flow (integration)', function () {
24
24
  this.timeout(60000);
25
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
25
26
  let sa;
26
27
  let admin;
27
28
  before(() => __awaiter(this, void 0, void 0, function* () {
28
29
  sa = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Flow SA' });
29
30
  admin = yield (0, seedUser_1.seedUser)('ADMIN', { name: 'Flow Admin' });
30
31
  }));
31
- after(() => __awaiter(this, void 0, void 0, function* () {
32
- yield sa.cleanup();
33
- yield admin.cleanup();
34
- // Anything created during the flow with the flow-bob email tag.
35
- yield prisma_1.prisma.user.deleteMany({ where: { email: { contains: 'flow-bob' } } });
36
- }));
37
32
  function buildApp() {
38
33
  const app = (0, express_1.default)();
39
34
  app.use(express_1.default.json());
@@ -44,27 +39,27 @@ describe('users CRUD flow (integration)', function () {
44
39
  it('SA creates an admin -> admin creates a member -> admin lists', () => __awaiter(this, void 0, void 0, function* () {
45
40
  const app = buildApp();
46
41
  // ADMIN role-create — should fail with 403 (admin can't create admins).
47
- const denied = yield (0, supertest_1.default)(app)
42
+ const denied = yield (0, loopbackRequest_1.default)(app)
48
43
  .post('/users')
49
44
  .set('Cookie', admin.cookie)
50
45
  .send({ email: 'flow-admin2@xenon.local', name: 'Admin 2', role: 'ADMIN' });
51
46
  (0, chai_1.expect)(denied.status).to.equal(403);
52
47
  // SA creates a member.
53
48
  const memberEmail = `flow-bob-${Date.now()}@xenon.local`;
54
- const created = yield (0, supertest_1.default)(app)
49
+ const created = yield (0, loopbackRequest_1.default)(app)
55
50
  .post('/users')
56
51
  .set('Cookie', sa.cookie)
57
52
  .send({ email: memberEmail, name: 'Bob', role: 'MEMBER' });
58
53
  (0, chai_1.expect)(created.status).to.equal(201);
59
54
  (0, chai_1.expect)(created.body.temporaryPassword).to.match(/^[A-Za-z0-9_-]{12,}$/);
60
55
  // Admin lists — sees the new member; does NOT see SA or other admins.
61
- const listAdmin = yield (0, supertest_1.default)(app).get('/users').set('Cookie', admin.cookie);
56
+ const listAdmin = yield (0, loopbackRequest_1.default)(app).get('/users').set('Cookie', admin.cookie);
62
57
  (0, chai_1.expect)(listAdmin.status).to.equal(200);
63
58
  const emailsAdmin = listAdmin.body.map((u) => u.email);
64
59
  (0, chai_1.expect)(emailsAdmin).to.include(memberEmail);
65
60
  (0, chai_1.expect)(emailsAdmin).to.not.include(sa.user.email);
66
61
  // SA lists — sees everyone.
67
- const listSA = yield (0, supertest_1.default)(app).get('/users').set('Cookie', sa.cookie);
62
+ const listSA = yield (0, loopbackRequest_1.default)(app).get('/users').set('Cookie', sa.cookie);
68
63
  const emailsSA = listSA.body.map((u) => u.email);
69
64
  (0, chai_1.expect)(emailsSA).to.include(sa.user.email);
70
65
  (0, chai_1.expect)(emailsSA).to.include(admin.user.email);
@@ -15,13 +15,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const users_1 = require("../../src/app/routers/users");
21
21
  const prisma_1 = require("../../src/prisma");
22
22
  const seedUser_1 = require("../helpers/seedUser");
23
+ const scratch_database_1 = require("../helpers/scratch-database");
23
24
  describe('users lockout protections (integration)', function () {
24
25
  this.timeout(60000);
26
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
25
27
  function buildApp() {
26
28
  const app = (0, express_1.default)();
27
29
  app.use(express_1.default.json());
@@ -32,7 +34,7 @@ describe('users lockout protections (integration)', function () {
32
34
  it('SA cannot delete themselves', () => __awaiter(this, void 0, void 0, function* () {
33
35
  const me = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Lockout Self' });
34
36
  try {
35
- const r = yield (0, supertest_1.default)(buildApp()).delete(`/users/${me.user.id}`).set('Cookie', me.cookie);
37
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).delete(`/users/${me.user.id}`).set('Cookie', me.cookie);
36
38
  (0, chai_1.expect)(r.status).to.equal(400);
37
39
  (0, chai_1.expect)(r.body.error).to.match(/yourself/);
38
40
  }
@@ -43,7 +45,7 @@ describe('users lockout protections (integration)', function () {
43
45
  it('SA cannot demote themselves', () => __awaiter(this, void 0, void 0, function* () {
44
46
  const me = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'Lockout Demote-Self' });
45
47
  try {
46
- const r = yield (0, supertest_1.default)(buildApp())
48
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
47
49
  .patch(`/users/${me.user.id}`)
48
50
  .set('Cookie', me.cookie)
49
51
  .send({ role: 'MEMBER' });
@@ -121,4 +121,70 @@ describe('EmailService', () => {
121
121
  }
122
122
  });
123
123
  });
124
+ // A reset link Xenon sends points at XENON_PUBLIC_URL, never the request's
125
+ // Host, so with SMTP but no address set no link is emailed at all. Said once
126
+ // at startup, so an operator upgrading learns why self-service reset went.
127
+ describe('warnAboutPublicUrl', () => {
128
+ it('warns when Xenon could send links but has no XENON_PUBLIC_URL to point them at', () => {
129
+ const orig = {
130
+ url: config_1.config.smtpUrl,
131
+ fb: config_1.config.passwordResetLogFallback,
132
+ pub: config_1.config.publicUrl,
133
+ };
134
+ const svc = new EmailService_1.EmailService();
135
+ const warn = sinon_1.default.stub(svc, 'warnLog');
136
+ try {
137
+ config_1.config.smtpUrl = undefined;
138
+ config_1.config.passwordResetLogFallback = false;
139
+ config_1.config.publicUrl = undefined;
140
+ svc.warnAboutPublicUrl();
141
+ (0, chai_1.expect)(warn.called, 'nothing could be sent anyway').to.equal(false);
142
+ config_1.config.smtpUrl = 'smtp://mail.example.com:587';
143
+ config_1.config.publicUrl = 'https://xenon.example.com';
144
+ svc.warnAboutPublicUrl();
145
+ (0, chai_1.expect)(warn.called).to.equal(false);
146
+ config_1.config.publicUrl = 'not a url';
147
+ svc.warnAboutPublicUrl();
148
+ (0, chai_1.expect)(warn.calledOnce).to.equal(true);
149
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.match(/XENON_PUBLIC_URL/);
150
+ }
151
+ finally {
152
+ config_1.config.smtpUrl = orig.url;
153
+ config_1.config.passwordResetLogFallback = orig.fb;
154
+ config_1.config.publicUrl = orig.pub;
155
+ }
156
+ });
157
+ // The variable also drives each device's dashboard_link, so a value
158
+ // Xenon refuses is said at startup whether or not mail is set up. It was
159
+ // silent without SMTP. The value itself is not logged: it may carry a
160
+ // user name and password, one of the reasons it is refused.
161
+ it('warns, once, when XENON_PUBLIC_URL is set but refused, mail or not', () => {
162
+ const orig = {
163
+ url: config_1.config.smtpUrl,
164
+ fb: config_1.config.passwordResetLogFallback,
165
+ pub: config_1.config.publicUrl,
166
+ };
167
+ const svc = new EmailService_1.EmailService();
168
+ const warn = sinon_1.default.stub(svc, 'warnLog');
169
+ try {
170
+ config_1.config.smtpUrl = undefined;
171
+ config_1.config.passwordResetLogFallback = false;
172
+ config_1.config.publicUrl = 'https://user:secret@lab.example.com/qa';
173
+ svc.warnAboutPublicUrl();
174
+ (0, chai_1.expect)(warn.calledOnce).to.equal(true);
175
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.match(/XENON_PUBLIC_URL is set/);
176
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.match(/dashboard/);
177
+ (0, chai_1.expect)(warn.firstCall.args[0]).to.not.include('secret');
178
+ warn.resetHistory();
179
+ config_1.config.smtpUrl = 'smtp://mail.example.com:587';
180
+ svc.warnAboutPublicUrl();
181
+ (0, chai_1.expect)(warn.calledOnce, 'one warning with SMTP too').to.equal(true);
182
+ }
183
+ finally {
184
+ config_1.config.smtpUrl = orig.url;
185
+ config_1.config.passwordResetLogFallback = orig.fb;
186
+ config_1.config.publicUrl = orig.pub;
187
+ }
188
+ });
189
+ });
124
190
  });
@@ -13,7 +13,8 @@ require("reflect-metadata");
13
13
  const chai_1 = require("chai");
14
14
  const types_1 = require("../../src/services/healing/types");
15
15
  const ResilioTreeHealingProvider_1 = require("../../src/services/healing/ResilioTreeHealingProvider");
16
- const { JSDOMParser, Path } = require('resiliotree');
16
+ const xmldom_1 = require("@xmldom/xmldom");
17
+ const resilioPath_1 = require("../../src/services/healing/resilioPath");
17
18
  describe('ResilioTreeHealingProvider', () => {
18
19
  let provider;
19
20
  let mockEtalonService;
@@ -43,32 +44,26 @@ describe('ResilioTreeHealingProvider', () => {
43
44
  };
44
45
  provider = new ResilioTreeHealingProvider_1.ResilioTreeHealingProvider(mockEtalonService);
45
46
  });
46
- it('should heal an element when a ResilioTree path exists', () => __awaiter(void 0, void 0, void 0, function* () {
47
- const parser = new JSDOMParser();
48
- const root = parser.parse(sourceXml);
49
- // Find the button in source
50
- const bodyNode = root.children[0];
51
- const hierarchyNode = bodyNode.children[0];
52
- const frameLayout = hierarchyNode.children[0];
53
- const linearLayout = frameLayout.children[0];
54
- const buttonNode = linearLayout.children[0];
55
- const path = new Path([root, bodyNode, hierarchyNode, frameLayout, linearLayout, buttonNode]);
47
+ // The button's id and text both changed, so it is another element as far as
48
+ // its path can tell: Resilio leaves it to Fuzzy XML rather than guess. (This
49
+ // test used to expect a heal, from a path made by an HTML parse and a driver
50
+ // stub that accepted any XPath, including the lowercase one Resilio wrote,
51
+ // which no real driver matches. See resilio-path-healing.spec.ts.)
52
+ it('leaves an element whose id and text changed to the next tier', () => __awaiter(void 0, void 0, void 0, function* () {
53
+ const doc = new xmldom_1.DOMParser().parseFromString(sourceXml, 'text/xml');
54
+ const button = doc.getElementsByTagName('android.widget.Button')[0];
56
55
  mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
57
56
  return ({
58
57
  selector: "//android.widget.Button[@text='Submit']",
59
58
  strategy: 'xpath',
60
59
  attributes: { text: 'Submit', 'resource-id': 'com.example:id/submit_btn' },
61
60
  nodeName: 'android.widget.Button',
62
- path: path.toJSON(),
61
+ path: (0, resilioPath_1.resilioPathOf)(button, sourceXml),
63
62
  lastSeen: Date.now(),
64
63
  });
65
64
  });
66
65
  const mockDriver = {
67
- findElement: (strategy, selector) => __awaiter(void 0, void 0, void 0, function* () {
68
- (0, chai_1.expect)(strategy).to.equal('xpath');
69
- (0, chai_1.expect)(selector.toLowerCase()).to.contain('android.widget.button');
70
- return { ELEMENT: 'healed-element-123' };
71
- }),
66
+ findElement: () => __awaiter(void 0, void 0, void 0, function* () { return ({ ELEMENT: 'any-element' }); }),
72
67
  };
73
68
  const context = {
74
69
  sessionId: 'test-session',
@@ -77,11 +72,43 @@ describe('ResilioTreeHealingProvider', () => {
77
72
  selector: "//android.widget.Button[@text='Submit']",
78
73
  pageSource: brokenXml,
79
74
  };
80
- const result = yield provider.heal(context);
81
- (0, chai_1.expect)(result).to.not.be.null;
75
+ (0, chai_1.expect)(yield provider.heal(context)).to.equal(null);
76
+ }));
77
+ it('heals the same element after the page changed around it', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ const doc = new xmldom_1.DOMParser().parseFromString(sourceXml, 'text/xml');
79
+ const button = doc.getElementsByTagName('android.widget.Button')[0];
80
+ mockEtalonService.getSignature = () => __awaiter(void 0, void 0, void 0, function* () {
81
+ return ({
82
+ selector: "//android.widget.Button[@text='Submit']",
83
+ strategy: 'xpath',
84
+ attributes: {},
85
+ nodeName: 'android.widget.Button',
86
+ path: (0, resilioPath_1.resilioPathOf)(button, sourceXml),
87
+ lastSeen: Date.now(),
88
+ });
89
+ });
90
+ // The button moved into a new row; it is still "Submit".
91
+ const movedXml = sourceXml
92
+ .replace('<android.widget.Button index="0"', '<android.widget.LinearLayout index="0"><android.widget.Button index="0"')
93
+ .replace('resource-id="com.example:id/submit_btn" />', 'resource-id="com.example:id/submit_btn" /></android.widget.LinearLayout>');
94
+ const asked = [];
95
+ const result = yield provider.heal({
96
+ sessionId: 'test-session',
97
+ driver: {
98
+ findElement: (_using, selector) => __awaiter(void 0, void 0, void 0, function* () {
99
+ asked.push(selector);
100
+ return { ELEMENT: 'healed-element-123' };
101
+ }),
102
+ },
103
+ strategy: 'xpath',
104
+ selector: "//android.widget.Button[@text='Submit']",
105
+ pageSource: movedXml,
106
+ });
82
107
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.equal('healed-element-123');
83
108
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.tier).to.equal(types_1.HealingTier.TIER_1_RECOVERY);
84
109
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.message).to.contain('ResilioTree');
110
+ // The element's own id, which it kept.
111
+ (0, chai_1.expect)(asked[0]).to.equal("//*[@resource-id='com.example:id/submit_btn']");
85
112
  }));
86
113
  it('should return null if no signature/path is found', () => __awaiter(void 0, void 0, void 0, function* () {
87
114
  const context = {
@@ -56,6 +56,7 @@ describe('TeamService (User-keyed)', () => {
56
56
  }));
57
57
  it('delete() blocks when team has devices OR active members', () => __awaiter(void 0, void 0, void 0, function* () {
58
58
  sinon_1.default.stub(prisma_1.prisma.device, 'count').resolves(1);
59
+ sinon_1.default.stub(prisma_1.prisma.deviceSetting, 'findMany').resolves([]);
59
60
  sinon_1.default.stub(prisma_1.prisma.teamMember, 'count').resolves(0);
60
61
  sinon_1.default.stub(prisma_1.prisma.app, 'count').resolves(0);
61
62
  let err;
@@ -72,6 +73,7 @@ describe('TeamService (User-keyed)', () => {
72
73
  // as for its phones: the admin moves them first.
73
74
  it('delete() blocks while the team still owns apps', () => __awaiter(void 0, void 0, void 0, function* () {
74
75
  sinon_1.default.stub(prisma_1.prisma.device, 'count').resolves(0);
76
+ sinon_1.default.stub(prisma_1.prisma.deviceSetting, 'findMany').resolves([]);
75
77
  sinon_1.default.stub(prisma_1.prisma.teamMember, 'count').resolves(0);
76
78
  const apps = sinon_1.default.stub(prisma_1.prisma.app, 'count').resolves(2);
77
79
  const teamDel = sinon_1.default.stub(prisma_1.prisma.team, 'delete').resolves({});
@@ -89,6 +91,7 @@ describe('TeamService (User-keyed)', () => {
89
91
  }));
90
92
  it('delete() drops FK on revoked apiKeys then deletes the team', () => __awaiter(void 0, void 0, void 0, function* () {
91
93
  sinon_1.default.stub(prisma_1.prisma.device, 'count').resolves(0);
94
+ sinon_1.default.stub(prisma_1.prisma.deviceSetting, 'findMany').resolves([]);
92
95
  sinon_1.default.stub(prisma_1.prisma.teamMember, 'count').resolves(0);
93
96
  sinon_1.default.stub(prisma_1.prisma.app, 'count').resolves(0);
94
97
  const apiKeyUpdate = sinon_1.default.stub(prisma_1.prisma.apiKey, 'updateMany').resolves({ count: 0 });
@@ -47,9 +47,11 @@ describe('XenonCapabilityManager.getXenonCapabilities — interceptor activation
47
47
  expect(out[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME]).to.equal('new-name');
48
48
  expect(out[XenonCapabilityManager_1.XENON_CAPABILITIES.BUILD_NAME]).to.equal('nightly');
49
49
  });
50
- it('treats no interceptor cap as disabled', () => {
50
+ it("leaves no interceptor cap unset, for the server's `interceptor` option to decide", () => {
51
+ // Off unless the server's option turns it on: resolveInterceptorOptions,
52
+ // test/unit/phone-network/interceptor-options.spec.ts.
51
53
  const out = (0, XenonCapabilityManager_1.getXenonCapabilities)(caps({}));
52
- expect(out[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(false);
54
+ expect(out[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]).to.equal(undefined);
53
55
  });
54
56
  it('does not throw when firstMatch is missing entirely', () => {
55
57
  const malformed = { alwaysMatch: { interceptor: { enabled: true } } };