@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
@@ -39,7 +39,12 @@ describe('profile router', () => {
39
39
  }));
40
40
  it('POST /profile/access-key/rotate returns a new accessKey', () => __awaiter(void 0, void 0, void 0, function* () {
41
41
  sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey').resolves({ id: 'u1', accessKey: 'xen_NEW' });
42
- const app = appWithAuth({ userId: 'u1', role: 'ADMIN', scopes: 'devices,sessions,read' });
42
+ const app = appWithAuth({
43
+ kind: 'user-session',
44
+ userId: 'u1',
45
+ role: 'ADMIN',
46
+ scopes: 'admin,devices,sessions,read',
47
+ });
43
48
  const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
44
49
  (0, chai_1.expect)(r.body.accessKey).to.equal('xen_NEW');
45
50
  }));
@@ -147,4 +152,120 @@ describe('profile router', () => {
147
152
  const r = yield (0, loopbackRequest_1.default)(app).delete('/profile/tokens/k1');
148
153
  (0, chai_1.expect)(r.status).to.equal(404);
149
154
  }));
155
+ // A credential can't do more to its owner's account than it could itself
156
+ // have been made to do.
157
+ describe('what the credential in use limits', () => {
158
+ // Rotating the access key ends every key pair its owner has, whatever
159
+ // their scopes, so it takes the person themselves (a dashboard sign-in)
160
+ // or a credential with the admin scope. Any credential, a read-only
161
+ // token included, could do it.
162
+ describe('POST /profile/access-key/rotate', () => {
163
+ let rotate;
164
+ beforeEach(() => {
165
+ rotate = sinon_1.default
166
+ .stub(typedi_1.Container.get(UserService_1.UserService), 'rotateAccessKey')
167
+ .resolves({ id: 'u1', accessKey: 'xen_NEW' });
168
+ });
169
+ for (const [kind, scopes] of [
170
+ ['bearer', 'read'],
171
+ ['api-key', 'read'],
172
+ ['api-key', 'devices,sessions,read'],
173
+ ['bearer', 'sessions'],
174
+ ]) {
175
+ it(`is refused to ${kind === 'bearer' ? 'a Bearer token' : 'an API key'} with ${scopes}`, () => __awaiter(void 0, void 0, void 0, function* () {
176
+ const app = appWithAuth({ kind, userId: 'u1', role: 'ADMIN', scopes });
177
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate');
178
+ (0, chai_1.expect)(r.status).to.equal(403);
179
+ (0, chai_1.expect)(r.body.error).to.match(/dashboard sign-in or a credential with the admin scope/);
180
+ (0, chai_1.expect)(rotate.called).to.equal(false);
181
+ }));
182
+ }
183
+ it('is allowed to a dashboard sign-in, a member included', () => __awaiter(void 0, void 0, void 0, function* () {
184
+ const app = appWithAuth({
185
+ kind: 'user-session',
186
+ userId: 'u1',
187
+ role: 'MEMBER',
188
+ scopes: 'devices,sessions,read',
189
+ });
190
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
191
+ }));
192
+ it('is allowed to a credential with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
193
+ const app = appWithAuth({
194
+ kind: 'api-key',
195
+ userId: 'u1',
196
+ role: 'SUPER_ADMIN',
197
+ scopes: 'admin',
198
+ });
199
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/access-key/rotate')).status).to.equal(200);
200
+ }));
201
+ });
202
+ // A token made with a credential that expires ends no later than it: a
203
+ // 1-hour Bearer token made itself a key that never expired.
204
+ describe('POST /profile/tokens from a credential that expires', () => {
205
+ const inAnHour = new Date(Date.now() + 3600000);
206
+ const bearer = () => appWithAuth({
207
+ kind: 'bearer',
208
+ userId: 'u1',
209
+ role: 'MEMBER',
210
+ scopes: 'sessions,read',
211
+ credentialExpiresAt: inAnHour.getTime(),
212
+ });
213
+ let create;
214
+ beforeEach(() => {
215
+ create = sinon_1.default
216
+ .stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
217
+ .resolves({ id: 'k9', raw: 'raw' });
218
+ });
219
+ it('expires with that credential when no expiresAt is asked for', () => __awaiter(void 0, void 0, void 0, function* () {
220
+ var _a;
221
+ const r = yield (0, loopbackRequest_1.default)(bearer()).post('/profile/tokens').send({ name: 'CI' });
222
+ (0, chai_1.expect)(r.status).to.equal(201);
223
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(inAnHour.toISOString());
224
+ (0, chai_1.expect)((_a = create.firstCall.args[0].expiresAt) === null || _a === void 0 ? void 0 : _a.getTime()).to.equal(inAnHour.getTime());
225
+ }));
226
+ it('refuses an expiresAt later than that credential', () => __awaiter(void 0, void 0, void 0, function* () {
227
+ const later = new Date(inAnHour.getTime() + 60000).toISOString();
228
+ const r = yield (0, loopbackRequest_1.default)(bearer())
229
+ .post('/profile/tokens')
230
+ .send({ name: 'CI', expiresAt: later });
231
+ (0, chai_1.expect)(r.status).to.equal(400);
232
+ (0, chai_1.expect)(r.body.error).to.match(/later than the credential/);
233
+ (0, chai_1.expect)(create.called).to.equal(false);
234
+ }));
235
+ // Verification allows 60 s of clock skew, so a token can still be
236
+ // accepted just after its exp: it must not make a key already expired.
237
+ it('refuses when that credential has already expired', () => __awaiter(void 0, void 0, void 0, function* () {
238
+ const app = appWithAuth({
239
+ kind: 'bearer',
240
+ userId: 'u1',
241
+ role: 'MEMBER',
242
+ scopes: 'sessions,read',
243
+ credentialExpiresAt: Date.now() - 1000,
244
+ });
245
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
246
+ (0, chai_1.expect)(r.status).to.equal(400);
247
+ (0, chai_1.expect)(r.body.error).to.match(/has expired/);
248
+ (0, chai_1.expect)(create.called).to.equal(false);
249
+ }));
250
+ it('keeps an earlier expiresAt', () => __awaiter(void 0, void 0, void 0, function* () {
251
+ const sooner = new Date(Date.now() + 600000).toISOString();
252
+ const r = yield (0, loopbackRequest_1.default)(bearer())
253
+ .post('/profile/tokens')
254
+ .send({ name: 'CI', expiresAt: sooner });
255
+ (0, chai_1.expect)(r.status).to.equal(201);
256
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(sooner);
257
+ }));
258
+ it('leaves a dashboard sign-in free to make a key that never expires', () => __awaiter(void 0, void 0, void 0, function* () {
259
+ const app = appWithAuth({
260
+ kind: 'user-session',
261
+ userId: 'u1',
262
+ role: 'MEMBER',
263
+ scopes: 'devices,sessions,read',
264
+ });
265
+ const r = yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' });
266
+ (0, chai_1.expect)(r.status).to.equal(201);
267
+ (0, chai_1.expect)(r.body.expiresAt).to.equal(null);
268
+ }));
269
+ });
270
+ });
150
271
  });
