@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
@@ -20,14 +20,17 @@ const plugin_1 = require("../../src/plugin");
20
20
  const ServerManager_1 = require("../../src/services/ServerManager");
21
21
  const PluginContext_1 = require("../../src/PluginContext");
22
22
  const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
23
+ const aiEngineSettings_1 = require("../../src/services/settings/aiEngineSettings");
23
24
  const logger_1 = __importDefault(require("../../src/logger"));
24
25
  /**
25
- * The self-healing value saved on the Settings page is read once at boot, so a
26
- * restart doesn't put the server back on the option it was started with. It
27
- * has to be loaded once the database is ready (migrations ran) and before the
28
- * first route can answer or the first command arrive.
26
+ * The self-healing value saved on the Settings page, and the AI engine page's
27
+ * provider, model and base URL, are read once at boot, so a restart doesn't put
28
+ * the server back on the options it was started with. They have to be loaded
29
+ * once the database is ready (migrations ran), after the startup options were
30
+ * applied (the AI engine's are what a cleared value goes back to), and before
31
+ * the first route can answer or the first command arrive.
29
32
  */
30
- describe('loading the saved self-healing setting, at boot', () => {
33
+ describe('loading the saved self-healing and AI engine settings, at boot', () => {
31
34
  let statics;
32
35
  beforeEach(() => {
33
36
  statics = {
@@ -57,6 +60,7 @@ describe('loading the saved self-healing setting, at boot', () => {
57
60
  // The real one kills every go-ios process on the machine.
58
61
  sinon_1.default.stub(boot, 'reapLeftoverGoIos').callsFake(step('reap'));
59
62
  sinon_1.default.stub(SelfHealingSwitch_1.SelfHealingSwitch.prototype, 'load').callsFake(step('self-healing setting'));
63
+ sinon_1.default.stub(aiEngineSettings_1.AiEngineSettings.prototype, 'load').callsFake(step('AI engine settings'));
60
64
  sinon_1.default.stub(boot, 'registerRoutes').callsFake(() => {
61
65
  steps.push('routes');
62
66
  throw new Error('boot stopped by the test');
@@ -69,6 +73,7 @@ describe('loading the saved self-healing setting, at boot', () => {
69
73
  'database and migrations',
70
74
  'reap',
71
75
  'self-healing setting',
76
+ 'AI engine settings',
72
77
  'routes',
73
78
  ]);
74
79
  }));
@@ -203,10 +203,10 @@ describe('the AI self-healing switch', () => {
203
203
  (0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
204
204
  }));
205
205
  it("still hands a session's own healing tiers to the healing it runs", () => __awaiter(void 0, void 0, void 0, function* () {
206
- sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns({
207
- getCapabilities: () => ({ 'xe:options': { healingTiers: [1, 2] } }),
208
- });
209
- const driver = { sessionId: 'sess-6' };
206
+ // Read from the session's driver, which every session has: SESSION_MANAGER
207
+ // holds a local one only with the dashboard on, and holds none here.
208
+ const driver = { sessionId: 'sess-6', caps: { 'xe:options': { healingTiers: [1, 2] } } };
209
+ (0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('sess-6')).to.equal(undefined);
210
210
  yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
211
211
  .handle(() => __awaiter(void 0, void 0, void 0, function* () {
212
212
  throw noSuchElement();
@@ -219,9 +219,10 @@ describe('the AI self-healing switch', () => {
219
219
  let learn;
220
220
  beforeEach(() => {
221
221
  sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
222
- learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning');
222
+ learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning').resolves();
223
223
  });
224
- const afterFind = () => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).runPostCommandHooks('sess-7', 'findElement', {}, ['id', 'there'], { ELEMENT: 'el-1' }, pluginArgs);
224
+ /** A findElement the driver answers with its element. */
225
+ const afterFind = () => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return ({ ELEMENT: 'el-1' }); }), { sessionId: 'sess-7' }, 'findElement', ['id', 'there', 'sess-7'], pluginArgs, false);
225
226
  it('learns the selector while the switch is on', () => __awaiter(void 0, void 0, void 0, function* () {
226
227
  yield afterFind();
227
228
  (0, chai_1.expect)(learn.calledOnce).to.equal(true);
@@ -145,7 +145,11 @@ describe('createSession — an app named by id follows the team rule', () => {
145
145
  return owned.includes('admin') || req.some((s) => owned.includes(s));
146
146
  },
147
147
  });
148
- typedi_1.Container.set(UserService_1.UserService, { findById: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
148
+ // Every key's owner is an ACTIVE member: a key names its owner only while
149
+ // they are. The admin key is unscoped by its admin scope.
150
+ typedi_1.Container.set(UserService_1.UserService, {
151
+ findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
152
+ });
149
153
  sinon_1.default.stub(app_service_1.APP_SERVICE, 'getAppById').callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = APPS[id]) !== null && _a !== void 0 ? _a : null; }));
150
154
  const device = { udid: 'u1', host: 'h1', platform: 'android', nodeId: 'node-hub' };
151
155
  allocate = sinon_1.default.stub(deviceUtils, 'allocateDeviceForSession').resolves(device);
@@ -41,10 +41,12 @@ describe('authorizeSessionRequest — identity', () => {
41
41
  authDisabledBefore = config_1.config.authDisabled;
42
42
  config_1.config.authDisabled = false;
43
43
  restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
44
- // No such user and no teams, as the developer's database answered for
45
- // these ids: the real lookups read that database. Nothing here is about
46
- // the owner's teams.
47
- typedi_1.Container.set(UserService_1.UserService, { findById: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
44
+ // Every id is an ACTIVE member in no team: a credential names its owner
45
+ // only while they are (session-create-credential-owner.spec.ts covers
46
+ // the other cases). The real lookups would read the developer's database.
47
+ typedi_1.Container.set(UserService_1.UserService, {
48
+ findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
49
+ });
48
50
  sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
49
51
  svc = new SessionLifecycleService_1.SessionLifecycleService();
50
52
  });
@@ -105,7 +107,7 @@ describe('authorizeSessionRequest — identity', () => {
105
107
  hasScope: () => false,
106
108
  });
107
109
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
108
- verify: sinon_1.default.stub().resolves({ sub: 'usr_carol' }),
110
+ verify: sinon_1.default.stub().resolves({ sub: 'usr_carol', scopes: 'sessions' }),
109
111
  });
110
112
  const res = yield invoke(svc, capsWith({ 'xe:options': { sessionToken: 'tok' } }));
111
113
  (0, chai_1.expect)(res.apiKeyId).to.equal(null);
@@ -156,13 +158,14 @@ describe('authorizeSessionRequest — leaseAccess', () => {
156
158
  findUser = sinon_1.default.stub().resolves(owner);
157
159
  typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
158
160
  };
159
- const withSessionToken = (owner, teamId = null) => {
161
+ // A session token carries the scopes of the credential that minted it.
162
+ const withSessionToken = (owner, teamId = null, scopes = 'sessions') => {
160
163
  typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
161
164
  verifyPair: sinon_1.default.stub().resolves(null),
162
165
  hasScope: () => false,
163
166
  });
164
167
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
165
- verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId }),
168
+ verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId, scopes }),
166
169
  });
167
170
  findUser = sinon_1.default.stub().resolves(owner);
168
171
  typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
@@ -208,9 +211,19 @@ describe('authorizeSessionRequest — leaseAccess', () => {
208
211
  withKey(keyRow('admin'), user('SUPER_ADMIN', 'INACTIVE'));
209
212
  (0, chai_1.expect)((yield access(pairCaps())).canOverride).to.equal(false);
210
213
  }));
211
- it('follows the role of a session-token caller, ADMIN included', () => __awaiter(void 0, void 0, void 0, function* () {
212
- withSessionToken(user('ADMIN'));
214
+ // By the scopes it was minted with, as a key is judged. By its user's role
215
+ // alone, an ADMIN's key without the admin scope minted itself an override.
216
+ it("follows resolveActor's rule on a session token's user's role now", () => __awaiter(void 0, void 0, void 0, function* () {
217
+ withSessionToken(user('ADMIN'), null, 'admin,sessions');
218
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
219
+ withSessionToken(user('ADMIN'), null, 'sessions');
220
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
221
+ withSessionToken(user('SUPER_ADMIN'), null, 'admin,sessions');
222
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
223
+ withSessionToken(user('SUPER_ADMIN'), null, 'sessions');
213
224
  (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
225
+ withSessionToken(user('MEMBER'), null, 'admin,sessions');
226
+ (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
214
227
  withSessionToken(user('MEMBER'));
215
228
  (0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
216
229
  }));
@@ -223,9 +236,24 @@ describe('authorizeSessionRequest — leaseAccess', () => {
223
236
  (0, chai_1.expect)(res.scoped).to.equal(false);
224
237
  (0, chai_1.expect)((yield res.leaseAccess()).canOverride).to.equal(false);
225
238
  }));
226
- it('is false when the lookup fails, and the teams fall back to the shared pool', () => __awaiter(void 0, void 0, void 0, function* () {
239
+ // The owner is looked up when the credential is checked: a key names them
240
+ // only while they are ACTIVE, so a lookup that fails refuses the session
241
+ // rather than run it as someone who may have been switched off.
242
+ it('refuses the session when the owner lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
227
243
  withKey(keyRow('admin'), null);
228
244
  typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
245
+ let error;
246
+ try {
247
+ yield invoke(svc, pairCaps());
248
+ }
249
+ catch (e) {
250
+ error = e;
251
+ }
252
+ (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal('db down');
253
+ }));
254
+ it('is false, and the teams fall back to the shared pool, when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
255
+ withKey(keyRow('sessions'), user('MEMBER'));
256
+ teamRows.rejects(new Error('db down'));
229
257
  (0, chai_1.expect)(yield access(pairCaps())).to.deep.equal({ canOverride: false, teamIds: [] });
230
258
  }));
231
259
  it('is true when auth is disabled, where every caller is an admin', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -331,9 +359,9 @@ describe('authorizeSessionRequest — leaseAccess', () => {
331
359
  }
332
360
  (0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("xe:options.team 'team_c' is not allowed for this API key");
333
361
  }));
334
- it('falls back to the shared pool when the lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
335
- withKey(keyRow('sessions'), null);
336
- typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
362
+ it('falls back to the shared pool when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
363
+ withKey(keyRow('sessions'), user('MEMBER'));
364
+ teamRows.rejects(new Error('db down'));
337
365
  (0, chai_1.expect)(yield scope(pairCaps())).to.deep.equal({ callerTeamIds: [], scoped: true });
338
366
  }));
339
367
  it('stays unscoped, with no lookup, for a session with no credentials', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const typedi_1 = require("typedi");
22
+ const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
23
+ const ApiKeyService_1 = require("../../src/services/ApiKeyService");
24
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
25
+ const UserService_1 = require("../../src/services/UserService");
26
+ const config_1 = require("../../src/config");
27
+ const prisma_1 = require("../../src/prisma");
28
+ const container_registration_1 = require("../helpers/container-registration");
29
+ const sessionCredentials_1 = require("../../src/services/session/sessionCredentials");
30
+ /**
31
+ * Session create judges the credentials it is given by REST's rule: a key
32
+ * pair or session token says who the caller is only while that user exists
33
+ * and is ACTIVE. Through 2.14 the pair was checked against the key alone
34
+ * (ApiKeyService.verifyPair) and the session token against its signature
35
+ * alone, so a user set to Inactive, or deleted, kept creating sessions as
36
+ * themselves, past XENON_REQUIRE_SESSION_TOKEN too.
37
+ *
38
+ * Such credentials now count as wrong ones: with the gate on the session is
39
+ * refused, with it off it runs with no owner, like any session presenting
40
+ * credentials that don't check out.
41
+ *
42
+ * A session token also has to carry the `sessions` scope, as a key pair
43
+ * must: POST /auth/token gave one to any credential, a read-only one
44
+ * included.
45
+ */
46
+ // authorizeSessionRequest is private: the single place a create's identity is
47
+ // decided. It is handed the credentials createSession took out of the caps.
48
+ const invoke = (svc, caps) => svc.authorizeSessionRequest(caps, (0, sessionCredentials_1.takeSessionCredentials)(caps));
49
+ const capsWith = (options) => ({
50
+ alwaysMatch: { 'xe:options': options },
51
+ firstMatch: [{}],
52
+ });
53
+ function refusalOf(promise) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ var _a;
56
+ try {
57
+ yield promise;
58
+ return null;
59
+ }
60
+ catch (err) {
61
+ return String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err);
62
+ }
63
+ });
64
+ }
65
+ describe('session create: the credentials of an inactive or deleted user', () => {
66
+ let svc;
67
+ let dir;
68
+ let keys;
69
+ let restore;
70
+ let authDisabledBefore;
71
+ let gateBefore;
72
+ let users;
73
+ let findById;
74
+ const keyRow = { id: 'key_alice', userId: 'usr_alice', scopes: 'sessions', teamId: null };
75
+ const alice = () => users.usr_alice;
76
+ const pair = () => capsWith({ accessKey: 'xen_alice', token: 'tk' });
77
+ const token = (claims = { sub: 'usr_alice', scopes: 'sessions' }) => keys.sign(claims, { audience: 'xenon-session', ttlSeconds: 600 });
78
+ const gate = (on) => {
79
+ if (on)
80
+ process.env.XENON_REQUIRE_SESSION_TOKEN = 'true';
81
+ else
82
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
83
+ };
84
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
85
+ authDisabledBefore = config_1.config.authDisabled;
86
+ config_1.config.authDisabled = false;
87
+ gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
88
+ restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
89
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-create-owner-'));
90
+ keys = new JwtKeyService_1.JwtKeyService();
91
+ yield keys.init(dir);
92
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
93
+ typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
94
+ verifyPair: sinon_1.default.stub().resolves(keyRow),
95
+ hasScope: (row, req) => {
96
+ const owned = row.scopes.split(',');
97
+ return owned.includes('admin') || req.some((s) => owned.includes(s));
98
+ },
99
+ });
100
+ users = { usr_alice: { id: 'usr_alice', role: 'MEMBER', status: 'ACTIVE' } };
101
+ findById = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = users[id]) !== null && _a !== void 0 ? _a : null; }));
102
+ typedi_1.Container.set(UserService_1.UserService, { findById });
103
+ sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
104
+ svc = new SessionLifecycleService_1.SessionLifecycleService();
105
+ }));
106
+ afterEach(() => {
107
+ config_1.config.authDisabled = authDisabledBefore;
108
+ if (gateBefore === undefined)
109
+ delete process.env.XENON_REQUIRE_SESSION_TOKEN;
110
+ else
111
+ process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
112
+ sinon_1.default.restore();
113
+ restore();
114
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
115
+ });
116
+ describe('with XENON_REQUIRE_SESSION_TOKEN on', () => {
117
+ beforeEach(() => gate(true));
118
+ it("refuses an Inactive user's access key and token", () => __awaiter(void 0, void 0, void 0, function* () {
119
+ alice().status = 'INACTIVE';
120
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
121
+ }));
122
+ it("refuses a deleted user's key, whose row outlived them", () => __awaiter(void 0, void 0, void 0, function* () {
123
+ users.usr_alice = undefined;
124
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
125
+ }));
126
+ it("refuses an Inactive user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
127
+ alice().status = 'INACTIVE';
128
+ const caps = capsWith({ sessionToken: yield token() });
129
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
130
+ }));
131
+ it("refuses a deleted user's session token, which is still within its lifetime", () => __awaiter(void 0, void 0, void 0, function* () {
132
+ users.usr_alice = undefined;
133
+ const caps = capsWith({ sessionToken: yield token() });
134
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
135
+ }));
136
+ it("still admits an Active user's key and token, and their session token", () => __awaiter(void 0, void 0, void 0, function* () {
137
+ const byPair = yield invoke(svc, pair());
138
+ (0, chai_1.expect)(byPair).to.include({ apiKeyId: 'key_alice', userId: 'usr_alice' });
139
+ const byToken = yield invoke(svc, capsWith({ sessionToken: yield token() }));
140
+ (0, chai_1.expect)(byToken).to.include({ apiKeyId: null, userId: 'usr_alice' });
141
+ }));
142
+ it('looks the owner up once, though the gate and attribution both verify the token', () => __awaiter(void 0, void 0, void 0, function* () {
143
+ yield invoke(svc, capsWith({ sessionToken: yield token() }));
144
+ (0, chai_1.expect)(findById.callCount).to.equal(1);
145
+ }));
146
+ });
147
+ describe('with XENON_REQUIRE_SESSION_TOKEN off', () => {
148
+ beforeEach(() => gate(false));
149
+ it("gives no owner to a session made with an Inactive user's key", () => __awaiter(void 0, void 0, void 0, function* () {
150
+ alice().status = 'INACTIVE';
151
+ const res = yield invoke(svc, pair());
152
+ (0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
153
+ }));
154
+ it("gives no owner to a session made with a deleted user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
155
+ users.usr_alice = undefined;
156
+ const res = yield invoke(svc, capsWith({ sessionToken: yield token() }));
157
+ (0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
158
+ }));
159
+ it('looks an Active owner up once for a key and token', () => __awaiter(void 0, void 0, void 0, function* () {
160
+ const res = yield invoke(svc, pair());
161
+ yield res.leaseAccess();
162
+ (0, chai_1.expect)(res.userId).to.equal('usr_alice');
163
+ (0, chai_1.expect)(findById.callCount).to.equal(1);
164
+ }));
165
+ });
166
+ describe('a session token needs the `sessions` scope', () => {
167
+ for (const on of [false, true]) {
168
+ describe(`(gate ${on ? 'on' : 'off'})`, () => {
169
+ beforeEach(() => gate(on));
170
+ it('refuses one minted for a read-only credential', () => __awaiter(void 0, void 0, void 0, function* () {
171
+ const caps = capsWith({
172
+ sessionToken: yield token({ sub: 'usr_alice', scopes: 'devices' }),
173
+ });
174
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/sessions` scope/);
175
+ }));
176
+ it('refuses one minted before session tokens carried their scopes, saying so', () => __awaiter(void 0, void 0, void 0, function* () {
177
+ const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice' }) });
178
+ const refusal = yield refusalOf(invoke(svc, caps));
179
+ (0, chai_1.expect)(refusal).to.match(/carries no `sessions` scope/);
180
+ (0, chai_1.expect)(refusal).to.match(/minted by Xenon 2\.14 or earlier carry none/);
181
+ (0, chai_1.expect)(refusal).to.match(/mint a new one/);
182
+ }));
183
+ it('admits one with `sessions` or `admin`', () => __awaiter(void 0, void 0, void 0, function* () {
184
+ for (const scopes of ['sessions', 'admin', 'devices,sessions']) {
185
+ const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
186
+ (0, chai_1.expect)((yield invoke(svc, caps)).userId).to.equal('usr_alice');
187
+ }
188
+ }));
189
+ });
190
+ }
191
+ });
192
+ // Taking over a lease someone else created is an ownership override: a
193
+ // SUPER_ADMIN, or a credential with the `admin` scope. A session token was
194
+ // judged by its user's role alone, so an ADMIN's key without the admin
195
+ // scope, which may not override, minted a session token that could.
196
+ describe('a session token overrides a lease only as the credential that minted it could', () => {
197
+ beforeEach(() => gate(false));
198
+ const overrideWith = (role, scopes) => __awaiter(void 0, void 0, void 0, function* () {
199
+ alice().role = role;
200
+ const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
201
+ return (yield (yield invoke(svc, caps)).leaseAccess()).canOverride;
202
+ });
203
+ it("is refused for an ADMIN's token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
204
+ (0, chai_1.expect)(yield overrideWith('ADMIN', 'sessions')).to.equal(false);
205
+ }));
206
+ it("is allowed for an ADMIN's or SUPER_ADMIN's token minted with the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
207
+ (0, chai_1.expect)(yield overrideWith('ADMIN', 'admin,sessions')).to.equal(true);
208
+ (0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'admin,sessions')).to.equal(true);
209
+ }));
210
+ // As the SUPER_ADMIN's key pair and Bearer token override.
211
+ it("is allowed for a SUPER_ADMIN's token minted without it", () => __awaiter(void 0, void 0, void 0, function* () {
212
+ (0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'sessions')).to.equal(true);
213
+ }));
214
+ // The scope is fixed when the token is minted; the role is read now. An
215
+ // admin demoted after minting overrides no more.
216
+ it('is refused once the admin who minted it is a member', () => __awaiter(void 0, void 0, void 0, function* () {
217
+ (0, chai_1.expect)(yield overrideWith('MEMBER', 'admin,sessions')).to.equal(false);
218
+ }));
219
+ it("is refused for a member's token", () => __awaiter(void 0, void 0, void 0, function* () {
220
+ (0, chai_1.expect)(yield overrideWith('MEMBER', 'sessions')).to.equal(false);
221
+ }));
222
+ });
223
+ // The owner lookup that fails is not a token that doesn't check out: the
224
+ // create is refused, as for a key whose owner can't be looked up. It ran
225
+ // with no owner (gate off) or was refused as "invalid or expired" (gate on).
226
+ describe('a session token whose owner cannot be looked up', () => {
227
+ for (const on of [false, true]) {
228
+ it(`refuses the create (gate ${on ? 'on' : 'off'})`, () => __awaiter(void 0, void 0, void 0, function* () {
229
+ gate(on);
230
+ findById.rejects(new Error('db down'));
231
+ const caps = capsWith({ sessionToken: yield token() });
232
+ (0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.equal('db down');
233
+ }));
234
+ }
235
+ it('still counts a token that does not check out as none (gate off)', () => __awaiter(void 0, void 0, void 0, function* () {
236
+ gate(false);
237
+ const res = yield invoke(svc, capsWith({ sessionToken: 'not-a-jwt' }));
238
+ (0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
239
+ (0, chai_1.expect)(findById.called).to.equal(false);
240
+ }));
241
+ });
242
+ });
@@ -146,7 +146,7 @@ describe('createSession — credentials never reach the driver or storage', () =
146
146
  verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
147
147
  if (!t.startsWith('jwt:'))
148
148
  throw new Error('bad token');
149
- return { sub: t.slice('jwt:'.length), teamId: null };
149
+ return { sub: t.slice('jwt:'.length), teamId: null, scopes: 'sessions' };
150
150
  }),
151
151
  });
152
152
  // The owner's teams: none, and not read from the developer's database.