@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
@@ -8,6 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
11
22
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
23
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
24
  };
@@ -15,6 +26,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
26
  exports.DeviceStoreFactory = void 0;
16
27
  const deviceFieldOwners_1 = require("./deviceFieldOwners");
17
28
  const deviceClaims_1 = require("./deviceClaims");
29
+ const appleFamily_1 = require("./appleFamily");
30
+ const deviceSettings_1 = require("./deviceSettings");
31
+ const lodash_1 = __importDefault(require("lodash"));
18
32
  const logger_1 = __importDefault(require("../logger"));
19
33
  const semver_1 = __importDefault(require("semver"));
20
34
  const db_1 = require("./db");
@@ -63,6 +77,10 @@ class LokiDeviceStore {
63
77
  if (!nameRegex.test(device.name || ''))
64
78
  return false;
65
79
  }
80
+ // iPhone / iPad Filter (see appleFamilyOf)
81
+ if (filterOptions.appleFamily && (0, appleFamily_1.appleFamilyOf)(device) !== filterOptions.appleFamily) {
82
+ return false;
83
+ }
66
84
  // UDID Filter
67
85
  if (filterOptions.udid) {
68
86
  if (Array.isArray(filterOptions.udid)) {
@@ -147,6 +165,16 @@ class LokiDeviceStore {
147
165
  }
148
166
  updateDevice(udid, host, updateData) {
149
167
  return __awaiter(this, void 0, void 0, function* () {
168
+ // Saved for the phone first, as PrismaDeviceStore.updateDevice does.
169
+ const settings = lodash_1.default.cloneDeep(lodash_1.default.omitBy((0, deviceFieldOwners_1.pickSettingFields)(updateData), (v) => v === undefined));
170
+ if (Object.keys(settings).length > 0) {
171
+ const model = yield db_1.XenonDatabase.DeviceSettingsModel;
172
+ const saved = model.findOne({ udid, host });
173
+ if (saved)
174
+ model.update(Object.assign(saved, settings));
175
+ else
176
+ model.insert(Object.assign({ udid, host, userBlocked: false }, settings));
177
+ }
150
178
  (yield db_1.XenonDatabase.DeviceModel)
151
179
  .chain()
152
180
  .find({ udid, host })
@@ -167,7 +195,10 @@ class LokiDeviceStore {
167
195
  }
168
196
  addDevices(devices_1) {
169
197
  return __awaiter(this, arguments, void 0, function* (devices, options = {}) {
198
+ var _a;
170
199
  const deviceModel = yield db_1.XenonDatabase.DeviceModel;
200
+ const settingsModel = yield db_1.XenonDatabase.DeviceSettingsModel;
201
+ const now = Date.now();
171
202
  const added = [];
172
203
  for (const device of devices) {
173
204
  const existing = deviceModel.findOne({ udid: device.udid, host: device.host });
@@ -184,10 +215,13 @@ class LokiDeviceStore {
184
215
  continue;
185
216
  }
186
217
  if (!existing) {
218
+ // The phone's saved settings, as PrismaDeviceStore.addDevices.
219
+ const saved = settingsModel.findOne({ udid: device.udid, host: (_a = device.host) !== null && _a !== void 0 ? _a : 'Local' });
220
+ const start = saved ? (0, deviceSettings_1.restoredColumns)(saved, now) : {};
187
221
  const cleanDevice = options.nodeReport
188
- ? Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(device)), { udid: device.udid, host: device.host, busy: nodeBusy, nodeBusy,
222
+ ? Object.assign(Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickNodeReportFields)(device)), start), { udid: device.udid, host: device.host, busy: nodeBusy, nodeBusy,
189
223
  nodeHold })
190
- : Object.assign({}, device);
224
+ : Object.assign(Object.assign({}, device), start);
191
225
  if (cleanDevice.host === undefined)
192
226
  cleanDevice.host = 'Local';
193
227
  if (cleanDevice.userBlocked === undefined)
@@ -227,6 +261,43 @@ class LokiDeviceStore {
227
261
  }
228
262
  });
229
263
  }
264
+ adoptSettings() {
265
+ return __awaiter(this, void 0, void 0, function* () {
266
+ const now = Date.now();
267
+ const settingsModel = yield db_1.XenonDatabase.DeviceSettingsModel;
268
+ let adopted = 0;
269
+ for (const device of (yield db_1.XenonDatabase.DeviceModel).find()) {
270
+ if (!(0, deviceSettings_1.hasSettings)(device, now))
271
+ continue;
272
+ if (settingsModel.findOne({ udid: device.udid, host: device.host }))
273
+ continue;
274
+ settingsModel.insert(Object.assign(Object.assign({}, (0, deviceFieldOwners_1.pickSettingFields)(device)), { userBlocked: device.userBlocked === true, udid: device.udid, host: device.host }));
275
+ adopted++;
276
+ }
277
+ return adopted;
278
+ });
279
+ }
280
+ forgetSettings(onlyHosts) {
281
+ return __awaiter(this, void 0, void 0, function* () {
282
+ const model = yield db_1.XenonDatabase.DeviceSettingsModel;
283
+ if (onlyHosts) {
284
+ model
285
+ .chain()
286
+ .find({ host: { $in: [...onlyHosts] } })
287
+ .remove();
288
+ }
289
+ else {
290
+ model.removeDataOnly();
291
+ }
292
+ });
293
+ }
294
+ findSavedPhones(udid) {
295
+ return __awaiter(this, void 0, void 0, function* () {
296
+ return (yield db_1.XenonDatabase.DeviceSettingsModel)
297
+ .find({ udid })
298
+ .map((saved) => ({ udid: saved.udid, host: saved.host }));
299
+ });
300
+ }
230
301
  findDevice(filter) {
231
302
  return __awaiter(this, void 0, void 0, function* () {
232
303
  return (yield db_1.XenonDatabase.DeviceModel).findOne(filter);
@@ -378,7 +449,9 @@ class LokiHealEtalonStore {
378
449
  const coll = yield this.getCollection();
379
450
  const existing = coll.findOne({ selector: etalon.selector });
380
451
  if (existing) {
381
- Object.assign(existing, etalon);
452
+ // A fingerprint saved without a path keeps the one stored, as in Prisma.
453
+ const { path } = etalon, rest = __rest(etalon, ["path"]);
454
+ Object.assign(existing, rest, path == null ? {} : { path });
382
455
  coll.update(existing);
383
456
  }
384
457
  else {
@@ -9,6 +9,8 @@ exports.hasNoClaim = hasNoClaim;
9
9
  exports.isUnheld = isUnheld;
10
10
  exports.nodeHoldOf = nodeHoldOf;
11
11
  exports.heldHere = heldHere;
12
+ exports.countsTowardMaxSessions = countsTowardMaxSessions;
13
+ exports.sessionCap = sessionCap;
12
14
  const manualLock_1 = require("../services/recording/manualLock");
13
15
  /**
14
16
  * Why a phone is busy, on the server that allocates it.
@@ -126,5 +128,40 @@ function heldHere(device) {
126
128
  return true;
127
129
  return device.session_id != null || device.claimedAt != null || device.claimSessionId != null;
128
130
  }
131
+ /**
132
+ * Whether a busy phone uses one of `maxSessions`' slots: an Appium session
133
+ * runs on it, or is being created on it.
134
+ *
135
+ * - **Counts:** this server's claim, pending (the session is being created) or
136
+ * with its session id; an Appium session's id in `session_id`; on a hub, a
137
+ * node's phone the node reports busy, unless the node says that is a preview.
138
+ * - **Does not count:** a live preview or a recording (`manual_...` in
139
+ * `session_id`), and a phone an SDK lease holds that has no session on it
140
+ * (`busy` alone, see above). Neither is an Appium session, and a person
141
+ * watching three phones must not stop tests running on a fourth. A session
142
+ * started on a leased phone claims it, so it counts like any other.
143
+ *
144
+ * `busy` alone is not enough: until this existed the cap counted every busy
145
+ * phone, and compared with `===`.
146
+ */
147
+ function countsTowardMaxSessions(device) {
148
+ if (!device.busy)
149
+ return false;
150
+ if (device.claimedAt != null || device.claimSessionId != null)
151
+ return true;
152
+ if (device.session_id != null)
153
+ return !(0, manualLock_1.isManualLock)(device.session_id);
154
+ return device.nodeBusy === true && !device.nodeHold;
155
+ }
156
+ /**
157
+ * The `maxSessions` setting as a limit: undefined (no limit) unless it is a
158
+ * positive number. 0 or less would hold every session back for ever, which
159
+ * nobody sets on purpose.
160
+ */
161
+ function sessionCap(maxSessions) {
162
+ return typeof maxSessions === 'number' && Number.isFinite(maxSessions) && maxSessions >= 1
163
+ ? maxSessions
164
+ : undefined;
165
+ }
129
166
  /** What ending a claim resets: the claim, and the session's bookkeeping. */
130
167
  exports.CLAIM_RESET = Object.assign(Object.assign({}, exports.NO_CLAIM), { session_id: null, lastCmdExecutedAt: null, sessionStartTime: 0, newCommandTimeout: null, sessionProgress: '', owningSessionId: null, lockedAt: null });
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.NODE_REPORT_FIELDS = exports.DISCOVERY_FIELDS = void 0;
6
+ exports.NODE_REPORT_FIELDS = exports.DISCOVERY_FIELDS = exports.SETTING_FIELDS = void 0;
7
+ exports.pickSettingFields = pickSettingFields;
7
8
  exports.pickDiscoveryFields = pickDiscoveryFields;
8
9
  exports.pickNodeReportFields = pickNodeReportFields;
9
10
  exports.discoveryChanges = discoveryChanges;
@@ -23,11 +24,15 @@ const lodash_1 = __importDefault(require("lodash"));
23
24
  * (`POST /xenon/api/register`, NODE_REPORT_FIELDS). The report carries what
24
25
  * the node observes of its own phone: `discovery`, and `observed` (its health,
25
26
  * which only the server driving the phone can check). It never writes a
26
- * `runtime` column: the hub's claim for a session, and the settings the hub
27
- * owns for the phone (team, tags, reservation, block). Whether the node says
28
- * the phone is busy goes to `nodeBusy` (`report`), never to `busy`
27
+ * `runtime` column (the hub's claim for a session) or a `setting`. Whether the
28
+ * node says the phone is busy goes to `nodeBusy` (`report`), never to `busy`
29
29
  * (deviceClaims.ts).
30
30
  *
31
+ * A `setting` is what people give the phone: an admin its team, tags and
32
+ * maintenance, a member a reservation. Those are also saved apart from the
33
+ * row (DeviceSetting, deviceSettings.ts), which is deleted whenever the phone
34
+ * goes, and a new row for the phone starts from them.
35
+ *
31
36
  * Typed over every column, so a new column won't compile until it is placed.
32
37
  */
33
38
  const OWNER = {
@@ -71,13 +76,6 @@ const OWNER = {
71
76
  lockedAt: 'runtime',
72
77
  claimSessionId: 'runtime',
73
78
  claimedAt: 'runtime',
74
- userBlocked: 'runtime',
75
- reservationReason: 'runtime',
76
- reservedBy: 'runtime',
77
- reservedByUserId: 'runtime',
78
- reservedUntil: 'runtime',
79
- teamId: 'runtime',
80
- tags: 'runtime',
81
79
  totalUtilizationTimeMilliSec: 'runtime',
82
80
  total_session_count: 'runtime',
83
81
  dashboard_link: 'runtime',
@@ -91,7 +89,23 @@ const OWNER = {
91
89
  storageFree: 'observed',
92
90
  nodeBusy: 'report',
93
91
  nodeHold: 'report',
92
+ teamId: 'setting',
93
+ tags: 'setting',
94
+ userBlocked: 'setting',
95
+ reservationReason: 'setting',
96
+ reservedBy: 'setting',
97
+ reservedByUserId: 'setting',
98
+ reservedUntil: 'setting',
94
99
  };
100
+ /**
101
+ * The settings people give a phone, saved apart from its row (DeviceSetting
102
+ * has exactly these columns, besides its key).
103
+ */
104
+ exports.SETTING_FIELDS = Object.keys(OWNER).filter((column) => OWNER[column] === 'setting');
105
+ /** Only the setting columns of `data`. */
106
+ function pickSettingFields(data) {
107
+ return lodash_1.default.pick(data, exports.SETTING_FIELDS);
108
+ }
95
109
  /** The columns a sync may write to a phone it already knows. */
96
110
  exports.DISCOVERY_FIELDS = new Set(Object.keys(OWNER).filter((column) => OWNER[column] === 'discovery'));
97
111
  /** Only the discovery columns of `data`. */
@@ -0,0 +1,79 @@
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
+ exports.restoredColumns = restoredColumns;
16
+ exports.settingsDiffer = settingsDiffer;
17
+ exports.hasSettings = hasSettings;
18
+ exports.resetDevicesAtBoot = resetDevicesAtBoot;
19
+ const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
20
+ const logger_1 = __importDefault(require("../logger"));
21
+ const deviceFieldOwners_1 = require("./deviceFieldOwners");
22
+ /**
23
+ * The setting columns a new row for the phone starts with: all of them, so
24
+ * what was cleared stays cleared, and a reservation only while it holds.
25
+ */
26
+ function restoredColumns(saved, now) {
27
+ var _a, _b, _c, _d, _e;
28
+ const holds = typeof saved.reservedUntil === 'number' && saved.reservedUntil > now;
29
+ return {
30
+ teamId: (_a = saved.teamId) !== null && _a !== void 0 ? _a : null,
31
+ // A copy: the in-memory store keeps the array itself.
32
+ tags: Array.isArray(saved.tags) ? [...saved.tags] : ((_b = saved.tags) !== null && _b !== void 0 ? _b : null),
33
+ userBlocked: saved.userBlocked === true,
34
+ reservationReason: holds ? ((_c = saved.reservationReason) !== null && _c !== void 0 ? _c : null) : null,
35
+ reservedBy: holds ? ((_d = saved.reservedBy) !== null && _d !== void 0 ? _d : null) : null,
36
+ reservedByUserId: holds ? ((_e = saved.reservedByUserId) !== null && _e !== void 0 ? _e : null) : null,
37
+ reservedUntil: holds ? saved.reservedUntil : null,
38
+ };
39
+ }
40
+ /** Whether two sets of setting columns differ (restoredColumns' shape). */
41
+ function settingsDiffer(a, b) {
42
+ return deviceFieldOwners_1.SETTING_FIELDS.some((field) => JSON.stringify(a[field]) !== JSON.stringify(b[field]));
43
+ }
44
+ function hasTags(tags) {
45
+ if (tags == null)
46
+ return false;
47
+ if (Array.isArray(tags))
48
+ return tags.length > 0;
49
+ return tags !== '' && tags !== '[]';
50
+ }
51
+ /** Whether a row carries a setting that is worth keeping (adoptSettings). */
52
+ function hasSettings(row, now) {
53
+ return (row.teamId != null ||
54
+ row.userBlocked === true ||
55
+ hasTags(row.tags) ||
56
+ (typeof row.reservedUntil === 'number' && row.reservedUntil > now));
57
+ }
58
+ /**
59
+ * The device table as a server starts. It forgets the phones
60
+ * devicesClearedAtBoot names, but first saves the settings of rows that have
61
+ * none saved (rows written through 2.13 kept them only on the row). With
62
+ * `removeDevicesFromDatabaseBeforeRunningThePlugin`, the saved settings of
63
+ * those phones are forgotten with them.
64
+ */
65
+ function resetDevicesAtBoot(store, args, local) {
66
+ return __awaiter(this, void 0, void 0, function* () {
67
+ const cleared = (0, localDeviceHosts_1.devicesClearedAtBoot)(args, local);
68
+ const adopted = yield store.adoptSettings();
69
+ if (adopted > 0) {
70
+ logger_1.default.info(`Saved the team, tags, maintenance or reservation of ${adopted} phone(s)`);
71
+ }
72
+ if (args.removeDevicesFromDatabaseBeforeRunningThePlugin) {
73
+ yield store.forgetSettings(cleared);
74
+ logger_1.default.info('removeDevicesFromDatabaseBeforeRunningThePlugin is on: forgot the saved settings of ' +
75
+ (cleared ? "this server's own phones" : 'every phone'));
76
+ }
77
+ yield store.clearStorage(cleared);
78
+ });
79
+ }
@@ -41,6 +41,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
41
41
  step((generator = generator.apply(thisArg, _arguments || [])).next());
42
42
  });
43
43
  };
44
+ var __rest = (this && this.__rest) || function (s, e) {
45
+ var t = {};
46
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
47
+ t[p] = s[p];
48
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
49
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
50
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
51
+ t[p[i]] = s[p[i]];
52
+ }
53
+ return t;
54
+ };
44
55
  var __importDefault = (this && this.__importDefault) || function (mod) {
45
56
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
57
  };
@@ -53,8 +64,13 @@ const logger_1 = __importDefault(require("../logger"));
53
64
  const deviceColumns_1 = require("./deviceColumns");
54
65
  const deviceFieldOwners_1 = require("./deviceFieldOwners");
55
66
  const deviceClaims_1 = require("./deviceClaims");
67
+ const appleFamily_1 = require("./appleFamily");
68
+ const deviceSettings_1 = require("./deviceSettings");
56
69
  /** Logged once per key, so a chatty node doesn't flood the log. */
57
70
  const droppedDeviceKeys = new Set();
71
+ const keyOf = (d) => `${d.udid}@${d.host}`;
72
+ /** Prisma's `select` for a phone's key and its setting columns. */
73
+ const SETTINGS_SELECT = Object.fromEntries(['udid', 'host', ...deviceFieldOwners_1.SETTING_FIELDS].map((field) => [field, true]));
58
74
  class PrismaDeviceStore {
59
75
  get prisma() {
60
76
  return typedi_1.Container.get(prisma_service_1.PrismaService).client;
@@ -104,7 +120,7 @@ class PrismaDeviceStore {
104
120
  }
105
121
  getDevices(filterOptions) {
106
122
  return __awaiter(this, void 0, void 0, function* () {
107
- var _a;
123
+ var _a, _b;
108
124
  const where = {};
109
125
  if (filterOptions.platform) {
110
126
  where.platform = filterOptions.platform;
@@ -191,6 +207,22 @@ class PrismaDeviceStore {
191
207
  return filterOptions.tags.every((tag) => d.tags.includes(tag));
192
208
  });
193
209
  }
210
+ // `appium:iPhoneOnly` / `appium:iPadOnly`. Read from the phone's model and
211
+ // form factor first, not its name: see appleFamilyOf.
212
+ if (filterOptions.appleFamily) {
213
+ results = results.filter((d) => (0, appleFamily_1.appleFamilyOf)(d) === filterOptions.appleFamily);
214
+ }
215
+ // A name filter, as the in-memory store has always applied it: a part of
216
+ // the phone's name. Case does not matter, and a phone's marketing name
217
+ // ("iPhone 16 Pro") counts as much as the name its owner gave it.
218
+ const wanted = (_b = filterOptions.name) === null || _b === void 0 ? void 0 : _b.trim().toLowerCase();
219
+ if (wanted) {
220
+ results = results.filter((d) => {
221
+ var _a, _b;
222
+ return ((_a = d.name) !== null && _a !== void 0 ? _a : '').toLowerCase().includes(wanted) ||
223
+ ((_b = d.marketingName) !== null && _b !== void 0 ? _b : '').toLowerCase().includes(wanted);
224
+ });
225
+ }
194
226
  return results;
195
227
  });
196
228
  }
@@ -202,6 +234,16 @@ class PrismaDeviceStore {
202
234
  updateDevice(udid, host, update) {
203
235
  return __awaiter(this, void 0, void 0, function* () {
204
236
  const data = this.fromIDevice(update);
237
+ // Saved first: a row created meanwhile reads them after this, or is
238
+ // corrected by addDevices' second look (settleRestored).
239
+ const settings = (0, deviceFieldOwners_1.pickSettingFields)(data);
240
+ if (Object.keys(settings).length > 0) {
241
+ yield this.prisma.deviceSetting.upsert({
242
+ where: { udid_host: { udid, host } },
243
+ create: Object.assign({ udid, host }, settings),
244
+ update: settings,
245
+ });
246
+ }
205
247
  const result = yield this.prisma.device.updateMany({
206
248
  where: { udid, host },
207
249
  data,
@@ -209,6 +251,21 @@ class PrismaDeviceStore {
209
251
  logger_1.default.debug(`[PrismaStore] Update device ${udid} at ${host}: ${result.count} records affected`);
210
252
  });
211
253
  }
254
+ /** The settings saved for these phones, by `udid@host`. */
255
+ savedSettings(phones) {
256
+ return __awaiter(this, void 0, void 0, function* () {
257
+ if (phones.length === 0)
258
+ return new Map();
259
+ const rows = yield this.prisma.deviceSetting.findMany({
260
+ where: { OR: phones.map(({ udid, host }) => ({ udid, host })) },
261
+ select: SETTINGS_SELECT,
262
+ });
263
+ return new Map(rows.map((r) => [
264
+ keyOf(r),
265
+ r,
266
+ ]));
267
+ });
268
+ }
212
269
  touchSession(sessionId, at) {
213
270
  return __awaiter(this, void 0, void 0, function* () {
214
271
  yield this.prisma.device.updateMany({
@@ -225,22 +282,58 @@ class PrismaDeviceStore {
225
282
  where: { OR: devices.map((d) => ({ udid: d.udid, host: d.host })) },
226
283
  select: { udid: true, host: true },
227
284
  });
228
- const known = new Set(existing.map((d) => `${d.udid}@${d.host}`));
285
+ const known = new Set(existing.map((d) => keyOf(d)));
286
+ const fresh = devices.filter((d) => !known.has(keyOf(d)));
287
+ // A new row starts from the phone's saved settings, in the write that
288
+ // creates it: it is never listed, even for a moment, outside its team.
289
+ const saved = yield this.savedSettings(fresh);
290
+ const now = Date.now();
291
+ const started = new Map();
229
292
  const added = [];
230
293
  for (const device of devices) {
294
+ const key = keyOf(device);
295
+ const was = known.has(key) ? undefined : saved.get(key);
296
+ const start = was ? (0, deviceSettings_1.restoredColumns)(was, now) : {};
297
+ if (was)
298
+ started.set(key, start);
231
299
  const d = options.nodeReport
232
- ? yield this.addNodeReport(device)
300
+ ? yield this.addNodeReport(device, start)
233
301
  : yield this.prisma.device.upsert({
234
302
  // Use upsert to avoid race conditions and unique constraint errors. A
235
303
  // phone already here gets only its discovery columns (AddDevicesOptions).
236
304
  where: { udid_host: { udid: device.udid, host: device.host } },
237
305
  update: (0, deviceFieldOwners_1.pickDiscoveryFields)(this.fromIDevice(device)),
238
- create: this.fromIDevice(device),
306
+ create: Object.assign(Object.assign({}, this.fromIDevice(device)), start),
239
307
  });
240
- if (!known.has(`${device.udid}@${device.host}`))
308
+ if (!known.has(key))
241
309
  added.push(this.toIDevice(d));
242
310
  }
243
- return added;
311
+ return this.settleRestored(added, started, now);
312
+ });
313
+ }
314
+ /**
315
+ * A second look at the settings of the rows just created. A setting saved
316
+ * between the read above and the create (updateDevice saves before it
317
+ * writes the row, and found no row to write) would otherwise be missing
318
+ * from the new row until the phone came back again. Rows that need it are
319
+ * corrected and read again; the others are returned as they are.
320
+ */
321
+ settleRestored(added, started, now) {
322
+ return __awaiter(this, void 0, void 0, function* () {
323
+ const saved = yield this.savedSettings(added);
324
+ return Promise.all(added.map((device) => __awaiter(this, void 0, void 0, function* () {
325
+ const latest = saved.get(keyOf(device));
326
+ if (!latest)
327
+ return device;
328
+ const columns = (0, deviceSettings_1.restoredColumns)(latest, now);
329
+ const before = started.get(keyOf(device));
330
+ if (before && !(0, deviceSettings_1.settingsDiffer)(before, columns))
331
+ return device;
332
+ const where = { udid: device.udid, host: device.host };
333
+ yield this.prisma.device.updateMany({ where, data: columns });
334
+ const row = yield this.prisma.device.findUnique({ where: { udid_host: where } });
335
+ return row ? this.toIDevice(row) : device;
336
+ })));
244
337
  });
245
338
  }
246
339
  /**
@@ -250,7 +343,7 @@ class PrismaDeviceStore {
250
343
  * update, so a report the node sent before the hub claimed the phone can't
251
344
  * undo the claim.
252
345
  */
253
- addNodeReport(device) {
346
+ addNodeReport(device, start) {
254
347
  return __awaiter(this, void 0, void 0, function* () {
255
348
  const { udid, host } = device;
256
349
  const nodeBusy = device.busy === true;
@@ -258,7 +351,8 @@ class PrismaDeviceStore {
258
351
  const row = yield this.prisma.device.upsert({
259
352
  where: { udid_host: { udid, host } },
260
353
  update: nodeBusy ? Object.assign(Object.assign({}, reported), { busy: true }) : reported,
261
- create: Object.assign(Object.assign({}, reported), { udid, host, busy: nodeBusy }),
354
+ // The hub's own settings for the phone, never the report's.
355
+ create: Object.assign(Object.assign(Object.assign({}, reported), start), { udid, host, busy: nodeBusy }),
262
356
  });
263
357
  if (nodeBusy)
264
358
  return row;
@@ -299,6 +393,54 @@ class PrismaDeviceStore {
299
393
  yield this.prisma.device.deleteMany(onlyHosts ? { where: { host: { in: [...onlyHosts] } } } : undefined);
300
394
  });
301
395
  }
396
+ adoptSettings() {
397
+ return __awaiter(this, void 0, void 0, function* () {
398
+ const now = Date.now();
399
+ const rows = (yield this.prisma.device.findMany({
400
+ where: {
401
+ OR: [
402
+ { teamId: { not: null } },
403
+ { userBlocked: true },
404
+ { tags: { notIn: ['', '[]'] } },
405
+ { reservedUntil: { gt: now } },
406
+ ],
407
+ },
408
+ select: SETTINGS_SELECT,
409
+ }));
410
+ const candidates = rows.filter((row) => (0, deviceSettings_1.hasSettings)(row, now));
411
+ if (candidates.length === 0)
412
+ return 0;
413
+ const saved = yield this.savedSettings(candidates);
414
+ let adopted = 0;
415
+ for (let _a of candidates) {
416
+ const { udid, host } = _a, settings = __rest(_a, ["udid", "host"]);
417
+ if (saved.has(keyOf({ udid, host })))
418
+ continue;
419
+ const data = (0, deviceFieldOwners_1.pickSettingFields)(settings);
420
+ yield this.prisma.deviceSetting.upsert({
421
+ where: { udid_host: { udid, host } },
422
+ create: Object.assign(Object.assign({}, data), { userBlocked: settings.userBlocked === true, udid, host }),
423
+ // Saved meanwhile: that one is newer.
424
+ update: {},
425
+ });
426
+ adopted++;
427
+ }
428
+ return adopted;
429
+ });
430
+ }
431
+ forgetSettings(onlyHosts) {
432
+ return __awaiter(this, void 0, void 0, function* () {
433
+ yield this.prisma.deviceSetting.deleteMany(onlyHosts ? { where: { host: { in: [...onlyHosts] } } } : undefined);
434
+ });
435
+ }
436
+ findSavedPhones(udid) {
437
+ return __awaiter(this, void 0, void 0, function* () {
438
+ return this.prisma.deviceSetting.findMany({
439
+ where: { udid },
440
+ select: { udid: true, host: true },
441
+ });
442
+ });
443
+ }
302
444
  findDevice(filter) {
303
445
  return __awaiter(this, void 0, void 0, function* () {
304
446
  const device = yield this.prisma.device.findFirst({ where: filter });
@@ -483,21 +625,24 @@ class PrismaHealEtalonStore {
483
625
  get prisma() {
484
626
  return typedi_1.Container.get(prisma_service_1.PrismaService).client;
485
627
  }
628
+ /**
629
+ * The fingerprint, and its path when it has one. A fingerprint saved without
630
+ * a path (a heal whose element's path couldn't be worked out) keeps the one
631
+ * stored. Through 2.14 the path was never stored, so the Resilio tier,
632
+ * which needs it, never found anything.
633
+ */
486
634
  saveSignature(etalon) {
487
635
  return __awaiter(this, void 0, void 0, function* () {
636
+ const path = etalon.path == null ? undefined : JSON.stringify(etalon.path);
488
637
  yield this.prisma.locatorEtalon.upsert({
489
638
  where: { selector: etalon.selector },
490
- update: {
491
- strategy: etalon.strategy,
492
- attributes: JSON.stringify(etalon.attributes),
493
- nodeName: etalon.nodeName,
494
- lastSeen: new Date(),
495
- },
639
+ update: Object.assign(Object.assign({ strategy: etalon.strategy, attributes: JSON.stringify(etalon.attributes), nodeName: etalon.nodeName }, (path !== undefined ? { path } : {})), { lastSeen: new Date() }),
496
640
  create: {
497
641
  selector: etalon.selector,
498
642
  strategy: etalon.strategy,
499
643
  attributes: JSON.stringify(etalon.attributes),
500
644
  nodeName: etalon.nodeName,
645
+ path,
501
646
  lastSeen: new Date(),
502
647
  },
503
648
  });
@@ -510,7 +655,16 @@ class PrismaHealEtalonStore {
510
655
  });
511
656
  if (!etalon)
512
657
  return null;
513
- return Object.assign(Object.assign({}, etalon), { attributes: JSON.parse(etalon.attributes), lastSeen: etalon.lastSeen.getTime() });
658
+ let path = null;
659
+ if (etalon.path) {
660
+ try {
661
+ path = JSON.parse(etalon.path);
662
+ }
663
+ catch (_a) {
664
+ path = null;
665
+ }
666
+ }
667
+ return Object.assign(Object.assign({}, etalon), { attributes: JSON.parse(etalon.attributes), path, lastSeen: etalon.lastSeen.getTime() });
514
668
  });
515
669
  }
516
670
  }