@xenon-device-management/xenon 2.14.0 → 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 (259) hide show
  1. package/README.md +7 -4
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.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-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
  40. package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +5 -5
  77. package/lib/src/app/dashboardPluginLink.js +117 -0
  78. package/lib/src/app/index.js +13 -34
  79. package/lib/src/app/openapi/control.yaml +6 -4
  80. package/lib/src/app/openapi/grid.yaml +8 -2
  81. package/lib/src/app/openapi/identity.yaml +103 -31
  82. package/lib/src/app/openapi/platform.yaml +33 -15
  83. package/lib/src/app/openapi/sessions.yaml +10 -3
  84. package/lib/src/app/routers/apikeys.js +14 -2
  85. package/lib/src/app/routers/auth.js +29 -5
  86. package/lib/src/app/routers/control.js +13 -5
  87. package/lib/src/app/routers/dashboard.js +42 -27
  88. package/lib/src/app/routers/grid.js +5 -3
  89. package/lib/src/app/routers/profile.js +21 -0
  90. package/lib/src/app/routers/users.js +10 -5
  91. package/lib/src/app/swagger.js +2 -0
  92. package/lib/src/app/ws/h264StreamWs.js +10 -1
  93. package/lib/src/config.js +1 -0
  94. package/lib/src/dashboard/commands.js +5 -5
  95. package/lib/src/dashboard/event-manager.js +49 -61
  96. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  97. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  98. package/lib/src/data-service/device-store.js +14 -1
  99. package/lib/src/data-service/prisma-store.js +19 -7
  100. package/lib/src/data-service/web-config-service.js +7 -0
  101. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  102. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  103. package/lib/src/device-utils.js +20 -2
  104. package/lib/src/gateway/defaultGateway.js +1 -1
  105. package/lib/src/gateway/forwardToNode.js +2 -0
  106. package/lib/src/gateway/healReport.js +87 -0
  107. package/lib/src/gateway/sessionGateway.js +12 -2
  108. package/lib/src/generated/client/edge.js +4 -3
  109. package/lib/src/generated/client/index-browser.js +1 -0
  110. package/lib/src/generated/client/index.d.ts +32 -0
  111. package/lib/src/generated/client/index.js +4 -3
  112. package/lib/src/generated/client/package.json +1 -1
  113. package/lib/src/generated/client/schema.prisma +3 -0
  114. package/lib/src/generated/client/wasm.js +1 -0
  115. package/lib/src/interceptors/CommandInterceptor.js +243 -181
  116. package/lib/src/middleware/authMiddleware.js +15 -3
  117. package/lib/src/middleware/commandCaller.js +3 -3
  118. package/lib/src/middleware/verifyCredential.js +58 -1
  119. package/lib/src/plugin.js +8 -3
  120. package/lib/src/services/AICommandService.js +9 -2
  121. package/lib/src/services/AIService.js +192 -42
  122. package/lib/src/services/EmailService.js +29 -0
  123. package/lib/src/services/OrphanSweeper.js +2 -0
  124. package/lib/src/services/SelectorVerificationJob.js +15 -4
  125. package/lib/src/services/ServerManager.js +11 -1
  126. package/lib/src/services/SessionLifecycleService.js +86 -26
  127. package/lib/src/services/SocketServer.js +19 -3
  128. package/lib/src/services/bug-report/video-slice.js +4 -1
  129. package/lib/src/services/device-access/leaseOverride.js +7 -5
  130. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  131. package/lib/src/services/healing/HealEtalonService.js +3 -2
  132. package/lib/src/services/healing/HealingOrchestrator.js +55 -72
  133. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  134. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  135. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  136. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  137. package/lib/src/services/healing/resilioPath.js +274 -0
  138. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  139. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  140. package/lib/src/services/ocr/ocrData.js +118 -0
  141. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  142. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  143. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  144. package/lib/src/services/passwordResetLink.js +50 -9
  145. package/lib/src/services/passwordResetMode.js +3 -1
  146. package/lib/src/services/publicUrl.js +45 -0
  147. package/lib/src/services/session/sessionIdentity.js +5 -3
  148. package/lib/src/services/session/shutdownReason.js +26 -0
  149. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  150. package/lib/src/services/token/mintedLifetime.js +51 -0
  151. package/lib/src/sessions/SessionManager.js +8 -2
  152. package/lib/src/sessions/sessionMemory.js +25 -0
  153. package/lib/test/helpers/fake-ai-provider.js +188 -0
  154. package/lib/test/helpers/loki-stores.js +33 -0
  155. package/lib/test/helpers/page-sources.js +164 -0
  156. package/lib/test/helpers/scratch-database.js +57 -13
  157. package/lib/test/integration/audit-ingest.spec.js +10 -10
  158. package/lib/test/integration/auth-flow.spec.js +9 -14
  159. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  160. package/lib/test/integration/bug-report-route.spec.js +22 -2
  161. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  162. package/lib/test/integration/forgot-password.spec.js +17 -18
  163. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  164. package/lib/test/integration/migration.spec.js +6 -1
  165. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  166. package/lib/test/integration/ports-router.spec.js +4 -4
  167. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  168. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  169. package/lib/test/integration/profile-tokens.spec.js +10 -16
  170. package/lib/test/integration/recordings-router.spec.js +16 -16
  171. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  172. package/lib/test/integration/role-matrix.spec.js +4 -9
  173. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  174. package/lib/test/integration/sdk-version.spec.js +2 -2
  175. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  176. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  177. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  178. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  179. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  180. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  181. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  182. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  183. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  184. package/lib/test/integration/users-flow.spec.js +7 -12
  185. package/lib/test/integration/users-lockout.spec.js +5 -3
  186. package/lib/test/unit/EmailService.test.js +66 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  188. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  189. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  190. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  191. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  192. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  193. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  194. package/lib/test/unit/apikeys-router.spec.js +84 -0
  195. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  196. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  197. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  198. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  199. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  200. package/lib/test/unit/device-log-book.spec.js +180 -0
  201. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  202. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  203. package/lib/test/unit/failure-analysis.spec.js +182 -0
  204. package/lib/test/unit/failure-categories.spec.js +128 -0
  205. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  206. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  207. package/lib/test/unit/heal-report.spec.js +71 -0
  208. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  209. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  210. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  211. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
  212. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  213. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  214. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  215. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  216. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  217. package/lib/test/unit/ocr-offline.spec.js +213 -0
  218. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  219. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  220. package/lib/test/unit/omni-vision.spec.js +5 -7
  221. package/lib/test/unit/package-engines.spec.js +27 -0
  222. package/lib/test/unit/passwordResetMode.test.js +10 -2
  223. package/lib/test/unit/profile-router.test.js +122 -1
  224. package/lib/test/unit/queue-requester.spec.js +1 -1
  225. package/lib/test/unit/reset-link-address.spec.js +266 -0
  226. package/lib/test/unit/reset-link.test.js +10 -4
  227. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  228. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  229. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  230. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  231. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  232. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  233. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  234. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  235. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  236. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  237. package/lib/test/unit/session-device-logs.spec.js +347 -0
  238. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  239. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  240. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  241. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  242. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  243. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  244. package/lib/test/unit/verify-credential.spec.js +33 -0
  245. package/lib/test/unit/vision-assertion.spec.js +3 -2
  246. package/lib/test/unit/web-config-service.spec.js +19 -0
  247. package/lib/tsconfig.tsbuildinfo +1 -1
  248. package/package.json +5 -5
  249. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  250. package/prisma/schema.prisma +3 -0
  251. package/schema.json +5 -5
  252. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  253. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  254. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  255. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  256. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  257. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  258. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  259. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -1,43 +1,10 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