@@ -126,7 +126,7 @@ describe('createSession records who asked on the pending-session row', () => {
126
126
  typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
127
127
  verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
128
128
  const [sub, teamId] = t.replace(/^jwt:/, '').split('@');
129
- return { sub, teamId: teamId !== null && teamId !== void 0 ? teamId : null };
129
+ return { sub, teamId: teamId !== null && teamId !== void 0 ? teamId : null, scopes: 'sessions' };
130
130
  }),
131
131
  });
132
132
  teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
@@ -0,0 +1,266 @@
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 express_1 = __importDefault(require("express"));
19
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
20
+ const typedi_1 = require("typedi");
21
+ const users_1 = require("../../src/app/routers/users");
22
+ const auth_1 = require("../../src/app/routers/auth");
23
+ const PasswordResetService_1 = require("../../src/services/PasswordResetService");
24
+ const EmailService_1 = require("../../src/services/EmailService");
25
+ const UserService_1 = require("../../src/services/UserService");
26
+ const prisma_1 = require("../../src/prisma");
27
+ const config_1 = require("../../src/config");
28
+ const passwordResetMode_1 = require("../../src/services/passwordResetMode");
29
+ const passwordResetLink_1 = require("../../src/services/passwordResetLink");
30
+ /**
31
+ * Where a password reset link that Xenon sends points.
32
+ *
33
+ * Through 2.14 it was built from the Host header of the request that asked
34
+ * for it. Anyone could ask for a reset of someone else's account with a Host
35
+ * of their own choosing (a matching Origin passes the CSRF check), and with
36
+ * SMTP set up Xenon emailed the victim a genuine link to the attacker's host:
37
+ * opening it handed the attacker the token, which opens the account.
38
+ *
39
+ * A link Xenon emails or logs is now built from XENON_PUBLIC_URL, the address
40
+ * people open the dashboard at. Without it Xenon sends no link at all: the
41
+ * sign-in page sends people to an administrator, and an administrator's
42
+ * "send reset link" hands the link back to them instead of emailing it.
43
+ */
44
+ const RAW = 'RAWTOKEN_abcdefghijklmnopqrstuvwxyz0123456789';
45
+ const EVIL = 'attacker.example';
46
+ function withConfig(patch) {
47
+ const orig = {};
48
+ for (const k of Object.keys(patch)) {
49
+ orig[k] = config_1.config[k];
50
+ config_1.config[k] = patch[k];
51
+ }
52
+ return () => Object.assign(config_1.config, orig);
53
+ }
54
+ function until(pred_1) {
55
+ return __awaiter(this, arguments, void 0, function* (pred, ms = 3000) {
56
+ const t0 = Date.now();
57
+ while (!pred()) {
58
+ if (Date.now() - t0 > ms)
59
+ throw new Error('timed out waiting for the route to finish');
60
+ yield new Promise((r) => setTimeout(r, 5));
61
+ }
62
+ });
63
+ }
64
+ describe('password reset links point at XENON_PUBLIC_URL, never the request', () => {
65
+ let restore;
66
+ afterEach(() => {
67
+ sinon_1.default.restore();
68
+ restore === null || restore === void 0 ? void 0 : restore();
69
+ restore = undefined;
70
+ });
71
+ function publicApp() {
72
+ const app = (0, express_1.default)();
73
+ app.use(express_1.default.json());
74
+ app.use('/auth', (0, auth_1.authPublicRouter)());
75
+ return app;
76
+ }
77
+ function usersApp() {
78
+ const app = (0, express_1.default)();
79
+ app.use(express_1.default.json());
80
+ app.use((req, _res, next) => {
81
+ req.auth = {
82
+ userId: 'admin1',
83
+ role: 'ADMIN',
84
+ scopes: 'admin,devices,sessions,read',
85
+ };
86
+ next();
87
+ });
88
+ app.use('/users', (0, users_1.usersRouter)());
89
+ return app;
90
+ }
91
+ const active = { id: 'u1', email: 'u@x.local', name: 'U', role: 'MEMBER', status: 'ACTIVE' };
92
+ describe('POST /auth/forgot-password', () => {
93
+ it('emails a link to XENON_PUBLIC_URL whatever Host the request names', () => __awaiter(void 0, void 0, void 0, function* () {
94
+ restore = withConfig({
95
+ smtpUrl: 'smtp://mail.example.com:587',
96
+ publicUrl: 'https://xenon.example.com',
97
+ });
98
+ sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
99
+ sinon_1.default
100
+ .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
101
+ .resolves({ raw: RAW, id: 't1' });
102
+ const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
103
+ yield (0, loopbackRequest_1.default)(publicApp())
104
+ .post('/auth/forgot-password')
105
+ .set('Host', EVIL)
106
+ .set('X-Forwarded-Proto', 'http')
107
+ .send({ email: 'u@x.local' });
108
+ yield until(() => send.called);
109
+ const text = send.firstCall.args[0].text;
110
+ (0, chai_1.expect)(text).to.include(`https://xenon.example.com/xenon/reset-password#${RAW}`);
111
+ (0, chai_1.expect)(text).to.not.include(EVIL);
112
+ }));
113
+ it("builds a link that opens when XENON_PUBLIC_URL is the dashboard's address", () => __awaiter(void 0, void 0, void 0, function* () {
114
+ restore = withConfig({
115
+ smtpUrl: 'smtp://mail.example.com:587',
116
+ publicUrl: 'http://localhost:4723/xenon/',
117
+ });
118
+ sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
119
+ sinon_1.default
120
+ .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
121
+ .resolves({ raw: RAW, id: 't1' });
122
+ const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
123
+ yield (0, loopbackRequest_1.default)(publicApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
124
+ yield until(() => send.called);
125
+ (0, chai_1.expect)(send.firstCall.args[0].text).to.include(`http://localhost:4723/xenon/reset-password#${RAW}`);
126
+ (0, chai_1.expect)(send.firstCall.args[0].text).to.not.include('/xenon/xenon/');
127
+ }));
128
+ it('sends nothing, and mints nothing, when XENON_PUBLIC_URL is not set', () => __awaiter(void 0, void 0, void 0, function* () {
129
+ restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587', publicUrl: undefined });
130
+ const find = sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
131
+ const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
132
+ const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
133
+ const r = yield (0, loopbackRequest_1.default)(publicApp())
134
+ .post('/auth/forgot-password')
135
+ .set('Host', EVIL)
136
+ .send({ email: 'u@x.local' });
137
+ yield until(() => find.called);
138
+ yield new Promise((resolve) => setImmediate(resolve));
139
+ (0, chai_1.expect)(r.status).to.equal(204);
140
+ (0, chai_1.expect)(create.called).to.equal(false);
141
+ (0, chai_1.expect)(send.called).to.equal(false);
142
+ }));
143
+ it('sends nothing through the log fallback without XENON_PUBLIC_URL either', () => __awaiter(void 0, void 0, void 0, function* () {
144
+ restore = withConfig({
145
+ smtpUrl: undefined,
146
+ passwordResetLogFallback: true,
147
+ publicUrl: undefined,
148
+ });
149
+ const find = sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail').resolves(active);
150
+ const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
151
+ yield (0, loopbackRequest_1.default)(publicApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
152
+ yield until(() => find.called);
153
+ yield new Promise((resolve) => setImmediate(resolve));
154
+ (0, chai_1.expect)(create.called).to.equal(false);
155
+ }));
156
+ });
157
+ describe('GET /auth/options tells the sign-in page whether self-service reset works', () => {
158
+ it("is 'admin' with SMTP but no XENON_PUBLIC_URL, 'email' with both", () => __awaiter(void 0, void 0, void 0, function* () {
159
+ restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587', publicUrl: undefined });
160
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(publicApp()).get('/auth/options')).body.passwordReset).to.equal('admin');
161
+ config_1.config.publicUrl = 'https://xenon.example.com';
162
+ (0, chai_1.expect)((yield (0, loopbackRequest_1.default)(publicApp()).get('/auth/options')).body.passwordReset).to.equal('email');
163
+ }));
164
+ });
165
+ describe('POST /users/:id/reset-link', () => {
166
+ beforeEach(() => {
167
+ sinon_1.default.stub(prisma_1.prisma.user, 'findUnique').resolves(active);
168
+ sinon_1.default
169
+ .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
170
+ .resolves({ raw: RAW, id: 't1' });
171
+ });
172
+ it('emails a link to XENON_PUBLIC_URL whatever Host the request names', () => __awaiter(void 0, void 0, void 0, function* () {
173
+ restore = withConfig({
174
+ smtpUrl: 'smtp://mail.example.com:587',
175
+ publicUrl: 'https://xenon.example.com',
176
+ });
177
+ const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
178
+ const r = yield (0, loopbackRequest_1.default)(usersApp()).post('/users/u1/reset-link').set('Host', EVIL);
179
+ (0, chai_1.expect)(r.body).to.include({ emailed: true });
180
+ (0, chai_1.expect)(send.firstCall.args[0].text).to.include(`https://xenon.example.com/xenon/reset-password#${RAW}`);
181
+ (0, chai_1.expect)(send.firstCall.args[0].text).to.not.include(EVIL);
182
+ }));
183
+ it('without XENON_PUBLIC_URL, hands the link to the admin instead of emailing it', () => __awaiter(void 0, void 0, void 0, function* () {
184
+ restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587', publicUrl: undefined });
185
+ const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
186
+ const r = yield (0, loopbackRequest_1.default)(usersApp()).post('/users/u1/reset-link');
187
+ (0, chai_1.expect)(r.status).to.equal(200);
188
+ (0, chai_1.expect)(r.body.emailed).to.equal(false);
189
+ (0, chai_1.expect)(r.body.link).to.match(new RegExp(`/xenon/reset-password#${RAW}$`));
190
+ (0, chai_1.expect)(send.called).to.equal(false);
191
+ }));
192
+ });
193
+ describe('resetLinkBase (XENON_PUBLIC_URL)', () => {
194
+ // The variable is the server's address. The dashboard's own address,
195
+ // with /xenon (README gives http://localhost:4723/xenon/), is taken too:
196
+ // kept as given it made /xenon/xenon/reset-password, which the dashboard
197
+ // routes to the sign-in page, losing the token.
198
+ it('is the server address, with or without a trailing slash', () => {
199
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://xenon.example.com' })).to.equal('https://xenon.example.com');
200
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://xenon.example.com/' })).to.equal('https://xenon.example.com');
201
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'http://lab-mac:4723' })).to.equal('http://lab-mac:4723');
202
+ });
203
+ for (const publicUrl of [
204
+ 'http://localhost:4723/xenon',
205
+ 'http://localhost:4723/xenon/',
206
+ 'http://localhost:4723/xenon//',
207
+ ]) {
208
+ it(`takes the dashboard's address too: ${publicUrl}`, () => {
209
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl })).to.equal('http://localhost:4723');
210
+ (0, chai_1.expect)((0, passwordResetLink_1.buildResetLink)(String((0, passwordResetLink_1.resetLinkBase)({ publicUrl })), 'TOK')).to.equal('http://localhost:4723/xenon/reset-password#TOK');
211
+ });
212
+ }
213
+ // The dashboard loads its pages, scripts and API from /xenon on the
214
+ // server's root, so it can't be served under another prefix: an address
215
+ // with any other path is a mistake, and no link is sent rather than one
216
+ // that can't open.
217
+ it('is null for an address with any other path', () => {
218
+ for (const publicUrl of [
219
+ 'https://lab.example.com/qa/',
220
+ 'https://lab.example.com/qa/xenon',
221
+ 'http://localhost:4723/xenon/login',
222
+ ]) {
223
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl }), publicUrl).to.equal(null);
224
+ }
225
+ });
226
+ it('is null when unset, or not an http(s) address', () => {
227
+ for (const publicUrl of [
228
+ undefined,
229
+ '',
230
+ ' ',
231
+ 'not a url',
232
+ 'ftp://x.example',
233
+ 'javascript:alert(1)',
234
+ ]) {
235
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl }), String(publicUrl)).to.equal(null);
236
+ }
237
+ });
238
+ it('is null for an address with a query or fragment, which would swallow the token', () => {
239
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://x.example/?a=1' })).to.equal(null);
240
+ (0, chai_1.expect)((0, passwordResetLink_1.resetLinkBase)({ publicUrl: 'https://x.example/#a' })).to.equal(null);
241
+ });
242
+ });
243
+ describe('passwordResetMode', () => {
244
+ it("is 'email' only with both SMTP and XENON_PUBLIC_URL", () => {
245
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://m:587', publicUrl: 'https://x.example' })).to.equal('email');
246
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ smtpUrl: 'smtp://m:587' })).to.equal('admin');
247
+ (0, chai_1.expect)((0, passwordResetMode_1.passwordResetMode)({ publicUrl: 'https://x.example' })).to.equal('admin');
248
+ });
249
+ });
250
+ // The email said "1 hour" whatever XENON_RESET_TOKEN_TTL_MS was.
251
+ describe('the email says how long the link really lasts', () => {
252
+ const user = { email: 'u@x.local', name: 'U' };
253
+ for (const [ms, words] of [
254
+ [60 * 60 * 1000, '1 hour'],
255
+ [30 * 60 * 1000, '30 minutes'],
256
+ [2 * 60 * 60 * 1000, '2 hours'],
257
+ [90 * 60 * 1000, '90 minutes'],
258
+ [24 * 60 * 60 * 1000, '24 hours'],
259
+ [60 * 1000, '1 minute'],
260
+ ]) {
261
+ it(`${words} for ${ms} ms`, () => {
262
+ (0, chai_1.expect)((0, passwordResetLink_1.resetEmail)(user, 'https://x/l', ms).text).to.include(`This link expires in ${words}.`);
263
+ });
264
+ }
265
+ });
266
+ });
@@ -82,7 +82,10 @@ describe('password-reset links', () => {
82
82
  (0, chai_1.expect)(logged.join('\n')).to.not.include(RAW);
83
83
  }));
