@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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useLokiStores = useLokiStores;
4
+ exports.usePrismaStores = usePrismaStores;
4
5
  const device_store_1 = require("../../src/data-service/device-store");
6
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
5
7
  // DeviceStoreFactory's cached stores, one per kind.
6
8
  const CACHES = ['_deviceStore', '_pendingSessionStore', '_cliArgsStore', '_healEtalonStore'];
7
9
  /**
@@ -39,3 +41,34 @@ function useLokiStores() {
39
41
  process.env.NODE_ENV = previousEnv;
40
42
  });
41
43
  }
44
+ /**
45
+ * The Prisma stores (what a server with SQLite runs) for the enclosing
46
+ * `describe`, however the file was started. Put the suite's database behind
47
+ * them first: `useScratchDatabase({ wholeSuite: true })`.
48
+ *
49
+ * `npm run test:all` sets NODE_ENV to 'test', so DeviceStoreFactory hands it
50
+ * Loki stores, while `npx mocha <file>` gets Prisma ones. A spec written
51
+ * against the Prisma store's queries then tested Loki in the full run, and its
52
+ * phones stayed in Loki's one collection for every later spec. This hands the
53
+ * factory new Prisma stores for the suite, then puts the old ones back. A
54
+ * module that took its store when it was imported (`grid.ts`) keeps that one.
55
+ *
56
+ * Call it inside a `describe`, before the hooks that use a store, never at the
57
+ * top of a file.
58
+ */
59
+ function usePrismaStores() {
60
+ const factory = device_store_1.DeviceStoreFactory;
61
+ const saved = {};
62
+ before(() => {
63
+ for (const key of CACHES)
64
+ saved[key] = factory[key];
65
+ factory._deviceStore = new prisma_store_1.PrismaDeviceStore();
66
+ factory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
67
+ factory._cliArgsStore = new prisma_store_1.PrismaCLIArgsStore();
68
+ factory._healEtalonStore = new prisma_store_1.PrismaHealEtalonStore();
69
+ });
70
+ after(() => {
71
+ for (const key of CACHES)
72
+ factory[key] = saved[key];
73
+ });
74
+ }
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ios = exports.android = void 0;
13
+ exports.elementIdOf = elementIdOf;
14
+ exports.pageDriver = pageDriver;
15
+ const xmldom_1 = require("@xmldom/xmldom");
16
+ const xpath_1 = require("xpath");
17
+ /**
18
+ * Page sources shaped like the drivers' own (UiAutomator2's `hierarchy`,
19
+ * XCUITest's `AppiumAUT`), and a driver that answers finds by running the
20
+ * XPath on them, as the real drivers do, for specs that check which element a
21
+ * healed locator selects. No phone.
22
+ */
23
+ const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
24
+ const attrs = (o) => Object.entries(o)
25
+ .map(([k, v]) => `${k}="${v}"`)
26
+ .join(' ');
27
+ function androidNode(tag, index, bounds, extra = {}, children) {
28
+ const a = attrs(Object.assign({ index, package: 'com.example.shop', class: tag, text: '', 'resource-id': '', 'content-desc': '', checkable: 'false', checked: 'false', clickable: 'false', enabled: 'true', focusable: 'false', focused: 'false', 'long-clickable': 'false', password: 'false', scrollable: 'false', selected: 'false', bounds, displayed: 'true' }, extra));
29
+ return children === undefined ? `<${tag} ${a} />` : `<${tag} ${a}>${children}</${tag}>`;
30
+ }
31
+ exports.android = {
32
+ button: (index, id, text, bounds) => androidNode('android.widget.Button', index, bounds, {
33
+ 'resource-id': `com.example.shop:id/${id}`,
34
+ text,
35
+ 'content-desc': text,
36
+ clickable: 'true',
37
+ focusable: 'true',
38
+ }),
39
+ field: (index, id, text, bounds) => androidNode('android.widget.EditText', index, bounds, {
40
+ 'resource-id': `com.example.shop:id/${id}`,
41
+ text,
42
+ clickable: 'true',
43
+ focusable: 'true',
44
+ }),
45
+ text: (index, id, text, bounds) => androidNode('android.widget.TextView', index, bounds, {
46
+ 'resource-id': `com.example.shop:id/${id}`,
47
+ text,
48
+ }),
49
+ image: (index, id, description, bounds) => androidNode('android.widget.ImageView', index, bounds, {
50
+ 'resource-id': `com.example.shop:id/${id}`,
51
+ 'content-desc': description,
52
+ }),
53
+ /** A clickable row holding other elements, as list rows and compound buttons are. */
54
+ row: (index, id, bounds, children) => androidNode('android.widget.LinearLayout', index, bounds, { 'resource-id': `com.example.shop:id/${id}`, clickable: 'true', focusable: 'true' }, children),
55
+ layout: (index, id, bounds, children) => androidNode('android.widget.LinearLayout', index, bounds, { 'resource-id': id ? `com.example.shop:id/${id}` : '' }, children),
56
+ page: (children) => "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>" +
57
+ '<hierarchy index="0" class="hierarchy" rotation="0" width="1080" height="2220">' +
58
+ androidNode('android.widget.FrameLayout', 0, '[0,0][1080,2220]', {}, children) +
59
+ '</hierarchy>',
60
+ };
61
+ function iosNode(type, a, children = '') {
62
+ return `<${type} ${attrs(Object.assign({ type, enabled: 'true', visible: 'true', accessible: 'true' }, a))}>${children}</${type}>`;
63
+ }
64
+ exports.ios = {
65
+ button: (name, label, y) => iosNode('XCUIElementTypeButton', { name, label, x: 20, y, width: 350, height: 44 }),
66
+ field: (name, value, y) => iosNode('XCUIElementTypeTextField', { name, value, x: 20, y, width: 350, height: 44 }),
67
+ group: (children) => iosNode('XCUIElementTypeOther', { x: 0, y: 100, width: 390, height: 600 }, children),
68
+ page: (children) => '<?xml version="1.0" encoding="UTF-8"?><AppiumAUT>' +
69
+ iosNode('XCUIElementTypeApplication', { name: 'Shop', label: 'Shop', x: 0, y: 0, width: 390, height: 844 }, iosNode('XCUIElementTypeWindow', { x: 0, y: 0, width: 390, height: 844 }, children)) +
70
+ '</AppiumAUT>',
71
+ };
72
+ /** The element id the fake driver gives an element: its resource-id or name. */
73
+ function elementIdOf(el) {
74
+ return `el:${el.getAttribute('resource-id') || el.getAttribute('name') || el.tagName}`;
75
+ }
76
+ /**
77
+ * A driver showing `pageSource`: finds run the XPath on it and answer the
78
+ * first match, attributes and rects are the element's own. `show` changes the
79
+ * screen.
80
+ */
81
+ function pageDriver(pageSource) {
82
+ let doc = new xmldom_1.DOMParser().parseFromString(pageSource, 'text/xml');
83
+ let source = pageSource;
84
+ const known = new Map();
85
+ const select = (using, value) => {
86
+ if (using !== 'xpath')
87
+ throw new Error(`unsupported locator strategy ${using}`);
88
+ return (0, xpath_1.select)(value, doc).filter((n) => n.nodeType === 1);
89
+ };
90
+ const ref = (el) => {
91
+ const id = elementIdOf(el);
92
+ known.set(id, el);
93
+ return { ELEMENT: id, [W3C_ELEMENT_KEY]: id };
94
+ };
95
+ const element = (id) => {
96
+ const el = known.get(id);
97
+ if (!el)
98
+ throw new Error(`no element ${id}`);
99
+ return el;
100
+ };
101
+ return {
102
+ show(next) {
103
+ source = next;
104
+ doc = new xmldom_1.DOMParser().parseFromString(next, 'text/xml');
105
+ },
106
+ getPageSource() {
107
+ return __awaiter(this, void 0, void 0, function* () {
108
+ return source;
109
+ });
110
+ },
111
+ findElement(using, value) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ const [first] = select(using, value);
114
+ if (!first) {
115
+ throw Object.assign(new Error('NoSuchElement: An element could not be located'), {
116
+ name: 'NoSuchElementError',
117
+ });
118
+ }
119
+ return ref(first);
120
+ });
121
+ },
122
+ findElements(using, value) {
123
+ return __awaiter(this, void 0, void 0, function* () {
124
+ return select(using, value).map(ref);
125
+ });
126
+ },
127
+ caps: { platformName: /<XCUIElementType/.test(pageSource) ? 'iOS' : 'Android' },
128
+ /**
129
+ * As the drivers answer: getAttribute(name, elementId). UiAutomator2
130
+ * answers String(value), so an attribute the element hasn't set is
131
+ * "null"; WebDriverAgent answers null.
132
+ */
133
+ getAttribute(name, id) {
134
+ return __awaiter(this, void 0, void 0, function* () {
135
+ const value = element(id).getAttribute(name);
136
+ if (value)
137
+ return value;
138
+ return /<XCUIElementType/.test(source) ? null : 'null';
139
+ });
140
+ },
141
+ getName(id) {
142
+ return __awaiter(this, void 0, void 0, function* () {
143
+ return element(id).tagName;
144
+ });
145
+ },
146
+ getElementRect(id) {
147
+ return __awaiter(this, void 0, void 0, function* () {
148
+ var _a;
149
+ const el = element(id);
150
+ const bounds = el.getAttribute('bounds');
151
+ if (bounds) {
152
+ const [x1, y1, x2, y2] = ((_a = bounds.match(/\d+/g)) !== null && _a !== void 0 ? _a : []).map(Number);
153
+ return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
154
+ }
155
+ return {
156
+ x: Number(el.getAttribute('x')),
157
+ y: Number(el.getAttribute('y')),
158
+ width: Number(el.getAttribute('width')),
159
+ height: Number(el.getAttribute('height')),
160
+ };
161
+ });
162
+ },
163
+ };
164
+ }
@@ -29,6 +29,7 @@ const MODELS = [
29
29
  'profiling',
30
30
  'app',
31
31
  'device',
32
+ 'deviceSetting',
32
33
  'pendingSession',
33
34
  'cLIArgs',
34
35
  'webhookConfig',
@@ -51,16 +52,54 @@ const MODELS = [
51
52
  'sessionMetric',
52
53
  'selectorEvent',
53
54
  ];
55
+ let scratchCount = 0;
56
+ let template;
57
+ /**
58
+ * A database migrated once for the whole process, which each suite copies.
59
+ * Removed when the process exits.
60
+ */
61
+ function migratedTemplate() {
62
+ if (template)
63
+ return template;
64
+ const file = path_1.default.join(os_1.default.tmpdir(), `xenon-scratch-template-${process.pid}-${Date.now()}.db`);
65
+ (0, child_process_1.execSync)('npx prisma migrate deploy', {
66
+ cwd: path_1.default.resolve(__dirname, '../..'),
67
+ env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: `file:${file}` }),
68
+ stdio: 'pipe',
69
+ });
70
+ process.once('exit', () => {
71
+ for (const f of [file, `${file}-journal`]) {
72
+ try {
73
+ fs_1.default.unlinkSync(f);
74
+ }
75
+ catch (_a) {
76
+ // already gone
77
+ }
78
+ }
79
+ });
80
+ template = file;
81
+ return file;
82
+ }
54
83
  /**
55
84
  * Back the whole `prisma` wrapper with a scratch SQLite file for the enclosing
56
85
  * `describe`, so the real queries (the device store, the session lifecycle,
57
86
  * SessionOwnerResolver) run against a real database that is not the server's.
58
87
  * Like useScratchPortLeases, for every model.
59
88
  *
60
- * Call it inside a `describe`, never at the top of a file. The database is
61
- * migrated once per suite; the stubs are made before each test and removed
62
- * after it, so a spec's own `sinon.restore()` can't leave the next test on
63
- * the real database. Tests empty the tables they use.
89
+ * Call it inside a `describe`, never at the top of a file. Each suite gets a
90
+ * fresh database: a copy of one migrated once per process (migrating takes
91
+ * seconds). The stubs are made before each test and removed after it, so a
92
+ * spec's own `sinon.restore()` can't leave the next test on the real
93
+ * database. Tests empty the tables they use.
94
+ *
95
+ * With `wholeSuite`, the stubs are made once, before the suite's own `before`
96
+ * hooks (call it first in the describe), and removed after the suite. Making
97
+ * them takes tens of milliseconds, which a suite of hundreds of short tests
98
+ * otherwise pays per test. Its `before` hooks can then seed the rows every
99
+ * test reads, and nothing needs deleting: the file goes with the suite. Only
100
+ * for a suite that doesn't stub or restore `prisma` itself. Its own `after`
101
+ * hooks run once the stubs are gone, so they must not touch `prisma`: it is
102
+ * the real database again by then.
64
103
  *
65
104
  * With `captureQueries`, `queries` holds the SQL each test ran, so a spec can
66
105
  * ask SQLite how it reads a table (`EXPLAIN QUERY PLAN`).
@@ -71,13 +110,10 @@ function useScratchDatabase(options = {}) {
71
110
  let dbPath = '';
72
111
  before(function () {
73
112
  this.timeout(90000);
74
- dbPath = path_1.default.join(os_1.default.tmpdir(), `xenon-scratch-${process.pid}-${Date.now()}.db`);
113
+ scratchCount += 1;
114
+ dbPath = path_1.default.join(os_1.default.tmpdir(), `xenon-scratch-${process.pid}-${Date.now()}-${scratchCount}.db`);
115
+ fs_1.default.copyFileSync(migratedTemplate(), dbPath);
75
116
  const url = `file:${dbPath}`;
76
- (0, child_process_1.execSync)('npx prisma migrate deploy', {
77
- cwd: path_1.default.resolve(__dirname, '../..'),
78
- env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
79
- stdio: 'pipe',
80
- });
81
117
  if (options.captureQueries) {
82
118
  const db = new client_1.PrismaClient({
83
119
  datasources: { db: { url } },
@@ -89,9 +125,10 @@ function useScratchDatabase(options = {}) {
89
125
  else {
90
126
  ctx.db = new client_1.PrismaClient({ datasources: { db: { url } } });
91
127
  }
128
+ if (options.wholeSuite)
129
+ stubPrisma();
92
130
  });
93
- beforeEach(() => {
94
- ctx.queries.length = 0;
131
+ function stubPrisma() {
95
132
  for (const model of MODELS) {
96
133
  const wrapper = prisma_1.prisma[model];
97
134
  const delegate = ctx.db[model];
@@ -101,8 +138,16 @@ function useScratchDatabase(options = {}) {
101
138
  sandbox.stub(wrapper, name).callsFake((...args) => delegate[name](...args));
102
139
  }
103
140
  }
141
+ }
142
+ beforeEach(() => {
143
+ ctx.queries.length = 0;
144
+ if (!options.wholeSuite)
145
+ stubPrisma();
146
+ });
147
+ afterEach(() => {
148
+ if (!options.wholeSuite)
149
+ sandbox.restore();
104
150
  });
105
- afterEach(() => sandbox.restore());
106
151
  after(() => __awaiter(this, void 0, void 0, function* () {
107
152
  sandbox.restore();
108
153
  if (ctx.db)
@@ -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 sinon_1 = __importDefault(require("sinon"));
20
20
  // Static import (matches the passing integration-spec idiom, e.g. selector-health-endpoint.spec):
21
21
  // a dynamic `await import('...routers/audit')` fails ESM resolution on the extensionless path
@@ -37,7 +37,7 @@ describe('POST /xenon/api/audit/events', () => {
37
37
  app.use('/xenon/api/audit', (0, audit_1.makeRouter)({ eventLogService: eventLogSvc }));
38
38
  });
39
39
  it('202s on a valid batch of 2 events and appends each via EventLogService.appendSafe with type mcp_audit', () => __awaiter(void 0, void 0, void 0, function* () {
40
- const res = yield (0, supertest_1.default)(app)
40
+ const res = yield (0, loopbackRequest_1.default)(app)
41
41
  .post('/xenon/api/audit/events')
42
42
  .send({
43
43
  events: [
@@ -70,20 +70,20 @@ describe('POST /xenon/api/audit/events', () => {
70
70
  (0, chai_1.expect)(secondArg.payload).to.deep.include({ sessionId: 'sess-1' });
71
71
  }));
72
72
  it('400s when events is missing', () => __awaiter(void 0, void 0, void 0, function* () {
73
- const res = yield (0, supertest_1.default)(app).post('/xenon/api/audit/events').send({});
73
+ const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send({});
74
74
  (0, chai_1.expect)(res.status).to.equal(400);
75
75
  (0, chai_1.expect)(res.body.error).to.equal('bad_request');
76
76
  (0, chai_1.expect)(eventLogSvc.appendSafe.called).to.be.false;
77
77
  }));
78
78
  it('400s when events is not an array', () => __awaiter(void 0, void 0, void 0, function* () {
79
- const res = yield (0, supertest_1.default)(app)
79
+ const res = yield (0, loopbackRequest_1.default)(app)
80
80
  .post('/xenon/api/audit/events')
81
81
  .send({ events: 'nope' });
82
82
  (0, chai_1.expect)(res.status).to.equal(400);
83
83
  (0, chai_1.expect)(res.body.error).to.equal('bad_request');
84
84
  }));
85
85
  it('400s when a batch event is malformed (missing tool)', () => __awaiter(void 0, void 0, void 0, function* () {
86
- const res = yield (0, supertest_1.default)(app)
86
+ const res = yield (0, loopbackRequest_1.default)(app)
87
87
  .post('/xenon/api/audit/events')
88
88
  .send({
89
89
  events: [{ subject: 'agent-1', decision: 'allow', latencyMs: 10 }],
@@ -100,7 +100,7 @@ describe('POST /xenon/api/audit/events', () => {
100
100
  decision: 'allow',
101
101
  latencyMs: i,
102
102
  }));
103
- const res = yield (0, supertest_1.default)(app).post('/xenon/api/audit/events').send({ events });
103
+ const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send({ events });
104
104
  (0, chai_1.expect)(res.status).to.equal(400);
105
105
  (0, chai_1.expect)(res.body.error).to.equal('bad_request');
106
106
  (0, chai_1.expect)(eventLogSvc.appendSafe.called).to.be.false;
@@ -131,12 +131,12 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
131
131
  });
132
132
  it('401s when unauthenticated', () => __awaiter(void 0, void 0, void 0, function* () {
133
133
  const app = yield makeApp(eventLogSvc)();
134
- const res = yield (0, supertest_1.default)(app).post('/xenon/api/audit/events').send(validBody);
134
+ const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send(validBody);
135
135
  (0, chai_1.expect)(res.status).to.equal(401);
136
136
  }));
137
137
  it('403s for a role below MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
138
138
  const app = yield makeApp(eventLogSvc)();
139
- const res = yield (0, supertest_1.default)(app)
139
+ const res = yield (0, loopbackRequest_1.default)(app)
140
140
  .post('/xenon/api/audit/events')
141
141
  .set('x-test-role', 'BOGUS')
142
142
  .set('x-test-scopes', 'admin')
@@ -145,7 +145,7 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
145
145
  }));
146
146
  it('403s for MEMBER role without the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
147
147
  const app = yield makeApp(eventLogSvc)();
148
- const res = yield (0, supertest_1.default)(app)
148
+ const res = yield (0, loopbackRequest_1.default)(app)
149
149
  .post('/xenon/api/audit/events')
150
150
  .set('x-test-role', 'MEMBER')
151
151
  .set('x-test-scopes', 'devices')
@@ -155,7 +155,7 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
155
155
  }));
156
156
  it('202s for MEMBER role with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
157
157
  const app = yield makeApp(eventLogSvc)();
158
- const res = yield (0, supertest_1.default)(app)
158
+ const res = yield (0, loopbackRequest_1.default)(app)
159
159
  .post('/xenon/api/audit/events')
160
160
  .set('x-test-role', 'MEMBER')
161
161
  .set('x-test-scopes', 'admin')
@@ -14,13 +14,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
- const supertest_1 = __importDefault(require("supertest"));
17
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
18
18
  const express_1 = __importDefault(require("express"));
19
19
  const auth_1 = require("../../src/app/routers/auth");
20
20
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
21
- const prisma_1 = require("../../src/prisma");
22
21
  const typedi_1 = require("typedi");
23
22
  const UserService_1 = require("../../src/services/UserService");
23
+ const scratch_database_1 = require("../helpers/scratch-database");
24
24
  function buildApp() {
25
25
  const app = (0, express_1.default)();
26
26
  app.use(express_1.default.json());
@@ -32,10 +32,9 @@ function buildApp() {
32
32
  }
33
33
  describe('auth flow (integration)', function () {
34
34
  this.timeout(30000);
35
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
35
36
  let user;
36
37
  before(() => __awaiter(this, void 0, void 0, function* () {
37
- yield prisma_1.prisma.userSession.deleteMany({ where: { user: { email: 'loginflow@xenon.local' } } });
38
- yield prisma_1.prisma.user.deleteMany({ where: { email: 'loginflow@xenon.local' } });
39
38
  user = yield typedi_1.Container.get(UserService_1.UserService).createUser({
40
39
  email: 'loginflow@xenon.local',
41
40
  name: 'Login Flow',
@@ -43,31 +42,27 @@ describe('auth flow (integration)', function () {
43
42
  role: 'ADMIN',
44
43
  });
45
44
  }));
46
- after(() => __awaiter(this, void 0, void 0, function* () {
47
- yield prisma_1.prisma.userSession.deleteMany({ where: { userId: user.id } });
48
- yield prisma_1.prisma.user.delete({ where: { id: user.id } });
49
- }));
50
45
  it('login → cookie → /whoami → logout → 401', () => __awaiter(this, void 0, void 0, function* () {
51
46
  var _a;
52
47
  const app = buildApp();
53
- const login = yield (0, supertest_1.default)(app)
48
+ const login = yield (0, loopbackRequest_1.default)(app)
54
49
  .post('/api/auth/login')
55
50
  .send({ email: 'loginflow@xenon.local', password: 'flow-test-12' });
56
51
  (0, chai_1.expect)(login.status).to.equal(204);
57
52
  const cookie = (_a = login.headers['set-cookie']) === null || _a === void 0 ? void 0 : _a[0];
58
53
  (0, chai_1.expect)(cookie).to.match(/^xenon_dashboard_session=/);
59
- const me = yield (0, supertest_1.default)(app).get('/api/whoami').set('Cookie', cookie);
54
+ const me = yield (0, loopbackRequest_1.default)(app).get('/api/whoami').set('Cookie', cookie);
60
55
  (0, chai_1.expect)(me.status).to.equal(200);
61
56
  (0, chai_1.expect)(me.body.userId).to.equal(user.id);
62
57
  (0, chai_1.expect)(me.body.kind).to.equal('user-session');
63
- const out = yield (0, supertest_1.default)(app).post('/api/auth/logout').set('Cookie', cookie);
58
+ const out = yield (0, loopbackRequest_1.default)(app).post('/api/auth/logout').set('Cookie', cookie);
64
59
  (0, chai_1.expect)(out.status).to.equal(204);
65
- const me2 = yield (0, supertest_1.default)(app).get('/api/whoami').set('Cookie', cookie);
60
+ const me2 = yield (0, loopbackRequest_1.default)(app).get('/api/whoami').set('Cookie', cookie);
66
61
  (0, chai_1.expect)(me2.status).to.equal(401);
67
62
  }));
68
63
  it('login with wrong password returns generic 401 (no enumeration)', () => __awaiter(this, void 0, void 0, function* () {
69
64
  const app = buildApp();
70
- const login = yield (0, supertest_1.default)(app)
65
+ const login = yield (0, loopbackRequest_1.default)(app)
71
66
  .post('/api/auth/login')
72
67
  .send({ email: 'loginflow@xenon.local', password: 'WRONG' });
73
68
  (0, chai_1.expect)(login.status).to.equal(401);
@@ -75,7 +70,7 @@ describe('auth flow (integration)', function () {
75
70
  }));
76
71
  it('login with unknown email returns the same generic 401', () => __awaiter(this, void 0, void 0, function* () {
77
72
  const app = buildApp();
78
- const login = yield (0, supertest_1.default)(app)
73
+ const login = yield (0, loopbackRequest_1.default)(app)
79
74
  .post('/api/auth/login')
80
75
  .send({ email: 'no-such@xenon.local', password: 'whatever' });
81
76
  (0, chai_1.expect)(login.status).to.equal(401);
@@ -15,22 +15,25 @@ 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 auth_1 = require("../../src/app/routers/auth");
20
+ const scratch_database_1 = require("../helpers/scratch-database");
20
21
  describe('login rate limit', function () {
21
22
  this.timeout(15000);
23
+ // Each attempt looks the email address up.
24
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
22
25
  it('5 bad logins from one IP → 6th returns 429 with Retry-After', () => __awaiter(this, void 0, void 0, function* () {
23
26
  const app = (0, express_1.default)();
24
27
  app.use(express_1.default.json());
25
28
  app.use('/auth', (0, auth_1.authPublicRouter)());
26
29
  for (let i = 0; i < 5; i++) {
27
- const r = yield (0, supertest_1.default)(app)
30
+ const r = yield (0, loopbackRequest_1.default)(app)
28
31
  .post('/auth/login')
29
32
  .set('X-Forwarded-For', '9.9.9.9')
30
33
  .send({ email: 'no@no.com', password: 'no' });
31
34
  (0, chai_1.expect)(r.status).to.equal(401);
32
35
  }
33
- const blocked = yield (0, supertest_1.default)(app)
36
+ const blocked = yield (0, loopbackRequest_1.default)(app)
34
37
  .post('/auth/login')
35
38
  .set('X-Forwarded-For', '9.9.9.9')
36
39
  .send({ email: 'no@no.com', password: 'no' });
@@ -48,7 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
48
48
  require("reflect-metadata");
49
49
  const chai_1 = require("chai");
50
50
  const express_1 = __importDefault(require("express"));
51
- const supertest_1 = __importDefault(require("supertest"));
51
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
52
52
  const sinon_1 = __importDefault(require("sinon"));
53
53
  const fs = __importStar(require("fs"));
54
54
  const path = __importStar(require("path"));
@@ -57,11 +57,14 @@ const unzipper = __importStar(require("unzipper"));
57
57
  const prisma_1 = require("../../src/prisma");
58
58
  const config_1 = require("../../src/config");
59
59
  const bug_report_1 = __importDefault(require("../../src/app/routers/bug-report"));
60
+ const authMiddleware_1 = require("../../src/middleware/authMiddleware");
60
61
  describe('POST /sessions/:id/bug-report (integration)', () => {
61
62
  let tmpAssets;
63
+ let savedAssetsPath;
62
64
  const fixtureMp4 = path.join(__dirname, 'fixtures/bug-report/recording.mp4');
63
65
  beforeEach(() => {
64
66
  tmpAssets = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-it-'));
67
+ savedAssetsPath = config_1.config.sessionAssetsPath;
65
68
  config_1.config.sessionAssetsPath = tmpAssets;
66
69
  const sessionDir = path.join(tmpAssets, 'sess-it', 'video');
67
70
  fs.mkdirSync(sessionDir, { recursive: true });
@@ -69,10 +72,27 @@ describe('POST /sessions/:id/bug-report (integration)', () => {
69
72
  });
70
73
  afterEach(() => {
71
74
  sinon_1.default.restore();
75
+ config_1.config.sessionAssetsPath = savedAssetsPath;
72
76
  fs.rmSync(tmpAssets, { recursive: true, force: true });
73
77
  });
78
+ /**
79
+ * The router sits behind authMiddleware in the API, and its roleGuard
80
+ * answers 401 to a request with no `req.auth` (27fd825, three days after
81
+ * this spec). Stand in for the middleware with an admin, as it sets one:
82
+ * no `teamIds`, so the session needs no Device row to be visible.
83
+ */
74
84
  function makeApp() {
75
85
  const app = (0, express_1.default)();
86
+ app.use((req, _res, next) => {
87
+ req.auth = {
88
+ kind: 'user-session',
89
+ userId: 'usr_admin',
90
+ role: 'ADMIN',
91
+ scopes: (0, authMiddleware_1.scopesForRole)('ADMIN'),
92
+ rateLimit: 100,
93
+ };
94
+ next();
95
+ });
76
96
  bug_report_1.default.register(app);
77
97
  return app;
78
98
  }