2
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
23
7
  };
24
- var __importStar = (this && this.__importStar) || (function () {
25
- var ownKeys = function(o) {
26
- ownKeys = Object.getOwnPropertyNames || function (o) {
27
- var ar = [];
28
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
- return ar;
30
- };
31
- return ownKeys(o);
32
- };
33
- return function (mod) {
34
- if (mod && mod.__esModule) return mod;
35
- var result = {};
36
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
- __setModuleDefault(result, mod);
38
- return result;
39
- };
40
- })();
41
8
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
9
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
10
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -52,22 +19,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
52
19
  };
53
20
  var OmniVisionService_1;
54
21
  Object.defineProperty(exports, "__esModule", { value: true });
55
- exports.OmniVisionService = void 0;
22
+ exports.OmniVisionService = exports.MAX_VIRTUAL_ELEMENTS = exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION = void 0;
56
23
  const typedi_1 = require("typedi");
57
- const tesseract_js_1 = __importStar(require("tesseract.js"));
24
+ const tesseract_js_1 = require("tesseract.js");
25
+ const ocrData_1 = require("../ocr/ocrData");
58
26
  const sharp_1 = __importDefault(require("sharp"));
59
27
  const AIService_1 = require("../AIService");
60
28
  const logger_1 = __importDefault(require("../../logger"));
61
29
  const crypto_1 = __importDefault(require("crypto"));
62
30
  const ocrTextMatch_1 = require("./ocrTextMatch");
63
31
  const screenScale_1 = require("./screenScale");
32
+ /**
33
+ * How many virtual elements Xenon keeps, for one session and in all. The
34
+ * oldest go first: a test acts on what it found last, and a command on an
35
+ * element that went answers "no such element".
36
+ */
37
+ exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION = 1000;
38
+ exports.MAX_VIRTUAL_ELEMENTS = 10000;
64
39
  // Mobile UIs have sparse text on complex backgrounds - reduce contrast to preserve readability
