@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
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const adbkit_1 = __importDefault(require("@devicefarmer/adbkit"));
19
+ const appium_adb_1 = require("appium-adb");
20
+ const test_container_1 = require("../helpers/test-container");
21
+ const RETRY_MS = 60000;
22
+ /**
23
+ * When adb couldn't be started, or its device tracking couldn't, the server
24
+ * logged "Could not find ADB" without the reason and never looked again: every
25
+ * later discovery answered no phones until a restart, even once adb worked
26
+ * (an SDK installed, a stuck adb server killed). It now says why, and tries
27
+ * again a minute later.
28
+ */
29
+ describe('Android discovery when adb is unavailable', () => {
30
+ const sandbox = sinon_1.default.createSandbox();
31
+ let manager;
32
+ let createADB;
33
+ let trackDevices;
34
+ let errors;
35
+ let clock;
36
+ let savedNodeEnv;
37
+ const workingAdb = () => ({
38
+ adbRemoteHost: null,
39
+ getConnectedDevices: sandbox.stub().resolves([]),
40
+ });
41
+ beforeEach(() => {
42
+ savedNodeEnv = process.env.NODE_ENV;
43
+ clock = sandbox.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
44
+ createADB = sandbox.stub(appium_adb_1.ADB, 'createADB');
45
+ trackDevices = sandbox.stub().resolves({ on: sandbox.stub() });
46
+ sandbox.stub(adbkit_1.default, 'createClient').returns({ trackDevices });
47
+ manager = (0, test_container_1.createTestAndroidManager)({ platform: 'android' });
48
+ sandbox.stub(manager, 'requireSdkRoot').resolves();
49
+ errors = sandbox.stub(manager.log, 'error');
50
+ });
51
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
52
+ sandbox.restore();
53
+ if (savedNodeEnv === undefined)
54
+ delete process.env.NODE_ENV;
55
+ else
56
+ process.env.NODE_ENV = savedNodeEnv;
57
+ yield (0, test_container_1.resetTestContainer)();
58
+ }));
59
+ const discover = () => manager.getDevices({ androidDeviceType: 'both' }, []);
60
+ it('says why adb could not be started', () => __awaiter(void 0, void 0, void 0, function* () {
61
+ createADB.rejects(new Error('Could not find adb. spawn adb ENOENT'));
62
+ (0, chai_1.expect)(yield discover()).to.deep.equal([]);
63
+ const logged = errors.getCalls().map((c) => String(c.args[0]));
64
+ (0, chai_1.expect)(logged.some((m) => m.includes('spawn adb ENOENT')), logged.join('\n')).to.equal(true);
65
+ }));
66
+ it('tries adb again a minute later, and not before', () => __awaiter(void 0, void 0, void 0, function* () {
67
+ const adb = workingAdb();
68
+ createADB.onFirstCall().rejects(new Error('spawn adb ENOENT'));
69
+ createADB.onSecondCall().resolves(adb);
70
+ yield discover();
71
+ clock.tick(RETRY_MS - 1000);
72
+ yield discover();
73
+ (0, chai_1.expect)(createADB.callCount, 'within the minute').to.equal(1);
74
+ clock.tick(2000);
75
+ yield discover();
76
+ (0, chai_1.expect)(createADB.callCount, 'after the minute').to.equal(2);
77
+ (0, chai_1.expect)(adb.getConnectedDevices.called, 'discovery ran').to.equal(true);
78
+ }));
79
+ it('says why device tracking failed, keeps listing phones, and tries it again', () => __awaiter(void 0, void 0, void 0, function* () {
80
+ process.env.NODE_ENV = 'development';
81
+ const adb = workingAdb();
82
+ createADB.resolves(adb);
83
+ trackDevices.onFirstCall().rejects(new Error('connect ECONNREFUSED 127.0.0.1:5037'));
84
+ yield discover();
85
+ const logged = errors.getCalls().map((c) => String(c.args[0]));
86
+ (0, chai_1.expect)(logged.some((m) => m.includes('ECONNREFUSED')), logged.join('\n')).to.equal(true);
87
+ yield discover();
88
+ (0, chai_1.expect)(adb.getConnectedDevices.callCount, 'phones still listed').to.equal(2);
89
+ (0, chai_1.expect)(trackDevices.callCount, 'within the minute').to.equal(1);
90
+ clock.tick(RETRY_MS + 1000);
91
+ yield discover();
92
+ (0, chai_1.expect)(trackDevices.callCount, 'after the minute').to.equal(2);
93
+ }));
94
+ it('starts one device tracker when two discoveries start adb at once', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ // Each tracker runs the whole plug-in handling again for every phone
96
+ // plugged in, and nothing could end the extra one.
97
+ process.env.NODE_ENV = 'development';
98
+ createADB.resolves(workingAdb());
99
+ const pending = [];
100
+ trackDevices.callsFake(() => new Promise((resolve) => pending.push(() => resolve({ on: sandbox.stub() }))));
101
+ const both = Promise.all([discover(), discover()]);
102
+ while (pending.length === 0)
103
+ yield new Promise((r) => setImmediate(r));
104
+ // Time for a second discovery to reach the tracker too, if it would.
105
+ for (let i = 0; i < 20; i++)
106
+ yield new Promise((r) => setImmediate(r));
107
+ pending.forEach((start) => start());
108
+ yield both;
109
+ (0, chai_1.expect)(createADB.callCount, 'adb started').to.equal(1);
110
+ (0, chai_1.expect)(trackDevices.callCount, 'trackers started').to.equal(1);
111
+ }));
112
+ });
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const http_1 = __importDefault(require("http"));
17
+ const chai_1 = require("chai");
18
+ const typedi_1 = require("typedi");
19
+ const ws_1 = require("ws");
20
+ const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
21
+ const h264StreamWs_1 = require("../../src/app/ws/h264StreamWs");
22
+ const PluginContext_1 = require("../../src/PluginContext");
23
+ const container_registration_1 = require("../helpers/container-registration");
24
+ const loopbackServer_1 = require("../helpers/loopbackServer");
25
+ /**
26
+ * Which viewers an H.264 capture outlives once a page has asked for MJPEG.
27
+ *
28
+ * A page whose H.264 player fails (a device control page, or a Live devices
29
+ * tile) asks for MJPEG (`stream/start` with `player: 'mjpeg'`). The phone may
30
+ * have other viewers still playing its H.264 preview: another tile, another
31
+ * tab, another admin. Ending the capture under them froze their picture on
32
+ * its last frame. So the capture ends when nobody watches it: at once if
33
+ * nobody does, else the moment the last one leaves, and never after the
34
+ * ten-minute idle wait an unwatched H.264 capture otherwise gets.
35
+ */
36
+ const UDID = 'R58M123';
37
+ const key = (n) => ({ type: 'key', data: Buffer.from([n]), ptsMs: n });
38
+ // The capture seam (openCapture) is stubbed, as in android-h264-service.spec.ts.
39
+ function make() {
40
+ const svc = Object.create(AndroidH264StreamService_1.default.prototype);
41
+ svc.sessions = new Map();
42
+ svc.startPromises = new Map();
43
+ svc.scrcpyIncompatible = new Set();
44
+ return svc;
45
+ }
46
+ /** A service whose next capture is under the test's control. */
47
+ function withCapture() {
48
+ const svc = make();
49
+ const capture = {
50
+ kills: 0,
51
+ push: () => undefined,
52
+ };
53
+ let release = () => undefined;
54
+ let held = false;
55
+ svc.openCapture = (_udid, onPacket) => __awaiter(this, void 0, void 0, function* () {
56
+ capture.push = onPacket;
57
+ if (held)
58
+ yield new Promise((r) => (release = r));
59
+ onPacket({ type: 'config', data: Buffer.from([0]), ptsMs: 0 });
60
+ return {
61
+ kill: () => {
62
+ capture.kills += 1;
63
+ },
64
+ };
65
+ });
66
+ return {
67
+ svc,
68
+ capture,
69
+ /** Keep the next capture starting until `finishStart()`. */
70
+ holdStart: () => (held = true),
71
+ finishStart: () => release(),
72
+ };
73
+ }
74
+ /** A viewer's socket: what it received, and how it leaves. */
75
+ function watch(mux) {
76
+ const got = [];
77
+ const leave = mux.addClient((p) => got.push(p.type));
78
+ return { got, leave };
79
+ }
80
+ /** Waits for `done()`, a few milliseconds at a time: a socket's close lands on the server later. */
81
+ function until(done, what) {
82
+ return __awaiter(this, void 0, void 0, function* () {
83
+ const by = Date.now() + 2000;
84
+ while (!done()) {
85
+ if (Date.now() > by)
86
+ throw new Error(`timed out waiting until ${what}`);
87
+ yield new Promise((r) => setTimeout(r, 5));
88
+ }
89
+ });
90
+ }
91
+ describe('AndroidH264StreamService: a page that asks for MJPEG', () => {
92
+ const loopback = (0, loopbackServer_1.loopbackServers)();
93
+ let restoreContainer;
94
+ beforeEach(() => {
95
+ restoreContainer = (0, container_registration_1.saveRegistrations)(PluginContext_1.PluginContext);
96
+ typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: {} });
97
+ });
98
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
99
+ restoreContainer();
100
+ yield loopback.closeAll();
101
+ }));
102
+ it('ends the H.264 capture at once when nobody watches it', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ const { svc, capture } = withCapture();
104
+ yield svc.start(UDID);
105
+ yield svc.endWhenUnwatched(UDID);
106
+ (0, chai_1.expect)(capture.kills).to.equal(1);
107
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
108
+ }));
109
+ it('keeps it for a viewer still playing it', () => __awaiter(void 0, void 0, void 0, function* () {
110
+ const { svc, capture } = withCapture();
111
+ const viewer = watch(yield svc.start(UDID));
112
+ yield svc.endWhenUnwatched(UDID);
113
+ (0, chai_1.expect)(capture.kills).to.equal(0);
114
+ capture.push(key(1));
115
+ (0, chai_1.expect)(viewer.got).to.deep.equal(['config', 'key']);
116
+ }));
117
+ it('ends it the moment its last viewer leaves, not after the idle wait', () => __awaiter(void 0, void 0, void 0, function* () {
118
+ const { svc, capture } = withCapture();
119
+ const mux = yield svc.start(UDID);
120
+ const a = watch(mux);
121
+ const b = watch(mux);
122
+ yield svc.endWhenUnwatched(UDID);
123
+ a.leave();
124
+ (0, chai_1.expect)(capture.kills, 'b still plays it').to.equal(0);
125
+ b.leave();
126
+ (0, chai_1.expect)(capture.kills).to.equal(1);
127
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
128
+ }));
129
+ it('serves a viewer who joins before then, and ends when they leave too', () => __awaiter(void 0, void 0, void 0, function* () {
130
+ const { svc, capture } = withCapture();
131
+ const a = watch(yield svc.start(UDID));
132
+ yield svc.endWhenUnwatched(UDID);
133
+ const late = watch(yield svc.start(UDID));
134
+ a.leave();
135
+ (0, chai_1.expect)(capture.kills).to.equal(0);
136
+ capture.push(key(2));
137
+ (0, chai_1.expect)(late.got).to.include('key');
138
+ late.leave();
139
+ (0, chai_1.expect)(capture.kills).to.equal(1);
140
+ }));
141
+ it('does nothing when no H.264 capture runs for the phone', () => __awaiter(void 0, void 0, void 0, function* () {
142
+ const { svc, capture } = withCapture();
143
+ yield svc.endWhenUnwatched(UDID);
144
+ (0, chai_1.expect)(capture.kills).to.equal(0);
145
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
146
+ }));
147
+ it('leaves a capture that is still starting to the viewer starting it', () => __awaiter(void 0, void 0, void 0, function* () {
148
+ // A tile's socket starts the capture it is about to watch: ending it
149
+ // mid-start would leave that capture running with nothing to stop it.
150
+ const { svc, capture, holdStart, finishStart } = withCapture();
151
+ holdStart();
152
+ const starting = svc.start(UDID);
153
+ yield Promise.resolve();
154
+ yield svc.endWhenUnwatched(UDID);
155
+ finishStart();
156
+ const viewer = watch(yield starting);
157
+ (0, chai_1.expect)(capture.kills).to.equal(0);
158
+ viewer.leave();
159
+ (0, chai_1.expect)(capture.kills).to.equal(1);
160
+ }));
161
+ it('ends a capture whose viewer never came at the watchdog’s next look', () => __awaiter(void 0, void 0, void 0, function* () {
162
+ const { svc, capture, holdStart, finishStart } = withCapture();
163
+ holdStart();
164
+ const starting = svc.start(UDID);
165
+ yield Promise.resolve();
166
+ yield svc.endWhenUnwatched(UDID);
167
+ finishStart();
168
+ yield starting;
169
+ svc.sweep(Date.now());
170
+ (0, chai_1.expect)(capture.kills).to.equal(1);
171
+ }));
172
+ it('keeps the idle wait for a capture no page asked to end', () => __awaiter(void 0, void 0, void 0, function* () {
173
+ const { svc, capture } = withCapture();
174
+ yield svc.start(UDID);
175
+ svc.sweep(Date.now());
176
+ svc.sweep(Date.now() + 60000);
177
+ (0, chai_1.expect)(capture.kills).to.equal(0);
178
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.not.equal(undefined);
179
+ }));
180
+ it('does not end a newer capture when an older one’s last viewer leaves', () => __awaiter(void 0, void 0, void 0, function* () {
181
+ const { svc, capture } = withCapture();
182
+ const old = watch(yield svc.start(UDID));
183
+ yield svc.endWhenUnwatched(UDID);
184
+ yield svc.stop(UDID); // a recording took the phone, say
185
+ (0, chai_1.expect)(capture.kills).to.equal(1);
186
+ yield svc.start(UDID);
187
+ old.leave();
188
+ (0, chai_1.expect)(capture.kills).to.equal(1);
189
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.not.equal(undefined);
190
+ }));
191
+ it('ends it when the last viewer’s H.264 socket closes', () => __awaiter(void 0, void 0, void 0, function* () {
192
+ // The socket route (h264StreamWs.ts), as a tile's player opens and closes it.
193
+ const { svc, capture } = withCapture();
194
+ const server = yield loopback.serve(http_1.default.createServer());
195
+ (0, h264StreamWs_1.attachH264Ws)(server, {
196
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'usr_alice' }); }),
197
+ startStream: (udid) => svc.start(udid),
198
+ });
199
+ const { port } = server.address();
200
+ // The config packet is sent the moment the socket joins the multiplexer.
201
+ const viewer = () => new Promise((resolve, reject) => {
202
+ const ws = new ws_1.WebSocket(`ws://127.0.0.1:${port}/xenon/api/control/${UDID}/stream/h264?ticket=t`);
203
+ ws.once('message', () => resolve(ws));
204
+ ws.once('error', reject);
205
+ });
206
+ const tile = yield viewer();
207
+ const otherTab = yield viewer();
208
+ const viewers = () => { var _a, _b; return (_b = (_a = svc.getMultiplexer(UDID)) === null || _a === void 0 ? void 0 : _a.clientCount) !== null && _b !== void 0 ? _b : 0; };
209
+ yield svc.endWhenUnwatched(UDID);
210
+ tile.close();
211
+ yield until(() => viewers() === 1, 'the tile’s socket has left');
212
+ (0, chai_1.expect)(capture.kills, 'the other tab still plays it').to.equal(0);
213
+ otherTab.close();
214
+ yield until(() => capture.kills === 1, 'the capture has ended');
215
+ (0, chai_1.expect)(svc.getMultiplexer(UDID)).to.equal(undefined);
216
+ }));
217
+ });
218
+ /**
219
+ * A capture stopped while viewers still play it: a recording starting on the
220
+ * phone (ensureMjpegForRecording), or a stream/stop. Their sockets stayed open
221
+ * with no frames, so the picture froze on its last frame and the page never
222
+ * fell back to MJPEG.
223
+ */
224
+ describe('AndroidH264StreamService: a capture stopped under its viewers', () => {
225
+ const loopback = (0, loopbackServer_1.loopbackServers)();
226
+ let restoreContainer;
227
+ beforeEach(() => {
228
+ restoreContainer = (0, container_registration_1.saveRegistrations)(PluginContext_1.PluginContext);
229
+ typedi_1.Container.set(PluginContext_1.PluginContext, { pluginArgs: {} });
230
+ });
231
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
232
+ restoreContainer();
233
+ yield loopback.closeAll();
234
+ }));
235
+ it('closes its viewers’ sockets with 1012 when the capture is stopped under them', () => __awaiter(void 0, void 0, void 0, function* () {
236
+ // 1012 says the stream ended: the player shows MJPEG, without asking the
237
+ // server to end an H.264 capture that is already gone.
238
+ const { svc, capture } = withCapture();
239
+ const server = yield loopback.serve(http_1.default.createServer());
240
+ (0, h264StreamWs_1.attachH264Ws)(server, {
241
+ redeem: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: 'usr_alice' }); }),
242
+ startStream: (udid) => svc.start(udid),
243
+ });
244
+ const { port } = server.address();
245
+ const sockets = [];
246
+ // How the socket ended, or 'still open' if it hadn't within 2 s.
247
+ const viewer = () => new Promise((resolve, reject) => {
248
+ const ws = new ws_1.WebSocket(`ws://127.0.0.1:${port}/xenon/api/control/${UDID}/stream/h264?ticket=t`);
249
+ sockets.push(ws);
250
+ const closed = new Promise((r) => {
251
+ const timer = setTimeout(() => r('still open'), 2000);
252
+ ws.once('close', (code, reason) => {
253
+ clearTimeout(timer);
254
+ r([code, reason.toString()]);
255
+ });
256
+ });
257
+ ws.once('message', () => resolve({ closed }));
258
+ ws.once('error', reject);
259
+ });
260
+ try {
261
+ const tile = yield viewer();
262
+ const otherTab = yield viewer();
263
+ yield svc.stop(UDID);
264
+ (0, chai_1.expect)(yield tile.closed).to.deep.equal([1012, 'stream ended']);
265
+ (0, chai_1.expect)(yield otherTab.closed).to.deep.equal([1012, 'stream ended']);
266
+ }
267
+ finally {
268
+ sockets.forEach((ws) => ws.terminate());
269
+ }
270
+ (0, chai_1.expect)(capture.kills).to.equal(1);
271
+ }));
272
+ });
@@ -54,6 +54,7 @@ const device_store_1 = require("../../src/data-service/device-store");
54
54
  const recording_store_1 = require("../../src/services/recording/recording-store");
