@xenon-device-management/xenon 2.13.2 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +17 -8
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
  13. package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
  20. package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
  40. package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
  47. package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/settings-C3uyGhyc.js +6 -0
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
  74. package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +23 -25
  77. package/lib/src/XenonCapabilityManager.js +22 -5
  78. package/lib/src/app/dashboardPluginLink.js +117 -0
  79. package/lib/src/app/index.js +13 -34
  80. package/lib/src/app/openapi/control.yaml +73 -20
  81. package/lib/src/app/openapi/grid.yaml +31 -7
  82. package/lib/src/app/openapi/identity.yaml +103 -31
  83. package/lib/src/app/openapi/platform.yaml +165 -39
  84. package/lib/src/app/openapi/sessions.yaml +10 -3
  85. package/lib/src/app/routers/apikeys.js +14 -2
  86. package/lib/src/app/routers/auth.js +29 -5
  87. package/lib/src/app/routers/control.js +20 -2
  88. package/lib/src/app/routers/dashboard.js +69 -26
  89. package/lib/src/app/routers/grid.js +37 -13
  90. package/lib/src/app/routers/profile.js +21 -0
  91. package/lib/src/app/routers/streamType.js +7 -2
  92. package/lib/src/app/routers/users.js +10 -5
  93. package/lib/src/app/routers/webhook.js +8 -4
  94. package/lib/src/app/swagger.js +2 -0
  95. package/lib/src/app/ws/h264StreamWs.js +10 -1
  96. package/lib/src/config.js +51 -5
  97. package/lib/src/dashboard/commands.js +185 -141
  98. package/lib/src/dashboard/event-manager.js +88 -67
  99. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  100. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  101. package/lib/src/data-service/appleFamily.js +43 -0
  102. package/lib/src/data-service/db.js +11 -0
  103. package/lib/src/data-service/device-service.js +6 -1
  104. package/lib/src/data-service/device-store.js +76 -3
  105. package/lib/src/data-service/deviceClaims.js +37 -0
  106. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  107. package/lib/src/data-service/deviceSettings.js +79 -0
  108. package/lib/src/data-service/prisma-store.js +169 -15
  109. package/lib/src/data-service/web-config-service.js +50 -92
  110. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  111. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  112. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  113. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  114. package/lib/src/device-managers/shellCommands.js +96 -0
  115. package/lib/src/device-utils.js +114 -40
  116. package/lib/src/gateway/defaultGateway.js +1 -1
  117. package/lib/src/gateway/forwardToNode.js +2 -0
  118. package/lib/src/gateway/healReport.js +87 -0
  119. package/lib/src/gateway/sessionGateway.js +12 -2
  120. package/lib/src/generated/client/edge.js +18 -3
  121. package/lib/src/generated/client/index-browser.js +15 -0
  122. package/lib/src/generated/client/index.d.ts +2086 -379
  123. package/lib/src/generated/client/index.js +18 -3
  124. package/lib/src/generated/client/package.json +1 -1
  125. package/lib/src/generated/client/schema.prisma +38 -7
  126. package/lib/src/generated/client/wasm.js +15 -0
  127. package/lib/src/interceptors/CommandInterceptor.js +271 -188
  128. package/lib/src/interceptors/xenonScripts.js +55 -0
  129. package/lib/src/interfaces/IPluginArgs.js +1 -2
  130. package/lib/src/middleware/authMiddleware.js +15 -3
  131. package/lib/src/middleware/commandCaller.js +3 -3
  132. package/lib/src/middleware/verifyCredential.js +58 -1
  133. package/lib/src/plugin.js +23 -4
  134. package/lib/src/prisma.js +1 -1
  135. package/lib/src/scripts/database-check.js +124 -0
  136. package/lib/src/services/AICommandService.js +9 -2
  137. package/lib/src/services/AIService.js +281 -32
  138. package/lib/src/services/CleanupService.js +6 -1
  139. package/lib/src/services/EmailService.js +29 -0
  140. package/lib/src/services/InterceptorService.js +70 -13
  141. package/lib/src/services/NetworkConditioningService.js +85 -88
  142. package/lib/src/services/NotificationService.js +36 -29
  143. package/lib/src/services/OrphanSweeper.js +7 -0
  144. package/lib/src/services/PortAllocator.js +5 -0
  145. package/lib/src/services/SelectorVerificationJob.js +15 -4
  146. package/lib/src/services/ServerManager.js +105 -9
  147. package/lib/src/services/SessionLifecycleService.js +133 -75
  148. package/lib/src/services/ShutdownCoordinator.js +33 -0
  149. package/lib/src/services/SocketServer.js +19 -3
  150. package/lib/src/services/TeamService.js +20 -3
  151. package/lib/src/services/bug-report/video-slice.js +4 -1
  152. package/lib/src/services/device-access/leaseOverride.js +7 -5
  153. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  154. package/lib/src/services/healing/HealEtalonService.js +3 -2
  155. package/lib/src/services/healing/HealingOrchestrator.js +85 -71
  156. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  157. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  158. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  159. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  160. package/lib/src/services/healing/resilioPath.js +274 -0
  161. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  162. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  163. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  164. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  165. package/lib/src/services/network/AndroidRadios.js +71 -0
  166. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  167. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  168. package/lib/src/services/network/adbForPhone.js +60 -0
  169. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  170. package/lib/src/services/network/xenonProxy.js +107 -0
  171. package/lib/src/services/ocr/ocrData.js +118 -0
  172. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  173. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  174. package/lib/src/services/omni-vision/OmniVisionService.js +132 -95
  175. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  176. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  177. package/lib/src/services/omni-vision/screenScale.js +68 -0
  178. package/lib/src/services/passwordResetLink.js +50 -9
  179. package/lib/src/services/passwordResetMode.js +3 -1
  180. package/lib/src/services/publicUrl.js +45 -0
  181. package/lib/src/services/recording/concurrency-gate.js +12 -3
  182. package/lib/src/services/session/sessionIdentity.js +5 -3
  183. package/lib/src/services/session/shutdownReason.js +26 -0
  184. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  185. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  186. package/lib/src/services/settings/labSettings.js +149 -0
  187. package/lib/src/services/token/mintedLifetime.js +51 -0
  188. package/lib/src/services/webhookEvents.js +173 -0
  189. package/lib/src/sessions/SessionManager.js +8 -2
  190. package/lib/src/sessions/sessionMemory.js +25 -0
  191. package/lib/test/helpers/fake-adb.js +58 -0
  192. package/lib/test/helpers/fake-ai-provider.js +188 -0
  193. package/lib/test/helpers/fake-device-table.js +63 -24
  194. package/lib/test/helpers/loki-stores.js +33 -0
  195. package/lib/test/helpers/page-sources.js +164 -0
  196. package/lib/test/helpers/scratch-database.js +58 -13
  197. package/lib/test/integration/audit-ingest.spec.js +10 -10
  198. package/lib/test/integration/auth-flow.spec.js +9 -14
  199. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  200. package/lib/test/integration/bug-report-route.spec.js +22 -2
  201. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  202. package/lib/test/integration/forgot-password.spec.js +17 -18
  203. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  204. package/lib/test/integration/migration.spec.js +6 -1
  205. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  206. package/lib/test/integration/ports-router.spec.js +4 -4
  207. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  208. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  209. package/lib/test/integration/profile-tokens.spec.js +10 -16
  210. package/lib/test/integration/recordings-router.spec.js +16 -16
  211. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  212. package/lib/test/integration/role-matrix.spec.js +4 -9
  213. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  214. package/lib/test/integration/sdk-version.spec.js +2 -2
  215. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  216. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  217. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  218. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  219. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  220. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  221. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  222. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  223. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  224. package/lib/test/integration/users-flow.spec.js +7 -12
  225. package/lib/test/integration/users-lockout.spec.js +5 -3
  226. package/lib/test/unit/EmailService.test.js +66 -0
  227. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  228. package/lib/test/unit/TeamService.test.js +3 -0
  229. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  230. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  231. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  232. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  233. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  234. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  235. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  236. package/lib/test/unit/apikeys-router.spec.js +84 -0
  237. package/lib/test/unit/apple-family.spec.js +36 -0
  238. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  239. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  240. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  241. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  242. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  243. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  244. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  245. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  246. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  247. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  248. package/lib/test/unit/database-check.spec.js +108 -0
  249. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  250. package/lib/test/unit/device-log-book.spec.js +180 -0
  251. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  252. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  253. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  254. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  255. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  256. package/lib/test/unit/emulator-boot.spec.js +91 -0
  257. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  258. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  259. package/lib/test/unit/failure-analysis.spec.js +182 -0
  260. package/lib/test/unit/failure-categories.spec.js +128 -0
  261. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  262. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  263. package/lib/test/unit/heal-report.spec.js +71 -0
  264. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  265. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  266. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  267. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
  268. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  269. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  270. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  271. package/lib/test/unit/json-logging-option.spec.js +74 -0
  272. package/lib/test/unit/lab-settings.spec.js +218 -0
  273. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  274. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  275. package/lib/test/unit/max-sessions.spec.js +240 -0
  276. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  277. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  278. package/lib/test/unit/ocr-offline.spec.js +213 -0
  279. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  280. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  281. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  282. package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
  283. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  284. package/lib/test/unit/omni-vision.spec.js +6 -8
  285. package/lib/test/unit/package-engines.spec.js +27 -0
  286. package/lib/test/unit/partial-config-file.spec.js +14 -0
  287. package/lib/test/unit/passwordResetMode.test.js +10 -2
  288. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  289. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  290. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  291. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  292. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  293. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  294. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  295. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  296. package/lib/test/unit/plugin.spec.js +3 -3
  297. package/lib/test/unit/profile-router.test.js +122 -1
  298. package/lib/test/unit/queue-requester.spec.js +1 -1
  299. package/lib/test/unit/recording-options.spec.js +181 -0
  300. package/lib/test/unit/reset-link-address.spec.js +266 -0
  301. package/lib/test/unit/reset-link.test.js +10 -4
  302. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  303. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  304. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  305. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  306. package/lib/test/unit/self-healing-boot.spec.js +80 -0
  307. package/lib/test/unit/self-healing-setting.spec.js +273 -0
  308. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  309. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  310. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  311. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  312. package/lib/test/unit/session-device-logs.spec.js +347 -0
  313. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  314. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  315. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  316. package/lib/test/unit/shell-commands.spec.js +133 -0
  317. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  318. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  319. package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
  320. package/lib/test/unit/stream-status-type.spec.js +4 -0
  321. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  322. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  323. package/lib/test/unit/verify-credential.spec.js +33 -0
  324. package/lib/test/unit/vision-assertion.spec.js +119 -0
  325. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  326. package/lib/test/unit/web-config-service.spec.js +98 -0
  327. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  328. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  329. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  330. package/lib/tsconfig.tsbuildinfo +1 -1
  331. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  332. package/package.json +5 -5
  333. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  334. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  335. package/prisma/schema.prisma +38 -7
  336. package/schema.json +23 -25
  337. package/scripts/generate-types-from-schema.js +1 -2
  338. package/scripts/lib/always-present-plugin-args.js +4 -1
  339. package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
  340. package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
  341. package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
  342. package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
  343. package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
  344. package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
  345. package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
  346. package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
  347. package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
  348. package/lib/public/assets/settings-MsVU2ukE.js +0 -6
  349. package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
  350. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const express_1 = __importDefault(require("express"));