84
84
  it('with SMTP, emails the user and returns no link', () => __awaiter(void 0, void 0, void 0, function* () {
85
- restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587' });
85
+ restore = withConfig({
86
+ smtpUrl: 'smtp://mail.example.com:587',
87
+ publicUrl: 'https://xenon.example.com',
88
+ });
86
89
  sinon_1.default.stub(prisma_1.prisma.user, 'findUnique').resolves(member);
87
90
  sinon_1.default
88
91
  .stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
@@ -164,8 +167,11 @@ describe('password-reset links', () => {
164
167
  (0, chai_1.expect)(r.status).to.equal(204);
165
168
  (0, chai_1.expect)(create.called).to.equal(false);
166
169
  }));
167
- it('still mints and sends when SMTP is configured', () => __awaiter(void 0, void 0, void 0, function* () {
168
- restore = withConfig({ smtpUrl: 'smtp://mail.example.com:587' });
170
+ it('still mints and sends when SMTP and XENON_PUBLIC_URL are configured', () => __awaiter(void 0, void 0, void 0, function* () {
171
+ restore = withConfig({
172
+ smtpUrl: 'smtp://mail.example.com:587',
173
+ publicUrl: 'https://xenon.example.com',
174
+ });
169
175
  sinon_1.default
170
176
  .stub(typedi_1.Container.get(UserService_1.UserService), 'findByEmail')
171
177
  .resolves({ id: 'u1', email: 'u@x.local', name: 'U', status: 'ACTIVE' });
@@ -181,7 +187,7 @@ describe('password-reset links', () => {
181
187
  });
182
188
  describe('token placement (never in a URL the server sees)', () => {
183
189
  it('puts the token in the fragment, which browsers never send', () => {
184
- const link = (0, passwordResetLink_1.buildResetLink)({ secure: false, headers: { host: 'lab:4723' } }, RAW);
190
+ const link = (0, passwordResetLink_1.buildResetLink)('http://lab:4723', RAW);
185
191
  (0, chai_1.expect)(link).to.equal(`http://lab:4723/xenon/reset-password#${RAW}`);
186
192
  (0, chai_1.expect)(new URL(link).pathname).to.not.include(RAW);
187
193
  });