@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.
- package/README.md +7 -4
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
- package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +5 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +49 -61
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +20 -2
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/interceptors/CommandInterceptor.js +243 -181
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +8 -3
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/OrphanSweeper.js +2 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +86 -26
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +55 -72
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +5 -5
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- /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({
|
|
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({
|
|
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
|
|
168
|
-
restore = withConfig({
|
|
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)(
|
|
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
|
});
|