55
55
  const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
56
56
  const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
57
+ const H264Multiplexer_1 = require("../../src/device-managers/android/H264Multiplexer");
57
58
  const previewHold_1 = require("../../src/device-managers/android/previewHold");
58
59
  const UDID = 'android-1';
59
60
  const HOLD = `manual_user-1_${UDID}`;
@@ -126,7 +127,7 @@ describe('Android live-preview hold', () => {
126
127
  it('releases the hold once nobody has watched for 10 minutes', () => __awaiter(void 0, void 0, void 0, function* () {
127
128
  const svc = new AndroidH264StreamService_1.default();
128
129
  const kill = sinon_1.default.spy();
129
- svc.sessions.set(UDID, { status: 'running', mux: { clientCount: 0 }, capture: { kill } });
130
+ svc.sessions.set(UDID, { status: 'running', mux: new H264Multiplexer_1.H264Multiplexer(), capture: { kill } });
130
131
  yield clock.tickAsync(60000); // first sweep notes it is empty
131
132
  yield clock.tickAsync(11 * 60000);
132
133
  (0, chai_1.expect)(kill.called, 'capture stopped').to.equal(true);
@@ -137,7 +138,7 @@ describe('Android live-preview hold', () => {
137
138
  const svc = new AndroidH264StreamService_1.default();
138
139
  svc.sessions.set(UDID, {
139
140
  status: 'running',
140
- mux: { clientCount: 0 },
141
+ mux: new H264Multiplexer_1.H264Multiplexer(),
141
142
  capture: { kill() { } },
142
143
  });
143
144
  yield clock.tickAsync(12 * 60000);
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const express_1 = __importDefault(require("express"));
19
+ const typedi_1 = require("typedi");
20
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
21
+ const apikeys_1 = require("../../src/app/routers/apikeys");
22
+ const ApiKeyService_1 = require("../../src/services/ApiKeyService");
23
+ /**
24
+ * POST /apikeys stores the scopes it is given. It stored any string, so a
25
+ * typo (`session`, `Admin`) made a key that silently had none of the scopes
26
+ * its maker meant, and a list shown back on the API keys page that no check
27
+ * reads. It takes the four scopes POST /profile/tokens takes.
28
+ */
29
+ describe('POST /apikeys takes only scopes that exist', () => {
30
+ let create;
31
+ function app(auth = {}) {
32
+ const a = (0, express_1.default)();
33
+ a.use(express_1.default.json());
34
+ a.use((req, _res, next) => {
35
+ req.auth = Object.assign({ kind: 'user-session', userId: 'admin1', role: 'SUPER_ADMIN', scopes: 'admin,devices,sessions,read' }, auth);
36
+ next();
37
+ });
38
+ a.use('/apikeys', (0, apikeys_1.apiKeysRouter)());
39
+ return a;
40
+ }
41
+ beforeEach(() => {
42
+ create = sinon_1.default.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create').resolves({ id: 'k1', raw: 'raw' });
43
+ });
44
+ afterEach(() => sinon_1.default.restore());
45
+ for (const scopes of [['root'], ['sessions', 'Admin'], ['session'], [42]]) {
46
+ it(`refuses ${JSON.stringify(scopes)}`, () => __awaiter(void 0, void 0, void 0, function* () {
47
+ const r = yield (0, loopbackRequest_1.default)(app()).post('/apikeys').send({ name: 'CI', scopes });
48
+ (0, chai_1.expect)(r.status).to.equal(400);
49
+ (0, chai_1.expect)(r.body.error).to.equal('scopes must be some of read, sessions, devices, admin');
50
+ (0, chai_1.expect)(create.called).to.equal(false);
51
+ }));
52
+ }
53
+ it('creates a key with known scopes', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const r = yield (0, loopbackRequest_1.default)(app())
55
+ .post('/apikeys')
56
+ .send({ name: 'CI', scopes: ['sessions', 'devices'] });
57
+ (0, chai_1.expect)(r.status).to.equal(200);
58
+ (0, chai_1.expect)(create.firstCall.args[0].scopes).to.deep.equal(['sessions', 'devices']);
59
+ }));
60
+ // Made with a credential that expires, a key ends no later than it: an
61
+ // admin's 1-hour Bearer token made itself a key that never expired.
62
+ describe('from a credential that expires', () => {
63
+ const inAnHour = new Date(Date.now() + 3600000);
64
+ const bearer = () => app({ kind: 'bearer', scopes: 'admin', credentialExpiresAt: inAnHour.getTime() });
65
+ it('expires with that credential when no expiresAt is asked for', () => __awaiter(void 0, void 0, void 0, function* () {
66
+ var _a;
67
+ const r = yield (0, loopbackRequest_1.default)(bearer())
68
+ .post('/apikeys')
69
+ .send({ name: 'CI', scopes: ['sessions'] });
70
+ (0, chai_1.expect)(r.status).to.equal(200);
71
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(inAnHour.toISOString());
72
+ (0, chai_1.expect)((_a = create.firstCall.args[0].expiresAt) === null || _a === void 0 ? void 0 : _a.getTime()).to.equal(inAnHour.getTime());
73
+ }));
74
+ it('refuses an expiresAt later than that credential', () => __awaiter(void 0, void 0, void 0, function* () {
75
+ const later = new Date(inAnHour.getTime() + 60000).toISOString();
76
+ const r = yield (0, loopbackRequest_1.default)(bearer())
77
+ .post('/apikeys')
78
+ .send({ name: 'CI', scopes: ['sessions'], expiresAt: later });
79
+ (0, chai_1.expect)(r.status).to.equal(400);
80
+ (0, chai_1.expect)(r.body.error).to.match(/later than the credential/);
81
+ (0, chai_1.expect)(create.called).to.equal(false);
82
+ }));
83
+ });
84
+ });
@@ -65,6 +65,15 @@ describe('authMiddleware — Bearer branch', () => {
65
65
  (0, chai_1.expect)(req.auth.userId).to.equal('u1');
66
66
  (0, chai_1.expect)(req.auth.scopes).to.equal('devices,read');
67
67
  }));
68
+ it("drops admin from a demoted user's token: req.auth.scopes follows the role now", () => __awaiter(void 0, void 0, void 0, function* () {
69
+ // `user` is a MEMBER: the token was minted while they were an admin.
70
+ const token = yield keySvc.sign({ sub: 'u1', scopes: 'admin,devices,sessions,read' }, { audience: 'xenon-rest', ttlSeconds: 60 });
71
+ const req = { headers: { authorization: `Bearer ${token}` }, query: {} };
72
+ const next = sinon_1.default.spy();
73
+ yield (0, authMiddleware_1.authMiddleware)(req, fakeRes(), next);
74
+ (0, chai_1.expect)(next.calledOnce).to.equal(true);
75
+ (0, chai_1.expect)(req.auth.scopes).to.equal('devices,sessions,read');
76
+ }));
68
77
  // Widened by Phase 2a Task 1: the MCP plugin's tools call this same REST
69
78
  // surface with a gateway-injected xenon-mcp-audience token, so it must be
70
79
  // accepted here too (see authMiddleware.mcpAudience.spec.ts for the full
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const typedi_1 = require("typedi");
22
+ const authMiddleware_1 = require("../../src/middleware/authMiddleware");
23
+ const ApiKeyService_1 = require("../../src/services/ApiKeyService");
24
+ const UserSessionService_1 = require("../../src/services/UserSessionService");
25
+ const UserService_1 = require("../../src/services/UserService");
26
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
27
+ const prisma_1 = require("../../src/prisma");
28
+ const container_registration_1 = require("../helpers/container-registration");
29
+ /**
30
+ * How long the credential behind a request lasts, as authMiddleware sees it.
31
+ *
32
+ * - req.auth.credentialExpiresAt: when a Bearer token or an API key stops
33
+ * working, so what it mints (POST /auth/token, POST /profile/tokens) can't
34
+ * outlive it.
35
+ * - The dashboard cookie's Max-Age, renewed on every request, follows
36
+ * XENON_USER_SESSION_TTL_MS. It was renewed at a fixed 24 hours, so a
37
+ * longer setting signed people out after a day away anyway.
38
+ */
39
+ describe('authMiddleware: how long the credential lasts', () => {
40
+ let dir;
41
+ let keys;
42
+ let restore;
43
+ let ttlBefore;
44
+ const member = { id: 'u1', role: 'MEMBER', status: 'ACTIVE' };
45
+ function res() {
46
+ const out = { cookies: [] };
47
+ out.status = (c) => {
48
+ out.code = c;
49
+ return out;
50
+ };
51
+ out.json = (b) => {
52
+ out.body = b;
53
+ return out;
54
+ };
55
+ out.cookie = (name, value, opts) => {
56
+ out.cookies.push({ name, value, opts });
57
+ return out;
58
+ };
59
+ return out;
60
+ }
61
+ function run(headers) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ const req = { headers, query: {}, path: '/x' };
64
+ const r = res();
65
+ const next = sinon_1.default.spy();
66
+ yield (0, authMiddleware_1.authMiddleware)(req, r, next);
67
+ (0, chai_1.expect)(next.calledOnce, `refused: ${JSON.stringify(r.body)}`).to.equal(true);
68
+ return { req, res: r };
69
+ });
70
+ }
71
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
72
+ ttlBefore = process.env.XENON_USER_SESSION_TTL_MS;
73
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, UserService_1.UserService, ApiKeyService_1.ApiKeyService, UserSessionService_1.UserSessionService);
74
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-cred-life-'));
75
+ keys = new JwtKeyService_1.JwtKeyService();
76
+ yield keys.init(dir);
77
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
78
+ typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().resolves(member) });
79
+ typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
80
+ verifyPair: sinon_1.default.stub().resolves(null),
81
+ verify: sinon_1.default.stub().resolves(null),
82
+ });
83
+ const sessions = new UserSessionService_1.UserSessionService();
84
+ sinon_1.default.stub(sessions, 'resolve').resolves(null);
85
+ typedi_1.Container.set(UserSessionService_1.UserSessionService, sessions);
86
+ sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
87
+ }));
88
+ afterEach(() => {
89
+ if (ttlBefore === undefined)
90
+ delete process.env.XENON_USER_SESSION_TTL_MS;
91
+ else
92
+ process.env.XENON_USER_SESSION_TTL_MS = ttlBefore;
93
+ sinon_1.default.restore();
94
+ restore();
95
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
96
+ });
97
+ describe('req.auth.credentialExpiresAt', () => {
98
+ it("is a Bearer token's exp", () => __awaiter(void 0, void 0, void 0, function* () {
99
+ const token = yield keys.sign({ sub: 'u1', scopes: 'read' }, { audience: 'xenon-rest', ttlSeconds: 600 });
100
+ const { req } = yield run({ authorization: `Bearer ${token}` });
101
+ const exp = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()).exp;
102
+ (0, chai_1.expect)(req.auth.credentialExpiresAt).to.equal(exp * 1000);
103
+ }));
104
+ it("is an API key's expiresAt, and absent for a key that never expires", () => __awaiter(void 0, void 0, void 0, function* () {
105
+ const expiresAt = new Date(Date.now() + 3600000);
106
+ const verifyPair = typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair;
107
+ verifyPair.resolves({ id: 'k1', userId: 'u1', scopes: 'read', rateLimit: 300, expiresAt });
108
+ const pair = { 'x-xenon-access-key': 'xen_a', 'x-xenon-token': 'tk' };
109
+ (0, chai_1.expect)((yield run(pair)).req.auth.credentialExpiresAt).to.equal(expiresAt.getTime());
110
+ verifyPair.resolves({
111
+ id: 'k1',
112
+ userId: 'u1',
113
+ scopes: 'read',
114
+ rateLimit: 300,
115
+ expiresAt: null,
116
+ });
117
+ (0, chai_1.expect)((yield run(pair)).req.auth.credentialExpiresAt).to.equal(undefined);
118
+ }));
119
+ it('is absent for a dashboard sign-in, which renews itself', () => __awaiter(void 0, void 0, void 0, function* () {
120
+ typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve.resolves({
121
+ id: 's1',
122
+ userId: 'u1',
123
+ });
124
+ const { req } = yield run({ cookie: 'xenon_dashboard_session=s1' });
125
+ (0, chai_1.expect)(req.auth.kind).to.equal('user-session');
126
+ (0, chai_1.expect)(req.auth.credentialExpiresAt).to.equal(undefined);
127
+ }));
128
+ });
129
+ describe("the dashboard cookie's renewed Max-Age", () => {
130
+ it('follows XENON_USER_SESSION_TTL_MS, longer than a day included', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ process.env.XENON_USER_SESSION_TTL_MS = String(7 * 24 * 3600000);
132
+ typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve.resolves({
133
+ id: 's1',
134
+ userId: 'u1',
135
+ });
136
+ const { res: r } = yield run({ cookie: 'xenon_dashboard_session=s1' });
137
+ (0, chai_1.expect)(r.cookies[0].opts.maxAge).to.equal(7 * 24 * 3600000);
138
+ }));
139
+ it('is 24 hours by default', () => __awaiter(void 0, void 0, void 0, function* () {
140
+ delete process.env.XENON_USER_SESSION_TTL_MS;
141
+ typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve.resolves({
142
+ id: 's1',
143
+ userId: 'u1',
144
+ });
145
+ const { res: r } = yield run({ cookie: 'xenon_dashboard_session=s1' });
146
+ (0, chai_1.expect)(r.cookies[0].opts.maxAge).to.equal(24 * 3600000);
147
+ }));
148
+ });
149
+ });