@xenon-device-management/xenon 2.14.0 → 2.16.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 (335) hide show
  1. package/README.md +44 -29
  2. package/lib/package.json +6 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
  36. package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
  38. package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
  39. package/lib/public/assets/index-B0JeuZbg.js +15 -0
  40. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
  56. package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
  60. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  61. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  62. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +16 -12
  77. package/lib/src/InternalHttpClient.js +27 -5
  78. package/lib/src/XenonCapabilityManager.js +10 -3
  79. package/lib/src/app/dashboardPluginLink.js +117 -0
  80. package/lib/src/app/index.js +13 -34
  81. package/lib/src/app/openapi/control.yaml +6 -4
  82. package/lib/src/app/openapi/grid.yaml +8 -2
  83. package/lib/src/app/openapi/identity.yaml +103 -31
  84. package/lib/src/app/openapi/platform.yaml +33 -15
  85. package/lib/src/app/openapi/sessions.yaml +12 -3
  86. package/lib/src/app/routers/apikeys.js +14 -2
  87. package/lib/src/app/routers/auth.js +31 -6
  88. package/lib/src/app/routers/control.js +13 -5
  89. package/lib/src/app/routers/dashboard.js +42 -27
  90. package/lib/src/app/routers/grid.js +5 -3
  91. package/lib/src/app/routers/profile.js +21 -0
  92. package/lib/src/app/routers/users.js +12 -6
  93. package/lib/src/app/swagger.js +2 -0
  94. package/lib/src/app/ws/h264StreamWs.js +10 -1
  95. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  96. package/lib/src/config.js +1 -0
  97. package/lib/src/dashboard/commandLogFields.js +42 -0
  98. package/lib/src/dashboard/commands.js +5 -5
  99. package/lib/src/dashboard/event-manager.js +64 -204
  100. package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
  101. package/lib/src/dashboard/services/failureCategories.js +264 -0
  102. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  103. package/lib/src/data-service/device-store.js +14 -1
  104. package/lib/src/data-service/prisma-store.js +19 -7
  105. package/lib/src/data-service/web-config-service.js +7 -0
  106. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  107. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  108. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  109. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  110. package/lib/src/device-utils.js +29 -3
  111. package/lib/src/gateway/defaultGateway.js +1 -1
  112. package/lib/src/gateway/forwardToNode.js +6 -11
  113. package/lib/src/gateway/healReport.js +87 -0
  114. package/lib/src/gateway/hubSessionToken.js +27 -4
  115. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  116. package/lib/src/gateway/sessionGateway.js +12 -2
  117. package/lib/src/generated/client/edge.js +4 -3
  118. package/lib/src/generated/client/index-browser.js +1 -0
  119. package/lib/src/generated/client/index.d.ts +32 -0
  120. package/lib/src/generated/client/index.js +4 -3
  121. package/lib/src/generated/client/package.json +1 -1
  122. package/lib/src/generated/client/schema.prisma +3 -0
  123. package/lib/src/generated/client/wasm.js +1 -0
  124. package/lib/src/helpers/outboundProxy.js +219 -0
  125. package/lib/src/index.js +21 -0
  126. package/lib/src/interceptors/CommandInterceptor.js +256 -196
  127. package/lib/src/interceptors/xenonScripts.js +28 -7
  128. package/lib/src/logger.js +3 -1
  129. package/lib/src/middleware/authMiddleware.js +15 -3
  130. package/lib/src/middleware/commandCaller.js +3 -3
  131. package/lib/src/middleware/verifyCredential.js +58 -1
  132. package/lib/src/plugin.js +16 -6
  133. package/lib/src/scripts/database-check.js +3 -3
  134. package/lib/src/scripts/initialize-database.js +35 -33
  135. package/lib/src/scripts/run-migrations.js +1079 -27
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +192 -42
  138. package/lib/src/services/ApiKeyService.js +16 -5
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +6 -2
  141. package/lib/src/services/OrphanSweeper.js +8 -0
  142. package/lib/src/services/SelectorStateService.js +11 -7
  143. package/lib/src/services/SelectorVerificationJob.js +21 -6
  144. package/lib/src/services/ServerManager.js +11 -1
  145. package/lib/src/services/SessionLifecycleService.js +118 -44
  146. package/lib/src/services/SocketClient.js +11 -0
  147. package/lib/src/services/SocketServer.js +380 -45
  148. package/lib/src/services/TeamService.js +5 -1
  149. package/lib/src/services/TracingService.js +171 -60
  150. package/lib/src/services/UserService.js +14 -3
  151. package/lib/src/services/UserSessionService.js +16 -4
  152. package/lib/src/services/bug-report/video-slice.js +4 -1
  153. package/lib/src/services/device-access/leaseOverride.js +7 -5
  154. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  155. package/lib/src/services/healing/HealEtalonService.js +3 -2
  156. package/lib/src/services/healing/HealingOrchestrator.js +57 -72
  157. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  158. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  159. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  160. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  161. package/lib/src/services/healing/resilioPath.js +274 -0
  162. package/lib/src/services/identity/identityChanges.js +41 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +164 -0
  165. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  166. package/lib/src/services/ocr/ocrData.js +118 -0
  167. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  168. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  169. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  170. package/lib/src/services/passwordResetLink.js +50 -9
  171. package/lib/src/services/passwordResetMode.js +3 -1
  172. package/lib/src/services/publicUrl.js +45 -0
  173. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  174. package/lib/src/services/session/sessionIdentity.js +5 -3
  175. package/lib/src/services/session/shutdownReason.js +26 -0
  176. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  177. package/lib/src/services/token/JwtKeyService.js +8 -2
  178. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  179. package/lib/src/services/token/mintedLifetime.js +51 -0
  180. package/lib/src/sessions/LocalSession.js +29 -1
  181. package/lib/src/sessions/RemoteSession.js +12 -2
  182. package/lib/src/sessions/SessionManager.js +4 -1
  183. package/lib/src/sessions/sessionMemory.js +25 -0
  184. package/lib/test/helpers/driver-log.js +38 -0
  185. package/lib/test/helpers/fake-ai-provider.js +188 -0
  186. package/lib/test/helpers/fake-http-proxy.js +106 -0
  187. package/lib/test/helpers/loki-stores.js +33 -0
  188. package/lib/test/helpers/otlp-probe.js +107 -0
  189. package/lib/test/helpers/page-sources.js +164 -0
  190. package/lib/test/helpers/scratch-database.js +57 -13
  191. package/lib/test/integration/androidDevices.spec.js +1 -1
  192. package/lib/test/integration/audit-ingest.spec.js +10 -10
  193. package/lib/test/integration/auth-flow.spec.js +9 -14
  194. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  195. package/lib/test/integration/bug-report-route.spec.js +22 -2
  196. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  197. package/lib/test/integration/forgot-password.spec.js +17 -18
  198. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  199. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  200. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  201. package/lib/test/integration/migration.spec.js +6 -1
  202. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  203. package/lib/test/integration/ports-router.spec.js +4 -4
  204. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  205. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  206. package/lib/test/integration/profile-tokens.spec.js +10 -16
  207. package/lib/test/integration/recordings-router.spec.js +16 -16
  208. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  209. package/lib/test/integration/role-matrix.spec.js +4 -9
  210. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  211. package/lib/test/integration/sdk-version.spec.js +2 -2
  212. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  213. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  214. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  215. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  216. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  217. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  218. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  219. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  220. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  221. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  222. package/lib/test/integration/users-flow.spec.js +7 -12
  223. package/lib/test/integration/users-lockout.spec.js +5 -3
  224. package/lib/test/unit/EmailService.test.js +66 -0
  225. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  226. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  227. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  228. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  229. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  230. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  231. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  232. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  233. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  234. package/lib/test/unit/apikeys-router.spec.js +84 -0
  235. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  236. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  237. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  238. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  239. package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
  240. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  241. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  242. package/lib/test/unit/database-check.spec.js +1 -0
  243. package/lib/test/unit/device-log-book.spec.js +180 -0
  244. package/lib/test/unit/device-utils.spec.js +13 -4
  245. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  246. package/lib/test/unit/event-log-session-data.spec.js +251 -0
  247. package/lib/test/unit/failure-analysis.spec.js +196 -0
  248. package/lib/test/unit/failure-categories.spec.js +130 -0
  249. package/lib/test/unit/failure-categorization.spec.js +393 -0
  250. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  251. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  252. package/lib/test/unit/heal-report.spec.js +71 -0
  253. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  254. package/lib/test/unit/healing-tier-gate.spec.js +74 -29
  255. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  256. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
  257. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  258. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  259. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  260. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  261. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  262. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  263. package/lib/test/unit/logcat-ws.spec.js +78 -37
  264. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  265. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  266. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  267. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  268. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  269. package/lib/test/unit/ocr-offline.spec.js +213 -0
  270. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  271. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  272. package/lib/test/unit/omni-vision.spec.js +5 -7
  273. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  274. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  275. package/lib/test/unit/package-engines.spec.js +27 -0
  276. package/lib/test/unit/passwordResetMode.test.js +10 -2
  277. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  278. package/lib/test/unit/profile-router.test.js +122 -1
  279. package/lib/test/unit/proxy-option.spec.js +358 -0
  280. package/lib/test/unit/queue-requester.spec.js +1 -1
  281. package/lib/test/unit/reset-link-address.spec.js +266 -0
  282. package/lib/test/unit/reset-link.test.js +10 -4
  283. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  284. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  285. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  286. package/lib/test/unit/run-migrations.test.js +864 -0
  287. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  288. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  289. package/lib/test/unit/selector-state-service.spec.js +72 -23
  290. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  291. package/lib/test/unit/selector-verification-job.spec.js +28 -19
  292. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  293. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  294. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  295. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  296. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  297. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  298. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  299. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  300. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  301. package/lib/test/unit/session-device-logs.spec.js +356 -0
  302. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  303. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  304. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  305. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  306. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  307. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  308. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  309. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  310. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  311. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  312. package/lib/test/unit/verify-credential.spec.js +33 -0
  313. package/lib/test/unit/vision-assertion.spec.js +3 -2
  314. package/lib/test/unit/web-config-service.spec.js +19 -0
  315. package/lib/tsconfig.tsbuildinfo +1 -1
  316. package/package.json +6 -5
  317. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  318. package/prisma/schema.prisma +3 -0
  319. package/schema.json +16 -12
  320. package/scripts/dev/readme-screenshots.js +276 -23
  321. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  322. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  323. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  324. package/scripts/export-openapi.js +32 -0
  325. package/scripts/lib/openapi-for-site.js +79 -0
  326. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  327. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  328. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  329. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  330. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  331. package/lib/public/assets/index-BbKEXUFH.js +0 -219
  332. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  333. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  334. package/lib/src/scripts/prepare-prisma.js +0 -89
  335. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -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);
