@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
@@ -53,7 +53,7 @@ describe('SelectorVerificationJob catches a heal the live check missed', functio
53
53
  yield scratch.db.selectorEvent.deleteMany();
54
54
  yield scratch.db.selectorState.deleteMany();
55
55
  yield scratch.db.sessionLog.deleteMany();
56
- socket = { emitToDashboard: sinon_1.default.stub() };
56
+ socket = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
57
57
  }));
58
58
  const find = (build, selector, healed, at) => scratch.db.sessionLog.create({
59
59
  data: {
@@ -105,7 +105,7 @@ describe('SelectorVerificationJob catches a heal the live check missed', functio
105
105
  (0, chai_1.expect)(row).to.include({ status: 'active', regression_count: 1, clean_builds_count: 0 });
106
106
  (0, chai_1.expect)(row.fixed_at).to.equal(null);
107
107
  (0, chai_1.expect)(yield actionsOf('//pending/broke')).to.deep.equal(['broke_again']);
108
- (0, chai_1.expect)(socket.emitToDashboard.firstCall.args[0]).to.equal('selector_regressed');
108
+ (0, chai_1.expect)(socket.emitToDashboardForSelector.firstCall.args[0]).to.equal('selector_regressed');
109
109
  }));
110
110
  it('sends a fixed selector back to fix', () => __awaiter(this, void 0, void 0, function* () {
111
111
  yield state('//resolved/broke', 'resolved');
@@ -0,0 +1,209 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ require("reflect-metadata");
13
+ const chai_1 = require("chai");
14
+ const typedi_1 = require("typedi");
15
+ const container_registration_1 = require("../helpers/container-registration");
16
+ const scratch_database_1 = require("../helpers/scratch-database");
17
+ const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
18
+ const SelectorVisibilityResolver_1 = require("../../src/services/selector-health/SelectorVisibilityResolver");
19
+ const KEY = { strategy: 'xpath', selector: '//x' };
20
+ const MEMBER_A = { userId: 'u-a', teamIds: ['team-a'] };
21
+ describe('SelectorVisibilityResolver: who may see a selector, for its live events', () => {
22
+ it('TypeDI builds it, as the socket server gets it (Container.get)', () => {
23
+ // A function-typed constructor parameter would make Container.get throw.
24
+ const restore = (0, container_registration_1.saveRegistrations)(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
25
+ try {
26
+ (0, chai_1.expect)(typedi_1.Container.get(SelectorVisibilityResolver_1.SelectorVisibilityResolver)).to.be.instanceOf(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
27
+ }
28
+ finally {
29
+ restore();
30
+ }
31
+ });
32
+ describe('caching', () => {
33
+ let clock;
34
+ let scopes;
35
+ let checks;
36
+ let visible;
37
+ function resolver(over = {}) {
38
+ return new SelectorVisibilityResolver_1.SelectorVisibilityResolver(Object.assign({ sessionScope: (viewer) => __awaiter(this, void 0, void 0, function* () {
39
+ var _a;
40
+ scopes.push((_a = viewer.userId) !== null && _a !== void 0 ? _a : '');
41
+ return { OR: [{ device_udid: { in: viewer.teamIds } }] };
42
+ }), healedIn: (key) => __awaiter(this, void 0, void 0, function* () {
43
+ checks.push(key.selector);
44
+ return visible;
45
+ }), now: () => clock }, over));
46
+ }
47
+ beforeEach(() => {
48
+ clock = 1000000;
49
+ scopes = [];
50
+ checks = [];
51
+ visible = true;
52
+ });
53
+ it('asks once per viewer and selector while the answer is fresh, then again', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const r = resolver();
55
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
56
+ visible = false;
57
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A), 'still the cached answer').to.equal(true);
58
+ (0, chai_1.expect)(checks).to.have.length(1);
59
+ clock += SelectorVisibilityResolver_1.SELECTOR_VISIBILITY_TTL_MS;
60
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A), 'asked again after the TTL').to.equal(false);
61
+ (0, chai_1.expect)(checks).to.have.length(2);
62
+ }));
63
+ it('concurrent questions share one lookup', () => __awaiter(void 0, void 0, void 0, function* () {
64
+ const r = resolver();
65
+ const answers = yield Promise.all([
66
+ r.canSee(KEY, MEMBER_A),
67
+ r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a'] }),
68
+ r.canSee(KEY, MEMBER_A),
69
+ ]);
70
+ (0, chai_1.expect)(answers).to.deep.equal([true, true, true]);
71
+ (0, chai_1.expect)(checks).to.have.length(1);
72
+ (0, chai_1.expect)(scopes).to.have.length(1);
73
+ }));
74
+ it("a viewer's sessions are read once for all their selectors", () => __awaiter(void 0, void 0, void 0, function* () {
75
+ const r = resolver();
76
+ yield r.canSee(KEY, MEMBER_A);
77
+ yield r.canSee({ strategy: 'id', selector: 'other' }, MEMBER_A);
78
+ (0, chai_1.expect)(scopes).to.deep.equal(['u-a']);
79
+ (0, chai_1.expect)(checks).to.deep.equal(['//x', 'other']);
80
+ }));
81
+ it('the same teams in another order are the same viewer; another user or team is not', () => __awaiter(void 0, void 0, void 0, function* () {
82
+ const r = resolver();
83
+ yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a', 'team-b'] });
84
+ yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-b', 'team-a'] });
85
+ (0, chai_1.expect)(checks, 'team order').to.have.length(1);
86
+ yield r.canSee(KEY, { userId: 'u-b', teamIds: ['team-a', 'team-b'] });
87
+ yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a'] });
88
+ (0, chai_1.expect)(checks, 'another user, other teams').to.have.length(3);
89
+ }));
90
+ it('a selector with no strategy is its own selector, not one with the same text', () => __awaiter(void 0, void 0, void 0, function* () {
91
+ const r = resolver();
92
+ yield r.canSee({ strategy: '', selector: '//x' }, MEMBER_A);
93
+ yield r.canSee({ strategy: 'xpath', selector: '//x' }, MEMBER_A);
94
+ (0, chai_1.expect)(checks).to.have.length(2);
95
+ }));
96
+ it('a lookup that fails answers no, and is asked again next time', () => __awaiter(void 0, void 0, void 0, function* () {
97
+ let fail = true;
98
+ const r = resolver({
99
+ healedIn: (key) => __awaiter(void 0, void 0, void 0, function* () {
100
+ checks.push(key.selector);
101
+ if (fail)
102
+ throw new Error('database gone');
103
+ return true;
104
+ }),
105
+ });
106
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
107
+ fail = false;
108
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
109
+ (0, chai_1.expect)(checks).to.have.length(2);
110
+ }));
111
+ it("a viewer's sessions that can't be read answer no, and are read again next time", () => __awaiter(void 0, void 0, void 0, function* () {
112
+ let fail = true;
113
+ const r = resolver({
114
+ sessionScope: (viewer) => __awaiter(void 0, void 0, void 0, function* () {
115
+ var _a;
116
+ scopes.push((_a = viewer.userId) !== null && _a !== void 0 ? _a : '');
117
+ if (fail)
118
+ throw new Error('database gone');
119
+ return undefined;
120
+ }),
121
+ });
122
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
123
+ fail = false;
124
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
125
+ (0, chai_1.expect)(scopes).to.have.length(2);
126
+ }));
127
+ it('a lookup that hangs answers no after the timeout, and is asked again next time', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ let hang = true;
129
+ const r = resolver({
130
+ healedIn: (key) => {
131
+ checks.push(key.selector);
132
+ return hang ? new Promise(() => undefined) : Promise.resolve(true);
133
+ },
134
+ lookupTimeoutMs: 20,
135
+ });
136
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
137
+ hang = false;
138
+ (0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
139
+ (0, chai_1.expect)(checks).to.have.length(2);
140
+ }));
141
+ });
142
+ describe("REST's rule, on a real database (Selector Health's own fixture)", () => {
143
+ const scratch = (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
144
+ const UNPLUGGED = 'sh-phone-unplugged';
145
+ before(() => __awaiter(void 0, void 0, void 0, function* () {
146
+ yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
147
+ // Priya's session on a phone that has since gone: no Device row.
148
+ yield scratch.db.session.create({
149
+ data: {
150
+ id: 'sh-s-unplugged',
151
+ device_udid: UNPLUGGED,
152
+ device_name: 'Gone',
153
+ device_platform: 'android',
154
+ desired_capabilities: '{}',
155
+ session_capabilities: '{}',
156
+ node_id: 'localhost',
157
+ has_live_video: false,
158
+ device_version: '14',
159
+ status: 'passed',
160
+ user_id: selector_health_fixture_1.USER.priya,
161
+ },
162
+ });
163
+ yield scratch.db.sessionLog.create({
164
+ data: {
165
+ session_id: 'sh-s-unplugged',
166
+ url: '/element',
167
+ method: 'POST',
168
+ title: 'findElement',
169
+ response: '{}',
170
+ command_name: 'findElement',
171
+ is_healed: true,
172
+ original_strategy: 'xpath',
173
+ original_selector: '//unplugged/only',
174
+ },
175
+ });
176
+ }));
177
+ const xpath = (selector) => ({ strategy: 'xpath', selector });
178
+ const teamA = { userId: selector_health_fixture_1.USER.priya, teamIds: [selector_health_fixture_1.TEAM.a] };
179
+ const teamB = { userId: selector_health_fixture_1.USER.alex, teamIds: [selector_health_fixture_1.TEAM.b] };
180
+ const noTeam = { userId: 'sh-u-nobody', teamIds: [] };
181
+ it("a team's member sees the selectors healed on their team's phone or a shared one, not another team's", () => __awaiter(void 0, void 0, void 0, function* () {
182
+ const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
183
+ (0, chai_1.expect)(yield r.canSee({ strategy: 'id', selector: selector_health_fixture_1.SEL.warm }, teamA), 'team A phone').to.equal(true);
184
+ (0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.hot), teamA), 'shared phone').to.equal(true);
185
+ (0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.bOnly), teamA), "team B's phone").to.equal(false);
186
+ (0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.bOnly), teamB)).to.equal(true);
187
+ (0, chai_1.expect)(yield r.canSee({ strategy: 'id', selector: selector_health_fixture_1.SEL.warm }, teamB)).to.equal(false);
188
+ }));
189
+ it('a member of no team sees only what healed on a shared phone', () => __awaiter(void 0, void 0, void 0, function* () {
190
+ const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
191
+ (0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.hot), noTeam)).to.equal(true);
192
+ (0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.muted), noTeam)).to.equal(false);
193
+ (0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.mutedB), noTeam)).to.equal(false);
194
+ }));
195
+ it('a heal recorded with no strategy is strategy ""', () => __awaiter(void 0, void 0, void 0, function* () {
196
+ const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
197
+ (0, chai_1.expect)(yield r.canSee({ strategy: '', selector: selector_health_fixture_1.SEL.legacy }, noTeam)).to.equal(true);
198
+ }));
199
+ it("a phone that has gone leaves its sessions' selectors to their owner", () => __awaiter(void 0, void 0, void 0, function* () {
200
+ const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
201
+ (0, chai_1.expect)(yield r.canSee(xpath('//unplugged/only'), teamA), 'Priya owns it').to.equal(true);
202
+ (0, chai_1.expect)(yield r.canSee(xpath('//unplugged/only'), teamB)).to.equal(false);
203
+ }));
204
+ it('an unknown selector is seen by no one', () => __awaiter(void 0, void 0, void 0, function* () {
205
+ const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
206
+ (0, chai_1.expect)(yield r.canSee(xpath('//never/healed'), teamA)).to.equal(false);
207
+ }));
208
+ });
209
+ });
@@ -20,14 +20,17 @@ const plugin_1 = require("../../src/plugin");
20
20
  const ServerManager_1 = require("../../src/services/ServerManager");
