@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
@@ -18,109 +18,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.WebConfigService = void 0;
19
19
  const prisma_1 = require("../prisma");
20
20
  const typedi_1 = require("typedi");
21
+ /** How each saved setting is read back: they are all stored as text. */
22
+ const SETTINGS = {
23
+ enableSelfHealing: 'boolean',
24
+ healthCheckIntervalMs: 'number',
25
+ healthCheckSchedule: 'text',
26
+ buildCleanupDays: 'number',
27
+ buildCleanupMaxCount: 'number',
28
+ buildCleanupSchedule: 'text',
29
+ deleteBuildAssets: 'boolean',
30
+ aiProvider: 'text',
31
+ aiModel: 'text',
32
+ aiBaseUrl: 'text',
33
+ geminiModel: 'text',
34
+ openaiModel: 'text',
35
+ anthropicModel: 'text',
36
+ ollamaModel: 'text',
37
+ };
38
+ /**
39
+ * The lab settings the dashboard saves, one `WebConfig` row each, keyed by the
40
+ * setting's name.
41
+ *
42
+ * A row's `id` is the table's primary key, and used to be written as 'global'
43
+ * for every setting, so the table held one: the first setting saved was kept,
44
+ * and saving a second failed with "Unique constraint failed on the fields
45
+ * (`id`)". A setting's row now has its own name as its id, as the metrics
46
+ * counters kept in the same table do. A row an earlier version saved under
47
+ * 'global' is still read, and still the one the next save of that setting
48
+ * updates.
49
+ */
21
50
  let WebConfigService = class WebConfigService {
22
- constructor() {
23
- this.CONFIG_ID = 'global';
24
- }
25
51
  getConfig() {
26
52
  return __awaiter(this, void 0, void 0, function* () {
27
- const configs = yield prisma_1.prisma.webConfig.findMany({
28
- where: { id: this.CONFIG_ID },
53
+ const rows = yield prisma_1.prisma.webConfig.findMany({
54
+ where: { name: { in: Object.keys(SETTINGS) } },
29
55
  });
30
56
  const result = {};
31
- for (const config of configs) {
32
- if (config.name === 'healthCheckIntervalMs') {
33
- result.healthCheckIntervalMs = parseInt(config.value);
34
- }
35
- else if (config.name === 'healthCheckSchedule') {
36
- result.healthCheckSchedule = config.value;
37
- }
38
- else if (config.name === 'buildCleanupDays') {
39
- result.buildCleanupDays = parseInt(config.value);
40
- }
41
- else if (config.name === 'buildCleanupMaxCount') {
42
- result.buildCleanupMaxCount = parseInt(config.value);
43
- }
44
- else if (config.name === 'buildCleanupSchedule') {
45
- result.buildCleanupSchedule = config.value;
46
- }
47
- else if (config.name === 'deleteBuildAssets') {
48
- result.deleteBuildAssets = config.value === 'true';
49
- }
57
+ for (const row of rows) {
58
+ const kind = SETTINGS[row.name];
59
+ if (kind === 'number')
60
+ result[row.name] = parseInt(row.value);
61
+ else if (kind === 'boolean')
62
+ result[row.name] = row.value === 'true';
63
+ else if (kind === 'text')
64
+ result[row.name] = row.value;
50
65
  }
51
66
  return result;
52
67
  });
53
68
  }
54
69
  setConfig(config) {
55
70
  return __awaiter(this, void 0, void 0, function* () {
56
- const promises = [];
57
- if (config.healthCheckIntervalMs !== undefined) {
58
- promises.push(prisma_1.prisma.webConfig.upsert({
59
- where: { name: 'healthCheckIntervalMs' },
60
- update: { value: config.healthCheckIntervalMs.toString() },
61
- create: {
62
- id: this.CONFIG_ID,
63
- name: 'healthCheckIntervalMs',
64
- value: config.healthCheckIntervalMs.toString(),
65
- },
66
- }));
67
- }
68
- if (config.healthCheckSchedule !== undefined) {
69
- promises.push(prisma_1.prisma.webConfig.upsert({
70
- where: { name: 'healthCheckSchedule' },
71
- update: { value: config.healthCheckSchedule },
72
- create: {
73
- id: this.CONFIG_ID,
74
- name: 'healthCheckSchedule',
75
- value: config.healthCheckSchedule,
76
- },
77
- }));
78
- }
79
- if (config.buildCleanupDays !== undefined) {
80
- promises.push(prisma_1.prisma.webConfig.upsert({
81
- where: { name: 'buildCleanupDays' },
82
- update: { value: config.buildCleanupDays.toString() },
83
- create: {
84
- id: this.CONFIG_ID,
85
- name: 'buildCleanupDays',
86
- value: config.buildCleanupDays.toString(),
87
- },
88
- }));
89
- }
90
- if (config.buildCleanupMaxCount !== undefined) {
91
- promises.push(prisma_1.prisma.webConfig.upsert({
92
- where: { name: 'buildCleanupMaxCount' },
93
- update: { value: config.buildCleanupMaxCount.toString() },
94
- create: {
95
- id: this.CONFIG_ID,
96
- name: 'buildCleanupMaxCount',
97
- value: config.buildCleanupMaxCount.toString(),
98
- },
99
- }));
100
- }
101
- if (config.buildCleanupSchedule !== undefined) {
102
- promises.push(prisma_1.prisma.webConfig.upsert({
103
- where: { name: 'buildCleanupSchedule' },
104
- update: { value: config.buildCleanupSchedule },
105
- create: {
106
- id: this.CONFIG_ID,
107
- name: 'buildCleanupSchedule',
108
- value: config.buildCleanupSchedule,
109
- },
110
- }));
111
- }
112
- if (config.deleteBuildAssets !== undefined) {
113
- promises.push(prisma_1.prisma.webConfig.upsert({
114
- where: { name: 'deleteBuildAssets' },
115
- update: { value: config.deleteBuildAssets.toString() },
116
- create: {
117
- id: this.CONFIG_ID,
118
- name: 'deleteBuildAssets',
119
- value: config.deleteBuildAssets.toString(),
120
- },
121
- }));
122
- }
123
- yield Promise.all(promises);
71
+ const writes = Object.keys(SETTINGS)
72
+ .filter((name) => config[name] !== undefined && config[name] !== null)
73
+ .map((name) => {
74
+ const value = String(config[name]);
75
+ return prisma_1.prisma.webConfig.upsert({
76
+ where: { name },
77
+ update: { value },
78
+ create: { id: name, name, value },
79
+ });
80
+ });
81
+ yield Promise.all(writes);
124
82
  });
125
83
  }
126
84
  };
@@ -77,6 +77,7 @@ const device_service_1 = require("../data-service/device-service");
77
77
  const device_store_1 = require("../data-service/device-store");
78
78
  const Devices_1 = __importDefault(require("./cloud/Devices"));
79
79
  const DeviceLockManager_1 = require("./android/DeviceLockManager");
80
+ const shellCommands_1 = require("./shellCommands");
80
81
  const androidIdentity_1 = require("./android/androidIdentity");
81
82
  const deviceIdentity_1 = require("./deviceIdentity");
82
83
  const localDeviceHosts_1 = require("./localDeviceHosts");
@@ -1151,43 +1152,20 @@ let AndroidDeviceManager = class AndroidDeviceManager {
1151
1152
  }
1152
1153
  executeShell(udid, command) {
1153
1154
  return __awaiter(this, void 0, void 0, function* () {
1154
- const ALLOWED_COMMANDS = [
1155
- 'ls',
1156
- 'ps',
1157
- 'top',
1158
- 'dumpsys battery',
1159
- 'dumpsys wifi',
1160
- 'dumpsys power',
1161
- 'whoami',
1162
- 'getprop',
1163
- 'pm list packages',
1164
- 'ip addr',
1165
- 'cat /proc/meminfo',
1166
- 'cat /proc/cpuinfo',
1167
- 'date',
1168
- 'uptime',
1169
- 'netstat',
1170
- ];
1171
- // Basic sanitation
1172
- const safeCommand = command.trim();
1173
- // Check if the command starts with any allowed prefix
1174
- const isAllowed = ALLOWED_COMMANDS.some((prefix) => safeCommand.startsWith(prefix));
1175
- if (!isAllowed) {
1176
- logger_1.default.warn(`Blocked potentially unsafe shell command on ${udid}: ${safeCommand}`);
1177
- throw new Error(`Command '${safeCommand}' is not allowed for security reasons.`);
1155
+ // An allowed command word for word, in words `sh` reads nothing into:
1156
+ // `adb shell` hands the joined line to the phone's shell (shellCommands.ts).
1157
+ const parsed = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.ANDROID_SHELL_COMMANDS);
1158
+ if ('refused' in parsed) {
1159
+ logger_1.default.warn(`Blocked shell command on ${udid}: ${command.trim()}`);
1160
+ throw new Error(parsed.refused);
1178
1161
  }
1179
- // Split command into args for adbExec
1180
- // This is a naive split, but safe enough for the allowed commands which don't use complex quoting
1181
- const args = safeCommand.split(/\s+/);
1182
- logger_1.default.info(`Executing shell command on ${udid}: ${safeCommand}`);
1162
+ logger_1.default.info(`Executing shell command on ${udid}: ${parsed.argv.join(' ')}`);
1183
1163
  const { adbInstance } = yield this.getAdb();
1184
1164
  if (!adbInstance)
1185
1165
  throw new Error('ADB is not available');
1186
1166
  // Use device lock to ensure thread safety
1187
1167
  return yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
1188
- // Direct raw shell execution might be better for piping, but adbExec is safer as it escapes args
1189
- // except we passed them as array, so standard child_process rules apply.
1190
- return yield adbInstance.adbExec(['-s', udid, 'shell', ...args], { timeout: 10000 });
1168
+ return yield adbInstance.adbExec(['-s', udid, 'shell', ...parsed.argv], { timeout: 10000 });
1191
1169
  }));
1192
1170
  });
1193
1171
  }