18
+ const sinon_1 = __importDefault(require("sinon"));
19
+ const typedi_1 = require("typedi");
20
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
21
+ const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
22
+ const PluginContext_1 = require("../../src/PluginContext");
23
+ const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
24
+ const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
25
+ const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
26
+ const web_config_service_1 = require("../../src/data-service/web-config-service");
27
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
28
+ const event_manager_1 = require("../../src/dashboard/event-manager");
29
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
30
+ const logger_1 = __importDefault(require("../../src/logger"));
31
+ const container_registration_1 = require("../helpers/container-registration");
32
+ const scratch_database_1 = require("../helpers/scratch-database");
33
+ /**
34
+ * The Settings page's "AI self-healing" toggle used to do nothing: POST /config
35
+ * stored only the fields its table knew, GET /config never sent the switch back
36
+ * (so the page always showed Enabled), and CommandInterceptor read the copy of
37
+ * the plugin options the server started with, which nothing updates.
38
+ *
39
+ * It follows the other lab settings now: a value saved in the dashboard wins
40
+ * over the startup option, which wins over the default, and applies to the very
41
+ * next command without a restart. The interceptor runs on every command, so it
42
+ * reads an in-memory value (SelfHealingSwitch), never the database.
43
+ */
44
+ describe('the AI self-healing switch', () => {
45
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
46
+ let context;
47
+ let startedWith;
48
+ let restoreContainer;
49
+ let healingSwitch;
50
+ const startup = (over = {}) => {
51
+ context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), over);
52
+ return context.pluginArgs;
53
+ };
54
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
55
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
56
+ startedWith = context.pluginArgs;
57
+ restoreContainer = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
58
+ // A switch of its own, as at boot, so no saved value leaks in from another spec.
59
+ typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
60
+ healingSwitch = typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch);
61
+ yield scratch.db.webConfig.deleteMany({});
62
+ }));
63
+ afterEach(() => {
64
+ context.pluginArgs = startedWith;
65
+ restoreContainer();
66
+ sinon_1.default.restore();
67
+ });
68
+ function app() {
69
+ const a = (0, express_1.default)();
70
+ a.use(express_1.default.json());
71
+ a.use((req, _res, next) => {
72
+ req.auth = {
73
+ kind: 'user-session',
74
+ userId: 'usr_x',
75
+ role: 'SUPER_ADMIN',
76
+ scopes: 'admin,devices,sessions,read',
77
+ rateLimit: 1000,
78
+ };
79
+ next();
80
+ });
81
+ dashboard_1.default.register(a);
82
+ return a;
83
+ }
84
+ const save = (body) => (0, loopbackRequest_1.default)(app()).post('/config').send(body).timeout(5000);
85
+ const read = () => (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
86
+ describe('as the Settings page saves and reads it', () => {
87
+ it('is kept: what POST /config saves, GET /config sends back', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ startup();
89
+ const saved = yield save({ enableSelfHealing: false });
90
+ (0, chai_1.expect)(saved.status).to.equal(200);
91
+ const shown = yield read();
92
+ (0, chai_1.expect)(shown.status).to.equal(200);
93
+ (0, chai_1.expect)(shown.body.enableSelfHealing).to.equal(false);
94
+ (0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
95
+ enableSelfHealing: false,
96
+ });
97
+ }));
98
+ it('can be turned back on', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ startup();
100
+ yield save({ enableSelfHealing: false });
101
+ yield save({ enableSelfHealing: true });
102
+ (0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(true);
103
+ }));
104
+ it('is on, and says so, when nothing was saved and the server was started with nothing', () => __awaiter(void 0, void 0, void 0, function* () {
105
+ startup();
106
+ const shown = yield read();
107
+ (0, chai_1.expect)(shown.body.enableSelfHealing).to.equal(true);
108
+ (0, chai_1.expect)(shown.body.defaults.enableSelfHealing).to.equal(true);
109
+ }));
110
+ it('shows the option the server started with when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ startup({ enableSelfHealing: false });
112
+ (0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(false);
113
+ }));
114
+ it('shows a saved value over the option the server started with', () => __awaiter(void 0, void 0, void 0, function* () {
115
+ startup({ enableSelfHealing: false });
116
+ yield save({ enableSelfHealing: true });
117
+ (0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(true);
118
+ }));
119
+ it('is saved on its own, without touching the other settings', () => __awaiter(void 0, void 0, void 0, function* () {
120
+ startup();
121
+ yield save({ buildCleanupDays: 7 });
122
+ yield save({ enableSelfHealing: false });
123
+ (0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
124
+ buildCleanupDays: 7,
125
+ enableSelfHealing: false,
126
+ });
127
+ }));
128
+ for (const bad of ['false', 'no', 0, 1, null, {}, []]) {
129
+ it(`refuses ${JSON.stringify(bad)} with a 400 naming the setting, and saves nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
130
+ startup();
131
+ const refused = yield save({ enableSelfHealing: bad, buildCleanupDays: 9 });
132
+ (0, chai_1.expect)(refused.status).to.equal(400);
133
+ (0, chai_1.expect)(refused.body).to.include({ error: 'invalid_setting', field: 'enableSelfHealing' });
134
+ (0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({});
135
+ }));
136
+ }
137
+ });
138
+ describe('on a running interceptor', () => {
139
+ const noSuchElement = () => Object.assign(new Error('NoSuchElement: An element could not be located'), {
140
+ name: 'NoSuchElementError',
141
+ });
142
+ let attemptHealing;
143
+ let pluginArgs;
144
+ beforeEach(() => {
145
+ pluginArgs = startup();
146
+ attemptHealing = sinon_1.default.stub().resolves(null);
147
+ typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
148
+ });
149
+ /** A findElement the driver answers with NoSuchElement, as a broken selector does. */
150
+ function findMissing(sessionId) {
151
+ return __awaiter(this, void 0, void 0, function* () {
152
+ let thrown;
153
+ try {
154
+ yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(this, void 0, void 0, function* () {
155
+ throw noSuchElement();
156
+ }), { sessionId }, 'findElement', ['xpath', '//android.widget.Button[@text="Gone"]', sessionId], pluginArgs, false);
157
+ }
158
+ catch (err) {
159
+ thrown = err;
160
+ }
161
+ return thrown;
162
+ });
163
+ }
164
+ it('heals a missing element while the switch is on, as it always did', () => __awaiter(void 0, void 0, void 0, function* () {
165
+ const thrown = yield findMissing('sess-on');
166
+ (0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
167
+ // Nothing healed it, so the test still sees its own NoSuchElement.
168
+ (0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
169
+ }));
170
+ it('stops healing for the next command once off is saved, with no restart', () => __awaiter(void 0, void 0, void 0, function* () {
171
+ yield findMissing('sess-1');
172
+ (0, chai_1.expect)(attemptHealing.callCount).to.equal(1);
173
+ (0, chai_1.expect)((yield save({ enableSelfHealing: false })).status).to.equal(200);
174
+ const thrown = yield findMissing('sess-1');
175
+ (0, chai_1.expect)(attemptHealing.callCount).to.equal(1);
176
+ // The failure reaches the test untouched, which is the point of turning it off.
177
+ (0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
178
+ }));
179
+ it('heals again once on is saved after off', () => __awaiter(void 0, void 0, void 0, function* () {
180
+ yield save({ enableSelfHealing: false });
181
+ yield findMissing('sess-2');
182
+ (0, chai_1.expect)(attemptHealing.called).to.equal(false);
183
+ yield save({ enableSelfHealing: true });
184
+ yield findMissing('sess-2');
185
+ (0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
186
+ }));
187
+ it('does not read the database for a command', () => __awaiter(void 0, void 0, void 0, function* () {
188
+ yield save({ enableSelfHealing: false });
189
+ const reads = sinon_1.default.spy(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig');
190
+ yield findMissing('sess-3');
191
+ yield findMissing('sess-3');
192
+ (0, chai_1.expect)(reads.called).to.equal(false);
193
+ }));
194
+ it('follows the startup option when nothing is saved', () => __awaiter(void 0, void 0, void 0, function* () {
195
+ pluginArgs = startup({ enableSelfHealing: false });
196
+ yield findMissing('sess-4');
197
+ (0, chai_1.expect)(attemptHealing.called).to.equal(false);
198
+ }));
199
+ it('lets a saved on beat a startup off', () => __awaiter(void 0, void 0, void 0, function* () {
200
+ pluginArgs = startup({ enableSelfHealing: false });
201
+ yield save({ enableSelfHealing: true });
202
+ yield findMissing('sess-5');
203
+ (0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
204
+ }));
205
+ it("still hands a session's own healing tiers to the healing it runs", () => __awaiter(void 0, void 0, void 0, function* () {
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
+ yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
211
+ .handle(() => __awaiter(void 0, void 0, void 0, function* () {
212
+ throw noSuchElement();
213
+ }), driver, 'findElement', ['id', 'gone', 'sess-6'], pluginArgs, false)
214
+ .catch(() => undefined);
215
+ (0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
216
+ (0, chai_1.expect)(attemptHealing.firstCall.args).to.deep.equal(['sess-6', driver, 'id', 'gone', [1, 2]]);
217
+ }));
218
+ describe('and what it learns from a found element', () => {
219
+ let learn;
220
+ beforeEach(() => {
221
+ sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
222
+ learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning').resolves();
223
+ });
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);
226
+ it('learns the selector while the switch is on', () => __awaiter(void 0, void 0, void 0, function* () {
227
+ yield afterFind();
228
+ (0, chai_1.expect)(learn.calledOnce).to.equal(true);
229
+ }));
230
+ it('learns nothing once off is saved', () => __awaiter(void 0, void 0, void 0, function* () {
231
+ yield save({ enableSelfHealing: false });
232
+ yield afterFind();
233
+ (0, chai_1.expect)(learn.called).to.equal(false);
234
+ }));
235
+ });
236
+ });
237
+ describe('at boot', () => {
238
+ it('is the saved value, over the startup option', () => __awaiter(void 0, void 0, void 0, function* () {
239
+ const pluginArgs = startup({ enableSelfHealing: true });
240
+ yield scratch.db.webConfig.create({
241
+ data: { id: 'enableSelfHealing', name: 'enableSelfHealing', value: 'false' },
242
+ });
243
+ yield healingSwitch.load();
244
+ (0, chai_1.expect)(healingSwitch.isEnabled(pluginArgs)).to.equal(false);
245
+ }));
246
+ it('is the startup option when nothing is saved', () => __awaiter(void 0, void 0, void 0, function* () {
247
+ yield healingSwitch.load();
248
+ (0, chai_1.expect)(healingSwitch.isEnabled(startup({ enableSelfHealing: false }))).to.equal(false);
249
+ (0, chai_1.expect)(healingSwitch.isEnabled(startup({ enableSelfHealing: true }))).to.equal(true);
250
+ }));
251
+ it('is on when neither was set', () => __awaiter(void 0, void 0, void 0, function* () {
252
+ yield healingSwitch.load();
253
+ (0, chai_1.expect)(healingSwitch.isEnabled({})).to.equal(true);
254
+ }));
255
+ it('is the startup option, and says so once, when the saved value cannot be read', () => __awaiter(void 0, void 0, void 0, function* () {
256
+ const warn = sinon_1.default.stub(logger_1.default, 'warn');
257
+ sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig').rejects(new Error('db is down'));
258
+ yield healingSwitch.load();
259
+ const off = startup({ enableSelfHealing: false });
260
+ const on = startup({ enableSelfHealing: true });
261
+ (0, chai_1.expect)(healingSwitch.isEnabled(off)).to.equal(false);
262
+ (0, chai_1.expect)(healingSwitch.isEnabled(on)).to.equal(true);
263
+ // However many commands follow, it is said once, at the load.
264
+ (0, chai_1.expect)(warn.calledOnce).to.equal(true);
265
+ (0, chai_1.expect)(String(warn.firstCall.args[0])).to.match(/self-healing/i);
266
+ (0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('db is down');
267
+ }));
268
+ it('uses the startup option until it has loaded', () => {
269
+ (0, chai_1.expect)(healingSwitch.isEnabled({ enableSelfHealing: false })).to.equal(false);
270
+ (0, chai_1.expect)(healingSwitch.isEnabled({ enableSelfHealing: true })).to.equal(true);
271
+ });
272
+ });
273
+ });
@@ -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* () {
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const typedi_1 = require("typedi");
22
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
23
+ const ApiKeyService_1 = require("../../src/services/ApiKeyService");
24
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
25
+ const UserService_1 = require("../../src/services/UserService");
26
+ const config_1 = require("../../src/config");
27
+ const prisma_1 = require("../../src/prisma");
28
+ const container_registration_1 = require("../helpers/container-registration");
29
+ const sessionCredentials_1 = require("../../src/services/session/sessionCredentials");
30
+ /**
31
+ * Session create judges the credentials it is given by REST's rule: a key
32
+ * pair or session token says who the caller is only while that user exists
33
+ * and is ACTIVE. Through 2.14 the pair was checked against the key alone
34
+ * (ApiKeyService.verifyPair) and the session token against its signature
35
+ * alone, so a user set to Inactive, or deleted, kept creating sessions as
36
+ * themselves, past XENON_REQUIRE_SESSION_TOKEN too.
37
+ *
38
+ * Such credentials now count as wrong ones: with the gate on the session is
39
+ * refused, with it off it runs with no owner, like any session presenting
40
+ * credentials that don't check out.
41
+ *
42
+ * A session token also has to carry the `sessions` scope, as a key pair
43
+ * must: POST /auth/token gave one to any credential, a read-only one
44
+ * included.
45
+ */
46
+ // authorizeSessionRequest is private: the single place a create's identity is
47
+ // decided. It is handed the credentials createSession took out of the caps.
48
+ const invoke = (svc, caps) => svc.authorizeSessionRequest(caps, (0, sessionCredentials_1.takeSessionCredentials)(caps));
49
+ const capsWith = (options) => ({
50
+ alwaysMatch: { 'xe:options': options },
51
+ firstMatch: [{}],
52
+ });
53
+ function refusalOf(promise) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ var _a;
56
+ try {
57
+ yield promise;
58
+ return null;
59
+ }
60
+ catch (err) {
61
+ return String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err);
62
+ }
63
+ });
64
+ }
65
+ describe('session create: the credentials of an inactive or deleted user', () => {
66
+ let svc;
67
+ let dir;
68
+ let keys;
69
+ let restore;
70
+ let authDisabledBefore;
71
+ let gateBefore;
72
+ let users;
73
+ let findById;
74
+ const keyRow = { id: 'key_alice', userId: 'usr_alice', scopes: 'sessions', teamId: null };
75
+ const alice = () => users.usr_alice;
76
+ const pair = () => capsWith({ accessKey: 'xen_alice', token: 'tk' });
77
+ const token = (claims = { sub: 'usr_alice', scopes: 'sessions' }) => keys.sign(claims, { audience: 'xenon-session', ttlSeconds: 600 });
78
+ const gate = (on) => {
79
+ if (on)
80
+ process.env.XENON_REQUIRE_SESSION_TOKEN = 'true';
81
+ else
82
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
83
+ };
84
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
85
+ authDisabledBefore = config_1.config.authDisabled;
86
+ config_1.config.authDisabled = false;
87
+ gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
88
+ restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
89
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-create-owner-'));
90
+ keys = new JwtKeyService_1.JwtKeyService();
91
+ yield keys.init(dir);
92
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
93
+ typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
94
+ verifyPair: sinon_1.default.stub().resolves(keyRow),
95
+ hasScope: (row, req) => {
96
+ const owned = row.scopes.split(',');
97
+ return owned.includes('admin') || req.some((s) => owned.includes(s));
98
+ },
99
+ });
100
+ users = { usr_alice: { id: 'usr_alice', role: 'MEMBER', status: 'ACTIVE' } };
101
+ findById = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = users[id]) !== null && _a !== void 0 ? _a : null; }));
102
+ typedi_1.Container.set(UserService_1.UserService, { findById });
103
+ sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
104
+ svc = new SessionLifecycleService_1.SessionLifecycleService();
105
+ }));
106
+ afterEach(() => {
107
+ config_1.config.authDisabled = authDisabledBefore;
108
+ if (gateBefore === undefined)
109
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
110
+ else
111
+ process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
112
+ sinon_1.default.restore();
113
+ restore();
114
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
115
+ });
116
+ describe('with XENON_REQUIRE_SESSION_TOKEN on', () => {
117
+ beforeEach(() => gate(true));
118
+ it("refuses an Inactive user's access key and token", () => __awaiter(void 0, void 0, void 0, function* () {
119
+ alice().status = 'INACTIVE';
120
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
121
+ }));
122
+ it("refuses a deleted user's key, whose row outlived them", () => __awaiter(void 0, void 0, void 0, function* () {
123
+ users.usr_alice = undefined;
124
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
125
+ }));
126
+ it("refuses an Inactive user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
127
+ alice().status = 'INACTIVE';
128
+ const caps = capsWith({ sessionToken: yield token() });
129
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
130
+ }));
131
+ it("refuses a deleted user's session token, which is still within its lifetime", () => __awaiter(void 0, void 0, void 0, function* () {
132
+ users.usr_alice = undefined;
133
+ const caps = capsWith({ sessionToken: yield token() });
134
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
135
+ }));
136
+ it("still admits an Active user's key and token, and their session token", () => __awaiter(void 0, void 0, void 0, function* () {
137
+ const byPair = yield invoke(svc, pair());
138
+ (0, chai_1.expect)(byPair).to.include({ apiKeyId: 'key_alice', userId: 'usr_alice' });
139
+ const byToken = yield invoke(svc, capsWith({ sessionToken: yield token() }));
140
+ (0, chai_1.expect)(byToken).to.include({ apiKeyId: null, userId: 'usr_alice' });
141
+ }));
142
+ it('looks the owner up once, though the gate and attribution both verify the token', () => __awaiter(void 0, void 0, void 0, function* () {
143
+ yield invoke(svc, capsWith({ sessionToken: yield token() }));
144
+ (0, chai_1.expect)(findById.callCount).to.equal(1);
145
+ }));
146
+ });
147
+ describe('with XENON_REQUIRE_SESSION_TOKEN off', () => {
148
+ beforeEach(() => gate(false));
149
+ it("gives no owner to a session made with an Inactive user's key", () => __awaiter(void 0, void 0, void 0, function* () {
150
+ alice().status = 'INACTIVE';
151
+ const res = yield invoke(svc, pair());
152
+ (0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
153
+ }));
154
+ it("gives no owner to a session made with a deleted user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
155
+ users.usr_alice = undefined;
156
+ const res = yield invoke(svc, capsWith({ sessionToken: yield token() }));
157
+ (0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
158
+ }));
159
+ it('looks an Active owner up once for a key and token', () => __awaiter(void 0, void 0, void 0, function* () {
160
+ const res = yield invoke(svc, pair());
161
+ yield res.leaseAccess();
162
+ (0, chai_1.expect)(res.userId).to.equal('usr_alice');
163
+ (0, chai_1.expect)(findById.callCount).to.equal(1);
164
+ }));
165
+ });
166
+ describe('a session token needs the `sessions` scope', () => {
167
+ for (const on of [false, true]) {
168
+ describe(`(gate ${on ? 'on' : 'off'})`, () => {
169
+ beforeEach(() => gate(on));
170
+ it('refuses one minted for a read-only credential', () => __awaiter(void 0, void 0, void 0, function* () {
171
+ const caps = capsWith({
172
+ sessionToken: yield token({ sub: 'usr_alice', scopes: 'devices' }),
173
+ });
174
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/sessions` scope/);
175
+ }));
176
+ it('refuses one minted before session tokens carried their scopes, saying so', () => __awaiter(void 0, void 0, void 0, function* () {
177
+ const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice' }) });
178
+ const refusal = yield refusalOf(invoke(svc, caps));
179
+ (0, chai_1.expect)(refusal).to.match(/carries no `sessions` scope/);
180
+ (0, chai_1.expect)(refusal).to.match(/minted by Xenon 2\.14 or earlier carry none/);
181
+ (0, chai_1.expect)(refusal).to.match(/mint a new one/);
182
+ }));
183
+ it('admits one with `sessions` or `admin`', () => __awaiter(void 0, void 0, void 0, function* () {
184
+ for (const scopes of ['sessions', 'admin', 'devices,sessions']) {
185
+ const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
186
+ (0, chai_1.expect)((yield invoke(svc, caps)).userId).to.equal('usr_alice');
187
+ }
188
+ }));
189
+ });
190
+ }
191
+ });
192
+ // Taking over a lease someone else created is an ownership override: a
193
+ // SUPER_ADMIN, or a credential with the `admin` scope. A session token was
194
+ // judged by its user's role alone, so an ADMIN's key without the admin
195
+ // scope, which may not override, minted a session token that could.
196
+ describe('a session token overrides a lease only as the credential that minted it could', () => {
197
+ beforeEach(() => gate(false));
198
+ const overrideWith = (role, scopes) => __awaiter(void 0, void 0, void 0, function* () {
199
+ alice().role = role;
200
+ const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
201
+ return (yield (yield invoke(svc, caps)).leaseAccess()).canOverride;
202
+ });
203
+ it("is refused for an ADMIN's token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
204
+ (0, chai_1.expect)(yield overrideWith('ADMIN', 'sessions')).to.equal(false);
205
+ }));
206
+ it("is allowed for an ADMIN's or SUPER_ADMIN's token minted with the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
207
+ (0, chai_1.expect)(yield overrideWith('ADMIN', 'admin,sessions')).to.equal(true);
208
+ (0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'admin,sessions')).to.equal(true);
209
+ }));
210
+ // As the SUPER_ADMIN's key pair and Bearer token override.
211
+ it("is allowed for a SUPER_ADMIN's token minted without it", () => __awaiter(void 0, void 0, void 0, function* () {
212
+ (0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'sessions')).to.equal(true);
213
+ }));
214
+ // The scope is fixed when the token is minted; the role is read now. An
215
+ // admin demoted after minting overrides no more.
216
+ it('is refused once the admin who minted it is a member', () => __awaiter(void 0, void 0, void 0, function* () {
217
+ (0, chai_1.expect)(yield overrideWith('MEMBER', 'admin,sessions')).to.equal(false);
218
+ }));
219
+ it("is refused for a member's token", () => __awaiter(void 0, void 0, void 0, function* () {
220
+ (0, chai_1.expect)(yield overrideWith('MEMBER', 'sessions')).to.equal(false);
221
+ }));
222
+ });
223
+ // The owner lookup that fails is not a token that doesn't check out: the
224
+ // create is refused, as for a key whose owner can't be looked up. It ran
225
+ // with no owner (gate off) or was refused as "invalid or expired" (gate on).
226
+ describe('a session token whose owner cannot be looked up', () => {
227
+ for (const on of [false, true]) {
228
+ it(`refuses the create (gate ${on ? 'on' : 'off'})`, () => __awaiter(void 0, void 0, void 0, function* () {
229
+ gate(on);
230
+ findById.rejects(new Error('db down'));
231
+ const caps = capsWith({ sessionToken: yield token() });
232
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.equal('db down');
233
+ }));
234
+ }
235
+ it('still counts a token that does not check out as none (gate off)', () => __awaiter(void 0, void 0, void 0, function* () {
236
+ gate(false);
237
+ const res = yield invoke(svc, capsWith({ sessionToken: 'not-a-jwt' }));
238
+ (0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
239
+ (0, chai_1.expect)(findById.called).to.equal(false);
240
+ }));
241
+ });
242
+ });
@@ -146,7 +146,7 @@ describe('createSession — credentials never reach the driver or storage', () =
146
146
  verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
147
147
  if (!t.startsWith('jwt:'))
148
148
  throw new Error('bad token');
149
- return { sub: t.slice('jwt:'.length), teamId: null };
149
+ return { sub: t.slice('jwt:'.length), teamId: null, scopes: 'sessions' };
150
150
  }),
151
151
  });
152
152
  // The owner's teams: none, and not read from the developer's database.