@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
@@ -1,10 +1,43 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
2
18
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
19
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
20
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
21
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
22
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
23
  };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
8
41
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
42
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
43
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -19,9 +52,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
19
52
  };
20
53
  var SocketServer_1;
21
54
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.SocketServer = void 0;
55
+ exports.SocketServer = exports.SOCKET_RECHECK_INTERVAL_MS = void 0;
23
56
  const socket_io_1 = require("socket.io");
24
57
  const typedi_1 = require("typedi");
58
+ const jose = __importStar(require("jose"));
25
59
  const logger_1 = __importDefault(require("../logger"));
26
60
  const config_1 = require("../config");
27
61
  const ApiKeyService_1 = require("./ApiKeyService");
@@ -33,9 +67,40 @@ const SocketEvents_1 = require("../enums/SocketEvents");
33
67
  const callerTeamIds_1 = require("./device-access/callerTeamIds");
34
68
  const DeviceTeamResolver_1 = require("./device-access/DeviceTeamResolver");
35
69
  const upgradeRouter_1 = require("../app/ws/upgradeRouter");
70
+ const sessionCommandSummary_1 = require("../dashboard/sessionCommandSummary");
71
+ const SelectorVisibilityResolver_1 = require("./selector-health/SelectorVisibilityResolver");
72
+ const identityChanges_1 = require("./identity/identityChanges");
36
73
  /** socket.io's default path, which the dashboard and nodes connect to. */
37
74
  const SOCKET_IO_PATH = '/socket.io';
75
+ /**
76
+ * How often every socket is checked again, for a change this server didn't
77
+ * make itself (another server sharing the database, a direct edit). Changes
78
+ * made through Xenon reach a socket at once (onIdentityChanged).
79
+ */
80
+ exports.SOCKET_RECHECK_INTERVAL_MS = 60000;
81
+ /** setTimeout fires at once for a longer delay. */
82
+ const MAX_TIMER_MS = 2147483647;
83
+ /** How many sockets a sweep checks at once, so a large lab's sweep is no burst of queries. */
84
+ const SWEEP_BATCH = 10;
85
+ /**
86
+ * The credential no longer admits the socket: wrong, revoked, expired, or its
87
+ * user inactive or gone. Anything else thrown while checking means the check
88
+ * could not run (the database or signing key unavailable).
89
+ */
90
+ class SocketRefused extends Error {
91
+ }
38
92
  const SESSION_COOKIE = 'xenon_dashboard_session';