@@ -93,27 +93,34 @@ let AndroidH264StreamService = class AndroidH264StreamService {
93
93
  startWatchdog() {
94
94
  // unref()ed below: the server's listener keeps the process alive; this only
95
95
  // needs to fire while it runs.
96
- setInterval(() => {
97
- const now = Date.now();
98
- for (const [udid, s] of this.sessions.entries()) {
99
- if (s.status !== 'running')
100
- continue;
101
- if (s.mux.clientCount > 0) {
102
- s.emptyAt = undefined;
103
- }
104
- else if (s.emptyAt === undefined) {
105
- s.emptyAt = now;
106
- }
107
- else if (now - s.emptyAt > IDLE_TIMEOUT_MS) {
108
- logger_1.default.info(`[${udid}] Stopping idle H.264 stream (no viewers for ${IDLE_TIMEOUT_MS}ms)`);
109
- // The stream went, but the live-preview hold stayed: with H.264
110
- // preview, a tab closed without releasing kept the device busy for
111
- // good. stop() itself must not release (a recording starting calls
112
- // it); an idle stop does, if nothing else uses the device.
113
- void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
114
- }
96
+ setInterval(() => this.sweep(Date.now()), 60000).unref();
97
+ }
98
+ /** One look at every capture: ends those nobody has watched for long enough. */
99
+ sweep(now) {
100
+ for (const [udid, s] of this.sessions.entries()) {
101
+ if (s.status !== 'running')
102
+ continue;
103
+ if (s.mux.clientCount > 0) {
104
+ s.emptyAt = undefined;
105
+ }
106
+ else if (s.endWhenUnwatched && !this.startPromises.has(udid)) {
107
+ // A page asked for MJPEG while this capture was starting for a viewer
108
+ // who then never came (endWhenUnwatched leaves a start alone).
109
+ logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
110
+ void this.stop(udid);
115
111
  }
116
- }, 60000).unref();
112
+ else if (s.emptyAt === undefined) {
113
+ s.emptyAt = now;
114
+ }
115
+ else if (now - s.emptyAt > IDLE_TIMEOUT_MS) {
116
+ logger_1.default.info(`[${udid}] Stopping idle H.264 stream (no viewers for ${IDLE_TIMEOUT_MS}ms)`);
117
+ // The stream went, but the live-preview hold stayed: with H.264
118
+ // preview, a tab closed without releasing kept the device busy for
119
+ // good. stop() itself must not release (a recording starting calls
120
+ // it); an idle stop does, if nothing else uses the device.
121
+ void this.stop(udid).then(() => (0, previewHold_1.releaseIdlePreviewHold)(udid));
122
+ }
123
+ }
117
124
  }
118
125
  getMultiplexer(udid) {
119
126
  var _a;
@@ -131,6 +138,7 @@ let AndroidH264StreamService = class AndroidH264StreamService {
131
138
  var _a, _b;
132
139
  const mux = new H264Multiplexer_1.H264Multiplexer();
133
140
  const session = { status: 'running', mux };
141
+ mux.onEmpty(() => this.endIfUnwatched(udid, session));
134
142
  this.sessions.set(udid, session);
135
143
  try {
136
144
  let resolveConfig = () => undefined;
@@ -178,6 +186,44 @@ let AndroidH264StreamService = class AndroidH264StreamService {
178
186
  }
179
187
  });
180
188
  }
189
+ /**
190
+ * A page that showed this phone's H.264 preview now shows MJPEG (its player
191
+ * failed, or it cannot play H.264), and the MJPEG capture is starting for
192
+ * it. One capture per phone where possible, but never at the expense of a
193
+ * viewer still playing H.264: another tile, tab or admin may be. So the
194
+ * capture ends now if nobody watches it, else the moment its last viewer
195
+ * leaves, never after the idle wait. A viewer who joins meanwhile is served
196
+ * and ends it in turn. A capture still starting is left to the viewer it is
197
+ * starting for, and the watchdog ends it if that viewer never comes.
198
+ */
199
+ endWhenUnwatched(udid) {
200
+ return __awaiter(this, void 0, void 0, function* () {
201
+ const session = this.sessions.get(udid);
202
+ if (!session || session.status !== 'running')
203
+ return;
204
+ session.endWhenUnwatched = true;
205
+ const viewers = session.mux.clientCount;
206
+ if (viewers > 0) {
207
+ logger_1.default.info(`[${udid}] H.264 stream kept beside MJPEG for its ${viewers} viewer(s); ` +
208
+ 'it ends when the last one leaves');
209
+ return;
210
+ }
211
+ if (this.startPromises.has(udid)) {
212
+ logger_1.default.info(`[${udid}] H.264 stream still starting: kept for the viewer it is starting for`);
213
+ return;
214
+ }
215
+ logger_1.default.info(`[${udid}] Stopping the unwatched H.264 stream: the phone is shown as MJPEG`);
216
+ yield this.stop(udid);
217
+ });
218
+ }
219
+ /** The last viewer left `session`: end it if a page asked (endWhenUnwatched). */
220
+ endIfUnwatched(udid, session) {
221
+ // A capture replaced since (a stop and a new start) is not this one.
222
+ if (this.sessions.get(udid) !== session || !session.endWhenUnwatched)
223
+ return;
224
+ logger_1.default.info(`[${udid}] Stopping the H.264 stream: its last viewer left, the phone is shown as MJPEG`);
225
+ void this.stop(udid);
226
+ }
181
227
  stop(udid) {
182
228
  return __awaiter(this, void 0, void 0, function* () {
183
229
  var _a;
@@ -192,6 +238,9 @@ let AndroidH264StreamService = class AndroidH264StreamService {
192
238
  /* best-effort */
193
239
  }
194
240
  this.sessions.delete(udid);
241
+ // Viewers still on it (a recording starting, a stream/stop) are told, so
242
+ // their players fall back to MJPEG instead of freezing on the last frame.
243
+ session.mux.close();
195
244
  logger_1.default.info(`[${udid}] H.264 stream terminated.`);
196
245
  });
197
246
  }
@@ -9,6 +9,7 @@ class H264Multiplexer {
9
9
  constructor() {
10
10
  this.clients = new Set();
11
11
  this.gop = []; // current GOP: [keyframe, ...deltas-since]
12
+ this.closed = false;
12
13
  }
13
14
  setConfig(p) {
14
15
  this.config = p;
@@ -16,9 +17,21 @@ class H264Multiplexer {
16
17
  get clientCount() {
17
18
  return this.clients.size;
18
19
  }
19
- /** Register a client sink. Returns a remover. */
20
- addClient(send) {
21
- const c = { send, started: false };
20
+ /** Called each time the last client leaves. */
21
+ onEmpty(listener) {
22
+ this.emptyListener = listener;
23
+ }
24
+ /**
25
+ * Register a client sink, and how to end it when the stream does (see
26
+ * `close`). Returns a remover (safe to call twice).
27
+ */
28
+ addClient(send, end) {
29
+ const c = { send, started: false, end };
30
+ if (this.closed) {
31
+ // Joined a stream already stopped: end it now rather than starve it.
32
+ end === null || end === void 0 ? void 0 : end();
33
+ return () => undefined;
34
+ }
22
35
  this.clients.add(c);
23
36
  if (this.config)
24
37
  send(this.config);
@@ -29,9 +42,30 @@ class H264Multiplexer {
29
42
  c.started = true;
30
43
  }
31
44
  return () => {
32
- this.clients.delete(c);
45
+ var _a;
46
+ if (this.clients.delete(c) && this.clients.size === 0)
47
+ (_a = this.emptyListener) === null || _a === void 0 ? void 0 : _a.call(this);
33
48
  };
34
49
  }
50
+ /**
51
+ * The capture stopped: end every client (its socket closes, so its player
52
+ * falls back to MJPEG instead of freezing on its last frame). Not an
53
+ * `onEmpty`: nobody left, the stream did.
54
+ */
55
+ close() {
56
+ var _a;
57
+ this.closed = true;
58
+ const ending = [...this.clients];
59
+ this.clients.clear();
60
+ for (const c of ending) {
61
+ try {
62
+ (_a = c.end) === null || _a === void 0 ? void 0 : _a.call(c);
63
+ }
64
+ catch (_b) {
65
+ /* one client's end must not keep the others open */
66
+ }
67
+ }
68
+ }
35
69
  /** Feed one upstream packet; fans out per the join semantics above. */
36
70
  push(p) {
37
71
  if (p.type === 'config') {
@@ -27,6 +27,7 @@ const typedi_1 = require("typedi");
27
27
  const ProcessRegistry_1 = require("../../services/ProcessRegistry");
28
28
  const IOSStreamService_1 = __importDefault(require("./IOSStreamService"));
29
29
  const IOSTunnels_1 = require("./IOSTunnels");
30
+ const shellCommands_1 = require("../shellCommands");
30
31
  const device_store_1 = require("../../data-service/device-store");
31
32
  const child_process_1 = require("child_process");
32
33
  const util_1 = require("util");
@@ -1005,57 +1006,57 @@ let WDAClient = WDAClient_1 = class WDAClient {
1005
1006
  }
1006
1007
  executeShell(udid, command) {
1007
1008
  return __awaiter(this, void 0, void 0, function* () {
1008
- // Separate allowlists for simulators vs real devices
1009
- const SIMCTL_COMMANDS = ['listapps', 'get_app_container', 'list', 'getenv'];
1010
- const GOIOS_COMMANDS = ['apps', 'info', 'syslog', 'list', 'deviceinfo', 'diagnostics'];
1011
- const GENERIC_COMMANDS = ['ls', 'ps', 'top', 'whoami', 'date', 'uptime', 'netstat', 'id'];
1012
- const safeCommand = command.trim();
1013
- const commandWord = safeCommand.split(/\s+/)[0]; // First word is the command
1014
- // Determine device type
1009
+ // Simulators: a few simctl commands, and generic commands spawned in the
1010
+ // simulator. Real iPhones: go-ios commands for this phone only. Every
1011
+ // command is matched word for word, in plain words (shellCommands.ts).
1015
1012
  const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
1016
- const isSimulator = device && !device.realDevice;
1017
- // Check allowlists based on device type
1018
- const isSimctlAllowed = SIMCTL_COMMANDS.includes(commandWord);
1019
- const isGoiosAllowed = GOIOS_COMMANDS.includes(commandWord);
1020
- const isGenericAllowed = GENERIC_COMMANDS.some((prefix) => safeCommand.startsWith(prefix));
1021
- if (!isSimctlAllowed && !isGoiosAllowed && !isGenericAllowed) {
1022
- this.log.warn(`Blocked shell command on ${udid}: ${safeCommand}`);
1023
- throw new Error(`Command '${safeCommand}' is not allowed for security reasons.`);
1024
- }
1025
- const args = safeCommand.split(/\s+/);
1026
- const s = typedi_1.Container.get(IOSStreamService_1.default);
1027
- // Route to the correct tool
1028
- if (isSimctlAllowed && isSimulator) {
1029
- // Simulator-specific: xcrun simctl <command> <udid> [args...]
1030
- const [cmd, ...rest] = args;
1031
- const { stdout } = yield execFilePromise('xcrun', ['simctl', cmd, udid, ...rest]);
1032
- return stdout;
1033
- }
1034
- if (isGoiosAllowed) {
1035
- // Real device: go-ios <command> --udid <udid> [args...]
1036
- if (yield s.isGoIOSAvailable()) {
1037
- const { stdout } = yield execFilePromise(s.goIOSPath, [...args, '--udid', udid], {
1038
- env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
1013
+ const isSimulator = !!device && !device.realDevice;
1014
+ const word = command.trim().split(/\s+/)[0];
1015
+ const refuse = (reason) => {
1016
+ this.log.warn(`Blocked shell command on ${udid}: ${command.trim()}`);
1017
+ return new Error(reason);
1018
+ };
1019
+ if (isSimulator) {
1020
+ const simctl = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_SIMCTL_COMMANDS);
1021
+ if ('argv' in simctl) {
1022
+ const [cmd, ...rest] = simctl.argv;
1023
+ const { stdout } = yield execFilePromise('xcrun', ['simctl', cmd, udid, ...rest], {
1024
+ timeout: 10000,
1039
1025
  });
1040
1026
  return stdout;
1041
1027
  }
1042
- throw new Error(`Command '${commandWord}' requires go-ios but it is not available.`);
1028
+ const spawned = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_SPAWN_COMMANDS);
1029
+ if ('refused' in spawned)
1030
+ throw refuse(spawned.refused);
1031
+ const { stdout } = yield execFilePromise('xcrun', ['simctl', 'spawn', udid, ...spawned.argv], {
1032
+ timeout: 10000,
1033
+ });
1034
+ return stdout;
1043
1035
  }
1044
- if (isSimctlAllowed && !isSimulator) {
1045
- // User tried a simctl command on a real device - give helpful error
1046
- throw new Error(`Command '${commandWord}' is only available for Simulators. For real devices, try: ${GOIOS_COMMANDS.join(', ')}`);
1036
+ const onDevice = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_DEVICE_COMMANDS);
1037
+ if ('refused' in onDevice) {
1038
+ const simulatorOnly = [...shellCommands_1.IOS_SIMCTL_COMMANDS, ...shellCommands_1.IOS_SPAWN_COMMANDS].some((c) => c.words[0] === word);
1039
+ if (simulatorOnly) {
1040
+ throw refuse(`'${word}' runs on simulators only. On a real iPhone, try: ${shellCommands_1.IOS_DEVICE_COMMANDS.map((c) => c.words.join(' ')).join(', ')}.`);
1041
+ }
1042
+ throw refuse(onDevice.refused);
1047
1043
  }
1048
- // Generic system command - try simctl first, fallback to go-ios
1049
- if (isSimulator) {
1050
- const { stdout } = yield execFilePromise('xcrun', ['simctl', 'spawn', udid, ...args]);
1051
- return stdout;
1044
+ // Xenon names the phone; a command may not name another one.
1045
+ if (onDevice.argv.some((arg) => arg.startsWith('--udid'))) {
1046
+ throw refuse(`Command '${command.trim()}' is not allowed: Xenon chooses the device.`);
1052
1047
  }
1053
- else if (yield s.isGoIOSAvailable()) {
1054
- // For real devices, generic commands aren't directly possible via go-ios
1055
- // Try go-ios launch/exec if available, otherwise inform user
1056
- throw new Error(`System command '${commandWord}' is not available on real devices. Use device-specific commands: ${GOIOS_COMMANDS.join(', ')}`);
1048
+ const s = typedi_1.Container.get(IOSStreamService_1.default);
1049
+ if (!(yield s.isGoIOSAvailable())) {
1050
+ throw new Error(`'${word}' needs go-ios, which is not installed on this server.`);
1057
1051
  }
1058
- throw new Error(`Could not execute command: no suitable runtime found for ${isSimulator ? 'simulator' : 'real device'}`);
1052
+ // On iOS 17+ go-ios reaches the phone only through its tunnel.
1053
+ const tunnels = typedi_1.Container.get(IOSTunnels_1.IOSTunnels);
1054
+ yield tunnels.borrow(udid);
1055
+ const { stdout } = yield execFilePromise(s.goIOSPath, [...onDevice.argv, '--udid', udid], {
1056
+ env: tunnels.envFor(udid),
1057
+ timeout: 10000,
1058
+ });
1059
+ return stdout;
1059
1060
  });
1060
1061
  }
1061
1062
  };
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /**
3
+ * What the device control Shell tab may run.
4
+ *
5
+ * Android runs a command through `adb shell`, which joins the words and hands
6
+ * them to the phone's `sh`. Through 2.13.2 the allow-list was a `startsWith`
7
+ * test on the whole line, so `ls; reboot`, `ls && pm uninstall …` and `lsof`
8
+ * all ran. Now a command must begin with an allowed command word for word, and
9
+ * every word of it may only use characters a shell gives no meaning to, so the
10
+ * phone's shell can't read anything into it. A command that changes or reads
11
+ * more than the entry is for (`dumpsys battery set …`, `cat` of another file)
12
+ * takes no arguments.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.IOS_DEVICE_COMMANDS = exports.IOS_SIMULATOR_COMMANDS = exports.IOS_SPAWN_COMMANDS = exports.IOS_SIMCTL_COMMANDS = exports.ANDROID_SHELL_COMMANDS = void 0;
16
+ exports.parseShellCommand = parseShellCommand;
17
+ /** Letters, digits and `._/:=@%+,-`: nothing `sh` expands, splits or redirects on. */
18
+ const PLAIN_WORD = /^[A-Za-z0-9._/:=@%+,-]+$/;
19
+ exports.ANDROID_SHELL_COMMANDS = [
20
+ { words: ['ls'], args: 'plain' },
21
+ { words: ['ps'], args: 'plain' },
22
+ { words: ['top'], args: 'plain' },
23
+ { words: ['dumpsys', 'battery'], args: 'none' },
24
+ { words: ['dumpsys', 'wifi'], args: 'none' },
25
+ { words: ['dumpsys', 'power'], args: 'none' },
26
+ { words: ['whoami'], args: 'none' },
27
+ { words: ['getprop'], args: 'plain' },
28
+ { words: ['pm', 'list', 'packages'], args: 'plain' },
29
+ { words: ['ip', 'addr'], args: 'plain' },
30
+ { words: ['cat', '/proc/meminfo'], args: 'none' },
31
+ { words: ['cat', '/proc/cpuinfo'], args: 'none' },
32
+ { words: ['date'], args: 'plain' },
33
+ { words: ['uptime'], args: 'none' },
34
+ { words: ['netstat'], args: 'plain' },
35
+ ];
36
+ /** `xcrun simctl <command> <udid> [args]` on a simulator. */
37
+ exports.IOS_SIMCTL_COMMANDS = [
38
+ { words: ['listapps'], args: 'none' },
39
+ { words: ['get_app_container'], args: 'plain' },
40
+ { words: ['getenv'], args: 'plain' },
41
+ ];
42
+ /**
43
+ * `xcrun simctl spawn <udid> <command>` on a simulator. A simulator's processes
44
+ * see this Mac's file system, so these take no arguments.
45
+ */
46
+ exports.IOS_SPAWN_COMMANDS = [
47
+ { words: ['ls'], args: 'none' },
48
+ { words: ['ps'], args: 'none' },
49
+ { words: ['whoami'], args: 'none' },
50
+ { words: ['date'], args: 'none' },
51
+ { words: ['uptime'], args: 'none' },
52
+ { words: ['id'], args: 'none' },
53
+ ];
54
+ exports.IOS_SIMULATOR_COMMANDS = [
55
+ ...exports.IOS_SIMCTL_COMMANDS,
56
+ ...exports.IOS_SPAWN_COMMANDS,
57
+ ];
58
+ /**
59
+ * go-ios commands on a real iPhone (`ios <command> [args] --udid <udid>`). None
60
+ * may name a device itself: Xenon adds this phone's `--udid`.
61
+ */
62
+ exports.IOS_DEVICE_COMMANDS = [
63
+ { words: ['apps'], args: 'plain' },
64
+ { words: ['info'], args: 'none' },
65
+ ];
66
+ /**
67
+ * The command's argv when it is allowed, or why it isn't. Words are split on
68
+ * spaces; quoting is not supported, since a quote is refused anyway.
69
+ */
70
+ function parseShellCommand(input, commands) {
71
+ const shown = input.trim();
72
+ // Words are separated by spaces only: a newline or tab means another line.
73
+ if (/[^\S ]/.test(shown)) {
74
+ return {
75
+ refused: `Command '${shown.split(/\s/)[0]}…' is not allowed: one line, words separated by spaces.`,
76
+ };
77
+ }
78
+ const argv = shown.split(/ +/).filter(Boolean);
79
+ if (argv.length === 0)
80
+ return { refused: 'Type a command.' };
81
+ const bad = argv.find((word) => !PLAIN_WORD.test(word));
82
+ if (bad !== undefined) {
83
+ return {
84
+ refused: `Command '${shown}' is not allowed: '${bad}' contains characters the shell would interpret.`,
85
+ };
86
+ }
87
+ const match = commands.find((command) => command.words.length <= argv.length && command.words.every((word, i) => argv[i] === word));
88
+ if (!match)
89
+ return { refused: `Command '${shown}' is not allowed for security reasons.` };
90
+ if (match.args === 'none' && argv.length > match.words.length) {
91
+ return {
92
+ refused: `Command '${shown}' is not allowed: '${match.words.join(' ')}' takes no arguments here.`,
93
+ };
94
+ }
95
+ return { argv };
96
+ }