@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
@@ -70,16 +70,14 @@ describe('OmniVisionService Unit Tests', () => {
70
70
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.id).to.contain('omni_ai_');
71
71
  (0, chai_1.expect)(result === null || result === void 0 ? void 0 : result.rect).to.deep.equal({ x: 30, y: 30, width: 40, height: 40 });
72
72
  }));
73
- it('getVirtualElement should retrieve stored elements', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ it('getVirtualElement gives back an element kept for its session, to that session only', () => __awaiter(void 0, void 0, void 0, function* () {
74
74
  sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').resolves({ x: 50, y: 50 });
75
75
  const created = yield omniService.findByIcon(mockDriver, 'back');
76
- if (created) {
77
- const retrieved = omniService.getVirtualElement(created.id);
78
- (0, chai_1.expect)(retrieved).to.deep.equal(created);
79
- }
80
- else {
76
+ if (!created)
81
77
  throw new Error('Failed to create mock virtual element');
82
- }
78
+ omniService.remember('session-1', created);
79
+ (0, chai_1.expect)(omniService.getVirtualElement(created.id, 'session-1')).to.deep.equal(created);
80
+ (0, chai_1.expect)(omniService.getVirtualElement(created.id, 'session-2')).to.equal(undefined);
83
81
  }));
84
82
  it('analyzeScreen should combine OCR and AI insights', () => __awaiter(void 0, void 0, void 0, function* () {
85
83
  const mockWorker = {
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ // scripts/ is plain CommonJS that the export script also loads, so it is
5
+ // required rather than imported.
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ const site = require('../../scripts/lib/openapi-for-site');
8
+ const { forSite, VERSION_TOKEN, assertNoRawDocumentLink } = site;
9
+ /**
10
+ * The documentation site publishes the server's OpenAPI document. The spec
11
+ * names the version it was built from in `info`, so a committed copy would
12
+ * change on every release. forSite swaps that version for a token the site's
13
+ * build fills in, and points the "raw document" link at the user's own server,
14
+ * since the link would otherwise lead to the docs host.
15
+ */
16
+ describe('openapi for the documentation site', () => {
17
+ const version = '2.13.1';
18
+ const makeSpec = () => ({
19
+ info: {
20
+ version,
21
+ description: `This reference describes version **${version}**. The raw OpenAPI document is at [\`/xenon/api-docs.json\`](/xenon/api-docs.json).`,
22
+ },
23
+ paths: { '/api/devices': {} },
24
+ });
25
+ it('puts the version token where the version was', () => {
26
+ const result = forSite(makeSpec(), version);
27
+ (0, chai_1.expect)(result.info.version).to.equal('__XENON_VERSION__');
28
+ (0, chai_1.expect)(VERSION_TOKEN).to.equal('__XENON_VERSION__');
29
+ (0, chai_1.expect)(result.info.description).to.contain('version **__XENON_VERSION__**');
30
+ });
31
+ it('leaves no trace of the version anywhere in the result', () => {
32
+ (0, chai_1.expect)(JSON.stringify(forSite(makeSpec(), version))).to.not.contain(version);
33
+ });
34
+ it('points the raw-document line at the reader’s own server, with no link', () => {
35
+ const { description } = forSite(makeSpec(), version).info;
36
+ (0, chai_1.expect)(description).to.contain('`/xenon/api-docs.json` on your server');
37
+ (0, chai_1.expect)(description).to.not.contain('](/xenon/api-docs.json)');
38
+ });
39
+ it('keeps the paths exactly as they were', () => {
40
+ (0, chai_1.expect)(forSite(makeSpec(), version).paths).to.deep.equal(makeSpec().paths);
41
+ });
42
+ it('does not change the object it is given', () => {
43
+ const spec = makeSpec();
44
+ forSite(spec, version);
45
+ (0, chai_1.expect)(spec).to.deep.equal(makeSpec());
46
+ });
47
+ it('refuses an empty or missing version', () => {
48
+ (0, chai_1.expect)(() => forSite(makeSpec(), '')).to.throw(/version/);
49
+ (0, chai_1.expect)(() => forSite(makeSpec(), undefined)).to.throw(/version/);
50
+ (0, chai_1.expect)(() => forSite(makeSpec(), 2.13)).to.throw(/version/);
51
+ });
52
+ it('only touches the version inside info', () => {
53
+ const spec = makeSpec();
54
+ spec.paths['/api/version'] = { get: { example: { pluginVersion: version } } };
55
+ const result = forSite(spec, version);
56
+ (0, chai_1.expect)(result.paths['/api/version'].get.example.pluginVersion).to.equal(version);
57
+ });
58
+ describe('the guard against a link to the raw document', () => {
59
+ const guarded = (description) => {
60
+ const spec = makeSpec();
61
+ spec.info.description = description;
62
+ return JSON.stringify(forSite(spec, version), null, 2);
63
+ };
64
+ it('passes the introduction as the server writes it, once forSite has rewritten the link', () => {
65
+ (0, chai_1.expect)(() => assertNoRawDocumentLink(guarded(makeSpec().info.description))).to.not.throw();
66
+ });
67
+ it('catches the link when the introduction is reworded', () => {
68
+ const text = guarded('Download the [raw document](/xenon/api-docs.json).');
69
+ (0, chai_1.expect)(() => assertNoRawDocumentLink(text)).to.throw(/still links to \/xenon\/api-docs\.json[\s\S]*src\/app\/swagger\.ts/);
70
+ });
71
+ it('catches a link that has a space, an angle bracket or a fragment in its target', () => {
72
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('a [b]( /xenon/api-docs.json)')).to.throw();
73
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('a [b](</xenon/api-docs.json>)')).to.throw();
74
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('a [b](/xenon/api-docs.json#info "title")')).to.throw();
75
+ });
76
+ it('lets plain code and other links through', () => {
77
+ (0, chai_1.expect)(() => assertNoRawDocumentLink('at `/xenon/api-docs.json` on your server, see [the profile](/xenon/profile)')).to.not.throw();
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,320 @@
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 http_1 = __importDefault(require("http"));
18
+ const axios_1 = __importDefault(require("axios"));
19
+ const typedi_1 = require("typedi");
20
+ const ws_1 = require("ws");
21
+ const forwardToNode_1 = require("../../src/gateway/forwardToNode");
22
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
23
+ const nodeSocketRelay_1 = require("../../src/app/ws/nodeSocketRelay");
24
+ const PluginContext_1 = require("../../src/PluginContext");
25
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
26
+ const outboundProxy_1 = require("../../src/helpers/outboundProxy");
27
+ const SocketClient_1 = require("../../src/services/SocketClient");
28
+ const socket_io_1 = require("socket.io");
29
+ const fake_http_proxy_1 = require("../helpers/fake-http-proxy");
30
+ const PROXY_ENV = [
31
+ 'HTTP_PROXY',
32
+ 'http_proxy',
33
+ 'HTTPS_PROXY',
34
+ 'https_proxy',
35
+ 'NO_PROXY',
36
+ 'no_proxy',
37
+ ];
38
+ /**
39
+ * Xenon's axios calls take a proxy from the environment by axios's rule: the
40
+ * URL's own scheme's variable, lower-case name first, unless NO_PROXY names
41
+ * the host. Its other calls to a node didn't:
42
+ * - a forwarded command, device control action, recording relay and socket
43
+ * ticket took HTTP_PROXY or HTTPS_PROXY whatever the scheme, ignored the
44
+ * lower-case names and ignored NO_PROXY;
45
+ * - the live-preview and logcat sockets ignored every proxy, after asking for
46
+ * their ticket through one;
47
+ * - a node fetching its hub's keys (JWKS) ignored every proxy.
48
+ * They all follow axios's rule now.
49
+ */
50
+ describe('proxies for the calls Xenon makes to another server', function () {
51
+ this.timeout(20000);
52
+ let saved;
53
+ let proxy;
54
+ const servers = [];
55
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
56
+ saved = Object.fromEntries(PROXY_ENV.map((k) => [k, process.env[k]]));
57
+ for (const k of PROXY_ENV)
58
+ delete process.env[k];
59
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)();
60
+ }));
61
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
62
+ for (const k of PROXY_ENV) {
63
+ if (saved[k] === undefined)
64
+ delete process.env[k];
65
+ else
66
+ process.env[k] = saved[k];
67
+ }
68
+ yield proxy.close();
69
+ yield Promise.all(servers.splice(0).map((s) => new Promise((resolve) => {
70
+ var _a;
71
+ (_a = s.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(s);
72
+ s.close(() => resolve());
73
+ })));
74
+ }));
75
+ /** What `promise` settled with (its error, if it failed), or 'pending' after `ms`. */
76
+ function settlesWithin(promise, ms) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ let timer;
79
+ const pending = new Promise((resolve) => {
80
+ timer = setTimeout(() => resolve('pending'), ms);
81
+ });
82
+ try {
83
+ return yield Promise.race([
84
+ promise.then((v) => v, (e) => e),
85
+ pending,
86
+ ]);
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ });
92
+ }
93
+ /** A server on 127.0.0.1 that answers everything 200, and its origin. */
94
+ function target(handler) {
95
+ return __awaiter(this, void 0, void 0, function* () {
96
+ const server = http_1.default.createServer(handler !== null && handler !== void 0 ? handler : ((_req, res) => {
97
+ res.writeHead(200, { 'content-type': 'application/json' });
98
+ res.end('{}');
99
+ }));
100
+ servers.push(server);
101
+ yield new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
102
+ return `http://127.0.0.1:${server.address().port}`;
103
+ });
104
+ }
105
+ describe('the rule', () => {
106
+ const env = (vars) => vars;
107
+ it("takes the URL's own scheme's variable, the lower-case name first", () => {
108
+ const vars = env({ HTTP_PROXY: 'http://upper:1', http_proxy: 'http://lower:1' });
109
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('http://node:4723/x', vars)).to.equal('http://lower:1');
110
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('https://node/x', env({ HTTP_PROXY: 'http://p:1' }))).to.equal(undefined);
111
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('https://node/x', env({ HTTPS_PROXY: 'http://p:2' }))).to.equal('http://p:2');
112
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('http://node/x', env({ HTTPS_PROXY: 'http://p:2' }))).to.equal(undefined);
113
+ });
114
+ it("treats a WebSocket URL as its scheme's HTTP counterpart", () => {
115
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('ws://node/x', env({ HTTP_PROXY: 'http://p:1' }))).to.equal('http://p:1');
116
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('wss://node/x', env({ HTTPS_PROXY: 'http://p:2' }))).to.equal('http://p:2');
117
+ });
118
+ it('takes none for a host NO_PROXY names: *, the host, or a .suffix of it', () => {
119
+ const proxied = { HTTP_PROXY: 'http://p:1' };
120
+ const via = (noProxy, url = 'http://node.lab.example:4723/x') => (0, outboundProxy_1.envProxyFor)(url, env(Object.assign(Object.assign({}, proxied), { NO_PROXY: noProxy })));
121
+ (0, chai_1.expect)(via('*')).to.equal(undefined);
122
+ (0, chai_1.expect)(via('other, node.lab.example')).to.equal(undefined);
123
+ (0, chai_1.expect)(via('.lab.example')).to.equal(undefined);
124
+ (0, chai_1.expect)(via('lab.example')).to.equal('http://p:1');
125
+ (0, chai_1.expect)(via('node.lab.example:4723')).to.equal('http://p:1');
126
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('http://node/x', env(Object.assign(Object.assign({}, proxied), { no_proxy: 'node', NO_PROXY: 'x' })))).to.equal(undefined);
127
+ });
128
+ });
129
+ describe('a call to a node takes the path an axios call takes', () => {
130
+ const cases = [
131
+ { name: 'HTTP_PROXY', vars: (p) => ({ HTTP_PROXY: p }) },
132
+ { name: 'http_proxy', vars: (p) => ({ http_proxy: p }) },
133
+ {
134
+ name: 'HTTP_PROXY, NO_PROXY naming the node',
135
+ vars: (p) => ({ HTTP_PROXY: p, NO_PROXY: '127.0.0.1' }),
136
+ },
137
+ { name: 'HTTP_PROXY, no_proxy=*', vars: (p) => ({ HTTP_PROXY: p, no_proxy: '*' }) },
138
+ {
139
+ name: 'HTTP_PROXY, NO_PROXY listing the node second',
140
+ vars: (p) => ({ HTTP_PROXY: p, NO_PROXY: 'hub.lab, 127.0.0.1' }),
141
+ },
142
+ { name: 'only HTTPS_PROXY, for an http node', vars: (p) => ({ HTTPS_PROXY: p }) },
143
+ ];
144
+ for (const { name, vars } of cases) {
145
+ it(name, () => __awaiter(this, void 0, void 0, function* () {
146
+ Object.assign(process.env, vars(proxy.url));
147
+ const node = yield target();
148
+ yield axios_1.default.get(`${node}/by-axios`);
149
+ yield (0, forwardToNode_1.readAnswer)(yield (0, forwardToNode_1.sendToNode)({ url: `${node}/by-xenon`, method: 'GET', headers: {} }));
150
+ const axiosProxied = proxy.seen.includes(`GET ${node}/by-axios`);
151
+ const xenonProxied = proxy.seen.includes(`GET ${node}/by-xenon`);
152
+ (0, chai_1.expect)(xenonProxied, `forwarded call via the proxy (axios: ${axiosProxied})`).to.equal(axiosProxied);
153
+ }));
154
+ }
155
+ });
156
+ describe("a node fetching its hub's keys", () => {
157
+ // A JWS that parses, so jose asks the key set for its key.
158
+ const token = [
159
+ Buffer.from(JSON.stringify({ alg: 'RS256', kid: 'k1' })).toString('base64url'),
160
+ Buffer.from(JSON.stringify({ sid: 's' })).toString('base64url'),
161
+ 'c2lnbmF0dXJl',
162
+ ].join('.');
163
+ function fetchKeys(hub) {
164
+ return __awaiter(this, void 0, void 0, function* () {
165
+ yield new hubSessionToken_1.HubSessionTokenVerifier(hub).verify(token, 's').catch(() => undefined);
166
+ });
167
+ }
168
+ it('goes through the proxy', () => __awaiter(this, void 0, void 0, function* () {
169
+ const hub = yield target((_req, res) => {
170
+ res.writeHead(200, { 'content-type': 'application/json' });
171
+ res.end('{"keys":[]}');
172
+ });
173
+ process.env.HTTP_PROXY = proxy.url;
174
+ yield fetchKeys(hub);
175
+ (0, chai_1.expect)(proxy.seen).to.include(`GET ${hub}/xenon/api/auth/jwks.json`);
176
+ }));
177
+ it('gives up in its time when the proxy never answers', () => __awaiter(this, void 0, void 0, function* () {
178
+ // The fetch's own timeout starts once the request has a socket, which a
179
+ // proxy agent hands it only after its CONNECT is answered.
180
+ yield proxy.close();
181
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ ignoreConnect: true });
182
+ process.env.HTTPS_PROXY = proxy.url;
183
+ const outcome = yield settlesWithin(new hubSessionToken_1.HubSessionTokenVerifier('https://hub.lab.example:4723', undefined, {
184
+ timeoutMs: 300,
185
+ }).verify(token, 's'), 3000);
186
+ (0, chai_1.expect)(outcome).to.be.instanceOf(hubSessionToken_1.HubTokenUnavailableError);
187
+ }));
188
+ it('goes straight to a hub NO_PROXY names', () => __awaiter(this, void 0, void 0, function* () {
189
+ const asked = [];
190
+ const hub = yield target((req, res) => {
191
+ asked.push(String(req.url));
192
+ res.writeHead(200, { 'content-type': 'application/json' });
193
+ res.end('{"keys":[]}');
194
+ });
195
+ process.env.HTTP_PROXY = proxy.url;
196
+ process.env.NO_PROXY = '127.0.0.1';
197
+ yield fetchKeys(hub);
198
+ (0, chai_1.expect)(proxy.seen).to.deep.equal([]);
199
+ (0, chai_1.expect)(asked).to.include('/xenon/api/auth/jwks.json');
200
+ }));
201
+ });
202
+ describe("a node's live-events connection to its hub", () => {
203
+ let client;
204
+ let hubIo;
205
+ afterEach(() => {
206
+ var _a;
207
+ (_a = client === null || client === void 0 ? void 0 : client.socket) === null || _a === void 0 ? void 0 : _a.close();
208
+ client = undefined;
209
+ hubIo === null || hubIo === void 0 ? void 0 : hubIo.close();
210
+ hubIo = undefined;
211
+ });
212
+ function connect() {
213
+ return __awaiter(this, void 0, void 0, function* () {
214
+ const hub = yield target();
215
+ hubIo = new socket_io_1.Server(servers[servers.length - 1]);
216
+ client = new SocketClient_1.SocketClient();
217
+ client.initialize(hub, 'http://node.lab:4723');
218
+ const deadline = Date.now() + 10000;
219
+ while (!client.isConnected()) {
220
+ if (Date.now() > deadline)
221
+ throw new Error('the node never connected to its hub');
222
+ yield new Promise((r) => setTimeout(r, 25));
223
+ }
224
+ return hub;
225
+ });
226
+ }
227
+ it('goes through the proxy: polling as plain requests, as axios sends them', () => __awaiter(this, void 0, void 0, function* () {
228
+ process.env.HTTP_PROXY = proxy.url;
229
+ const hub = yield connect();
230
+ (0, chai_1.expect)(proxy.seen.some((s) => s.startsWith(`GET ${hub}/socket.io/?EIO=4&transport=polling`)), proxy.seen.join('\n')).to.equal(true);
231
+ }));
232
+ it('stays connected through a proxy that refuses tunnels, by polling', () => __awaiter(this, void 0, void 0, function* () {
233
+ // A stock Squid refuses CONNECT to any port but 443, and a node's hub
234
+ // is on 4723: the WebSocket upgrade fails, polling carries on.
235
+ yield proxy.close();
236
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ refuseConnect: true });
237
+ process.env.HTTP_PROXY = proxy.url;
238
+ yield connect();
239
+ (0, chai_1.expect)(client === null || client === void 0 ? void 0 : client.isConnected()).to.equal(true);
240
+ }));
241
+ it('goes straight to a hub NO_PROXY names', () => __awaiter(this, void 0, void 0, function* () {
242
+ process.env.HTTP_PROXY = proxy.url;
243
+ process.env.NO_PROXY = '127.0.0.1';
244
+ yield connect();
245
+ (0, chai_1.expect)(proxy.seen).to.deep.equal([]);
246
+ }));
247
+ });
248
+ describe("the live-preview and logcat sockets to a node's phone", () => {
249
+ let context;
250
+ let savedContext;
251
+ const sockets = [];
252
+ beforeEach(() => {
253
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
254
+ savedContext = Object.assign({}, context);
255
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), 1, 'hub-1', '');
256
+ });
257
+ afterEach(() => {
258
+ Object.assign(context, savedContext);
259
+ for (const wss of sockets.splice(0)) {
260
+ for (const client of wss.clients)
261
+ client.terminate();
262
+ wss.close();
263
+ }
264
+ });
265
+ function nodeWithSocket() {
266
+ return __awaiter(this, void 0, void 0, function* () {
267
+ const origin = yield target((req, res) => {
268
+ res.writeHead(200, { 'content-type': 'application/json' });
269
+ res.end(JSON.stringify({ ticket: 'node-ticket', expiresIn: 60 }));
270
+ });
271
+ const wss = new ws_1.WebSocketServer({ server: servers[servers.length - 1] });
272
+ sockets.push(wss);
273
+ return origin;
274
+ });
275
+ }
276
+ function open(origin) {
277
+ return __awaiter(this, void 0, void 0, function* () {
278
+ const socket = yield (0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor: { actorId: 'alice' }, path: 'logcat' }, {
279
+ findDevice: () => __awaiter(this, void 0, void 0, function* () { return ({ udid: 'p', host: origin, nodeId: 'node-1' }); }),
280
+ controlToken: () => __awaiter(this, void 0, void 0, function* () { return null; }),
281
+ });
282
+ socket.terminate();
283
+ });
284
+ }
285
+ it('take the proxy their ticket takes', () => __awaiter(this, void 0, void 0, function* () {
286
+ const node = yield nodeWithSocket();
287
+ process.env.HTTP_PROXY = proxy.url;
288
+ yield open(node);
289
+ (0, chai_1.expect)(proxy.seen).to.include(`POST ${node}/xenon/api/control/p/stream/ticket`);
290
+ (0, chai_1.expect)(proxy.seen).to.include(`CONNECT ${node.replace('http://', '')}`);
291
+ }));
292
+ it('go straight to the node when the proxy refuses a tunnel, as they did before', () => __awaiter(this, void 0, void 0, function* () {
293
+ yield proxy.close();
294
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ refuseConnect: true });
295
+ const node = yield nodeWithSocket();
296
+ process.env.HTTP_PROXY = proxy.url;
297
+ yield open(node);
298
+ (0, chai_1.expect)(proxy.seen).to.include(`CONNECT ${node.replace('http://', '')}`);
299
+ }));
300
+ it('give up in their time when the proxy never answers the tunnel', () => __awaiter(this, void 0, void 0, function* () {
301
+ yield proxy.close();
302
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ ignoreConnect: true });
303
+ const node = yield nodeWithSocket();
304
+ process.env.HTTP_PROXY = proxy.url;
305
+ const outcome = yield settlesWithin((0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor: { actorId: 'alice' }, path: 'logcat' }, {
306
+ findDevice: () => __awaiter(this, void 0, void 0, function* () { return ({ udid: 'p', host: node, nodeId: 'node-1' }); }),
307
+ controlToken: () => __awaiter(this, void 0, void 0, function* () { return null; }),
308
+ timeoutMs: 300,
309
+ }), 3000);
310
+ (0, chai_1.expect)(outcome).to.be.instanceOf(Error);
311
+ }));
312
+ it('go straight to a node NO_PROXY names', () => __awaiter(this, void 0, void 0, function* () {
313
+ const node = yield nodeWithSocket();
314
+ process.env.HTTP_PROXY = proxy.url;
315
+ process.env.NO_PROXY = '127.0.0.1';
316
+ yield open(node);
317
+ (0, chai_1.expect)(proxy.seen).to.deep.equal([]);
318
+ }));
319
+ });
320
+ });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chai_1 = require("chai");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const root = path_1.default.join(__dirname, '..', '..');
10
+ const readJson = (file) => JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
11
+ const pkg = readJson(path_1.default.join(root, 'package.json'));
12
+ const lock = readJson(path_1.default.join(root, 'package-lock.json'));
13
+ const appium = readJson(require.resolve('appium/package.json'));
14
+ /**
15
+ * The plugin runs inside Appium, so it supports the Node versions Appium 3
16
+ * does and no others. `engines` used to say `^14.17.0 || ^16.13.0 || >=18.0.0`
17
+ * while every Appium 3 release needs 20.19, 22.12 or 24 and later.
18
+ */
19
+ describe('package.json engines', () => {
20
+ it("are the installed Appium 3's, the Appium the plugin is built and tested against", () => {
21
+ (0, chai_1.expect)(appium.version).to.match(/^3\./);
22
+ (0, chai_1.expect)(pkg.engines).to.deep.equal({ node: appium.engines.node, npm: appium.engines.npm });
23
+ });
24
+ it('match the copy in package-lock.json', () => {
25
+ (0, chai_1.expect)(lock.packages[''].engines).to.deep.equal(pkg.engines);
26
+ });
27
+ });
@@ -3,8 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const chai_1 = require("chai");
4
4
  const passwordResetMode_1 = require("../../src/services/passwordResetMode");
