@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const SessionLifecycleService_1 = require("../../../src/services/SessionLifecycleService");
20
+ const NetworkConditioningService_1 = require("../../../src/services/NetworkConditioningService");
21
+ const InterceptorService_1 = require("../../../src/services/InterceptorService");
22
+ const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
23
+ const PluginContext_1 = require("../../../src/PluginContext");
24
+ const IPluginArgs_1 = require("../../../src/interfaces/IPluginArgs");
25
+ const XenonCapabilityManager_1 = require("../../../src/XenonCapabilityManager");
26
+ const SessionManager_1 = require("../../../src/sessions/SessionManager");
27
+ const SessionType_1 = __importDefault(require("../../../src/enums/SessionType"));
28
+ const container_registration_1 = require("../../helpers/container-registration");
29
+ const scratch_database_1 = require("../../helpers/scratch-database");
30
+ const android = { udid: 'R5CT', platform: 'android', deviceType: 'real', host: 'h' };
31
+ const sessionOf = (type) => ({
32
+ getId: () => 's1',
33
+ getType: () => type,
34
+ getCapabilities: () => ({}),
35
+ apiKeyId: null,
36
+ userId: null,
37
+ startVideoRecording: sinon_1.default.stub().resolves(),
38
+ });
39
+ const capsWith = (extra) => (Object.assign({ [XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING]: false, [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS]: [] }, extra));
40
+ /**
41
+ * Where a session's network changes are made. Only the server that drives the
42
+ * phone makes them: a hub used to run its own adb against a node's phone (on
43
+ * one Mac, the very same phone), and start a second proxy for it.
44
+ */
45
+ describe('a session’s network changes at its start', function () {
46
+ this.timeout(30000);
47
+ // The row a hub writes for a session another server runs (recordRoutedSession).
48
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
49
+ let restoreRegs;
50
+ let order;
51
+ let applyProfile;
52
+ let start;
53
+ let restoreLeftovers;
54
+ let context;
55
+ beforeEach(() => {
56
+ restoreRegs = (0, container_registration_1.saveRegistrations)(NetworkConditioningService_1.NetworkConditioningService, InterceptorService_1.InterceptorService, PhoneNetworkRestore_1.PhoneNetworkRestore, PluginContext_1.PluginContext);
57
+ order = [];
58
+ applyProfile = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('profile'); }));
59
+ start = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('interceptor'); }));
60
+ restoreLeftovers = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
61
+ order.push('leftovers');
62
+ return 0;
63
+ }));
64
+ typedi_1.Container.set(NetworkConditioningService_1.NetworkConditioningService, { applyProfile });
65
+ typedi_1.Container.set(InterceptorService_1.InterceptorService, { start });
66
+ typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreLeftovers });
67
+ context = new PluginContext_1.PluginContext();
68
+ context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
69
+ typedi_1.Container.set(PluginContext_1.PluginContext, context);
70
+ });
71
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
72
+ sinon_1.default.restore();
73
+ SessionManager_1.SESSION_MANAGER.removeSession('s1');
74
+ restoreRegs();
75
+ yield scratch.db.session.deleteMany({});
76
+ }));
77
+ const apply = (type, caps) => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).applyPostSessionLogic(sessionOf(type), caps, android);
78
+ it('puts back what an earlier session left on the phone, then applies its own', () => __awaiter(this, void 0, void 0, function* () {
79
+ yield apply(SessionType_1.default.LOCAL, capsWith({
80
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
81
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
82
+ }));
83
+ (0, chai_1.expect)(restoreLeftovers.calledOnceWith({ udid: 'R5CT' })).to.equal(true);
84
+ (0, chai_1.expect)(order).to.deep.equal(['leftovers', 'profile', 'interceptor']);
85
+ }));
86
+ it("doesn't touch a phone another server drives", () => __awaiter(this, void 0, void 0, function* () {
87
+ for (const type of [SessionType_1.default.REMOTE, SessionType_1.default.CLOUD]) {
88
+ yield apply(type, capsWith({
89
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
90
+ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
91
+ }));
92
+ SessionManager_1.SESSION_MANAGER.removeSession('s1');
93
+ }
94
+ (0, chai_1.expect)(order).to.deep.equal([]);
95
+ }));
96
+ it("starts the interceptor by the server's option when the session doesn't say", () => __awaiter(this, void 0, void 0, function* () {
97
+ context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true, bufferSize: 9 } });
98
+ yield apply(SessionType_1.default.LOCAL, capsWith({}));
99
+ (0, chai_1.expect)(start.calledOnce).to.equal(true);
100
+ const [sessionId, device, opts] = start.firstCall.args;
101
+ (0, chai_1.expect)(sessionId).to.equal('s1');
102
+ (0, chai_1.expect)(device).to.equal(android);
103
+ (0, chai_1.expect)(opts).to.include({ enabled: true, bufferSize: 9, captureBodies: true });
104
+ }));
105
+ it("leaves the interceptor off when the session turns it off over the server's option", () => __awaiter(this, void 0, void 0, function* () {
106
+ context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true } });
107
+ yield apply(SessionType_1.default.LOCAL, capsWith({ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: false }));
108
+ (0, chai_1.expect)(start.called).to.equal(false);
109
+ }));
110
+ });
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const net_1 = __importDefault(require("net"));
18
+ const xenonProxy_1 = require("../../../src/services/network/xenonProxy");
19
+ describe('a phone proxy setting, read', () => {
20
+ it('parses host:port', () => {
21
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('127.0.0.1:11100')).to.deep.equal({ host: '127.0.0.1', port: 11100 });
22
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(' 10.0.2.2:11105 ')).to.deep.equal({ host: '10.0.2.2', port: 11105 });
23
+ });
24
+ it('reads no proxy from what Android stores for none', () => {
25
+ for (const none of [null, undefined, '', 'null', ':0', ' ']) {
26
+ (0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)(none), String(none)).to.equal(true);
27
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(none)).to.equal(null);
28
+ }
29
+ (0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)('proxy.corp:3128')).to.equal(false);
30
+ });
31
+ it('refuses a value that is not host:port', () => {
32
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('proxy.corp')).to.equal(null);
33
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:b')).to.equal(null);
34
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:0')).to.equal(null);
35
+ (0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:70000')).to.equal(null);
36
+ });
37
+ it('knows the addresses a phone uses to reach this machine', () => {
38
+ const own = ['192.168.1.20'];
39
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('127.0.0.1', own)).to.equal(true);
40
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('localhost', own)).to.equal(true);
41
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('10.0.2.2', own)).to.equal(true);
42
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.20', own)).to.equal(true);
43
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.21', own)).to.equal(false);
44
+ (0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('proxy.corp', own)).to.equal(false);
45
+ });
46
+ });
47
+ describe('isPortListening', function () {
48
+ let server;
49
+ let livePort;
50
+ let deadPort;
51
+ before(() => __awaiter(this, void 0, void 0, function* () {
52
+ server = net_1.default.createServer((s) => s.destroy());
53
+ yield new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
54
+ livePort = server.address().port;
55
+ const closed = net_1.default.createServer();
56
+ yield new Promise((r) => closed.listen(0, '127.0.0.1', () => r()));
57
+ deadPort = closed.address().port;
58
+ yield new Promise((r) => closed.close(() => r()));
59
+ }));
60
+ after(() => __awaiter(this, void 0, void 0, function* () {
61
+ yield new Promise((r) => server.close(() => r()));
62
+ }));
63
+ it('says a port with a listener answers', () => __awaiter(this, void 0, void 0, function* () {
64
+ (0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(livePort)).to.equal(true);
65
+ }));
66
+ it('says a closed port does not', () => __awaiter(this, void 0, void 0, function* () {
67
+ (0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(deadPort)).to.equal(false);
68
+ }));
69
+ });
@@ -81,7 +81,7 @@ describe('Device filter tests', () => {
81
81
  (0, chai_1.expect)(filter).to.deep.equal({
82
82
  platform: 'ios',
83
83
  platformVersion: '14.0',
84
- name: 'iPhone',
84
+ appleFamily: 'iphone',
85
85
  deviceType: 'real',
86
86
  udid: ['21112-1111-1111-111'],
87
87
  minSDK: undefined,
@@ -113,7 +113,7 @@ describe('Device filter tests', () => {
113
113
  (0, chai_1.expect)(filter).to.deep.equal({
114
114
  platform: 'ios',
115
115
  platformVersion: '14.0',
116
- name: 'iPhone',
116
+ appleFamily: 'iphone',
117
117
  filterByHost: undefined,
118
118
  deviceType: 'simulator',
119
119
  udid: [],
@@ -140,7 +140,7 @@ describe('Device filter tests', () => {
140
140
  platform: 'ios',
141
141
  filterByHost: undefined,
142
142
  platformVersion: undefined,
143
- name: 'iPhone',
143
+ appleFamily: 'iphone',
144
144
  deviceType: 'simulator',
145
145
  udid: [],
146
146
  minSDK: '10.2.0',
@@ -39,7 +39,12 @@ describe('profile router', () => {
39
39
  }));
40
40
  it('POST /profile/access-key/rotate returns a new accessKey', () => __awaiter(void 0, void 0, void 0, function* () {
41
41
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey').resolves({ id: 'u1', accessKey: 'xen_NEW' });
42
- const app = appWithAuth({ userId: 'u1', role: 'ADMIN', scopes: 'devices,sessions,read' });
42
+ const app = appWithAuth({
43
+ kind: 'user-session',
44
+ userId: 'u1',
45
+ role: 'ADMIN',
46
+ scopes: 'admin,devices,sessions,read',
47
+ });
43
48
  const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
44
49
  (0, chai_1.expect)(r.body.accessKey).to.equal('xen_NEW');
45
50
  }));
@@ -147,4 +152,120 @@ describe('profile router', () => {
147
152
  const r = yield (0, loopbackRequest_1.default)(app).delete('/profile/tokens/k1');
148
153
  (0, chai_1.expect)(r.status).to.equal(404);
149
154
  }));
155
+ // A credential can't do more to its owner's account than it could itself
156
+ // have been made to do.
157
+ describe('what the credential in use limits', () => {
158
+ // Rotating the access key ends every key pair its owner has, whatever
159
+ // their scopes, so it takes the person themselves (a dashboard sign-in)
160
+ // or a credential with the admin scope. Any credential, a read-only
161
+ // token included, could do it.
162
+ describe('POST /profile/access-key/rotate', () => {
163
+ let rotate;
164
+ beforeEach(() => {
165
+ rotate = sinon_1.default
166
+ .stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey')
167
+ .resolves({ id: 'u1', accessKey: 'xen_NEW' });
168
+ });
169
+ for (const [kind, scopes] of [
170
+ ['bearer', 'read'],
171
+ ['api-key', 'read'],
172
+ ['api-key', 'devices,sessions,read'],
173
+ ['bearer', 'sessions'],
174
+ ]) {
175
+ it(`is refused to ${kind === 'bearer' ? 'a Bearer token' : 'an API key'} with ${scopes}`, () => __awaiter(void 0, void 0, void 0, function* () {
176
+ const app = appWithAuth({ kind, userId: 'u1', role: 'ADMIN', scopes });
177
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
178
+ (0, chai_1.expect)(r.status).to.equal(403);
179
+ (0, chai_1.expect)(r.body.error).to.match(/dashboard sign-in or a credential with the admin scope/);
180
+ (0, chai_1.expect)(rotate.called).to.equal(false);
181
+ }));
182
+ }
183
+ it('is allowed to a dashboard sign-in, a member included', () => __awaiter(void 0, void 0, void 0, function* () {
184
+ const app = appWithAuth({
185
+ kind: 'user-session',
186
+ userId: 'u1',
187
+ role: 'MEMBER',
188
+ scopes: 'devices,sessions,read',
189
+ });
190
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
191
+ }));
192
+ it('is allowed to a credential with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
193
+ const app = appWithAuth({
194
+ kind: 'api-key',
195
+ userId: 'u1',
196
+ role: 'SUPER_ADMIN',
197
+ scopes: 'admin',
198
+ });
199
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
200
+ }));
201
+ });
202
+ // A token made with a credential that expires ends no later than it: a
203
+ // 1-hour Bearer token made itself a key that never expired.
204
+ describe('POST /profile/tokens from a credential that expires', () => {
205
+ const inAnHour = new Date(Date.now() + 3600000);
206
+ const bearer = () => appWithAuth({
207
+ kind: 'bearer',
208
+ userId: 'u1',
209
+ role: 'MEMBER',
210
+ scopes: 'sessions,read',
211
+ credentialExpiresAt: inAnHour.getTime(),
212
+ });
213
+ let create;
214
+ beforeEach(() => {
215
+ create = sinon_1.default
216
+ .stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
217
+ .resolves({ id: 'k9', raw: 'raw' });
218
+ });
219
+ it('expires with that credential when no expiresAt is asked for', () => __awaiter(void 0, void 0, void 0, function* () {
220
+ var _a;
221
+ const r = yield (0, loopbackRequest_1.default)(bearer()).post('/profile/tokens').send({ name: 'CI' });
222
+ (0, chai_1.expect)(r.status).to.equal(201);
223
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(inAnHour.toISOString());
224
+ (0, chai_1.expect)((_a = create.firstCall.args[0].expiresAt) === null || _a === void 0 ? void 0 : _a.getTime()).to.equal(inAnHour.getTime());
225
+ }));
226
+ it('refuses an expiresAt later than that credential', () => __awaiter(void 0, void 0, void 0, function* () {
227
+ const later = new Date(inAnHour.getTime() + 60000).toISOString();
228
+ const r = yield (0, loopbackRequest_1.default)(bearer())
229
+ .post('/profile/tokens')
230
+ .send({ name: 'CI', expiresAt: later });
231
+ (0, chai_1.expect)(r.status).to.equal(400);
232
+ (0, chai_1.expect)(r.body.error).to.match(/later than the credential/);
233
+ (0, chai_1.expect)(create.called).to.equal(false);
234
+ }));
235
+ // Verification allows 60 s of clock skew, so a token can still be
236
+ // accepted just after its exp: it must not make a key already expired.
237
+ it('refuses when that credential has already expired', () => __awaiter(void 0, void 0, void 0, function* () {
238
+ const app = appWithAuth({
239
+ kind: 'bearer',
240
+ userId: 'u1',
241
+ role: 'MEMBER',
242
+ scopes: 'sessions,read',
243
+ credentialExpiresAt: Date.now() - 1000,
244
+ });
245
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
246
+ (0, chai_1.expect)(r.status).to.equal(400);
247
+ (0, chai_1.expect)(r.body.error).to.match(/has expired/);
248
+ (0, chai_1.expect)(create.called).to.equal(false);
249
+ }));
250
+ it('keeps an earlier expiresAt', () => __awaiter(void 0, void 0, void 0, function* () {
251
+ const sooner = new Date(Date.now() + 600000).toISOString();
252
+ const r = yield (0, loopbackRequest_1.default)(bearer())
253
+ .post('/profile/tokens')
254
+ .send({ name: 'CI', expiresAt: sooner });
255
+ (0, chai_1.expect)(r.status).to.equal(201);
256
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(sooner);
257
+ }));
258
+ it('leaves a dashboard sign-in free to make a key that never expires', () => __awaiter(void 0, void 0, void 0, function* () {
259
+ const app = appWithAuth({
260
+ kind: 'user-session',
261
+ userId: 'u1',
262
+ role: 'MEMBER',
263
+ scopes: 'devices,sessions,read',
264
+ });
265
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
266
+ (0, chai_1.expect)(r.status).to.equal(201);
267
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(null);
268
+ }));
269
+ });
270
+ });
150
271
  });
@@ -126,7 +126,7 @@ describe('createSession records who asked on the pending-session row', () => {
126
126
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
127
127
  verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
128
128
  const [sub, teamId] = t.replace(/^jwt:/, '').split('@');
129
- return { sub, teamId: teamId !== null && teamId !== void 0 ? teamId : null };
129
+ return { sub, teamId: teamId !== null && teamId !== void 0 ? teamId : null, scopes: 'sessions' };
130
130
  }),
131
131
  });
132
132
  teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const typedi_1 = require("typedi");
19
+ const plugin_1 = require("../../src/plugin");
20
+ const ServerManager_1 = require("../../src/services/ServerManager");
21
+ const PluginContext_1 = require("../../src/PluginContext");
22
+ const concurrency_gate_1 = require("../../src/services/recording/concurrency-gate");
23
+ const config_1 = require("../../src/config");
24
+ const logger_1 = __importDefault(require("../../src/logger"));
25
+ /**
26
+ * `maxConcurrentRecordings` and `recordingsAssetsPath` are declared in
27
+ * schema.json, so Appium accepts them in a config file, but nothing read them:
28
+ * only the XENON_MAX_CONCURRENT_RECORDINGS and XENON_RECORDINGS_ASSETS_PATH
29
+ * environment variables reached src/config.ts. The option now wins, the
30
+ * variable is the fallback, and both are applied at boot before anything that
31
+ * reads them (the artifact store, the concurrency gate).
32
+ */
33
+ describe('the recording options', () => {
34
+ let saved;
35
+ let env;
36
+ let statics;
37
+ beforeEach(() => {
38
+ saved = { max: config_1.config.maxConcurrentRecordings, path: config_1.config.recordingsAssetsPath };
39
+ env = {
40
+ max: process.env.XENON_MAX_CONCURRENT_RECORDINGS,
41
+ path: process.env.XENON_RECORDINGS_ASSETS_PATH,
42
+ };
43
+ delete process.env.XENON_MAX_CONCURRENT_RECORDINGS;
44
+ delete process.env.XENON_RECORDINGS_ASSETS_PATH;
45
+ statics = {
46
+ nodeId: plugin_1.XenonPlugin.NODE_ID,
47
+ port: plugin_1.XenonPlugin.port,
48
+ basePath: plugin_1.XenonPlugin.nodeBasePath,
49
+ };
50
+ });
51
+ afterEach(() => {
52
+ sinon_1.default.restore();
53
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: saved.max, recordingsAssetsPath: saved.path });
54
+ for (const [name, value] of [
55
+ ['XENON_MAX_CONCURRENT_RECORDINGS', env.max],
56
+ ['XENON_RECORDINGS_ASSETS_PATH', env.path],
57
+ ]) {
58
+ if (value === undefined)
59
+ delete process.env[name];
60
+ else
61
+ process.env[name] = value;
62
+ }
63
+ plugin_1.XenonPlugin.NODE_ID = statics.nodeId;
64
+ plugin_1.XenonPlugin.port = statics.port;
65
+ plugin_1.XenonPlugin.nodeBasePath = statics.basePath;
66
+ });
67
+ /**
68
+ * Boots as far as the database step and says what the options were when it
69
+ * began: the artifact store is built there from `recordingsAssetsPath`.
70
+ */
71
+ function seenAtDatabaseStep(pluginArgs) {
72
+ return __awaiter(this, void 0, void 0, function* () {
73
+ const seen = {};
74
+ const boot = ServerManager_1.ServerManager.prototype;
75
+ sinon_1.default.stub(boot, 'resolvePluginArgs').resolves(pluginArgs);
76
+ sinon_1.default.stub(logger_1.default, 'banner');
77
+ sinon_1.default.stub(PluginContext_1.PluginContext.prototype, 'setContext');
78
+ sinon_1.default.stub(boot, 'syncDatabaseAndAIConfig').resolves();
79
+ sinon_1.default.stub(boot, 'initializeCoreSubsystems').callsFake(() => __awaiter(this, void 0, void 0, function* () {
80
+ seen.max = config_1.config.maxConcurrentRecordings;
81
+ seen.path = config_1.config.recordingsAssetsPath;
82
+ throw new Error('boot stopped by the test');
83
+ }));
84
+ yield typedi_1.Container.get(ServerManager_1.ServerManager)
85
+ .updateServer({}, null, { port: 4723 })
86
+ .catch((err) => (0, chai_1.expect)(err.message).to.equal('boot stopped by the test'));
87
+ return seen;
88
+ });
89
+ }
90
+ describe('applied at boot', () => {
91
+ it('takes the maxConcurrentRecordings option', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: 2 });
93
+ (0, chai_1.expect)(seen.max).to.equal(2);
94
+ }));
95
+ it('takes the recordingsAssetsPath option', () => __awaiter(void 0, void 0, void 0, function* () {
96
+ const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: '/srv/xenon/recordings' });
97
+ (0, chai_1.expect)(seen.path).to.equal('/srv/xenon/recordings');
98
+ }));
99
+ it('falls back to the environment variables when the options are not set', () => __awaiter(void 0, void 0, void 0, function* () {
100
+ process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
101
+ process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
102
+ const seen = yield seenAtDatabaseStep({});
103
+ (0, chai_1.expect)(seen).to.deep.equal({ max: 7, path: '/mnt/recordings' });
104
+ }));
105
+ it('lets the option win over the environment variable', () => __awaiter(void 0, void 0, void 0, function* () {
106
+ process.env.XENON_MAX_CONCURRENT_RECORDINGS = '7';
107
+ process.env.XENON_RECORDINGS_ASSETS_PATH = '/mnt/recordings';
108
+ const seen = yield seenAtDatabaseStep({
109
+ maxConcurrentRecordings: 3,
110
+ recordingsAssetsPath: '/srv/xenon/recordings',
111
+ });
112
+ (0, chai_1.expect)(seen).to.deep.equal({ max: 3, path: '/srv/xenon/recordings' });
113
+ }));
114
+ it('keeps the default cap of 4 when neither is set', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ const seen = yield seenAtDatabaseStep({});
116
+ (0, chai_1.expect)(seen.max).to.equal(4);
117
+ }));
118
+ it('ignores a cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
119
+ for (const bad of [0, -2, 2.5, NaN, '3', null]) {
120
+ const seen = yield seenAtDatabaseStep({ maxConcurrentRecordings: bad });
121
+ (0, chai_1.expect)(seen.max, `option ${JSON.stringify(bad)}`).to.equal(4);
122
+ sinon_1.default.restore();
123
+ }
124
+ }));
125
+ it('ignores an environment cap that is not a whole number of at least 1', () => __awaiter(void 0, void 0, void 0, function* () {
126
+ process.env.XENON_MAX_CONCURRENT_RECORDINGS = 'many';
127
+ const seen = yield seenAtDatabaseStep({});
128
+ (0, chai_1.expect)(seen.max).to.equal(4);
129
+ }));
130
+ it('ignores a blank recordingsAssetsPath', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ const seen = yield seenAtDatabaseStep({ recordingsAssetsPath: ' ' });
132
+ (0, chai_1.expect)(seen.path).to.equal(saved.path);
133
+ }));
134
+ });
135
+ describe('the concurrency gate', () => {
136
+ it('reads the cap when it admits, not when it is built', () => {
137
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
138
+ const gate = new concurrency_gate_1.ConcurrencyGate();
139
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: 1 });
140
+ (0, chai_1.expect)(gate.getLimit()).to.equal(1);
141
+ (0, chai_1.expect)(gate.tryAcquire(['r1'])).to.equal(true);
142
+ (0, chai_1.expect)(gate.tryAcquire(['r2'])).to.equal(false);
143
+ });
144
+ it('keeps an explicit limit over the configured one', () => {
145
+ (0, config_1.updateConfig)({ maxConcurrentRecordings: 4 });
146
+ (0, chai_1.expect)(new concurrency_gate_1.ConcurrencyGate(2).getLimit()).to.equal(2);
147
+ });
148
+ });
149
+ });
150
+ describe('resolving the recording options', () => {
151
+ describe('maxConcurrentRecordings', () => {
152
+ it('takes the option, then the variable, then 4', () => {
153
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(2, '7')).to.equal(2);
154
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, '7')).to.equal(7);
155
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, undefined)).to.equal(4);
156
+ });
157
+ it('passes over a value that is not a whole number of at least 1', () => {
158
+ for (const bad of [0, -1, 1.5, NaN, Infinity, '2', null, {}]) {
159
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(bad, '7'), JSON.stringify(bad)).to.equal(7);
160
+ }
161
+ for (const bad of ['', ' ', '0', '-3', '2.5', 'many', 'NaN']) {
162
+ (0, chai_1.expect)((0, config_1.resolveMaxConcurrentRecordings)(undefined, bad), JSON.stringify(bad)).to.equal(4);
163
+ }
164
+ });
165
+ });
166
+ describe('recordingsAssetsPath', () => {
167
+ it('takes the option, then the variable, then the default folder', () => {
168
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)('/a', '/b')).to.equal('/a');
169
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '/b')).to.equal('/b');
170
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, undefined)).to.match(/\.cache\/xenon\/assets\/sessions\/recordings$/);
171
+ });
172
+ it('counts a blank value as unset', () => {
173
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' ', '/b')).to.equal('/b');
174
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(undefined, '')).to.match(/recordings$/);
175
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(42, undefined)).to.match(/recordings$/);
176
+ });
177
+ it('trims what it is given', () => {
178
+ (0, chai_1.expect)((0, config_1.resolveRecordingsAssetsPath)(' /a ', undefined)).to.equal('/a');
179
+ });
180
+ });
181
+ });