65
40
  const DEFAULT_CONTRAST = 1.2; // Slightly above 1.0 for mild enhancement (was 0.8 which was too aggressive)
66
41
  const UPSCALE_THRESHOLD = 800; // Only upscale very small images (was 1500)
67
42
  let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
68
43
  constructor() {
69
44
  this.logger = logger_1.default.scope('OmniVision');
70
- this.virtualElementStore = new Map();
45
+ /**
46
+ * The virtual elements tests may still act on, by id, each with the session
47
+ * that found it, oldest first. Through 2.14 every element ever found stayed
48
+ * here until the server restarted, and any session could act on another's.
49
+ */
50
+ this.elements = new Map();
51
+ this.bySession = new Map();
71
52
  this.sharedWorker = null;
72
53
  this.workerBusy = false;
73
54
  this.uiLensCache = new Map();
@@ -77,7 +58,7 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
77
58
  if (this.sharedWorker)
78
59
  return this.sharedWorker;
79
60
  this.logger.info('Initializing persistent OCR worker with explicit configuration...');
80
- const worker = yield tesseract_js_1.default.createWorker('eng');
61
+ const worker = yield (0, ocrData_1.createOcrWorker)();
81
62
  // PSM.SPARSE_TEXT is optimized for irregular text layouts like mobile UIs
82
63
  // AUTO mode assumes document structure which fails on sparse mobile screens
83
64
  yield worker.setParameters({
@@ -385,6 +366,21 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
385
366
  wordBoxes(words) {
386
367
  return words.map((w) => this.normalizeWordBBox(w)).filter(Boolean);
387
368
  }
369
+ /** Where `text` is among OCR's words, read with over 60% confidence, in reading order. */
370
+ textMatches(words, text) {
371
+ return (0, ocrTextMatch_1.findText)(this.wordBoxes(words), String(text !== null && text !== void 0 ? text : '')).filter((m) => m.confidence > 60);
372
+ }
373
+ /**
374
+ * Where `text` is on a screenshot, matched as `-custom:ai-text` matches it:
375
+ * in the screenshot's pixels, in reading order. Self-healing's OCR tier
376
+ * uses it. Throws when OCR fails.
377
+ */
378
+ findTextInScreenshot(screenshotBase64, text) {
379
+ return __awaiter(this, void 0, void 0, function* () {
380
+ const { words } = yield this.performOcr(Buffer.from(screenshotBase64, 'base64'));
381
+ return this.textMatches(words, text);
382
+ });
383
+ }
388
384
  /**
389
385
  * Proactive OCR Search: Finds elements matching text even if not in XML.
390
386
  *
@@ -407,20 +403,16 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
407
403
  this.logger.warn(`OCR proactive search yielded no words. Text content: "${ocrText.trim().substring(0, 100)}..."`);
408
404
  return [];
409
405
  }
410
- const matches = (0, ocrTextMatch_1.findText)(this.wordBoxes(words), String(text !== null && text !== void 0 ? text : '')).filter((m) => m.confidence > 60);
406
+ const matches = this.textMatches(words, text);
411
407
  if (matches.length === 0)
412
408
  return [];
413
409
  const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
414
- return matches.map((m) => {
415
- const el = {
416
- id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
417
- text: m.text,
418
- rect: (0, screenScale_1.toDriverRect)({ x: m.x0, y: m.y0, width: m.x1 - m.x0, height: m.y1 - m.y0 }, scale),
419
- confidence: m.confidence / 100,
420
- };
421
- this.virtualElementStore.set(el.id, el);
422
- return el;
423
- });
410
+ return matches.map((m) => ({
411
+ id: `omni_ocr_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
412
+ text: m.text,
413
+ rect: (0, screenScale_1.toDriverRect)({ x: m.x0, y: m.y0, width: m.x1 - m.x0, height: m.y1 - m.y0 }, scale),
414
+ confidence: m.confidence / 100,
415
+ }));
424
416
  }
425
417
  catch (err) {
426
418
  this.logger.error(`OCR proactive search failed: ${err.message}`);
@@ -443,13 +435,11 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
443
435
  // The AI answers in the screenshot's pixels; the rect is the driver's
444
436
  // (points on iOS), like a real element's.
445
437
  const scale = yield (0, screenScale_1.screenScaleOf)(driver, screenshot);
446
- const el = {
447
- id: `omni_ai_${Date.now()}`,
438
+ return {
439
+ id: `omni_ai_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
448
440
  rect: (0, screenScale_1.toDriverRect)({ x: coordinates.x - 20, y: coordinates.y - 20, width: 40, height: 40 }, scale),
449
441
  confidence: 0.85,
450
442
  };
451
- this.virtualElementStore.set(el.id, el);
452
- return el;
453
443
  }
454
444
  }
455
445
  catch (err) {
@@ -505,11 +495,48 @@ let OmniVisionService = OmniVisionService_1 = class OmniVisionService {
505
495
  }
506
496
  });
507
497
  }
508
- addVirtualElement(element) {
509
- this.virtualElementStore.set(element.id, element);
498
+ /**
499
+ * Keep `element` for `sessionId`, whose test was handed its id (a
500
+ * `-custom:ai-*` find, a heal), until the session ends (`forgetSession`)
501
+ * or the limits push it out. A find doesn't keep what it finds: device
502
+ * control's locator test only shows the ids, and a tap uses its element at
503
+ * once.
504
+ */
505
+ remember(sessionId, element) {
506
+ this.drop(element.id);
507
+ this.elements.set(element.id, { element, sessionId });
508
+ let ids = this.bySession.get(sessionId);
509
+ if (!ids) {
510
+ ids = new Set();
511
+ this.bySession.set(sessionId, ids);
512
+ }
513
+ ids.add(element.id);
514
+ if (ids.size > exports.MAX_VIRTUAL_ELEMENTS_PER_SESSION)
515
+ this.drop(ids.values().next().value);
516
+ if (this.elements.size > exports.MAX_VIRTUAL_ELEMENTS)
517
+ this.drop(this.elements.keys().next().value);
518
+ }
519
+ /** The element, when `sessionId` found it and still has it. */
520
+ getVirtualElement(id, sessionId) {
521
+ const held = this.elements.get(id);
522
+ return held && held.sessionId === sessionId ? held.element : undefined;
523
+ }
524
+ /** Drop every element a session found: it has ended (sessionMemory.ts). */
525
+ forgetSession(sessionId) {
526
+ var _a;
527
+ for (const id of (_a = this.bySession.get(sessionId)) !== null && _a !== void 0 ? _a : [])
528
+ this.elements.delete(id);
529
+ this.bySession.delete(sessionId);
510
530
  }
511
- getVirtualElement(id) {
512
- return this.virtualElementStore.get(id);
531
+ drop(id) {
532
+ const held = this.elements.get(id);
533
+ if (!held)
534
+ return;
535
+ this.elements.delete(id);
536
+ const ids = this.bySession.get(held.sessionId);
537
+ ids === null || ids === void 0 ? void 0 : ids.delete(id);
538
+ if (ids && ids.size === 0)
539
+ this.bySession.delete(held.sessionId);
513
540
  }
514
541
  /**
515
542
  * Omni-Click: Find a target by OCR text and click its center using W3C actions.
@@ -1,25 +1,66 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetLinkBase = resetLinkBase;
3
4
  exports.buildResetLink = buildResetLink;
5
+ exports.requestBase = requestBase;
6
+ exports.describeLifetime = describeLifetime;
4
7
  exports.resetEmail = resetEmail;
8
+ const config_1 = require("../config");
9
+ const publicUrl_1 = require("./publicUrl");
5
10
  /**
6
- * The URL a reset token is redeemed at. Built from the request so it matches
7
- * the host the caller reached the server on (honouring a TLS-terminating
8
- * proxy's x-forwarded-proto). The raw token is a credential for the account
9
- * until it expires or is used: never log the result.
11
+ * Where a password reset link points, and the email that carries it. The raw
12
+ * token in a link is a credential for the account until it expires or is
13
+ * used: never log the result.
10
14
  *
11
15
  * The token goes in the fragment (#), not the path. Browsers never send the
12
16
  * fragment to the server, so opening the link can't put the token in any
13
17
  * server-side log. As a path segment it was logged on every open: Appium's
14
18
  * [HTTP] request line and Xenon's UI-fallback line both record the URL.
15
19
  */
16
- function buildResetLink(req, rawToken) {
20
+ /**
21
+ * The address a link Xenon sends (by email, or to the log fallback) points
22
+ * at: this server's address from XENON_PUBLIC_URL (publicServerBase), or
23
+ * null when it is unset or not a usable address.
24
+ *
25
+ * Never the request's Host header. Through 2.14 the link was built from it,
26
+ * so anyone could ask for a reset of someone else's account naming a host of
27
+ * their own (a matching Origin passes the CSRF check), and Xenon emailed the
28
+ * victim a genuine link to that host, which handed over the token.
29
+ */
30
+ function resetLinkBase(cfg = config_1.config) {
31
+ return (0, publicUrl_1.publicServerBase)(cfg);
32
+ }
33
+ /** The reset page for `rawToken`, under `base` (resetLinkBase, or requestBase). */
34
+ function buildResetLink(base, rawToken) {
35
+ return `${base}/xenon/reset-password#${rawToken}`;
36
+ }
37
+ /**
38
+ * The address the caller reached this server on, honouring a TLS-terminating
39
+ * proxy's x-forwarded-proto. Only for a link handed back to the signed-in
40
+ * administrator who asked for it (POST /users/:id/reset-link when it can't be
41
+ * emailed): they see where it points before passing it on, and hold the token
42
+ * anyway. Never for a link Xenon sends to someone else.
43
+ */
44
+ function requestBase(req) {
17
45
  const proto = req.secure || req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
18
46
  const host = req.headers.host || 'localhost';
19
- return `${proto}://${host}/xenon/reset-password#${rawToken}`;
47
+ return `${proto}://${host}`;
48
+ }
49
+ /** "1 hour", "30 minutes", "2 hours": a reset link's lifetime, for the email. */
50
+ function describeLifetime(ms) {
51
+ const minutes = Math.max(1, Math.round(ms / 60000));
52
+ if (minutes % 60 === 0) {
53
+ const hours = minutes / 60;
54
+ return `${hours} hour${hours === 1 ? '' : 's'}`;
55
+ }
56
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
20
57
  }
21
- /** The reset email, shared by self-service and admin-issued resets. */
22
- function resetEmail(user, link) {
58
+ /**
59
+ * The reset email, shared by self-service and admin-issued resets. `ttlMs` is
60
+ * how long the link lasts (PasswordResetService.ttlMs(), from
61
+ * XENON_RESET_TOKEN_TTL_MS): the email said "1 hour" whatever it was.
62
+ */
63
+ function resetEmail(user, link, ttlMs) {
23
64
  return {
24
65
  to: user.email,
25
66
  subject: 'Reset your Xenon password',
@@ -27,6 +68,6 @@ function resetEmail(user, link) {
27
68
  `Someone requested a password reset for your Xenon account. ` +
28
69
  `If that was you, click the link below to choose a new password:\n\n` +
29
70
  `${link}\n\n` +
30
- `This link expires in 1 hour. If you did not request this, ignore this email — no action is needed.\n`,
71
+ `This link expires in ${describeLifetime(ttlMs)}. If you did not request this, ignore this email — no action is needed.\n`,
31
72
  };
32
73
  }
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.passwordResetMode = passwordResetMode;
4
+ const passwordResetLink_1 = require("./passwordResetLink");
4
5
  function passwordResetMode(cfg) {
5
- return cfg.smtpUrl && cfg.smtpUrl.trim() ? 'email' : 'admin';
6
+ const smtp = !!(cfg.smtpUrl && cfg.smtpUrl.trim());
7
+ return smtp && (0, passwordResetLink_1.resetLinkBase)(cfg) !== null ? 'email' : 'admin';
6
8
  }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.publicServerBase = publicServerBase;
4
+ const config_1 = require("../config");
5
+ /**
6
+ * XENON_PUBLIC_URL: the address people reach this Xenon server at, such as
7
+ * https://xenon.example.com or http://lab-mac:4723. Links Xenon hands out
8
+ * (password reset links, a device's dashboard_link) are built from it, never
9
+ * from the request's Host header, which whoever sends the request chooses.
10
+ *
11
+ * The dashboard's own address is taken too: a trailing `/xenon` or `/xenon/`
12
+ * is dropped, so `http://localhost:4723/xenon/` gives `http://localhost:4723`.
13
+ * Kept as given it built `/xenon/xenon/reset-password`, which the dashboard
14
+ * sends to its sign-in page, losing the token.
15
+ *
16
+ * Any other path is refused: the dashboard loads its pages, scripts and API
17
+ * from `/xenon` on the server's root (its base path is fixed at build time),
18
+ * so it can't be served under a reverse-proxy prefix, and a link under one
19
+ * wouldn't open. So are an address that isn't http(s), and one with a query,
20
+ * fragment or user name (a fragment would swallow a reset token).
21
+ *
22
+ * Returns the origin (scheme, host and port, no trailing slash), or null when
23
+ * the variable is unset or not such an address.
24
+ */
25
+ function publicServerBase(cfg = config_1.config) {
26
+ var _a;
27
+ const raw = (_a = cfg.publicUrl) === null || _a === void 0 ? void 0 : _a.trim();
28
+ if (!raw)
29
+ return null;
30
+ let url;
31
+ try {
32
+ url = new URL(raw);
33
+ }
34
+ catch (_b) {
35
+ return null;
36
+ }
37
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
38
+ return null;
39
+ if (url.search || url.hash || url.username || url.password)
40
+ return null;
41
+ const path = url.pathname.replace(/\/+$/, '');
42
+ if (path !== '' && path !== '/xenon')
43
+ return null;
44
+ return url.origin;
45
+ }
@@ -21,9 +21,11 @@ function resolveSessionIdentity(input) {
21
21
  if (!input.sessionToken)
22
22
  return NONE;
23
23
  // Attribution is decoupled from enforcement: a token that does not verify is
24
- // IGNORED here, never rejected. assertSessionTokenGate remains the only
25
- // decider of whether the session is admitted. That separation is what lets a
26
- // valid token identify its caller even when the gate is switched off.
24
+ // IGNORED here, never rejected. Whether the session is admitted is decided
25
+ // by the caller: assertSessionTokenGate for a session with no valid
26
+ // credentials, and authorizeSessionRequest's `sessions` scope check for a
27
+ // token that verifies. That separation is what lets a valid token identify
28
+ // its caller even when the gate is switched off.
27
29
  try {
28
30
  const payload = yield input.verify(input.sessionToken);
29
31
  const sub = payload === null || payload === void 0 ? void 0 : payload.sub;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UNEXPECTED_SHUTDOWN_REASON = void 0;
4
+ exports.unexpectedShutdownReason = unexpectedShutdownReason;
5
+ /** The reason recorded when Appium ends a session and gives no cause of its own. */
6
+ exports.UNEXPECTED_SHUTDOWN_REASON = 'Driver shut down unexpectedly';
7
+ /**
8
+ * The failure reason of a session Appium ended by itself
9
+ * (`XenonPlugin.onUnexpectedShutdown`): the message of the cause Appium
10
+ * passed, else UNEXPECTED_SHUTDOWN_REASON.
11
+ *
12
+ * Appium's new-command timeout is the usual cause, and its message ("New
13
+ * Command Timeout of 60 seconds expired. Try customizing the timeout using
14
+ * the 'newCommandTimeout' desired capability") is what files the session
15
+ * under TIMEOUT. A driver that gives up on itself passes its own error, such
16
+ * as base-driver's "The driver was unexpectedly shut down!". The umbrella's
17
+ * placeholder for a missing cause, "Unknown error", says nothing, so it gets
18
+ * the fallback too. Through 2.14 every such session was recorded with the
19
+ * fixed reason, which no failure pattern matches, so an idle session was
20
+ * filed UNKNOWN.
21
+ */
22
+ function unexpectedShutdownReason(cause) {
23
+ const message = typeof cause === 'string' ? cause : cause === null || cause === void 0 ? void 0 : cause.message;
24
+ const text = typeof message === 'string' ? message.trim() : '';
25
+ return text && text !== 'Unknown error' ? text : exports.UNEXPECTED_SHUTDOWN_REASON;
26
+ }
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
9
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
10
+ return new (P || (P = Promise))(function (resolve, reject) {
11
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
12
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
13
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
14
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
15
+ });
16
+ };
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.AiEngineSettings = exports.AI_ENGINE_SETTINGS = exports.AI_PROVIDERS = void 0;
22
+ exports.pickAiEngine = pickAiEngine;
23
+ exports.effectiveAiEngine = effectiveAiEngine;
24
+ exports.validateAiEngineUpdate = validateAiEngineUpdate;
25
+ exports.aiEngineUpdateOf = aiEngineUpdateOf;
26
+ const typedi_1 = require("typedi");
27
+ const logger_1 = __importDefault(require("../../logger"));
28
+ const config_1 = require("../../config");
29
+ const web_config_service_1 = require("../../data-service/web-config-service");
30
+ /**
31
+ * The AI engine page's settings: which provider answers the AI calls (failure
32
+ * analysis, the visual and LLM healing tiers, visual assertions), and the model
33
+ * and address it uses.
34
+ *
35
+ * They follow the rule every lab setting does (labSettings.ts): a value saved
36
+ * in the dashboard (`WebConfig`, written by `POST /config`), else the plugin
37
+ * option or environment variable the server was started with, else the
38
+ * default. The result is written to `config`, which every AI call reads, at
39
+ * boot and on each save, so a change applies to the next call without a
40
+ * restart. Through 2.14 `POST /config` changed `config` and saved nothing: a
41
+ * restart went back to the provider the server was started with.
42
+ *
43
+ * API keys are not among them. They come from the server's environment only,
44
+ * are never saved, and are never sent to the dashboard.
45
+ */
46
+ exports.AI_PROVIDERS = ['gemini', 'openai', 'anthropic', 'ollama'];
47
+ /** The settings decided here, by their names in `config`, `WebConfig` and `/config`. */
48
+ exports.AI_ENGINE_SETTINGS = [
49
+ 'aiProvider',
50
+ 'aiModel',
51
+ 'aiBaseUrl',
52
+ 'geminiModel',
53
+ 'openaiModel',
54
+ 'anthropicModel',
55
+ 'ollamaModel',
56
+ ];
57
+ const MODEL_SETTINGS = [
58
+ 'aiModel',
59
+ 'geminiModel',
60
+ 'openaiModel',
61
+ 'anthropicModel',
62
+ 'ollamaModel',
63
+ ];
64
+ const DEFAULT_PROVIDER = 'gemini';
65
+ const MODEL_MAX_LENGTH = 200;
66
+ const BASE_URL_MAX_LENGTH = 2048;
67
+ const isProvider = (v) => typeof v === 'string' && exports.AI_PROVIDERS.includes(v);
68
+ const isNonEmpty = (v) => typeof v === 'string' && v.trim() !== '';
69
+ /** The AI engine settings in `source` (the running config, the saved rows), and nothing else. */
70
+ function pickAiEngine(source) {
71
+ const out = {};
72
+ for (const name of exports.AI_ENGINE_SETTINGS) {
73
+ if (typeof source[name] === 'string')
74
+ out[name] = source[name];
75
+ }
76
+ return out;
77
+ }
78
+ /**
79
+ * The settings in effect, field by field: the saved value when it can work,
80
+ * else the one the server was started with, else the default (gemini; a model
81
+ * or base URL has none, and the provider's own then applies). An empty saved
82
+ * value is the person clearing it. The started-with provider is kept as it
83
+ * is, even one Xenon doesn't know: falling back to another provider would
84
+ * send screenshots somewhere the server's owner didn't choose. AIService then
85
+ * runs with no provider, and says so.
86
+ */
87
+ function effectiveAiEngine(startup, saved) {
88
+ const out = {
89
+ aiProvider: isProvider(saved.aiProvider)
90
+ ? saved.aiProvider
91
+ : isNonEmpty(startup.aiProvider)
92
+ ? startup.aiProvider
93
+ : DEFAULT_PROVIDER,
94
+ };
95
+ for (const name of exports.AI_ENGINE_SETTINGS) {
96
+ if (name === 'aiProvider')
97
+ continue;
98
+ const mine = saved[name];
99
+ out[name] = isNonEmpty(mine)
100
+ ? mine.trim()
101
+ : isNonEmpty(startup[name])
102
+ ? startup[name]
103
+ : undefined;
104
+ }
105
+ return out;
106
+ }
107
+ /** Why `url` can't be saved as the base URL, or null. */
108
+ function baseUrlProblem(url) {
109
+ const why = "aiBaseUrl must be an http or https address with no user name, password or query (the API key comes from the server's environment), or empty to use the server's own.";
110
+ if (url.length > BASE_URL_MAX_LENGTH)
111
+ return why;
112
+ let parsed;
113
+ try {
114
+ parsed = new URL(url);
115
+ }
116
+ catch (_a) {
117
+ return why;
118
+ }
119
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
120
+ return why;
121
+ // Anything that could carry a credential would be one saved in the database.
122
+ if (parsed.username || parsed.password || parsed.search || parsed.hash)
123
+ return why;
124
+ return null;
125
+ }
126
+ /**
127
+ * What is wrong with the AI engine fields of a settings update, or null.
128
+ * Checked before anything is saved: a saved value outlives the restart that
129
+ * used to undo it. Each may be empty, to go back to what the server was
130
+ * started with. Fields not sent are not checked.
131
+ */
132
+ function validateAiEngineUpdate(body) {
133
+ const has = (key) => body[key] !== undefined;
134
+ if (has('aiProvider') && !(body.aiProvider === '' || isProvider(body.aiProvider))) {
135
+ return {
136
+ field: 'aiProvider',
137
+ message: `aiProvider must be one of ${exports.AI_PROVIDERS.join(', ')}, or empty to use the server's own.`,
138
+ };
139
+ }
140
+ for (const field of MODEL_SETTINGS) {
141
+ if (!has(field))
142
+ continue;
143
+ const value = body[field];
144
+ const name = typeof value === 'string' ? value.trim() : null;
145
+ if (name === null || name.length > MODEL_MAX_LENGTH || /[\s\u0000-\u001f\u007f]/.test(name)) {
146
+ return {
147
+ field,
148
+ message: `${field} must be a model name with no spaces, of at most ${MODEL_MAX_LENGTH} characters, or empty to use the server's own.`,
149
+ };
150
+ }
151
+ }
152
+ if (has('aiBaseUrl')) {
153
+ const value = body.aiBaseUrl;
154
+ const problem = typeof value !== 'string'
155
+ ? 'aiBaseUrl must be text.'
156
+ : value.trim() === ''
157
+ ? null
158
+ : baseUrlProblem(value.trim());
159
+ if (problem)
160
+ return { field: 'aiBaseUrl', message: problem };
161
+ }
162
+ return null;
163
+ }
164
+ /** The AI engine fields of a checked update, as they are saved: names trimmed. */
165
+ function aiEngineUpdateOf(body) {
166
+ const picked = pickAiEngine(body);
167
+ for (const name of Object.keys(picked))
168
+ picked[name] = picked[name].trim();
169
+ return picked;
170
+ }
171
+ /**
172
+ * Keeps `config`'s AI engine settings at the values in effect. Loaded once at
173
+ * boot, after the startup options were applied and the database is ready, and
174
+ * told of each save by `POST /config`.
175
+ */
176
+ let AiEngineSettings = class AiEngineSettings {
177
+ constructor() {
178
+ /** What the dashboard saved. */
179
+ this.saved = {};
180
+ }
181
+ /**
182
+ * Reads the saved settings and applies them. A database that can't be read
183
+ * leaves the startup settings in charge, as it does for the other settings.
184
+ */
185
+ load() {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ this.startedWith();
188
+ try {
189
+ this.saved = pickAiEngine((yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()));
190
+ }
191
+ catch (err) {
192
+ this.saved = {};
193
+ logger_1.default.warn(`Could not read the saved AI engine settings, using the startup options: ${err === null || err === void 0 ? void 0 : err.message}`);
194
+ }
195
+ this.apply();
196
+ });
197
+ }
198
+ /** What `POST /config` just saved, in force for the next AI call. */
199
+ noteSaved(update) {
200
+ this.startedWith();
201
+ this.saved = Object.assign(Object.assign({}, this.saved), update);
202
+ this.apply();
203
+ }
204
+ startedWith() {
205
+ if (!this.startup)
206
+ this.startup = pickAiEngine(config_1.config);
207
+ return this.startup;
208
+ }
209
+ apply() {
210
+ const before = config_1.config.aiProvider;
211
+ (0, config_1.updateConfig)(effectiveAiEngine(this.startedWith(), this.saved));
212
+ if (config_1.config.aiProvider !== before)
213
+ logger_1.default.info(`AI provider: ${config_1.config.aiProvider}`);
214
+ }
215
+ };
216
+ exports.AiEngineSettings = AiEngineSettings;
217
+ exports.AiEngineSettings = AiEngineSettings = __decorate([
218
+ (0, typedi_1.Service)()
219
+ ], AiEngineSettings);
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Nothing a credential mints outlives it.
4
+ *
5
+ * `credentialExpiresAt` is req.auth.credentialExpiresAt: when the credential
6
+ * making the request stops working (a Bearer token's `exp`, an API key's
7
+ * `expiresAt`), as epoch ms, or undefined for one that doesn't expire (a key
8
+ * without an expiry, a dashboard sign-in, which renews itself).
9
+ *
10
+ * Through 2.14 a 1-hour Bearer token could make itself an API key that never
11
+ * expired (POST /profile/tokens, POST /apikeys), or mint a fresh 1-hour token
12
+ * before it expired, and so on for ever (POST /auth/token).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.mintedLifetimeSec = mintedLifetimeSec;
16
+ exports.keyExpiryWithin = keyExpiryWithin;
17
+ /**
18
+ * A minted token's lifetime in seconds: `defaultSec`, cut to what is left of
19
+ * the credential minting it, less a second so the minted token's `exp`
20
+ * can't round past it. Throws when nothing is left.
21
+ */
22
+ function mintedLifetimeSec(defaultSec, credentialExpiresAt) {
23
+ if (credentialExpiresAt === undefined)
24
+ return defaultSec;
25
+ const left = Math.floor((credentialExpiresAt - Date.now()) / 1000) - 1;
26
+ if (left < 1)
27
+ throw new Error('the credential used to mint this token has expired');
28
+ return Math.min(defaultSec, left);
29
+ }
30
+ /**
31
+ * An API key's expiry, made with a credential that may itself expire: the
32
+ * expiry asked for, or the credential's when none was asked for; an error
33
+ * when the one asked for is later than the credential's, or the credential
34
+ * has already expired.
35
+ */
36
+ function keyExpiryWithin(requested, credentialExpiresAt) {
37
+ if (credentialExpiresAt === undefined)
38
+ return { expiresAt: requested };
39
+ // Token verification allows 60 s of clock skew, so a request can arrive on
40
+ // a credential just past its exp: it must not make a key already expired.
41
+ if (credentialExpiresAt <= Date.now()) {
42
+ return { error: 'the credential you are using has expired' };
43
+ }
44
+ if (requested && requested.getTime() > credentialExpiresAt) {
45
+ return {
46
+ error: 'expiresAt can be no later than the credential you are using, which expires at ' +
47
+ new Date(credentialExpiresAt).toISOString(),
48
+ };
49
+ }
50
+ return { expiresAt: requested !== null && requested !== void 0 ? requested : new Date(credentialExpiresAt) };
51
+ }