@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
@@ -48,7 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
48
48
  require("reflect-metadata");
49
49
  const chai_1 = require("chai");
50
50
  const express_1 = __importStar(require("express"));
51
- const supertest_1 = __importDefault(require("supertest"));
51
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
52
52
  describe('GET /xenon/api/sdk/version', () => {
53
53
  it('returns pluginVersion + supports array', () => __awaiter(void 0, void 0, void 0, function* () {
54
54
  const register = (yield Promise.resolve().then(() => __importStar(require('../../src/app/routers/sdk-version')))).default;
@@ -61,7 +61,7 @@ describe('GET /xenon/api/sdk/version', () => {
61
61
  const apiRouter = (0, express_1.Router)();
62
62
  register(apiRouter);
63
63
  app.use('/xenon/api', apiRouter);
64
- const res = yield (0, supertest_1.default)(app).get('/xenon/api/sdk/version');
64
+ const res = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/sdk/version');
65
65
  (0, chai_1.expect)(res.status).to.equal(200);
66
66
  (0, chai_1.expect)(res.body.pluginVersion).to.be.a('string');
67
67
  (0, chai_1.expect)(res.body.supports).to.include.members(['leases', 'ports', 'heartbeat']);
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const sinon_1 = __importDefault(require("sinon"));
18
18
  const express_1 = __importDefault(require("express"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const dashboard_1 = require("../../src/app/routers/dashboard");
21
21
  const roleGuard_1 = require("../../src/middleware/roleGuard");
22
22
  const prisma_1 = require("../../src/prisma");
@@ -209,23 +209,23 @@ describe('GET /healing/selector-health — MEMBER guard (router-level)', () => {
209
209
  });
210
210
  afterEach(() => sinon_1.default.restore());
211
211
  it('401s when unauthenticated', () => __awaiter(void 0, void 0, void 0, function* () {
212
- const r = yield (0, supertest_1.default)(makeApp()).get('/xenon/api/healing/selector-health');
212
+ const r = yield (0, loopbackRequest_1.default)(makeApp()).get('/xenon/api/healing/selector-health');
213
213
  (0, chai_1.expect)(r.status).to.equal(401);
214
214
  }));
215
215
  it('403s for a role below MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
216
- const r = yield (0, supertest_1.default)(makeApp())
216
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
217
217
  .get('/xenon/api/healing/selector-health')
218
218
  .set('x-test-role', 'BOGUS');
219
219
  (0, chai_1.expect)(r.status).to.equal(403);
220
220
  }));
221
221
  it('200s for MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
222
- const r = yield (0, supertest_1.default)(makeApp())
222
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
223
223
  .get('/xenon/api/healing/selector-health')
224
224
  .set('x-test-role', 'MEMBER');
225
225
  (0, chai_1.expect)(r.status).to.equal(200);
226
226
  }));
227
227
  it('200s for ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
228
- const r = yield (0, supertest_1.default)(makeApp())
228
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
229
229
  .get('/xenon/api/healing/selector-health')
230
230
  .set('x-test-role', 'ADMIN');
231
231
  (0, chai_1.expect)(r.status).to.equal(200);
@@ -8,28 +8,89 @@ 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 __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  require("reflect-metadata");
13
16
  const chai_1 = require("chai");
17
+ const child_process_1 = require("child_process");
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
14
21
  const typedi_1 = require("typedi");
22
+ const client_1 = require("../../src/generated/client");
15
23
  const UserService_1 = require("../../src/services/UserService");
16
24
  const TeamService_1 = require("../../src/services/TeamService");
17
25
  const prisma_1 = require("../../src/prisma");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
27
+ const ROOT = path_1.default.resolve(__dirname, '../..');
28
+ const MIGRATIONS = path_1.default.join(ROOT, 'prisma', 'migrations');
29
+ /** The migration that backfills TeamMember from ApiKey.teamId. */
30
+ const BACKFILL = '20260430001747_phase_3_team_members';
31
+ /**
32
+ * `prisma migrate deploy` into `url` of the repository's migrations that
33
+ * `include` names, from a copy of the schema in `dir`. Run again with more
34
+ * of them, it applies only the ones not yet applied.
35
+ */
36
+ function deploy(dir, url, include) {
37
+ const target = path_1.default.join(dir, 'migrations');
38
+ fs_1.default.mkdirSync(target, { recursive: true });
39
+ fs_1.default.copyFileSync(path_1.default.join(MIGRATIONS, 'migration_lock.toml'), path_1.default.join(target, 'migration_lock.toml'));
40
+ for (const name of fs_1.default.readdirSync(MIGRATIONS)) {
41
+ const sql = path_1.default.join(MIGRATIONS, name, 'migration.sql');
42
+ if (!include(name) || !fs_1.default.existsSync(sql))
43
+ continue;
44
+ fs_1.default.mkdirSync(path_1.default.join(target, name), { recursive: true });
45
+ fs_1.default.copyFileSync(sql, path_1.default.join(target, name, 'migration.sql'));
46
+ }
47
+ fs_1.default.copyFileSync(path_1.default.join(ROOT, 'prisma', 'schema.prisma'), path_1.default.join(dir, 'schema.prisma'));
48
+ (0, child_process_1.execSync)(`npx prisma migrate deploy --schema "${path_1.default.join(dir, 'schema.prisma')}"`, {
49
+ cwd: ROOT,
50
+ env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
51
+ stdio: 'pipe',
52
+ });
53
+ }
18
54
  describe('team-membership migration backfill (integration)', function () {
19
55
  this.timeout(60000);
20
- it('no TeamMember row points at Legacy Admin', () => __awaiter(this, void 0, void 0, function* () {
21
- const legacy = yield prisma_1.prisma.user.findFirst({
22
- where: { email: 'legacy-admin@xenon.local' },
23
- });
24
- if (!legacy) {
25
- // Legacy Admin only exists if Phase 1's adoption migration actually ran
26
- // against a non-empty ApiKey table. Skip if absent.
27
- // eslint-disable-next-line no-console
28
- console.log('skipped: no Legacy Admin in this DB');
29
- return;
56
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
57
+ // Through 2.14 this read the developer's own database, and returned early
58
+ // unless an old Legacy Admin happened to be in it. It runs the migration
59
+ // itself now, on a database migrated up to it and seeded as such a lab was.
60
+ it('backfills TeamMember from team keys, and gives Legacy Admin no membership', () => __awaiter(this, void 0, void 0, function* () {
61
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-team-backfill-'));
62
+ const url = `file:${path_1.default.join(dir, 'xenon.db')}`;
63
+ try {
64
+ deploy(dir, url, (name) => name < BACKFILL);
65
+ const before = new client_1.PrismaClient({ datasources: { db: { url } } });
66
+ try {
67
+ yield before.$executeRawUnsafe('INSERT INTO "Team" ("id", "name") VALUES (?, ?)', 'team-1', 'Team 1');
68
+ for (const [id, email] of [
69
+ ['legacy', 'legacy-admin@xenon.local'],
70
+ ['alice', 'alice@xenon.local'],
71
+ ]) {
72
+ yield before.$executeRawUnsafe(`INSERT INTO "User" ("id", "email", "name", "passwordHash", "accessKey", "updatedAt")
73
+ VALUES (?, ?, ?, 'x', ?, CURRENT_TIMESTAMP)`, id, email, id, `ak-${id}`);
74
+ yield before.$executeRawUnsafe(`INSERT INTO "ApiKey" ("id", "name", "keyHash", "scopes", "teamId", "userId")
75
+ VALUES (?, ?, ?, 'devices', 'team-1', ?)`, `key-${id}`, id, `hash-${id}`, id);
76
+ }
77
+ }
78
+ finally {
79
+ yield before.$disconnect();
80
+ }
81
+ deploy(dir, url, () => true);
82
+ const after = new client_1.PrismaClient({ datasources: { db: { url } } });
83
+ try {
84
+ const members = yield after.teamMember.findMany({ select: { teamId: true, userId: true } });
85
+ (0, chai_1.expect)(members).to.deep.equal([{ teamId: 'team-1', userId: 'alice' }]);
86
+ }
87
+ finally {
88
+ yield after.$disconnect();
89
+ }
90
+ }
91
+ finally {
92
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
30
93
  }
31
- const polluted = yield prisma_1.prisma.teamMember.count({ where: { userId: legacy.id } });
32
- (0, chai_1.expect)(polluted).to.equal(0);
33
94
  }));
34
95
  it('addMember / removeMember round-trip an arbitrary user', () => __awaiter(this, void 0, void 0, function* () {
35
96
  const u = yield typedi_1.Container.get(UserService_1.UserService).createUser({
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const fs_1 = __importDefault(require("fs"));
20
20
  const os_1 = __importDefault(require("os"));
21
21
  const path_1 = __importDefault(require("path"));
@@ -29,17 +29,17 @@ const app_service_1 = require("../../src/dashboard/services/app-service");
29
29
  const prisma_1 = require("../../src/prisma");
30
30
  const seedUser_1 = require("../helpers/seedUser");
31
31
  const container_registration_1 = require("../helpers/container-registration");
32
+ const scratch_database_1 = require("../helpers/scratch-database");
32
33
  /**
33
34
  * Uploaded apps follow the device team rule, run as real queries: the list
34
35
  * builds a Prisma filter (`OR: [{ teamId: null }, { teamId: { in } }]`, with an
35
36
  * empty `in` for a member in no team) that a stub would only echo back.
36
37
  *
37
- * Run on a migrated database:
38
- * DATABASE_URL=file:/tmp/x.db npx prisma migrate deploy
39
- * DATABASE_URL=file:/tmp/x.db npx mocha test/integration/team-visibility-apps.spec.ts
38
+ * On a scratch database of its own, so `npx mocha <file>` is enough.
40
39
  */
41
40
  describe('team visibility on uploaded apps (integration)', function () {
42
41
  this.timeout(60000);
42
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
43
43
  let sa;
44
44
  let alice;
45
45
  let loner;
@@ -87,17 +87,10 @@ describe('team visibility on uploaded apps (integration)', function () {
87
87
  });
88
88
  }
89
89
  }));
90
- after(() => __awaiter(this, void 0, void 0, function* () {
91
- yield prisma_1.prisma.app.deleteMany({ where: { id: { in: Object.values(A) } } });
92
- yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
93
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
94
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
95
- yield sa.cleanup();
96
- yield alice.cleanup();
97
- yield loner.cleanup();
90
+ after(() => {
98
91
  restore();
99
92
  fs_1.default.rmSync(dir, { recursive: true, force: true });
100
- }));
93
+ });
101
94
  function app() {
102
95
  const a = (0, express_1.default)();
103
96
  a.use(express_1.default.json());
@@ -106,13 +99,13 @@ describe('team visibility on uploaded apps (integration)', function () {
106
99
  return a;
107
100
  }
108
101
  const ids = (cookie) => __awaiter(this, void 0, void 0, function* () {
109
- return (yield (0, supertest_1.default)(app()).get('/apps').set('Cookie', cookie)).body
102
+ return (yield (0, loopbackRequest_1.default)(app()).get('/apps').set('Cookie', cookie)).body
110
103
  .map((r) => r.id)
111
104
  .filter((id) => Object.values(A).includes(id));
112
105
  });
113
106
  const UNKNOWN = { error: 'App not found' };
114
107
  it("lists a member's shared and own-team apps, with the team's name", () => __awaiter(this, void 0, void 0, function* () {
115
- const res = yield (0, supertest_1.default)(app()).get('/apps').set('Cookie', alice.cookie);
108
+ const res = yield (0, loopbackRequest_1.default)(app()).get('/apps').set('Cookie', alice.cookie);
116
109
  (0, chai_1.expect)(res.status).to.equal(200);
117
110
  const mine = res.body.filter((r) => Object.values(A).includes(r.id));
118
111
  (0, chai_1.expect)(mine.map((r) => r.id)).to.have.members([A.shared, A.a]);
@@ -127,20 +120,20 @@ describe('team visibility on uploaded apps (integration)', function () {
127
120
  (0, chai_1.expect)(yield ids(sa.cookie)).to.have.members(Object.values(A));
128
121
  }));
129
122
  it("answers another team's app exactly as an unknown id", () => __awaiter(this, void 0, void 0, function* () {
130
- const hidden = yield (0, supertest_1.default)(app()).get(`/apps/${A.b}/download`).set('Cookie', alice.cookie);
131
- const unknown = yield (0, supertest_1.default)(app())
123
+ const hidden = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.b}/download`).set('Cookie', alice.cookie);
124
+ const unknown = yield (0, loopbackRequest_1.default)(app())
132
125
  .get(`/apps/nope-${stamp}/download`)
133
126
  .set('Cookie', alice.cookie);
134
127
  (0, chai_1.expect)(hidden.status).to.equal(404);
135
128
  (0, chai_1.expect)(hidden.body).to.deep.equal(UNKNOWN);
136
129
  (0, chai_1.expect)(unknown.status).to.equal(404);
137
130
  (0, chai_1.expect)(unknown.body).to.deep.equal(UNKNOWN);
138
- const own = yield (0, supertest_1.default)(app()).get(`/apps/${A.a}/download`).set('Cookie', alice.cookie);
131
+ const own = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.a}/download`).set('Cookie', alice.cookie);
139
132
  (0, chai_1.expect)(own.status).to.equal(200);
140
133
  }));
141
134
  it('a download ticket serves its app once, with no login', () => __awaiter(this, void 0, void 0, function* () {
142
135
  const t = yield typedi_1.Container.get(AppDownloadTicketService_1.AppDownloadTicketService).mint(A.b);
143
- const first = yield (0, supertest_1.default)(app())
136
+ const first = yield (0, loopbackRequest_1.default)(app())
144
137
  .get(`/apps/${A.b}/download`)
145
138
  .query({ ticket: t })
146
139
  .buffer(true)
@@ -151,16 +144,16 @@ describe('team visibility on uploaded apps (integration)', function () {
151
144
  });
152
145
  (0, chai_1.expect)(first.status).to.equal(200);
153
146
  (0, chai_1.expect)(Buffer.compare(first.body, bytesOf(A.b))).to.equal(0);
154
- const again = yield (0, supertest_1.default)(app()).get(`/apps/${A.b}/download`).query({ ticket: t });
147
+ const again = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.b}/download`).query({ ticket: t });
155
148
  (0, chai_1.expect)(again.status).to.equal(401);
156
149
  }));
157
150
  it('moving an app to a team changes who sees it', () => __awaiter(this, void 0, void 0, function* () {
158
- const put = (teamId) => (0, supertest_1.default)(app()).put(`/apps/${A.b}/team`).set('Cookie', sa.cookie).send({ teamId });
151
+ const put = (teamId) => (0, loopbackRequest_1.default)(app()).put(`/apps/${A.b}/team`).set('Cookie', sa.cookie).send({ teamId });
159
152
  (0, chai_1.expect)((yield put(teamA.id)).body).to.deep.equal({ ok: true, updated: 1 });
160
153
  (0, chai_1.expect)(yield ids(alice.cookie)).to.include(A.b);
161
154
  (0, chai_1.expect)((yield put(teamB.id)).status).to.equal(200);
162
155
  (0, chai_1.expect)(yield ids(alice.cookie)).to.not.include(A.b);
163
- const member = yield (0, supertest_1.default)(app())
156
+ const member = yield (0, loopbackRequest_1.default)(app())
164
157
  .put(`/apps/${A.b}/team`)
165
158
  .set('Cookie', alice.cookie)
166
159
  .send({ teamId: teamA.id });
@@ -16,7 +16,7 @@ require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
18
  const cors_1 = __importDefault(require("cors"));
19
- const supertest_1 = __importDefault(require("supertest"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
20
  const fs_1 = __importDefault(require("fs"));
21
21
  const os_1 = __importDefault(require("os"));
22
22
  const path_1 = __importDefault(require("path"));
@@ -31,6 +31,10 @@ const prisma_1 = require("../../src/prisma");
31
31
  const seedUser_1 = require("../helpers/seedUser");
32
32
  const container_registration_1 = require("../helpers/container-registration");
33
33
  const device_store_1 = require("../../src/data-service/device-store");
34
+ const db_1 = require("../../src/data-service/db");
35
+ const scratch_database_1 = require("../helpers/scratch-database");
36
+ const loki_stores_1 = require("../helpers/loki-stores");
37
+ const own_node_id_1 = require("../helpers/own-node-id");
34
38
  /**
35
39
  * Teams are a device boundary on /control, end to end: the real auth
36
40
  * middleware computes req.auth.teamIds from TeamMember rows, and the real
@@ -88,23 +92,37 @@ const METHODS_BY_ACTION = CONTROL_ROUTES.reduce((acc, { method, action }) => {
88
92
  }, new Map());
89
93
  describe('team boundary on /control (integration)', function () {
90
94
  this.timeout(60000);
95
+ // Hermetic, so it runs in `npm run test:all` and on its own alike: users,
96
+ // teams and sessions go to a scratch database, never ~/.cache/xenon/xenon.db,
97
+ // and the phones to the in-memory device store. Nothing here stubs `prisma`
98
+ // itself, so the scratch database stands in for the whole suite.
99
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
100
+ (0, loki_stores_1.useLokiStores)();
101
+ // The fixture phones are this server's own; see DEVICE_HOST.
102
+ (0, own_node_id_1.useOwnNodeId)();
91
103
  let sa;
92
104
  let alice;
93
- let teamA;
94
- let teamB;
95
105
  let keyDir;
106
+ let deviceRows;
96
107
  const restores = [];
97
108
  const stamp = Date.now();
98
109
  const SHARED_UDID = `tb-shared-${stamp}`;
99
110
  const TEAM_A_UDID = `tb-team-a-${stamp}`;
100
111
  const TEAM_B_UDID = `tb-team-b-${stamp}`;
101
- // The same length as TEAM_B_UDID, so an answer that echoes the path (Express's
102
- // "Cannot GET …") has the same length for both.
112
+ // Team B's phone on a node. /control sends a node's phone's actions on to
113
+ // the node, or refuses them naming it (nodePhoneControl.ts), so the team
114
+ // guard has to have hidden it before that gate runs.
115
+ const TEAM_B_NODE_UDID = `tb-node-b-${stamp}`;
116
+ // The same length as the hidden udids, so an answer that echoes the path
117
+ // (Express's "Cannot GET …") has the same length for each.
103
118
  const UNKNOWN_UDID = `tb-nobody-${stamp}`;
104
- // The tap handler proxies to the device's host unless it is this server, so
105
- // every device lives "here" and each request says it is addressed here.
106
- const DEVICE_HOST = 'http://127.0.0.1:4723';
107
- const HOST_HEADER = '127.0.0.1:4723';
119
+ // Whose a phone is comes from its row: its nodeId, else its exact host
120
+ // (isOtherServersPhone). A row with neither of this server's is another
121
+ // server's phone, and /control sends its requests on to that host. So the
122
+ // fixtures carry this server's node id, and /control runs them here with
123
+ // the fake manager below. Port 1 has nothing listening, so a request sent
124
+ // on by mistake fails at once instead of reaching a real server here.
125
+ const DEVICE_HOST = 'http://127.0.0.1:1';
108
126
  before(() => __awaiter(this, void 0, void 0, function* () {
109
127
  keyDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-team-boundary-'));
110
128
  const keys = new JwtKeyService_1.JwtKeyService();
@@ -112,21 +130,28 @@ describe('team boundary on /control (integration)', function () {
112
130
  restores.push((0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, device_managers_1.XenonManager));
113
131
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
114
132
  typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return [new IOSDeviceManager()]; }) });
133
+ // The scratch database goes with the suite, and every row seeded here
134
+ // with it.
115
135
  sa = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'TB SA' });
116
136
  alice = yield (0, seedUser_1.seedUser)('MEMBER', { name: 'Alice (team A)' });
117
- teamA = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-a-${stamp}`);
118
- teamB = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-b-${stamp}`);
137
+ const teamA = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-a-${stamp}`);
138
+ const teamB = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-b-${stamp}`);
119
139
  yield typedi_1.Container.get(TeamService_1.TeamService).addMember(teamA.id, alice.user.id);
140
+ // The in-memory store is one collection for the whole process. Held here,
141
+ // so `after` removes these rows whatever useLokiStores has put back by then.
142
+ deviceRows = yield db_1.XenonDatabase.DeviceModel;
120
143
  const store = device_store_1.DeviceStoreFactory.getStore();
121
- for (const [udid, teamId] of [
122
- [SHARED_UDID, null],
123
- [TEAM_A_UDID, teamA.id],
124
- [TEAM_B_UDID, teamB.id],
144
+ for (const [udid, teamId, nodeId] of [
145
+ [SHARED_UDID, null, own_node_id_1.OWN_NODE_ID],
146
+ [TEAM_A_UDID, teamA.id, own_node_id_1.OWN_NODE_ID],
147
+ [TEAM_B_UDID, teamB.id, own_node_id_1.OWN_NODE_ID],
148
+ [TEAM_B_NODE_UDID, teamB.id, 'tb-another-node'],
125
149
  ]) {
126
150
  const row = yield prisma_1.prisma.device.create({
127
151
  data: {
128
152
  udid,
129
153
  host: DEVICE_HOST,
154
+ nodeId,
130
155
  name: udid,
131
156
  platform: 'ios',
132
157
  // Known screen size, so stream/ticket's after-response size fetch
@@ -139,19 +164,14 @@ describe('team boundary on /control (integration)', function () {
139
164
  yield store.addDevices([row]);
140
165
  }
141
166
  }));
142
- after(() => __awaiter(this, void 0, void 0, function* () {
143
- yield prisma_1.prisma.device.deleteMany({
144
- where: { udid: { in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID] } },
167
+ after(() => {
168
+ deviceRows === null || deviceRows === void 0 ? void 0 : deviceRows.findAndRemove({
169
+ udid: { $in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID, TEAM_B_NODE_UDID] },
145
170
  });
146
- yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
147
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
148
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
149
- yield sa.cleanup();
150
- yield alice.cleanup();
151
171
  for (const restore of restores.reverse())
152
172
  restore();
153
173
  fs_1.default.rmSync(keyDir, { recursive: true, force: true });
154
- }));
174
+ });
155
175
  /**
156
176
  * The /xenon/api stack as src/app/index.ts builds it, as far as /control
157
177
  * goes: cors, auth, the control router, then `tail`.
@@ -181,8 +201,8 @@ describe('team boundary on /control (integration)', function () {
181
201
  }
182
202
  function send(who, method, udid, action, tail = 'api') {
183
203
  const url = `/xenon/api/control/${encodeURIComponent(udid)}/${action}`;
184
- const req = (0, supertest_1.default)(buildApp(tail))[method](url);
185
- return req.set('Cookie', who.cookie).set('Host', HOST_HEADER);
204
+ const req = (0, loopbackRequest_1.default)(buildApp(tail))[method](url);
205
+ return req.set('Cookie', who.cookie);
186
206
  }
187
207
  /** What a caller can observe of an answer, with the requested udid masked out. */
188
208
  function observed(res, udid) {
@@ -195,12 +215,12 @@ describe('team boundary on /control (integration)', function () {
195
215
  allow: res.headers['allow'],
196
216
  };
197
217
  }
198
- function expectSameAnswer(method, action, tail) {
218
+ function expectSameAnswer(udid, method, action, tail) {
199
219
  return __awaiter(this, void 0, void 0, function* () {
200
220
  var _a;
201
- const hidden = yield send(alice, method, TEAM_B_UDID, action, tail);
221
+ const hidden = yield send(alice, method, udid, action, tail);
202
222
  const unknown = yield send(alice, method, UNKNOWN_UDID, action, tail);
203
- (0, chai_1.expect)(observed(hidden, TEAM_B_UDID)).to.deep.equal(observed(unknown, UNKNOWN_UDID));
223
+ (0, chai_1.expect)(observed(hidden, udid)).to.deep.equal(observed(unknown, UNKNOWN_UDID));
204
224
  // The guard's placeholder never reaches the caller, in a body or a header.
205
225
  (0, chai_1.expect)((_a = hidden.text) !== null && _a !== void 0 ? _a : '', 'body').to.not.include(controlDevice_1.HIDDEN_DEVICE_UDID);
206
226
  (0, chai_1.expect)(JSON.stringify(hidden.headers), 'headers').to.not.include(controlDevice_1.HIDDEN_DEVICE_UDID);
@@ -274,51 +294,58 @@ describe('team boundary on /control (integration)', function () {
274
294
  // has this phone. Hold every request to that: another team's phone has to
275
295
  // get the same status, body, content type, length and Allow header as a
276
296
  // udid that does not exist, whether a route handles the request or not.
277
- describe("another team's phone answers exactly like an unknown udid", () => {
278
- describe('on every route', () => {
279
- for (const { method, action } of CONTROL_ROUTES) {
280
- it(`${method.toUpperCase()} ${action}`, () => __awaiter(this, void 0, void 0, function* () {
281
- const res = yield expectSameAnswer(method, action, 'api');
282
- (0, chai_1.expect)(res.status, 'status').to.equal(404);
283
- }));
284
- }
285
- });
286
- const TAIL_LABEL = {
287
- api: "the API's JSON 404",
288
- express: "Express's own 404",
289
- 'echo-url': 'a later layer that echoes req.url',
290
- };
291
- for (const tail of ['api', 'express', 'echo-url']) {
292
- describe(`where no route handles the request (${TAIL_LABEL[tail]})`, () => {
293
- for (const [method, action] of [
294
- ['get', 'bogus'],
295
- ['post', 'bogus'],
296
- ['post', 'stream/bogus'],
297
- ['get', ''],
298
- ]) {
299
- it(`${method.toUpperCase()} ${action || '(no action)'}`, () => __awaiter(this, void 0, void 0, function* () {
300
- yield expectSameAnswer(method, action, tail);
297
+ // On this server or on a node: a node's phone must not get the gate's
298
+ // forward or its refusal naming the node.
299
+ for (const [label, hiddenUdid] of [
300
+ ["another team's phone", TEAM_B_UDID],
301
+ ["another team's phone on a node", TEAM_B_NODE_UDID],
302
+ ]) {
303
+ describe(`${label} answers exactly like an unknown udid`, () => {
304
+ describe('on every route', () => {
305
+ for (const { method, action } of CONTROL_ROUTES) {
306
+ it(`${method.toUpperCase()} ${action}`, () => __awaiter(this, void 0, void 0, function* () {
307
+ const res = yield expectSameAnswer(hiddenUdid, method, action, 'api');
308
+ (0, chai_1.expect)(res.status, 'status').to.equal(404);
301
309
  }));
302
310
  }
311
+ });
312
+ const TAIL_LABEL = {
313
+ api: "the API's JSON 404",
314
+ express: "Express's own 404",
315
+ 'echo-url': 'a later layer that echoes req.url',
316
+ };
317
+ for (const tail of ['api', 'express', 'echo-url']) {
318
+ describe(`where no route handles the request (${TAIL_LABEL[tail]})`, () => {
319
+ for (const [method, action] of [
320
+ ['get', 'bogus'],
321
+ ['post', 'bogus'],
322
+ ['post', 'stream/bogus'],
323
+ ['get', ''],
324
+ ]) {
325
+ it(`${method.toUpperCase()} ${action || '(no action)'}`, () => __awaiter(this, void 0, void 0, function* () {
326
+ yield expectSameAnswer(hiddenUdid, method, action, tail);
327
+ }));
328
+ }
329
+ for (const [action, methods] of METHODS_BY_ACTION) {
330
+ const wrong = methods.includes('get') ? 'post' : 'get';
331
+ if (methods.includes(wrong))
332
+ continue;
333
+ it(`${wrong.toUpperCase()} ${action} (the wrong method)`, () => __awaiter(this, void 0, void 0, function* () {
334
+ yield expectSameAnswer(hiddenUdid, wrong, action, tail);
335
+ }));
336
+ }
337
+ });
338
+ }
339
+ describe('OPTIONS', () => {
303
340
  for (const [action, methods] of METHODS_BY_ACTION) {
304
- const wrong = methods.includes('get') ? 'post' : 'get';
305
- if (methods.includes(wrong))
306
- continue;
307
- it(`${wrong.toUpperCase()} ${action} (the wrong method)`, () => __awaiter(this, void 0, void 0, function* () {
308
- yield expectSameAnswer(wrong, action, tail);
341
+ it(`OPTIONS ${action}`, () => __awaiter(this, void 0, void 0, function* () {
342
+ const res = yield expectSameAnswer(hiddenUdid, 'options', action, 'api');
343
+ (0, chai_1.expect)(res.headers['allow'], 'allow').to.be.a('string');
344
+ for (const m of methods)
345
+ (0, chai_1.expect)(res.headers['allow']).to.include(m.toUpperCase());
309
346
  }));
310
347
  }
311
348
  });
312
- }
313
- describe('OPTIONS', () => {
314
- for (const [action, methods] of METHODS_BY_ACTION) {
315
- it(`OPTIONS ${action}`, () => __awaiter(this, void 0, void 0, function* () {
316
- const res = yield expectSameAnswer('options', action, 'api');
317
- (0, chai_1.expect)(res.headers['allow'], 'allow').to.be.a('string');
318
- for (const m of methods)
319
- (0, chai_1.expect)(res.headers['allow']).to.include(m.toUpperCase());
320
- }));
321
- }
322
349
  });
323
- });
350
+ }
324
351
  });
@@ -15,15 +15,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  require("reflect-metadata");
16
16
  const chai_1 = require("chai");
17
17
  const express_1 = __importDefault(require("express"));
18
- const supertest_1 = __importDefault(require("supertest"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
19
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
21
21
  const typedi_1 = require("typedi");
22
22
  const TeamService_1 = require("../../src/services/TeamService");
23
23
  const prisma_1 = require("../../src/prisma");
24
24
  const seedUser_1 = require("../helpers/seedUser");
25
+ const scratch_database_1 = require("../helpers/scratch-database");
25
26
  describe('team visibility on /dashboard reads (integration)', function () {
26
27
  this.timeout(60000);
28
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
27
29
  let sa;
28
30
  let aliceMember;
29
31
  let teamA;
@@ -94,24 +96,6 @@ describe('team visibility on /dashboard reads (integration)', function () {
94
96
  data: Object.assign({ id: `sess-b-${stamp}`, device_udid: TEAM_B_UDID, build_id: buildTeamB.id }, baseSession),
95
97
  });
96
98
  }));
97
- after(() => __awaiter(this, void 0, void 0, function* () {
98
- yield prisma_1.prisma.session.deleteMany({
99
- where: { id: { in: [sessionShared.id, sessionTeamA.id, sessionTeamB.id] } },
100
- });
101
- yield prisma_1.prisma.build.deleteMany({
102
- where: { id: { in: [buildShared.id, buildTeamA.id, buildTeamB.id] } },
103
- });
104
- yield prisma_1.prisma.device.deleteMany({
105
- where: { udid: { in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID] } },
106
- });
107
- yield prisma_1.prisma.teamMember.deleteMany({
108
- where: { teamId: { in: [teamA.id, teamB.id] } },
109
- });
110
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
111
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
112
- yield sa.cleanup();
113
- yield aliceMember.cleanup();
114
- }));
115
99
  function buildApp() {
116
100
  const app = (0, express_1.default)();
117
101
  app.use(express_1.default.json());
@@ -121,13 +105,13 @@ describe('team visibility on /dashboard reads (integration)', function () {
121
105
  }
122
106
  it('SA sees all three sessions; Alice sees only shared + team-A', () => __awaiter(this, void 0, void 0, function* () {
123
107
  const app = buildApp();
124
- const saList = yield (0, supertest_1.default)(app).get('/session').set('Cookie', sa.cookie);
108
+ const saList = yield (0, loopbackRequest_1.default)(app).get('/session').set('Cookie', sa.cookie);
125
109
  (0, chai_1.expect)(saList.status).to.equal(200);
126
110
  const saIds = saList.body.map((s) => s.id);
127
111
  (0, chai_1.expect)(saIds).to.include(sessionShared.id);
128
112
  (0, chai_1.expect)(saIds).to.include(sessionTeamA.id);
129
113
  (0, chai_1.expect)(saIds).to.include(sessionTeamB.id);
130
- const aliceList = yield (0, supertest_1.default)(app).get('/session').set('Cookie', aliceMember.cookie);
114
+ const aliceList = yield (0, loopbackRequest_1.default)(app).get('/session').set('Cookie', aliceMember.cookie);
131
115
  (0, chai_1.expect)(aliceList.status).to.equal(200);
132
116
  const aliceIds = aliceList.body.map((s) => s.id);
133
117
  (0, chai_1.expect)(aliceIds).to.include(sessionShared.id);
@@ -135,13 +119,13 @@ describe('team visibility on /dashboard reads (integration)', function () {
135
119
  (0, chai_1.expect)(aliceIds).to.not.include(sessionTeamB.id);
136
120
  }));
137
121
  it('Alice GET /session/:teamB-id → 404', () => __awaiter(this, void 0, void 0, function* () {
138
- const r = yield (0, supertest_1.default)(buildApp())
122
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
139
123
  .get(`/session/${sessionTeamB.id}`)
140
124
  .set('Cookie', aliceMember.cookie);
141
125
  (0, chai_1.expect)(r.status).to.equal(404);
142
126
  }));
143
127
  it('Alice GET /build → does not include team-B-only build', () => __awaiter(this, void 0, void 0, function* () {
144
- const r = yield (0, supertest_1.default)(buildApp()).get('/build').set('Cookie', aliceMember.cookie);
128
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/build').set('Cookie', aliceMember.cookie);
145
129
  (0, chai_1.expect)(r.status).to.equal(200);
146
130
  const ids = r.body.map((b) => b.id);
147
131
  (0, chai_1.expect)(ids).to.include(buildShared.id);