@@ -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 authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const grid_1 = __importDefault(require("../../src/app/routers/grid"));
21
21
  const typedi_1 = require("typedi");
@@ -23,8 +23,11 @@ const TeamService_1 = require("../../src/services/TeamService");
23
23
  const prisma_1 = require("../../src/prisma");
24
24
  const seedUser_1 = require("../helpers/seedUser");
25
25
  const device_store_1 = require("../../src/data-service/device-store");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
27
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
26
28
  describe('team visibility on /grid/devices (integration)', function () {
27
29
  this.timeout(60000);
30
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
28
31
  let sa;
29
32
  let aliceMember;
30
33
  let bobUnaffiliated;
@@ -43,6 +46,9 @@ describe('team visibility on /grid/devices (integration)', function () {
43
46
  teamB = yield typedi_1.Container.get(TeamService_1.TeamService).create(`p4a-team-b-${stamp}`);
44
47
  // Alice on team A only.
45
48
  yield typedi_1.Container.get(TeamService_1.TeamService).addMember(teamA.id, aliceMember.user.id);
49
+ // /devices lists the store grid.ts took when it was imported: Prisma under
50
+ // `npx mocha <file>`, Loki under `test:all` (NODE_ENV=test). So each phone
51
+ // is a Device row and is added to that store.
46
52
  store = device_store_1.DeviceStoreFactory.getStore();
47
53
  // Three test devices. Use minimal field set; the schema may have
48
54
  // additional optional fields we don't need to populate for this test.
@@ -77,18 +83,13 @@ describe('team visibility on /grid/devices (integration)', function () {
77
83
  });
78
84
  yield store.addDevices([teamBDev]);
79
85
  }));
86
+ // Loki is one collection for the whole process: take the phones back out.
87
+ // A Prisma store's rows went with the scratch database.
80
88
  after(() => __awaiter(this, void 0, void 0, function* () {
81
- yield prisma_1.prisma.device.deleteMany({
82
- where: { udid: { in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID] } },
83
- });
84
- yield prisma_1.prisma.teamMember.deleteMany({
85
- where: { teamId: { in: [teamA.id, teamB.id] } },
86
- });
87
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
88
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
89
- yield sa.cleanup();
90
- yield aliceMember.cleanup();
91
- yield bobUnaffiliated.cleanup();
89
+ if (store instanceof prisma_store_1.PrismaDeviceStore)
90
+ return;
91
+ for (const udid of [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID])
92
+ yield store.removeDevices({ udid });
92
93
  }));