@@ -94,7 +114,7 @@ describe('POST /sessions/:id/bug-report (integration)', () => {
94
114
  sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findMany').resolves([
95
115
  { createdAt: new Date(), title: 'click', response: 'fail' },
96
116
  ]);
97
- const res = yield (0, supertest_1.default)(makeApp())
117
+ const res = yield (0, loopbackRequest_1.default)(makeApp())
98
118
  .post('/sessions/sess-it/bug-report?mode=full')
99
119
  .buffer(true)
100
120
  .parse((r, cb) => {
@@ -15,22 +15,25 @@ 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 auth_1 = require("../../src/app/routers/auth");
20
+ const scratch_database_1 = require("../helpers/scratch-database");
20
21
  describe('forgot-password rate limit', function () {
21
22
  this.timeout(15000);
23
+ // Each attempt looks the email address up.
24
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
22
25
  it('3 attempts from one IP -> 4th returns 429 with Retry-After', () => __awaiter(this, void 0, void 0, function* () {
23
26
  const app = (0, express_1.default)();
24
27
  app.use(express_1.default.json());
25
28
  app.use('/auth', (0, auth_1.authPublicRouter)());
26
29
  for (let i = 0; i < 3; i++) {
27
- const r = yield (0, supertest_1.default)(app)
30
+ const r = yield (0, loopbackRequest_1.default)(app)
28
31
  .post('/auth/forgot-password')
29
32
  .set('X-Forwarded-For', '7.7.7.7')
30
33
  .send({ email: 'whatever@xenon.local' });
31
34
  (0, chai_1.expect)(r.status).to.equal(204);
32
35
  }
33
- const blocked = yield (0, supertest_1.default)(app)
36
+ const blocked = yield (0, loopbackRequest_1.default)(app)
34
37
  .post('/auth/forgot-password')
35
38
  .set('X-Forwarded-For', '7.7.7.7')
36
39
  .send({ email: 'whatever@xenon.local' });