5
5
  describe('passwordResetMode', () => {
6
- it('is email when an SMTP URL is configured', () => {
7
- (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://mail.example.com:587' })).to.equal('email');
6
+ it('is email when an SMTP URL and XENON_PUBLIC_URL are configured', () => {
7
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({
8
+ smtpUrl: 'smtp://mail.example.com:587',
9
+ publicUrl: 'https://xenon.example.com',
10
+ })).to.equal('email');
11
+ });
12
+ // The emailed link points at XENON_PUBLIC_URL, never at the request's Host:
13
+ // without it Xenon emails no link, so the form must not promise one.
14
+ it('is admin with SMTP but no XENON_PUBLIC_URL', () => {
15
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://mail.example.com:587' })).to.equal('admin');
8
16
  });
9
17
  it('is admin without SMTP, even though the log fallback defaults on', () => {
10
18
  // The fallback writes the link to the server log, which the person who
@@ -196,8 +196,9 @@ describe('iOSCapabilities port acquisition', () => {
196
196
  sinon_1.default
197
197
  .stub(typedi_1.Container.get(IOSStreamService), 'getStreamStatus')
198
198
  .returns({ status: 'running', wdaPort: 8100 });
199
+ // An iPhone: a simulator's WDA is the driver's, never the stream's.
199
200
  const caps = { alwaysMatch: {}, firstMatch: [{}] };
200
- yield (0, XenonCapabilityManager_1.iOSCapabilities)(caps, device);
201
+ yield (0, XenonCapabilityManager_1.iOSCapabilities)(caps, Object.assign(Object.assign({}, device), { realDevice: true }));
201
202
  (0, chai_1.expect)(caps.firstMatch[0]['appium:webDriverAgentUrl']).to.equal('http://127.0.0.1:8100');
202
203
  (0, chai_1.expect)(caps.firstMatch[0]['appium:wdaLocalPort']).to.equal(undefined);
203
204
  (0, chai_1.expect)(acquireStub.called).to.equal(false);
@@ -39,7 +39,12 @@ describe('profile router', () => {
39
39
  }));
40
40
  it('POST /profile/access-key/rotate returns a new accessKey', () => __awaiter(void 0, void 0, void 0, function* () {
41
41
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey').resolves({ id: 'u1', accessKey: 'xen_NEW' });
42
- const app = appWithAuth({ userId: 'u1', role: 'ADMIN', scopes: 'devices,sessions,read' });
42
+ const app = appWithAuth({
43
+ kind: 'user-session',
44
+ userId: 'u1',
45
+ role: 'ADMIN',
46
+ scopes: 'admin,devices,sessions,read',
47
+ });
43
48
  const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
44
49
  (0, chai_1.expect)(r.body.accessKey).to.equal('xen_NEW');
45
50
  }));
@@ -147,4 +152,120 @@ describe('profile router', () => {
147
152
  const r = yield (0, loopbackRequest_1.default)(app).delete('/profile/tokens/k1');
148
153
  (0, chai_1.expect)(r.status).to.equal(404);
149
154
  }));
155
+ // A credential can't do more to its owner's account than it could itself
156
+ // have been made to do.
157
+ describe('what the credential in use limits', () => {
158
+ // Rotating the access key ends every key pair its owner has, whatever
159
+ // their scopes, so it takes the person themselves (a dashboard sign-in)
160
+ // or a credential with the admin scope. Any credential, a read-only
161
+ // token included, could do it.
162
+ describe('POST /profile/access-key/rotate', () => {
163
+ let rotate;
164
+ beforeEach(() => {
165
+ rotate = sinon_1.default
166
+ .stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey')
167
+ .resolves({ id: 'u1', accessKey: 'xen_NEW' });
168
+ });
169
+ for (const [kind, scopes] of [
170
+ ['bearer', 'read'],
171
+ ['api-key', 'read'],
172
+ ['api-key', 'devices,sessions,read'],
173
+ ['bearer', 'sessions'],
174
+ ]) {
175
+ it(`is refused to ${kind === 'bearer' ? 'a Bearer token' : 'an API key'} with ${scopes}`, () => __awaiter(void 0, void 0, void 0, function* () {
176
+ const app = appWithAuth({ kind, userId: 'u1', role: 'ADMIN', scopes });
177
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
178
+ (0, chai_1.expect)(r.status).to.equal(403);
179
+ (0, chai_1.expect)(r.body.error).to.match(/dashboard sign-in or a credential with the admin scope/);
180
+ (0, chai_1.expect)(rotate.called).to.equal(false);
181
+ }));
182
+ }
183
+ it('is allowed to a dashboard sign-in, a member included', () => __awaiter(void 0, void 0, void 0, function* () {
184
+ const app = appWithAuth({
185
+ kind: 'user-session',
186
+ userId: 'u1',
187
+ role: 'MEMBER',
188
+ scopes: 'devices,sessions,read',
189
+ });
190
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
191
+ }));
192
+ it('is allowed to a credential with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
193
+ const app = appWithAuth({
194
+ kind: 'api-key',
195
+ userId: 'u1',
196
+ role: 'SUPER_ADMIN',
197
+ scopes: 'admin',
198
+ });
199
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
200
+ }));
201
+ });
202
+ // A token made with a credential that expires ends no later than it: a
203
+ // 1-hour Bearer token made itself a key that never expired.
204
+ describe('POST /profile/tokens from a credential that expires', () => {
205
+ const inAnHour = new Date(Date.now() + 3600000);
206
+ const bearer = () => appWithAuth({
207
+ kind: 'bearer',
208
+ userId: 'u1',
209
+ role: 'MEMBER',
210
+ scopes: 'sessions,read',
211
+ credentialExpiresAt: inAnHour.getTime(),
212
+ });
213
+ let create;
214
+ beforeEach(() => {
215
+ create = sinon_1.default
216
+ .stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
217
+ .resolves({ id: 'k9', raw: 'raw' });
218
+ });
219
+ it('expires with that credential when no expiresAt is asked for', () => __awaiter(void 0, void 0, void 0, function* () {
220
+ var _a;
221
+ const r = yield (0, loopbackRequest_1.default)(bearer()).post('/profile/tokens').send({ name: 'CI' });
222
+ (0, chai_1.expect)(r.status).to.equal(201);
223
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(inAnHour.toISOString());
224
+ (0, chai_1.expect)((_a = create.firstCall.args[0].expiresAt) === null || _a === void 0 ? void 0 : _a.getTime()).to.equal(inAnHour.getTime());
225
+ }));
226
+ it('refuses an expiresAt later than that credential', () => __awaiter(void 0, void 0, void 0, function* () {
227
+ const later = new Date(inAnHour.getTime() + 60000).toISOString();
228
+ const r = yield (0, loopbackRequest_1.default)(bearer())
229
+ .post('/profile/tokens')
230
+ .send({ name: 'CI', expiresAt: later });
231
+ (0, chai_1.expect)(r.status).to.equal(400);
232
+ (0, chai_1.expect)(r.body.error).to.match(/later than the credential/);
233
+ (0, chai_1.expect)(create.called).to.equal(false);
234
+ }));
235
+ // Verification allows 60 s of clock skew, so a token can still be
236
+ // accepted just after its exp: it must not make a key already expired.
237
+ it('refuses when that credential has already expired', () => __awaiter(void 0, void 0, void 0, function* () {
238
+ const app = appWithAuth({
239
+ kind: 'bearer',
240
+ userId: 'u1',
241
+ role: 'MEMBER',
242
+ scopes: 'sessions,read',
243
+ credentialExpiresAt: Date.now() - 1000,
244
+ });
245
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
246
+ (0, chai_1.expect)(r.status).to.equal(400);
247
+ (0, chai_1.expect)(r.body.error).to.match(/has expired/);
248
+ (0, chai_1.expect)(create.called).to.equal(false);
249
+ }));
250
+ it('keeps an earlier expiresAt', () => __awaiter(void 0, void 0, void 0, function* () {
251
+ const sooner = new Date(Date.now() + 600000).toISOString();
252
+ const r = yield (0, loopbackRequest_1.default)(bearer())
253
+ .post('/profile/tokens')
254
+ .send({ name: 'CI', expiresAt: sooner });
255
+ (0, chai_1.expect)(r.status).to.equal(201);
256
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(sooner);
257
+ }));
258
+ it('leaves a dashboard sign-in free to make a key that never expires', () => __awaiter(void 0, void 0, void 0, function* () {
259
+ const app = appWithAuth({
260
+ kind: 'user-session',
261
+ userId: 'u1',
262
+ role: 'MEMBER',
263
+ scopes: 'devices,sessions,read',
264
+ });
265
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
266
+ (0, chai_1.expect)(r.status).to.equal(201);
267
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(null);
268
+ }));
269
+ });
270
+ });
150
271
  });