93
94
  function buildApp() {
94
95
  const app = (0, express_1.default)();
@@ -101,7 +102,7 @@ describe('team visibility on /grid/devices (integration)', function () {
101
102
  return rows.map((r) => r.udid);
102
103
  }
103
104
  it('SUPER_ADMIN sees all three test devices', () => __awaiter(this, void 0, void 0, function* () {
104
- const r = yield (0, supertest_1.default)(buildApp()).get('/devices').set('Cookie', sa.cookie);
105
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/devices').set('Cookie', sa.cookie);
105
106
  (0, chai_1.expect)(r.status).to.equal(200);
106
107
  const got = udids(r.body);
107
108
  (0, chai_1.expect)(got).to.include(SHARED_UDID);
@@ -109,7 +110,7 @@ describe('team visibility on /grid/devices (integration)', function () {
109
110
  (0, chai_1.expect)(got).to.include(TEAM_B_UDID);
110
111
  }));
111
112
  it('Alice (team A member) sees shared + team A; not team B', () => __awaiter(this, void 0, void 0, function* () {
112
- const r = yield (0, supertest_1.default)(buildApp()).get('/devices').set('Cookie', aliceMember.cookie);
113
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/devices').set('Cookie', aliceMember.cookie);
113
114
  (0, chai_1.expect)(r.status).to.equal(200);
114
115
  const got = udids(r.body);
115
116
  (0, chai_1.expect)(got).to.include(SHARED_UDID);
@@ -117,7 +118,7 @@ describe('team visibility on /grid/devices (integration)', function () {
117
118
  (0, chai_1.expect)(got).to.not.include(TEAM_B_UDID);
118
119
  }));
119
120
  it('Bob (no team) sees only the shared device', () => __awaiter(this, void 0, void 0, function* () {
120
- const r = yield (0, supertest_1.default)(buildApp()).get('/devices').set('Cookie', bobUnaffiliated.cookie);
121
+ const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/devices').set('Cookie', bobUnaffiliated.cookie);
121
122
  (0, chai_1.expect)(r.status).to.equal(200);
122
123
  const got = udids(r.body);
123
124
  (0, chai_1.expect)(got).to.include(SHARED_UDID);
@@ -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 typedi_1 = require("typedi");
20
20
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
21
21
  const sdk_leases_1 = require("../../src/app/routers/sdk-leases");
@@ -24,6 +24,8 @@ const TeamService_1 = require("../../src/services/TeamService");
24
24
  const device_store_1 = require("../../src/data-service/device-store");
25
25
  const prisma_1 = require("../../src/prisma");
26
26
  const seedUser_1 = require("../helpers/seedUser");
27
+ const scratch_database_1 = require("../helpers/scratch-database");
28
+ const loki_stores_1 = require("../helpers/loki-stores");
27
29
  /**
28
30
  * SDK leases against the production store: the real auth middleware computes
29
31
  * the member's teams, and the Prisma store's match (a SQL where clause, not
@@ -32,6 +34,10 @@ const seedUser_1 = require("../helpers/seedUser");
32
34
  */
33
35
  describe('team boundary on SDK leases (integration)', function () {
34
36
  this.timeout(60000);
37
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
38
+ // The Prisma store this suite is about, under `test:all` too, whose
39
+ // NODE_ENV would hand the factory a Loki one.
40
+ (0, loki_stores_1.usePrismaStores)();
35
41
  let sa;
36
42
  let alice;
37
43
  let teamA;
@@ -63,15 +69,6 @@ describe('team boundary on SDK leases (integration)', function () {
63
69
  yield prisma_1.prisma.lease.deleteMany({ where: { deviceUdid: { in: UDIDS } } });
64
70
  yield prisma_1.prisma.device.updateMany({ where: { udid: { in: UDIDS } }, data: { busy: false } });
65
71
  }));
66
- after(() => __awaiter(this, void 0, void 0, function* () {
67
- yield prisma_1.prisma.lease.deleteMany({ where: { deviceUdid: { in: UDIDS } } });
68
- yield prisma_1.prisma.device.deleteMany({ where: { udid: { in: UDIDS } } });
69
- yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
70
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
71
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
72
- yield sa.cleanup();
73
- yield alice.cleanup();
74
- }));
75
72
  function lease(who, filters) {
76
73
  const svc = new LeaseService_1.LeaseService(prisma_1.prisma, device_store_1.DeviceStoreFactory.getStore(), {
77
74
  allocate: () => __awaiter(this, void 0, void 0, function* () { return ({ systemPort: 9001, chromedriverPort: 9002, mjpegServerPort: 9003 }); }),
@@ -80,7 +77,7 @@ describe('team boundary on SDK leases (integration)', function () {
80
77
  app.use(express_1.default.json());
81
78
  app.use(authMiddleware_1.authMiddleware);
82
79
  app.use('/sdk/leases', (0, sdk_leases_1.makeRouter)({ leaseService: svc }));
83
- return (0, supertest_1.default)(app)
80
+ return (0, loopbackRequest_1.default)(app)
84
81
  .post('/sdk/leases')
85
82
  .set('Cookie', who.cookie)
86
83
  .send({ filters: Object.assign({ platform: 'android', filterByHost: HOST }, filters) });
@@ -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 authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const recordings_1 = __importDefault(require("../../src/app/routers/recordings"));
21
21
  const typedi_1 = require("typedi");
@@ -23,8 +23,10 @@ const TeamService_1 = require("../../src/services/TeamService");
23
23
  const prisma_1 = require("../../src/prisma");
24
24
  const seedUser_1 = require("../helpers/seedUser");
25
25
  const artifact_store_1 = require("../helpers/artifact-store");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
26
27
  describe('team visibility on /recordings reads (integration)', function () {
27
28
  this.timeout(60000);
29
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
28
30
  // GET /recordings/:groupId resolves ARTIFACT_STORE from the container, which
29
31
  // ServerManager registers at boot. Without it every allowed read is a 500.
30
32
  (0, artifact_store_1.useArtifactStore)();
@@ -90,21 +92,6 @@ describe('team visibility on /recordings reads (integration)', function () {
90
92
  data: Object.assign({ id: `rec-b-${stamp}`, group_id: GROUP_TEAM_B, device_udid: TEAM_B_UDID }, baseRecording),
91
93
  });
92
94
  }));
93
- after(() => __awaiter(this, void 0, void 0, function* () {
94
- yield prisma_1.prisma.recording.deleteMany({
95
- where: { group_id: { in: [GROUP_SHARED, GROUP_TEAM_A, GROUP_TEAM_B] } },
96
- });
97
- yield prisma_1.prisma.device.deleteMany({
98
- where: { udid: { in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID] } },
99
- });
100
- yield prisma_1.prisma.teamMember.deleteMany({
101
- where: { teamId: { in: [teamA.id, teamB.id] } },
102
- });
103
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
104
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
105
- yield sa.cleanup();
106
- yield aliceMember.cleanup();
107
- }));
108
95
  function buildApp() {
109
96
  const app = (0, express_1.default)();
110
97
  app.use(express_1.default.json());
@@ -113,19 +100,19 @@ describe('team visibility on /recordings reads (integration)', function () {
113
100
  return app;
114
101
  }
115
102
  it('Alice GET /recordings/:teamBGroup → 404', () => __awaiter(this, void 0, void 0, function* () {
116
- const r = yield (0, supertest_1.default)(buildApp())
103
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
117
104
  .get(`/recordings/${GROUP_TEAM_B}`)
118
105
  .set('Cookie', aliceMember.cookie);
119
106
  (0, chai_1.expect)(r.status).to.equal(404);
120
107
  }));
121
108
  it('SA GET /recordings/:teamBGroup → 200 (admin sees all)', () => __awaiter(this, void 0, void 0, function* () {
122
- const r = yield (0, supertest_1.default)(buildApp())
109
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
123
110
  .get(`/recordings/${GROUP_TEAM_B}`)
124
111
  .set('Cookie', sa.cookie);
125
112
  (0, chai_1.expect)(r.status).to.equal(200);
126
113
  }));
127
114
  it('Alice GET /recordings/:teamAGroup → 200 (member of team A)', () => __awaiter(this, void 0, void 0, function* () {
128
- const r = yield (0, supertest_1.default)(buildApp())
115
+ const r = yield (0, loopbackRequest_1.default)(buildApp())
129
116
  .get(`/recordings/${GROUP_TEAM_A}`)
130
117
  .set('Cookie', aliceMember.cookie);
131
118
  (0, chai_1.expect)(r.status).to.equal(200);