93
+ const EVENT_LOG_KEEPS = new Map([
94
+ [SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, null],
95
+ [SocketEvents_1.SocketEvents.SESSION_COMMAND, sessionCommandSummary_1.sessionCommandSummary],
96
+ ]);
97
+ /** A row's expiresAt as epoch ms, or undefined for none. */
98
+ function msOf(at) {
99
+ if (!at)
100
+ return undefined;
101
+ const ms = new Date(at).getTime();
102
+ return Number.isNaN(ms) ? undefined : ms;
103
+ }
39
104
  function readCookie(cookieHeader, name) {
40
105
  if (!cookieHeader)
41
106
  return undefined;
@@ -55,8 +120,23 @@ let SocketServer = SocketServer_1 = class SocketServer {
55
120
  this.nodes = new Map(); // socketId -> nodeHost
56
121
  /** The tail of each phone's pending event deliveries (see emitToDashboardForDevices). */
57
122
  this.deliveries = new Map();
123
+ /** The tail of each selector's pending event deliveries (see emitToDashboardForSelector). */
124
+ this.selectorDeliveries = new Map();
125
+ /** The timer that checks a socket again when its credential ends, by socket id. */
126
+ this.expiryTimers = new Map();
127
+ /**
128
+ * Each socket's checks, numbered as they start. A check's outcome is
129
+ * applied unless one that started after it has already applied its own.
130
+ */
131
+ this.checks = new WeakMap();
132
+ /** The check of a socket whose handshake overlapped a change; it joins the dashboard room after it. */
133
+ this.connectChecks = new WeakMap();
134
+ /** Counts the identity changes, so a handshake that overlapped one checks again. */
135
+ this.identityEpoch = 0;
136
+ this.sweeping = false;
58
137
  }
59
- initialize(server) {
138
+ initialize(server, options = {}) {
139
+ var _a;
60
140
  // socket.io (engine.io) takes its websocket transport by adding its own
61
141
  // `upgrade` listener, which would also see every other upgrade and end
62
142
  // the ones it doesn't own after 1 s. The upgrade router moves that
@@ -74,9 +154,12 @@ let SocketServer = SocketServer_1 = class SocketServer {
74
154
  }));
75
155
  this.io.use((socket, next) => __awaiter(this, void 0, void 0, function* () {
76
156
  try {
77
- const identity = yield this.authenticate(socket);
157
+ const epoch = this.identityEpoch;
158
+ const { identity, endsAt } = yield this.check(socket);
78
159
  socket.data.identity = identity;
79
160
  socket.data.principal = identity.principal;
161
+ socket.data.endsAt = endsAt;
162
+ socket.data.checkedEpoch = epoch;
80
163
  next();
81
164
  }
82
165
  catch (err) {
@@ -88,6 +171,13 @@ let SocketServer = SocketServer_1 = class SocketServer {
88
171
  const socketId = socket.id;
89
172
  const principal = socket.data.principal;
90
173
  logger_1.default.info(`[SocketServer] New connection: ${socketId} (principal=${principal})`);
174
+ this.armExpiry(socket);
175
+ // A user changed while this handshake was being checked. The change
176
+ // found no socket of theirs to check yet, so this one may hold what was
177
+ // true before it.
178
+ if (socket.data.checkedEpoch !== this.identityEpoch) {
179
+ this.connectChecks.set(socket, this.recheck(socket));
180
+ }
91
181
  // Protocol version handshake — still runs after auth so a logged-in
92
182
  // client on the wrong protocol version still gets a clean disconnect.
93
183
  socket.on(SocketEvents_1.SocketEvents.HANDSHAKE, (data) => {
@@ -112,16 +202,22 @@ let SocketServer = SocketServer_1 = class SocketServer {
112
202
  // Notify dashboard about new node
113
203
  this.emitToDashboard(SocketEvents_1.SocketEvents.NODE_CONNECTED, { host });
114
204
  });
115
- socket.on(SocketEvents_1.SocketEvents.REGISTER_DASHBOARD, () => {
205
+ socket.on(SocketEvents_1.SocketEvents.REGISTER_DASHBOARD, () => __awaiter(this, void 0, void 0, function* () {
116
206
  if (!SocketServer_1.canRegisterAs(principal, 'dashboard')) {
117
207
  logger_1.default.warn(`[SocketServer] Rejected REGISTER_DASHBOARD from principal=${principal} on ${socketId}`);
118
208
  socket.disconnect();
119
209
  return;
120
210
  }
211
+ // Its events wait for the identity the overlapping change gave it.
212
+ yield this.connectChecks.get(socket);
213
+ if (!socket.connected)
214
+ return;
121
215
  logger_1.default.info(`[SocketServer] Dashboard client registered (Socket: ${socketId})`);
122
216
  socket.join('dashboard');
123
- });
217
+ }));
124
218
  socket.on('disconnect', () => {
219
+ clearTimeout(this.expiryTimers.get(socketId));
220
+ this.expiryTimers.delete(socketId);
125
221
  if (this.nodes.has(socketId)) {
126
222
  const host = this.nodes.get(socketId);
127
223
  logger_1.default.info(`[SocketServer] Node disconnected: ${host} (Socket: ${socketId})`);
@@ -133,8 +229,128 @@ let SocketServer = SocketServer_1 = class SocketServer {
133
229
  }
134
230
  });
135
231
  });
232
+ const stopListening = (0, identityChanges_1.onIdentityChanged)((userId) => this.recheckUser(userId));
233
+ const sweep = setInterval(() => void this.sweep(), (_a = options.recheckIntervalMs) !== null && _a !== void 0 ? _a : exports.SOCKET_RECHECK_INTERVAL_MS);
234
+ sweep.unref();
235
+ server.once('close', () => {
236
+ stopListening();
237
+ clearInterval(sweep);
238
+ });
136
239
  logger_1.default.info('[SocketServer] WebSocket server initialized (auth enabled)');
137
240
  }
241
+ /**
242
+ * Checks every socket of `userId` again, on its own credential (see
243
+ * recheck). Settles once each has its new identity or is closed (or, if it
244
+ * could not be checked, is as it was).
245
+ */
246
+ recheckUser(userId) {
247
+ return __awaiter(this, void 0, void 0, function* () {
248
+ this.identityEpoch += 1;
249
+ if (!this.io || config_1.config.authDisabled === true)
250
+ return;
251
+ const sockets = Array.from(this.io.sockets.sockets.values()).filter((socket) => { var _a, _b; return ((_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.userId) === userId; });
252
+ yield Promise.all(sockets.map((socket) => this.recheck(socket)));
253
+ });
254
+ }
255
+ /**
256
+ * Every socket again, for a change made where this server doesn't hear of
257
+ * it, SWEEP_BATCH at a time. One sweep at a time.
258
+ */
259
+ sweep() {
260
+ return __awaiter(this, void 0, void 0, function* () {
261
+ if (this.sweeping || !this.io || config_1.config.authDisabled === true)
262
+ return;
263
+ this.sweeping = true;
264
+ try {
265
+ const sockets = Array.from(this.io.sockets.sockets.values());
266
+ for (let i = 0; i < sockets.length; i += SWEEP_BATCH) {
267
+ yield Promise.all(sockets.slice(i, i + SWEEP_BATCH).map((s) => this.recheck(s)));
268
+ }
269
+ }
270
+ finally {
271
+ this.sweeping = false;
272
+ }
273
+ });
274
+ }
275
+ /**
276
+ * The handshake's check, again, on the socket's own credential, renewing
277
+ * nothing: a sign-in's TTL doesn't slide and a key's `lastUsedAt` doesn't
278
+ * move. Its outcome is applied unless a check that started after it has
279
+ * already applied its own. So once a check settles, the socket is at least
280
+ * as current as what it read: a change's check lands before REST answers,
281
+ * though a sweep's started meanwhile, and a newer check that could not run
282
+ * doesn't cancel an older refusal.
283
+ *
284
+ * - Admitted: the socket gets its new identity where it is, still
285
+ * connected, so a demoted admin's tab keeps its member's events.
286
+ * - Refused: its connection is closed (the transport, not
287
+ * `disconnect()`). The client reconnects as it does after any drop, and
288
+ * the handshake decides on the credential it has now: a fresh bearer
289
+ * token from its `auth` function (Xenon Studio, the documented client),
290
+ * or the browser's new cookie, is admitted; the refused one is refused,
291
+ * and socket.io-client doesn't try again after a refused handshake.
292
+ * `disconnect()` would have the client never reconnect at all
293
+ * (`io server disconnect`), so every token's `exp` would end a
294
+ * long-running client's events for good. Once closing, the socket is
295
+ * sent nothing more.
296
+ * - Could not check: the socket is kept as it was. Closing every
297
+ * dashboard over a database hiccup would have their reconnects refused
298
+ * too, leaving them all without live events.
299
+ */
300
+ recheck(socket) {
301
+ return __awaiter(this, void 0, void 0, function* () {
302
+ var _a;
303
+ let state = this.checks.get(socket);
304
+ if (!state) {
305
+ state = { started: 0, applied: 0 };
306
+ this.checks.set(socket, state);
307
+ }
308
+ const checks = state;
309
+ const n = ++checks.started;
310
+ const current = () => n > checks.applied && socket.connected;
311
+ const before = socket.data.identity;
312
+ let checked;
313
+ try {
314
+ checked = yield this.check(socket, true);
315
+ }
316
+ catch (err) {
317
+ if (!current())
318
+ return;
319
+ if (err instanceof SocketRefused) {
320
+ checks.applied = n;
321
+ logger_1.default.info(`[SocketServer] Closing ${socket.id} (user ${before === null || before === void 0 ? void 0 : before.userId}): ${err.message}`);
322
+ socket.conn.close();
323
+ }
324
+ else {
325
+ logger_1.default.warn(`[SocketServer] Could not check ${socket.id} (user ${before === null || before === void 0 ? void 0 : before.userId}) again, so it is kept as it was: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
326
+ }
327
+ return;
328
+ }
329
+ if (!current())
330
+ return;
331
+ checks.applied = n;
332
+ const { identity, endsAt } = checked;
333
+ socket.data.identity = identity;
334
+ socket.data.endsAt = endsAt;
335
+ this.armExpiry(socket);
336
+ if ((before === null || before === void 0 ? void 0 : before.role) !== identity.role ||
337
+ JSON.stringify(before === null || before === void 0 ? void 0 : before.teamIds) !== JSON.stringify(identity.teamIds)) {
338
+ logger_1.default.info(`[SocketServer] ${socket.id} (user ${identity.userId}) is now ${identity.role}, teams ${identity.teamIds ? `[${identity.teamIds.join(', ')}]` : 'all'}`);
339
+ }
340
+ });
341
+ }
342
+ /** Checks the socket again when its credential ends by itself (`socket.data.endsAt`). */
343
+ armExpiry(socket) {
344
+ clearTimeout(this.expiryTimers.get(socket.id));
345
+ this.expiryTimers.delete(socket.id);
346
+ const endsAt = socket.data.endsAt;
347
+ if (endsAt === undefined || !socket.connected)
348
+ return;
349
+ const delay = Math.min(Math.max(endsAt - Date.now(), 0), MAX_TIMER_MS);
350
+ const timer = setTimeout(() => void this.recheck(socket), delay);
351
+ timer.unref();
352
+ this.expiryTimers.set(socket.id, timer);
353
+ }
138
354
  // Role-based registration guard. A dashboard-authed socket can't join the
139
355
  // 'nodes' broadcast room and a node-authed socket can't subscribe to
140
356
  // dashboard events — keeps a stolen credential of one class from acting
@@ -146,13 +362,27 @@ let SocketServer = SocketServer_1 = class SocketServer {
146
362
  }
147
363
  authenticate(socket) {
148
364
  return __awaiter(this, void 0, void 0, function* () {
149
- var _a, _b, _c, _d;
365
+ return (yield this.check(socket)).identity;
366
+ });
367
+ }
368
+ /**
369
+ * Who the socket's handshake credential says it is now, and when that
370
+ * credential ends by itself (`endsAt`, epoch ms; none for one that doesn't).
371
+ * Throws SocketRefused when the credential no longer admits it, anything
372
+ * else when it could not be checked. A `recheck` renews nothing (see
373
+ * recheck).
374
+ */
375
+ check(socket_1) {
376
+ return __awaiter(this, arguments, void 0, function* (socket, recheck = false) {
377
+ var _a, _b, _c, _d, _e;
150
378
  if (config_1.config.authDisabled === true) {
151
379
  return {
152
- principal: 'auth-disabled',
153
- userId: 'auth-disabled',
154
- role: 'SUPER_ADMIN',
155
- teamIds: undefined,
380
+ identity: {
381
+ principal: 'auth-disabled',
382
+ userId: 'auth-disabled',
383
+ role: 'SUPER_ADMIN',
384
+ teamIds: undefined,
385
+ },
156
386
  };
157
387
  }
158
388
  const auth = (socket.handshake.auth || {});
@@ -166,58 +396,74 @@ let SocketServer = SocketServer_1 = class SocketServer {
166
396
  try {
167
397
  payload = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(bearer, { audience: 'xenon-rest' });
168
398
  }
169
- catch (_e) {
170
- throw new Error('invalid bearer token');
399
+ catch (err) {
400
+ if (err instanceof jose.errors.JOSEError)
401
+ throw new SocketRefused('invalid bearer token');
402
+ throw new Error(`bearer token could not be checked: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
171
403
  }
404
+ // The socket closes when REST stops taking the token: verify() allows
405
+ // JWT_CLOCK_TOLERANCE_SEC past exp, and refuses it from then on.
406
+ const endsAt = typeof payload.exp === 'number'
407
+ ? (payload.exp + JwtKeyService_1.JWT_CLOCK_TOLERANCE_SEC) * 1000
408
+ : undefined;
172
409
  const owner = yield prisma_1.prisma.user.findUnique({
173
410
  where: { id: String(payload.sub) },
174
411
  select: { status: true, role: true },
175
412
  });
176
413
  if (!owner || owner.status !== 'ACTIVE')
177
- throw new Error('inactive user');
414
+ throw new SocketRefused('inactive user');
178
415
  // The team claim is read exactly as REST's bearer path reads it.
179
- return this.identify('dashboard', String(payload.sub), owner.role, (_a = payload.teamId) !== null && _a !== void 0 ? _a : null);
416
+ const identity = yield this.identify('dashboard', String(payload.sub), owner.role, (_b = payload.teamId) !== null && _b !== void 0 ? _b : null);
417
+ return { identity, endsAt };
180
418
  }
181
419
  // Node path: per-node (accessKey, token) pair. Resolves to a real
182
420
  // ApiKey row owned by a real User; we additionally enforce that the
183
421
  // User is ACTIVE so a disabled account can't keep a stolen key alive.
184
422
  const pairKey = (typeof auth.accessKey === 'string' && auth.accessKey) ||
185
- ((_b = headers['x-xenon-access-key']) !== null && _b !== void 0 ? _b : '');
423
+ ((_c = headers['x-xenon-access-key']) !== null && _c !== void 0 ? _c : '');
186
424
  const pairTok = (typeof auth.token === 'string' && auth.token) ||
187
- ((_c = headers['x-xenon-token']) !== null && _c !== void 0 ? _c : '');
425
+ ((_d = headers['x-xenon-token']) !== null && _d !== void 0 ? _d : '');
188
426
  if (pairKey && pairTok) {
189
- const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair(pairKey, pairTok);
427
+ const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair(pairKey, pairTok, {
428
+ touch: !recheck,
429
+ });
190
430
  if (!row)
191
- throw new Error('invalid (accessKey, token) pair');
431
+ throw new SocketRefused('invalid (accessKey, token) pair');
192
432
  const owner = yield prisma_1.prisma.user.findUnique({
193
433
  where: { id: row.userId },
194
434
  select: { status: true, role: true },
195
435
  });
196
436
  if (!owner || owner.status !== 'ACTIVE')
197
- throw new Error('inactive user');
198
- return this.identify('node', row.userId, owner.role, row.teamId);
437
+ throw new SocketRefused('inactive user');
438
+ const identity = yield this.identify('node', row.userId, owner.role, row.teamId);
439
+ return { identity, endsAt: msOf(row.expiresAt) };
199
440
  }
200
441
  // Dashboard path: the cookie. As in REST's authMiddleware, it is first a
201
442
  // UserSession id (set by /auth/login), then a raw API key (the older
202
443
  // /api-key-gate cookie). A session whose user is disabled is refused, not
203
444
  // retried as a key.
204
- const cookieValue = (_d = readCookie(headers.cookie, SESSION_COOKIE)) !== null && _d !== void 0 ? _d : '';
445
+ const cookieValue = (_e = readCookie(headers.cookie, SESSION_COOKIE)) !== null && _e !== void 0 ? _e : '';
205
446
  if (!cookieValue) {
206
- throw new Error('missing credentials (need (accessKey, token) pair or dashboard cookie)');
447
+ throw new SocketRefused('missing credentials (need (accessKey, token) pair or dashboard cookie)');
207
448
  }
208
- const session = yield typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve(cookieValue);
449
+ // A sign-in's expiresAt slides as REST uses it: checked again then, it
450
+ // has moved on and the timer is set again.
451
+ const session = yield typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve(cookieValue, {
452
+ renew: !recheck,
453
+ });
209
454
  if (session) {
210
455
  const user = yield prisma_1.prisma.user.findUnique({
211
456
  where: { id: session.userId },
212
457
  select: { status: true, role: true },
213
458
  });
214
459
  if (!user || user.status !== 'ACTIVE')
215
- throw new Error('inactive user');
216
- return this.identify('dashboard', session.userId, user.role, null);
460
+ throw new SocketRefused('inactive user');
461
+ const identity = yield this.identify('dashboard', session.userId, user.role, null);
462
+ return { identity, endsAt: msOf(session.expiresAt) };
217
463
  }
218
- const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verify(cookieValue);
464
+ const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verify(cookieValue, { touch: !recheck });
219
465
  if (!row) {
220
- throw new Error('invalid or revoked dashboard session');
466
+ throw new SocketRefused('invalid or revoked dashboard session');
221
467
  }
222
468
  // The key owner's role decides the team scope, and an inactive owner is
223
469
  // refused here as REST refuses them.
@@ -226,8 +472,9 @@ let SocketServer = SocketServer_1 = class SocketServer {
226
472
  select: { status: true, role: true },
227
473
  });
228
474
  if (!owner || owner.status !== 'ACTIVE')
229
- throw new Error('inactive user');
230
- return this.identify('dashboard', row.userId, owner.role, row.teamId);
475
+ throw new SocketRefused('inactive user');
476
+ const identity = yield this.identify('dashboard', row.userId, owner.role, row.teamId);
477
+ return { identity, endsAt: msOf(row.expiresAt) };
231
478
  });
232
479
  }
233
480
  identify(principal, userId, role, apiKeyTeamId) {
@@ -237,17 +484,80 @@ let SocketServer = SocketServer_1 = class SocketServer {
237
484
  return { principal, userId, role: r, teamIds };
238
485
  });
239
486
  }
240
- /** Unscoped: every dashboard socket. For events that aren't one phone's data (selectors, nodes). */
487
+ /**
488
+ * Unscoped: every dashboard socket. Only for the hub's node events, which
489
+ * carry a node's address and nothing of a phone, a session or a selector.
490
+ */
241
491
  emitToDashboard(event, data) {
242
492
  if (this.io) {
243
493
  this.io.to('dashboard').emit(event, data);
244
494
  }
245
- typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({ type: event, payload: data });
495
+ this.logEvent(event, data);
496
+ }
497
+ /** Once per event, unscoped: whole, or what EVENT_LOG_KEEPS keeps of it. */
498
+ logEvent(event, data) {
499
+ const keep = EVENT_LOG_KEEPS.get(event);
500
+ if (keep === null)
501
+ return;
502
+ typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({
503
+ type: event,
504
+ payload: keep ? keep(data) : data,
505
+ });
506
+ }
507
+ /**
508
+ * A Selector Health event (`selector_*`): each dashboard socket gets it only
509
+ * if its caller may see the selector, by REST's rule (a selector healed in a
510
+ * session the caller may see; SelectorVisibilityResolver). Admins get every
511
+ * one. The event log records it once, unscoped.
512
+ *
513
+ * Each selector's events are delivered through one chain, in the order they
514
+ * were emitted. With no scoped socket connected and nothing pending for the
515
+ * selector, it is the synchronous room broadcast, with no lookup. The
516
+ * returned promise settles once the event is delivered and never rejects.
517
+ */
518
+ emitToDashboardForSelector(event, data, selector) {
519
+ var _a;
520
+ this.logEvent(event, data);
521
+ const key = JSON.stringify([selector.strategy, selector.selector]);
522
+ if (!this.hasScopedDashboard() && !this.selectorDeliveries.has(key)) {
523
+ (_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
524
+ return Promise.resolve();
525
+ }
526
+ return this.enqueue(event, [key], () => this.deliverForSelector(event, data, selector), this.selectorDeliveries);
527
+ }
528
+ deliverForSelector(event, data, selector) {
529
+ return __awaiter(this, void 0, void 0, function* () {
530
+ var _a;
531
+ // The scoped sockets may have left while this waited its turn.
532
+ if (!this.hasScopedDashboard()) {
533
+ (_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
534
+ return;
535
+ }
536
+ const resolver = typedi_1.Container.get(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
537
+ const sockets = this.dashboardSockets();
538
+ const asked = sockets.map((socket) => { var _a; return (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity; });
539
+ const allowed = yield Promise.all(sockets.map((socket) => {
540
+ var _a, _b;
541
+ const teamIds = teamIdsOf(socket);
542
+ if (teamIds === undefined)
543
+ return true;
544
+ const userId = (_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.userId;
545
+ return resolver.canSee(selector, { userId, teamIds });
546
+ }));
547
+ // A socket whose identity was replaced meanwhile (a role or team
548
+ // change) was not the one asked about: it gets nothing.
549
+ sockets.forEach((socket, i) => {
550
+ var _a;
551
+ if (allowed[i] && ((_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === asked[i])
552
+ socket.emit(event, data);
553
+ });
554
+ });
246
555
  }
247
556
  /**
248
557
  * A dashboard event about one or more phones: each dashboard socket gets it
249
558
  * only if its caller can see the phone ({@link DeviceEventScope}), by the
250
- * same team rule as REST. The event log records it once, unscoped.
559
+ * same team rule as REST. The event log records it once, unscoped, as
560
+ * EVENT_LOG_KEEPS says.
251
561
  *
252
562
  * Each phone's events are delivered through one chain, in the order they
253
563
  * were emitted, whatever their scope shape: one that waits on a team lookup
@@ -262,9 +572,9 @@ let SocketServer = SocketServer_1 = class SocketServer {
262
572
  */
263
573
  emitToDashboardForDevices(event, data, scope) {
264
574
  var _a, _b;
265
- typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({ type: event, payload: data });
575
+ this.logEvent(event, data);
266
576
  const udids = 'udids' in scope ? scope.udids : [(_a = scope.udid) !== null && _a !== void 0 ? _a : ''];
267
- if (!this.hasScopedDashboard() && udids.every((udid) => !this.deliveries.has(udid))) {
577
+ if (this.reachesEverySocket(scope) && udids.every((udid) => !this.deliveries.has(udid))) {
268
578
  this.noteRow(scope);
269
579
  (_b = this.io) === null || _b === void 0 ? void 0 : _b.to('dashboard').emit(event, data);
270
580
  return Promise.resolve();
@@ -289,34 +599,53 @@ let SocketServer = SocketServer_1 = class SocketServer {
289
599
  hasScopedDashboard() {
290
600
  return this.dashboardSockets().some((socket) => teamIdsOf(socket) !== undefined);
291
601
  }
292
- /** Runs `step` after every pending delivery for `udids`, and makes it their new tail. */
293
- enqueue(event, udids, step) {
294
- const before = udids.map((udid) => { var _a; return (_a = this.deliveries.get(udid)) !== null && _a !== void 0 ? _a : Promise.resolve(); });
602
+ /** Runs `step` after every pending delivery for `keys` in `chains`, and makes it their new tail. */
603
+ enqueue(event, keys, step, chains = this.deliveries) {
604
+ const before = keys.map((key) => { var _a; return (_a = chains.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve(); });
295
605
  const done = Promise.all(before)
296
606
  .then(step)
297
607
  .catch((err) => { var _a; return logger_1.default.warn(`[SocketServer] ${event} not delivered: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
298
- for (const udid of udids)
299
- this.deliveries.set(udid, done);
608
+ for (const key of keys)
609
+ chains.set(key, done);
300
610
  void done.then(() => {
301
- for (const udid of udids) {
302
- if (this.deliveries.get(udid) === done)
303
- this.deliveries.delete(udid);
611
+ for (const key of keys) {
612
+ if (chains.get(key) === done)
613
+ chains.delete(key);
304
614
  }
305
615
  });
306
616
  return done;
307
617
  }
618
+ /**
619
+ * Whether every dashboard socket gets an event of this scope whatever its
620
+ * phones are, so it can be the room broadcast with no lookup.
621
+ */
622
+ reachesEverySocket(scope) {
623
+ return this.dashboardSockets().every((socket) => teamIdsOf(socket) === undefined && (!scope.adminOnly || isAdmin(socket)));
624
+ }
308
625
  deliver(event, data, scope) {
309
626
  return __awaiter(this, void 0, void 0, function* () {
310
627
  var _a;
311
628
  // The scoped sockets may have left while this waited its turn.
312
- if (!this.hasScopedDashboard()) {
629
+ if (this.reachesEverySocket(scope)) {
313
630
  (_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
314
631
  return;
315
632
  }
316
633
  const resolver = typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver);
634
+ // Read once the team is known, so a socket that joined meanwhile is included.
635
+ const recipients = () => this.dashboardSockets().filter((socket) => !scope.adminOnly || isAdmin(socket));
636
+ // An admin-only event goes to admins, who see every phone: no lookup, so
637
+ // a slow store holds no captured request back.
638
+ if (scope.adminOnly) {
639
+ const admins = recipients();
640
+ if (admins.every((socket) => teamIdsOf(socket) === undefined)) {
641
+ for (const socket of admins)
642
+ socket.emit(event, data);
643
+ return;
644
+ }
645
+ }
317
646
  if ('udids' in scope) {
318
647
  const teams = yield Promise.all(scope.udids.map((udid) => resolver.resolve(udid)));
319
- for (const socket of this.dashboardSockets()) {
648
+ for (const socket of recipients()) {
320
649
  const teamIds = teamIdsOf(socket);
321
650
  if (teamIds === undefined) {
322
651
  socket.emit(event, data);
@@ -332,7 +661,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
332
661
  const team = scope.teamId !== undefined
333
662
  ? { known: true, teamId: scope.teamId }
334
663
  : yield resolver.resolve(scope.udid);
335
- for (const socket of this.dashboardSockets()) {
664
+ for (const socket of recipients()) {
336
665
  if ((0, DeviceTeamResolver_1.canSeeDeviceTeam)(team, teamIdsOf(socket)))
337
666
  socket.emit(event, data);
338
667
  }
@@ -368,3 +697,9 @@ function teamIdsOf(socket) {
368
697
  const identity = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity;
369
698
  return identity ? identity.teamIds : [];
370
699
  }
700
+ /** An admin by role, as `roleGuard('ADMIN')` decides. A socket with no identity is not one. */
701
+ function isAdmin(socket) {
702
+ var _a, _b;
703
+ const role = (_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.role;
704
+ return role === 'ADMIN' || role === 'SUPER_ADMIN';
705
+ }
@@ -22,6 +22,7 @@ exports.TeamService = void 0;
22
22
  const typedi_1 = require("typedi");
23
23
  const prisma_1 = require("../prisma");
24
24
  const logger_1 = __importDefault(require("../logger"));
25
+ const identityChanges_1 = require("./identity/identityChanges");
25
26
  let TeamService = class TeamService {
26
27
  constructor() {
27
28
  this.log = logger_1.default.scope('Team');
@@ -101,7 +102,9 @@ let TeamService = class TeamService {
101
102
  }
102
103
  addMember(teamId, userId) {
103
104
  return __awaiter(this, void 0, void 0, function* () {
104
- return prisma_1.prisma.teamMember.create({ data: { teamId, userId } });
105
+ const member = yield prisma_1.prisma.teamMember.create({ data: { teamId, userId } });
106
+ yield (0, identityChanges_1.identityChanged)(userId);
107
+ return member;
105
108
  });
106
109
  }
107
110
  removeMember(teamId, userId) {
@@ -109,6 +112,7 @@ let TeamService = class TeamService {
109
112
  yield prisma_1.prisma.teamMember.delete({
110
113
  where: { teamId_userId: { teamId, userId } },
111
114
  });
115
+ yield (0, identityChanges_1.identityChanged)(userId);
112
116
  });
113
117
  }
114
118
  };