21
21
  const PluginContext_1 = require("../../src/PluginContext");
22
22
  const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
23
+ const aiEngineSettings_1 = require("../../src/services/settings/aiEngineSettings");
23
24
  const logger_1 = __importDefault(require("../../src/logger"));
24
25
  /**
25
- * The self-healing value saved on the Settings page is read once at boot, so a
26
- * restart doesn't put the server back on the option it was started with. It
27
- * has to be loaded once the database is ready (migrations ran) and before the
28
- * first route can answer or the first command arrive.
26
+ * The self-healing value saved on the Settings page, and the AI engine page's
27
+ * provider, model and base URL, are read once at boot, so a restart doesn't put
28
+ * the server back on the options it was started with. They have to be loaded
29
+ * once the database is ready (migrations ran), after the startup options were
30
+ * applied (the AI engine's are what a cleared value goes back to), and before
31
+ * the first route can answer or the first command arrive.
29
32
  */
30
- describe('loading the saved self-healing setting, at boot', () => {
33
+ describe('loading the saved self-healing and AI engine settings, at boot', () => {
31
34
  let statics;
32
35
  beforeEach(() => {
33
36
  statics = {
@@ -57,6 +60,7 @@ describe('loading the saved self-healing setting, at boot', () => {
57
60
  // The real one kills every go-ios process on the machine.
58
61
  sinon_1.default.stub(boot, 'reapLeftoverGoIos').callsFake(step('reap'));
59
62
  sinon_1.default.stub(SelfHealingSwitch_1.SelfHealingSwitch.prototype, 'load').callsFake(step('self-healing setting'));
63
+ sinon_1.default.stub(aiEngineSettings_1.AiEngineSettings.prototype, 'load').callsFake(step('AI engine settings'));
60
64
  sinon_1.default.stub(boot, 'registerRoutes').callsFake(() => {
61
65
  steps.push('routes');
62
66
  throw new Error('boot stopped by the test');
@@ -69,6 +73,7 @@ describe('loading the saved self-healing setting, at boot', () => {
69
73
  'database and migrations',
70
74
  'reap',
71
75
  'self-healing setting',
76
+ 'AI engine settings',
72
77
  'routes',
73
78
  ]);
74
79
  }));
@@ -203,10 +203,10 @@ describe('the AI self-healing switch', () => {
203
203
  (0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
204
204
  }));
205
205
  it("still hands a session's own healing tiers to the healing it runs", () => __awaiter(void 0, void 0, void 0, function* () {
206
- sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns({
207
- getCapabilities: () => ({ 'xe:options': { healingTiers: [1, 2] } }),
208
- });
209
- const driver = { sessionId: 'sess-6' };
206
+ // Read from the session's driver, which every session has: SESSION_MANAGER
207
+ // holds a local one only with the dashboard on, and holds none here.
208
+ const driver = { sessionId: 'sess-6', caps: { 'xe:options': { healingTiers: [1, 2] } } };
209
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('sess-6')).to.equal(undefined);
210
210
  yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
211
211
  .handle(() => __awaiter(void 0, void 0, void 0, function* () {
212
212
  throw noSuchElement();
@@ -219,9 +219,10 @@ describe('the AI self-healing switch', () => {
219
219
  let learn;
220
220
  beforeEach(() => {
221
221
  sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
222
- learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning');
222
+ learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning').resolves();
223
223
  });
224
- const afterFind = () => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).runPostCommandHooks('sess-7', 'findElement', {}, ['id', 'there'], { ELEMENT: 'el-1' }, pluginArgs);
224
+ /** A findElement the driver answers with its element. */
225
+ const afterFind = () => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return ({ ELEMENT: 'el-1' }); }), { sessionId: 'sess-7' }, 'findElement', ['id', 'there', 'sess-7'], pluginArgs, false);
225
226
  it('learns the selector while the switch is on', () => __awaiter(void 0, void 0, void 0, function* () {
226
227
  yield afterFind();
227
228
  (0, chai_1.expect)(learn.calledOnce).to.equal(true);
@@ -145,7 +145,11 @@ describe('createSession — an app named by id follows the team rule', () => {
145
145
  return owned.includes('admin') || req.some((s) => owned.includes(s));
146
146
  },
147
147
  });
148
- typedi_1.Container.set(UserService_1.UserService, { findById: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
148
+ // Every key's owner is an ACTIVE member: a key names its owner only while
149
+ // they are. The admin key is unscoped by its admin scope.
150
+ typedi_1.Container.set(UserService_1.UserService, {
151
+ findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
152
+ });
149
153
  sinon_1.default.stub(app_service_1.APP_SERVICE, 'getAppById').callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = APPS[id]) !== null && _a !== void 0 ? _a : null; }));
150
154
  const device = { udid: 'u1', host: 'h1', platform: 'android', nodeId: 'node-hub' };
151
155
  allocate = sinon_1.default.stub(deviceUtils, 'allocateDeviceForSession').resolves(device);
@@ -41,10 +41,12 @@ describe('authorizeSessionRequest — identity', () => {
41
41
  authDisabledBefore = config_1.config.authDisabled;
42
42
  config_1.config.authDisabled = false;
43
43
  restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
44
- // No such user and no teams, as the developer's database answered for
45
- // these ids: the real lookups read that database. Nothing here is about
46
- // the owner's teams.
47
- typedi_1.Container.set(UserService_1.UserService, { findById: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
44
+ // Every id is an ACTIVE member in no team: a credential names its owner
45
+ // only while they are (session-create-credential-owner.spec.ts covers
46
+ // the other cases). The real lookups would read the developer's database.
47
+ typedi_1.Container.set(UserService_1.UserService, {
48
+ findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
49
+ });
48
50
  sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
49
51
  svc = new SessionLifecycleService_1.SessionLifecycleService();
50
52
  });
@@ -105,7 +107,7 @@ describe('authorizeSessionRequest — identity', () => {
105
107
  hasScope: () => false,
106
108
  });
107
109
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
108
- verify: sinon_1.default.stub().resolves({ sub: 'usr_carol' }),
110
+ verify: sinon_1.default.stub().resolves({ sub: 'usr_carol', scopes: 'sessions' }),
109
111
  });
110
112
  const res = yield invoke(svc, capsWith({ 'xe:options': { sessionToken: 'tok' } }));
111
113
  (0, chai_1.expect)(res.apiKeyId).to.equal(null);
@@ -156,13 +158,14 @@ describe('authorizeSessionRequest — leaseAccess', () => {
156
158
  findUser = sinon_1.default.stub().resolves(owner);
157
159
  typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
158
160
  };
159
- const withSessionToken = (owner, teamId = null) => {
161
+ // A session token carries the scopes of the credential that minted it.
162
+ const withSessionToken = (owner, teamId = null, scopes = 'sessions') => {
160
163
  typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
161
164
  verifyPair: sinon_1.default.stub().resolves(null),
162
165
  hasScope: () => false,
163
166
  });
164
167
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
165
- verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId }),
168
+ verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId, scopes }),
166
169
  });
167
170
  findUser = sinon_1.default.stub().resolves(owner);
168
171
  typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
@@ -208,9 +211,19 @@ describe('authorizeSessionRequest — leaseAccess', () => {
208
211
  withKey(keyRow('admin'), user('SUPER_ADMIN', 'INACTIVE'));
209
212
  (0, chai_1.expect)((yield access(pairCaps())).canOverride).to.equal(false);
210
213
  }));
211
- it('follows the role of a session-token caller, ADMIN included', () => __awaiter(void 0, void 0, void 0, function* () {
212
- withSessionToken(user('ADMIN'));
214
+ // By the scopes it was minted with, as a key is judged. By its user's role
215
+ // alone, an ADMIN's key without the admin scope minted itself an override.
216
+ it("follows resolveActor's rule on a session token's user's role now", () => __awaiter(void 0, void 0, void 0, function* () {
217
+ withSessionToken(user('ADMIN'), null, 'admin,sessions');
218
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
219
+ withSessionToken(user('ADMIN'), null, 'sessions');
220
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
221
+ withSessionToken(user('SUPER_ADMIN'), null, 'admin,sessions');
222
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
223
+ withSessionToken(user('SUPER_ADMIN'), null, 'sessions');
213
224
  (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
225
+ withSessionToken(user('MEMBER'), null, 'admin,sessions');
226
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
214
227
  withSessionToken(user('MEMBER'));
215
228
  (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
216
229
  }));
@@ -223,9 +236,24 @@ describe('authorizeSessionRequest — leaseAccess', () => {
223
236
  (0, chai_1.expect)(res.scoped).to.equal(false);
224
237
  (0, chai_1.expect)((yield res.leaseAccess()).canOverride).to.equal(false);
225
238
  }));
226
- it('is false when the lookup fails, and the teams fall back to the shared pool', () => __awaiter(void 0, void 0, void 0, function* () {
239
+ // The owner is looked up when the credential is checked: a key names them
240
+ // only while they are ACTIVE, so a lookup that fails refuses the session
241
+ // rather than run it as someone who may have been switched off.
242
+ it('refuses the session when the owner lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
227
243
  withKey(keyRow('admin'), null);
228
244
  typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
245
+ let error;
246
+ try {
247
+ yield invoke(svc, pairCaps());
248
+ }
249
+ catch (e) {
250
+ error = e;
251
+ }
252
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal('db down');
253
+ }));
254
+ it('is false, and the teams fall back to the shared pool, when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
255
+ withKey(keyRow('sessions'), user('MEMBER'));
256
+ teamRows.rejects(new Error('db down'));
229
257
  (0, chai_1.expect)(yield access(pairCaps())).to.deep.equal({ canOverride: false, teamIds: [] });
230
258
  }));
231
259
  it('is true when auth is disabled, where every caller is an admin', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -331,9 +359,9 @@ describe('authorizeSessionRequest — leaseAccess', () => {
331
359
  }
332
360
  (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("xe:options.team 'team_c' is not allowed for this API key");
333
361
  }));
334
- it('falls back to the shared pool when the lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
335
- withKey(keyRow('sessions'), null);
336
- typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
362
+ it('falls back to the shared pool when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
363
+ withKey(keyRow('sessions'), user('MEMBER'));
364
+ teamRows.rejects(new Error('db down'));
337
365
  (0, chai_1.expect)(yield scope(pairCaps())).to.deep.equal({ callerTeamIds: [], scoped: true });
338
366
  }));
339
367
  it('stays unscoped, with no lookup, for a session with no credentials', () => __awaiter(void 0, void 0, void 0, function* () {