@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
|
@@ -108,13 +108,13 @@ paths:
|
|
|
108
108
|
name: Checkout with saved card
|
|
109
109
|
status: failed
|
|
110
110
|
node_id: 3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d
|
|
111
|
-
has_live_video:
|
|
111
|
+
has_live_video: false
|
|
112
112
|
video_recording_enabled: true
|
|
113
113
|
video_recording: 6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b.mp4
|
|
114
114
|
startTime: '2026-10-03T14:20:11.004Z'
|
|
115
115
|
endTime: '2026-10-03T14:22:05.120Z'
|
|
116
116
|
failure_reason: 'NoSuchElementError: An element could not be located'
|
|
117
|
-
failure_category:
|
|
117
|
+
failure_category: ELEMENT_NOT_FOUND
|
|
118
118
|
device_udid: emulator-5554
|
|
119
119
|
device_platform: android
|
|
120
120
|
device_version: '14'
|
|
@@ -923,7 +923,7 @@ paths:
|
|
|
923
923
|
type: string
|
|
924
924
|
example: |
|
|
925
925
|
id,build_id,status,failure_category,failure_reason,device_platform,device_version,device_name,node_id,startTime,endTime,name
|
|
926
|
-
6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,
|
|
926
|
+
6f1c2a8e-3b4d-4e5f-9a7b-1c2d3e4f5a6b,9d2f4c1e-7a3b-4e8f-b6c5-2d1e0f9a8b7c,failed,ELEMENT_NOT_FOUND,"NoSuchElementError: An element could not be located",android,14,Pixel 7,3c9a1f20-8d4e-4b7a-a1c2-5e6f7a8b9c0d,2026-10-03T14:20:11.004Z,2026-10-03T14:22:05.120Z,Checkout with saved card
|
|
927
927
|
'400':
|
|
928
928
|
description: '`format` is neither `json` nor `csv`.'
|
|
929
929
|
content:
|
|
@@ -2688,6 +2688,13 @@ components:
|
|
|
2688
2688
|
failure_category:
|
|
2689
2689
|
type: string
|
|
2690
2690
|
nullable: true
|
|
2691
|
+
description: >-
|
|
2692
|
+
What kind of failure it was: `ELEMENT_NOT_FOUND`, `APP_CRASH`, `TIMEOUT`,
|
|
2693
|
+
`PERMISSION_BLOCKED`, `WDA_FAILURE`, `XENON_COMMAND_FAILURE`, `SYSTEM_OVERLOAD` or
|
|
2694
|
+
`UNKNOWN`, read from the failure reason and the last failed commands when the
|
|
2695
|
+
session fails; or `HUB_RESTART` for a session the server's restart ended. Null for a
|
|
2696
|
+
session that hasn't failed, and for a failure recorded without that analysis.
|
|
2697
|
+
example: ELEMENT_NOT_FOUND
|
|
2691
2698
|
is_profiling_available:
|
|
2692
2699
|
type: boolean
|
|
2693
2700
|
device_info:
|
|
@@ -15,6 +15,8 @@ const typedi_1 = require("typedi");
|
|
|
15
15
|
const ApiKeyService_1 = require("../../services/ApiKeyService");
|
|
16
16
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
17
17
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
18
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
19
|
+
const KNOWN_SCOPES = ['read', 'sessions', 'devices', 'admin'];
|
|
18
20
|
function apiKeysRouter() {
|
|
19
21
|
const r = (0, express_1.Router)();
|
|
20
22
|
r.use((0, roleGuard_1.roleGuard)('ADMIN'));
|
|
@@ -23,11 +25,16 @@ function apiKeysRouter() {
|
|
|
23
25
|
res.json(yield svc.list());
|
|
24
26
|
}));
|
|
25
27
|
r.post('/', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
26
|
-
var _a;
|
|
28
|
+
var _a, _b;
|
|
27
29
|
const { name, scopes, rateLimit, teamId, expiresAt } = req.body;
|
|
28
30
|
if (!name || !Array.isArray(scopes) || scopes.length === 0) {
|
|
29
31
|
return res.status(400).json({ error: 'name and scopes required' });
|
|
30
32
|
}
|
|
33
|
+
// Only the scopes that exist. Any string was stored, so a typo made a key
|
|
34
|
+
// with none of the scopes its maker meant.
|
|
35
|
+
if (!scopes.every((s) => KNOWN_SCOPES.includes(s))) {
|
|
36
|
+
return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
|
|
37
|
+
}
|
|
31
38
|
let expiresAtDate;
|
|
32
39
|
if (expiresAt !== undefined && expiresAt !== null) {
|
|
33
40
|
const d = new Date(expiresAt);
|
|
@@ -39,6 +46,11 @@ function apiKeysRouter() {
|
|
|
39
46
|
}
|
|
40
47
|
expiresAtDate = d;
|
|
41
48
|
}
|
|
49
|
+
// Made with a credential that expires, a key ends no later than it.
|
|
50
|
+
const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.credentialExpiresAt);
|
|
51
|
+
if ('error' in within)
|
|
52
|
+
return res.status(400).json({ error: within.error });
|
|
53
|
+
expiresAtDate = within.expiresAt;
|
|
42
54
|
const { id, raw } = yield svc.create({
|
|
43
55
|
name,
|
|
44
56
|
scopes,
|
|
@@ -47,7 +59,7 @@ function apiKeysRouter() {
|
|
|
47
59
|
userId: req.auth.userId,
|
|
48
60
|
expiresAt: expiresAtDate,
|
|
49
61
|
});
|
|
50
|
-
res.json({ id, key: raw, expiresAt: (
|
|
62
|
+
res.json({ id, key: raw, expiresAt: (_b = expiresAtDate === null || expiresAtDate === void 0 ? void 0 : expiresAtDate.toISOString()) !== null && _b !== void 0 ? _b : null });
|
|
51
63
|
}));
|
|
52
64
|
r.delete('/:id', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
53
65
|
yield svc.revoke(req.params.id);
|
|
@@ -28,11 +28,15 @@ const JwtKeyService_1 = require("../../services/token/JwtKeyService");
|
|
|
28
28
|
const mcpScopes_1 = require("../../services/token/mcpScopes");
|
|
29
29
|
const passwordResetMode_1 = require("../../services/passwordResetMode");
|
|
30
30
|
const passwordResetLink_1 = require("../../services/passwordResetLink");
|
|
31
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
31
32
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
32
33
|
const isSecureFromReq = (req) => req.secure || req.headers['x-forwarded-proto'] === 'https';
|
|
33
34
|
const MCP_TTL_SEC = Number(process.env.XENON_MCP_TOKEN_TTL_SEC || 86400);
|
|
34
35
|
const REST_TTL_SEC = 3600;
|
|
35
36
|
const MINTABLE_AUDIENCES = ['xenon-rest', 'xenon-mcp'];
|
|
37
|
+
function grantsScope(scopesCsv, scope) {
|
|
38
|
+
return scopesCsv.split(',').some((s) => s.trim() === scope);
|
|
39
|
+
}
|
|
36
40
|
function issueToken(auth, body) {
|
|
37
41
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
42
|
var _a, _b, _c, _d;
|
|
@@ -40,7 +44,7 @@ function issueToken(auth, body) {
|
|
|
40
44
|
if (!MINTABLE_AUDIENCES.includes(audience)) {
|
|
41
45
|
throw new Error(`unsupported audience: ${audience}`);
|
|
42
46
|
}
|
|
43
|
-
const expiresIn = audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC;
|
|
47
|
+
const expiresIn = (0, mintedLifetime_1.mintedLifetimeSec)(audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC, auth.credentialExpiresAt);
|
|
44
48
|
const svc = typedi_1.Container.get(JwtKeyService_1.JwtKeyService);
|
|
45
49
|
if (audience === 'xenon-mcp') {
|
|
46
50
|
// Validate the untrusted `scopes` body field before it reaches the mapper:
|
|
@@ -66,7 +70,23 @@ function issueToken(auth, body) {
|
|
|
66
70
|
// Session-token capability (spec §3 item 6 / R9): a sibling credential the
|
|
67
71
|
// client injects as `xe:options.sessionToken` so the Appium createSession
|
|
68
72
|
// interceptor can refuse tokenless direct-connect sessions when the gate is on.
|
|
69
|
-
|
|
73
|
+
//
|
|
74
|
+
// It creates Appium sessions, so it comes only with a grant that includes
|
|
75
|
+
// `appium:use`, which needs the `sessions` scope. Through 2.14 every
|
|
76
|
+
// credential got one, a read-only key included. It carries the scopes a
|
|
77
|
+
// session is judged by: `sessions`, and `admin` (a lease override) only
|
|
78
|
+
// when the minting credential has it and the grant isn't narrower than
|
|
79
|
+
// it: the default grant (no scopes asked for), or a full-admin one.
|
|
80
|
+
if (!grant.granular.includes('appium:use')) {
|
|
81
|
+
return { token, expiresIn, audience, scopes: grant.granular };
|
|
82
|
+
}
|
|
83
|
+
const askedForScopes = Array.isArray(body.scopes) && body.scopes.length > 0;
|
|
84
|
+
const sessionAdmin = grantsScope(auth.scopes, 'admin') && (!askedForScopes || grant.roles.includes('admin'));
|
|
85
|
+
const sessionToken = yield svc.sign({
|
|
86
|
+
sub: auth.userId,
|
|
87
|
+
teamId: (_c = auth.teamId) !== null && _c !== void 0 ? _c : null,
|
|
88
|
+
scopes: sessionAdmin ? 'admin,sessions' : 'sessions',
|
|
89
|
+
}, { audience: 'xenon-session', ttlSeconds: expiresIn });
|
|
70
90
|
return { token, expiresIn, audience, scopes: grant.granular, sessionToken };
|
|
71
91
|
}
|
|
72
92
|
const token = yield svc.sign({ sub: auth.userId, role: auth.role, scopes: auth.scopes, teamId: (_d = auth.teamId) !== null && _d !== void 0 ? _d : null }, { audience, ttlSeconds: expiresIn });
|
|
@@ -139,11 +159,15 @@ function authPublicRouter() {
|
|
|
139
159
|
const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
|
|
140
160
|
// No SMTP and no opt-in log fallback: nothing can reach the user, so
|
|
141
161
|
// don't mint a live credential. Admins issue links from the Users page.
|
|
142
|
-
|
|
162
|
+
// No XENON_PUBLIC_URL: nothing says where the link may point. The
|
|
163
|
+
// request's Host is the asker's to choose, and the asker may not be
|
|
164
|
+
// the account's owner, so it is never used here.
|
|
165
|
+
const base = (0, passwordResetLink_1.resetLinkBase)();
|
|
166
|
+
if (user && user.status === 'ACTIVE' && emailSvc.canDeliver() && base) {
|
|
143
167
|
const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
|
|
144
168
|
const { raw } = yield resetSvc.createToken(user.id);
|
|
145
|
-
const link = (0, passwordResetLink_1.buildResetLink)(
|
|
146
|
-
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link));
|
|
169
|
+
const link = (0, passwordResetLink_1.buildResetLink)(base, raw);
|
|
170
|
+
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(user, link, resetSvc.ttlMs()));
|
|
147
171
|
}
|
|
148
172
|
}
|
|
149
173
|
catch (e) {
|
|
@@ -724,12 +724,20 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
724
724
|
yield typedi_1.Container.get(AndroidH264StreamService_1.default).start(udid, { source: h264Cfg.source });
|
|
725
725
|
}
|
|
726
726
|
else {
|
|
727
|
-
// One capture pipeline per device
|
|
728
|
-
// (a tile's, or this page's own before its player failed) ends before the
|
|
729
|
-
// screencap loop starts.
|
|
727
|
+
// One capture pipeline per device where possible.
|
|
730
728
|
const h264 = typedi_1.Container.get(AndroidH264StreamService_1.default);
|
|
731
|
-
if (
|
|
732
|
-
|
|
729
|
+
if (recording) {
|
|
730
|
+
// A recording reads the MJPEG capture, and H.264 never runs beside it.
|
|
731
|
+
if (h264.getMultiplexer(udid))
|
|
732
|
+
yield h264.stop(udid);
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
// This page shows MJPEG. An H.264 capture still running for the phone
|
|
736
|
+
// (this page's own before its player failed, or a tile's) ends before
|
|
737
|
+
// the screencap loop starts if nobody watches it, else when its last
|
|
738
|
+
// viewer leaves: another tile or tab may still be playing it.
|
|
739
|
+
yield h264.endWhenUnwatched(udid);
|
|
740
|
+
}
|
|
733
741
|
mjpegPort = (yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(udid)).mjpegPort;
|
|
734
742
|
}
|
|
735
743
|
// Lazy-load the screen size if missing: tiles need it to accept taps.
|
|
@@ -59,6 +59,7 @@ const web_config_service_1 = require("../../data-service/web-config-service");
|
|
|
59
59
|
const PluginContext_1 = require("../../PluginContext");
|
|
60
60
|
const labSettings_1 = require("../../services/settings/labSettings");
|
|
61
61
|
const SelfHealingSwitch_1 = require("../../services/settings/SelfHealingSwitch");
|
|
62
|
+
const aiEngineSettings_1 = require("../../services/settings/aiEngineSettings");
|
|
62
63
|
const typedi_1 = require("typedi");
|
|
63
64
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
64
65
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
@@ -1102,6 +1103,34 @@ function streamLiveSessionVideo(request, response) {
|
|
|
1102
1103
|
}
|
|
1103
1104
|
});
|
|
1104
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* The base URL as the dashboard may see it: a user name, password or query
|
|
1108
|
+
* value it carries is masked. Saving one is refused (aiEngineSettings.ts), but
|
|
1109
|
+
* the server's environment can still hold one, and every admin can read this.
|
|
1110
|
+
*/
|
|
1111
|
+
function maskedBaseUrl(url) {
|
|
1112
|
+
if (!url)
|
|
1113
|
+
return url;
|
|
1114
|
+
let parsed;
|
|
1115
|
+
try {
|
|
1116
|
+
parsed = new URL(url);
|
|
1117
|
+
}
|
|
1118
|
+
catch (_a) {
|
|
1119
|
+
return url;
|
|
1120
|
+
}
|
|
1121
|
+
// Shown as given unless something needs masking: toString() would add a '/'.
|
|
1122
|
+
if (!parsed.username && !parsed.password && !parsed.search && !parsed.hash)
|
|
1123
|
+
return url;
|
|
1124
|
+
if (parsed.username || parsed.password) {
|
|
1125
|
+
parsed.username = '***';
|
|
1126
|
+
parsed.password = '';
|
|
1127
|
+
}
|
|
1128
|
+
for (const name of [...new Set(parsed.searchParams.keys())]) {
|
|
1129
|
+
parsed.searchParams.set(name, '***');
|
|
1130
|
+
}
|
|
1131
|
+
parsed.hash = '';
|
|
1132
|
+
return parsed.toString();
|
|
1133
|
+
}
|
|
1105
1134
|
function getGlobalConfig(request, response) {
|
|
1106
1135
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1107
1136
|
try {
|
|
@@ -1110,13 +1139,13 @@ function getGlobalConfig(request, response) {
|
|
|
1110
1139
|
// the default. The pages show this (a saved value alone left them to guess
|
|
1111
1140
|
// at what a setting never saved was), and `defaults` for "restore defaults".
|
|
1112
1141
|
const effective = (0, labSettings_1.effectiveSettings)(typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs, dbConfig);
|
|
1113
|
-
//
|
|
1142
|
+
// The AI engine settings in effect (aiEngineSettings.ts keeps `config` at
|
|
1143
|
+
// them). Never a key: only whether one is set.
|
|
1114
1144
|
const { config } = yield Promise.resolve().then(() => __importStar(require('../../config')));
|
|
1115
|
-
// Sanitize keys - return boolean existence only
|
|
1116
1145
|
const aiConfig = {
|
|
1117
1146
|
aiProvider: config.aiProvider,
|
|
1118
1147
|
aiModel: config.aiModel,
|
|
1119
|
-
aiBaseUrl: config.aiBaseUrl,
|
|
1148
|
+
aiBaseUrl: maskedBaseUrl(config.aiBaseUrl),
|
|
1120
1149
|
geminiModel: config.geminiModel,
|
|
1121
1150
|
openaiModel: config.openaiModel,
|
|
1122
1151
|
anthropicModel: config.anthropicModel,
|
|
@@ -1136,39 +1165,25 @@ function getGlobalConfig(request, response) {
|
|
|
1136
1165
|
}
|
|
1137
1166
|
function updateGlobalConfig(request, response) {
|
|
1138
1167
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1168
|
+
var _a;
|
|
1139
1169
|
try {
|
|
1140
1170
|
const payload = request.body;
|
|
1141
1171
|
// Before anything is saved: a value the cleanup job would act on, such as a
|
|
1142
|
-
// retention window of 0, is refused rather than stored
|
|
1143
|
-
|
|
1172
|
+
// retention window of 0, is refused rather than stored, and so is an AI
|
|
1173
|
+
// provider or address that could not work, now that it outlives a restart.
|
|
1174
|
+
const problem = (_a = (0, labSettings_1.validateSettingsUpdate)(payload !== null && payload !== void 0 ? payload : {})) !== null && _a !== void 0 ? _a : (0, aiEngineSettings_1.validateAiEngineUpdate)(payload !== null && payload !== void 0 ? payload : {});
|
|
1144
1175
|
if (problem) {
|
|
1145
1176
|
return response
|
|
1146
1177
|
.status(400)
|
|
1147
1178
|
.json({ error: 'invalid_setting', field: problem.field, message: problem.message });
|
|
1148
1179
|
}
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
runtimeOverrides.aiModel = payload.aiModel;
|
|
1156
|
-
if (payload.aiBaseUrl !== undefined)
|
|
1157
|
-
runtimeOverrides.aiBaseUrl = payload.aiBaseUrl;
|
|
1158
|
-
if (payload.geminiModel !== undefined)
|
|
1159
|
-
runtimeOverrides.geminiModel = payload.geminiModel;
|
|
1160
|
-
if (payload.openaiModel !== undefined)
|
|
1161
|
-
runtimeOverrides.openaiModel = payload.openaiModel;
|
|
1162
|
-
if (payload.anthropicModel !== undefined)
|
|
1163
|
-
runtimeOverrides.anthropicModel = payload.anthropicModel;
|
|
1164
|
-
if (payload.ollamaModel !== undefined)
|
|
1165
|
-
runtimeOverrides.ollamaModel = payload.ollamaModel;
|
|
1166
|
-
if (Object.keys(runtimeOverrides).length > 0) {
|
|
1167
|
-
const { updateConfig } = yield Promise.resolve().then(() => __importStar(require('../../config')));
|
|
1168
|
-
updateConfig(runtimeOverrides);
|
|
1180
|
+
// Saved, then in force for the next AI call. Through 2.14 the AI fields
|
|
1181
|
+
// changed the running server only, and a restart undid them.
|
|
1182
|
+
const aiEngine = (0, aiEngineSettings_1.aiEngineUpdateOf)(payload !== null && payload !== void 0 ? payload : {});
|
|
1183
|
+
yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(Object.assign(Object.assign({}, payload), aiEngine));
|
|
1184
|
+
if (Object.keys(aiEngine).length > 0) {
|
|
1185
|
+
typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).noteSaved(aiEngine);
|
|
1169
1186
|
}
|
|
1170
|
-
// Persist Web Configs to DB
|
|
1171
|
-
yield typedi_1.Container.get(web_config_service_1.WebConfigService).setConfig(payload);
|
|
1172
1187
|
// The command interceptor asks at every command and never reads the
|
|
1173
1188
|
// database: hand it the saved value, in force from the next command.
|
|
1174
1189
|
if (payload.enableSelfHealing !== undefined) {
|
|
@@ -89,10 +89,12 @@ function getDevices(request, response) {
|
|
|
89
89
|
const [enriched] = yield (0, device_service_1.enrichDevicesWithTeamNames)([match]);
|
|
90
90
|
return response.json(enriched);
|
|
91
91
|
}
|
|
92
|
-
/* dashboard-plugin-url is
|
|
93
|
-
*
|
|
92
|
+
/* dashboard-plugin-url is where this server reaches the appium-dashboard-plugin,
|
|
93
|
+
* and dashboard-plugin-link the base of the link people open; both from
|
|
94
|
+
* configuration, never from the request (app/dashboardPluginLink.ts).
|
|
94
95
|
*/
|
|
95
96
|
const dashboardPluginUrl = request['dashboard-plugin-url'];
|
|
97
|
+
const dashboardPluginLink = request['dashboard-plugin-link'];
|
|
96
98
|
if (dashboardPluginUrl) {
|
|
97
99
|
const response = yield InternalHttpClient_1.InternalHttpClient.get(`${dashboardPluginUrl}/api/sessions?start_time=${SERVER_UP_TIME}`);
|
|
98
100
|
const sessions = ((_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.rows) || [];
|
|
@@ -105,7 +107,7 @@ function getDevices(request, response) {
|
|
|
105
107
|
});
|
|
106
108
|
devices = devices.map((d) => {
|
|
107
109
|
var _a;
|
|
108
|
-
d.dashboard_link = `${
|
|
110
|
+
d.dashboard_link = `${dashboardPluginLink}?device_udid=${d.udid}&start_time=${SERVER_UP_TIME}`;
|
|
109
111
|
d.total_session_count = ((_a = deviceSessionMap[d.udid]) === null || _a === void 0 ? void 0 : _a.length) || 0;
|
|
110
112
|
return d;
|
|
111
113
|
});
|
|
@@ -17,6 +17,7 @@ const ApiKeyService_1 = require("../../services/ApiKeyService");
|
|
|
17
17
|
const UserService_1 = require("../../services/UserService");
|
|
18
18
|
const prisma_1 = require("../../prisma");
|
|
19
19
|
const profileIdentity_1 = require("./profileIdentity");
|
|
20
|
+
const mintedLifetime_1 = require("../../services/token/mintedLifetime");
|
|
20
21
|
const ROLE_SCOPES = {
|
|
21
22
|
SUPER_ADMIN: ['admin'],
|
|
22
23
|
ADMIN: ['devices', 'sessions', 'read'],
|
|
@@ -79,10 +80,23 @@ function profileRouter() {
|
|
|
79
80
|
return res.status(404).json({ error: 'user not found' });
|
|
80
81
|
return res.json({ accessKey: user.accessKey });
|
|
81
82
|
}));
|
|
83
|
+
// Rotating the access key ends every key pair its owner has, whatever their
|
|
84
|
+
// scopes, so it takes the person themselves (a dashboard sign-in) or a
|
|
85
|
+
// credential with the admin scope. Through 2.14 any credential could, a
|
|
86
|
+
// read-only token included, and cut off every one of its owner's clients.
|
|
82
87
|
r.post('/access-key/rotate', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
var _a;
|
|
83
89
|
const auth = requireAuth(req, res);
|
|
84
90
|
if (!auth)
|
|
85
91
|
return;
|
|
92
|
+
const credentialScopes = String((_a = auth.scopes) !== null && _a !== void 0 ? _a : '')
|
|
93
|
+
.split(',')
|
|
94
|
+
.map((s) => s.trim());
|
|
95
|
+
if (auth.kind !== 'user-session' && !credentialScopes.includes('admin')) {
|
|
96
|
+
return res.status(403).json({
|
|
97
|
+
error: 'rotating the access key needs a dashboard sign-in or a credential with the admin scope',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
86
100
|
const userId = yield effectiveUserId(auth.userId);
|
|
87
101
|
if (!userId)
|
|
88
102
|
return res.status(404).json({ error: 'user not found' });
|
|
@@ -132,6 +146,13 @@ function profileRouter() {
|
|
|
132
146
|
}
|
|
133
147
|
expiresAtDate = d;
|
|
134
148
|
}
|
|
149
|
+
// A token made with a credential that expires (a Bearer token, an API key
|
|
150
|
+
// with an expiry) ends no later than it. Through 2.14 a 1-hour Bearer
|
|
151
|
+
// token could make itself a key that never expired.
|
|
152
|
+
const within = (0, mintedLifetime_1.keyExpiryWithin)(expiresAtDate, auth.credentialExpiresAt);
|
|
153
|
+
if ('error' in within)
|
|
154
|
+
return res.status(400).json({ error: within.error });
|
|
155
|
+
expiresAtDate = within.expiresAt;
|
|
135
156
|
const allowed = (_a = ROLE_SCOPES[auth.role]) !== null && _a !== void 0 ? _a : ROLE_SCOPES.MEMBER;
|
|
136
157
|
if (scopes !== undefined && (!Array.isArray(scopes) || !scopes.every(isScope))) {
|
|
137
158
|
return res.status(400).json({ error: `scopes must be some of ${KNOWN_SCOPES.join(', ')}` });
|
|
@@ -141,8 +141,11 @@ function usersRouter() {
|
|
|
141
141
|
});
|
|
142
142
|
}));
|
|
143
143
|
// Admin-issued password reset. Replaces relaying a link out of the server
|
|
144
|
-
// log: with SMTP the link is emailed to the user
|
|
145
|
-
//
|
|
144
|
+
// log: with SMTP and XENON_PUBLIC_URL the link is emailed to the user;
|
|
145
|
+
// otherwise it is returned once, to the admin who asked, and never logged.
|
|
146
|
+
// An emailed link points at XENON_PUBLIC_URL only, never the request's
|
|
147
|
+
// Host: the admin's request could name any host, and the user would get a
|
|
148
|
+
// genuine Xenon email sending their token there.
|
|
146
149
|
r.post('/:id/reset-link', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
147
150
|
const auth = getAuth(req);
|
|
148
151
|
const targetId = req.params.id;
|
|
@@ -168,10 +171,11 @@ function usersRouter() {
|
|
|
168
171
|
const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
|
|
169
172
|
const emailSvc = typedi_1.Container.get(EmailService_1.EmailService);
|
|
170
173
|
const { raw } = yield resetSvc.createToken(target.id);
|
|
171
|
-
const
|
|
174
|
+
const publicBase = (0, passwordResetLink_1.resetLinkBase)();
|
|
172
175
|
const expiresAt = new Date(Date.now() + resetSvc.ttlMs()).toISOString();
|
|
173
|
-
if (emailSvc.hasSmtp()) {
|
|
174
|
-
|
|
176
|
+
if (emailSvc.hasSmtp() && publicBase) {
|
|
177
|
+
const link = (0, passwordResetLink_1.buildResetLink)(publicBase, raw);
|
|
178
|
+
yield emailSvc.send((0, passwordResetLink_1.resetEmail)(target, link, resetSvc.ttlMs()));
|
|
175
179
|
auditLog.info(`${auth.userId} emailed a password-reset link to user ${target.id}`);
|
|
176
180
|
return res.json({ emailed: true, expiresAt });
|
|
177
181
|
}
|
|
@@ -179,6 +183,7 @@ function usersRouter() {
|
|
|
179
183
|
auditLog.warn(`${auth.userId} issued a password-reset link for user ${target.id} (returned to the admin)`);
|
|
180
184
|
// The body is a credential: keep it out of browser and proxy caches.
|
|
181
185
|
res.set('Cache-Control', 'no-store');
|
|
186
|
+
const link = (0, passwordResetLink_1.buildResetLink)(publicBase !== null && publicBase !== void 0 ? publicBase : (0, passwordResetLink_1.requestBase)(req), raw);
|
|
182
187
|
return res.json({ emailed: false, link, expiresAt });
|
|
183
188
|
}));
|
|
184
189
|
r.delete('/:id', (req, res) => __awaiter(this, void 0, void 0, function* () {
|
package/lib/src/app/swagger.js
CHANGED
|
@@ -75,6 +75,8 @@ Appium's WebDriver API (\`POST /session\` and the session's commands, under Appi
|
|
|
75
75
|
- \`accessKey\` and \`token\`;
|
|
76
76
|
- \`sessionToken\`, minted by \`POST /api/auth/token\`.
|
|
77
77
|
|
|
78
|
+
Either names the session's owner only while that user exists and is active, and a session token needs the \`sessions\` scope, as a key does. A key or token that doesn't check out counts as none: with \`XENON_REQUIRE_SESSION_TOKEN\` the session is refused, without it the session runs with no owner.
|
|
79
|
+
|
|
78
80
|
A leased device also takes \`leaseId\` and \`leaseToken\`, which the lease's \`appiumCapabilities\` already carry. Xenon removes these credentials before the driver or any stored record sees them.
|
|
79
81
|
|
|
80
82
|
## Live streams
|
|
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.STREAM_ENDED = void 0;
|
|
15
16
|
exports.encodeWsFrame = encodeWsFrame;
|
|
16
17
|
exports.parseH264WsPath = parseH264WsPath;
|
|
17
18
|
exports.attachH264Ws = attachH264Ws;
|
|
@@ -29,6 +30,12 @@ const nodeSocketRelay_1 = require("./nodeSocketRelay");
|
|
|
29
30
|
* so it is not put on the wire).
|
|
30
31
|
*/
|
|
31
32
|
const TYPE_CODE = { config: 0, key: 1, delta: 2 };
|
|
33
|
+
/**
|
|
34
|
+
* The close code for a stream whose capture stopped under its viewer, as the
|
|
35
|
+
* logcat socket uses it for an upstream that died. The player then shows MJPEG
|
|
36
|
+
* without asking stream/start: there is no H.264 capture left to end.
|
|
37
|
+
*/
|
|
38
|
+
exports.STREAM_ENDED = 1012;
|
|
32
39
|
function encodeWsFrame(p) {
|
|
33
40
|
return Buffer.concat([Buffer.from([TYPE_CODE[p.type]]), p.data]);
|
|
34
41
|
}
|
|
@@ -139,7 +146,9 @@ function attachH264Ws(server, deps) {
|
|
|
139
146
|
if (p.type === 'delta' && ws.bufferedAmount > maxBuffered)
|
|
140
147
|
return;
|
|
141
148
|
ws.send(encodeWsFrame(p));
|
|
142
|
-
}
|
|
149
|
+
},
|
|
150
|
+
// The capture stopped under this viewer (a recording, a stream/stop).
|
|
151
|
+
() => ws.close(exports.STREAM_ENDED, 'stream ended'));
|
|
143
152
|
logger_1.default.info(`[${parsed.udid}] H.264 WS client connected`);
|
|
144
153
|
}));
|
|
145
154
|
}
|
package/lib/src/config.js
CHANGED
|
@@ -87,6 +87,7 @@ exports.config = {
|
|
|
87
87
|
userSessionTtlMs: Number(process.env.XENON_USER_SESSION_TTL_MS) || 24 * 60 * 60 * 1000,
|
|
88
88
|
smtpUrl: process.env.XENON_SMTP_URL,
|
|
89
89
|
smtpFrom: process.env.XENON_SMTP_FROM,
|
|
90
|
+
publicUrl: process.env.XENON_PUBLIC_URL,
|
|
90
91
|
resetTokenTtlMs: Number(process.env.XENON_RESET_TOKEN_TTL_MS) || 60 * 60 * 1000,
|
|
91
92
|
// Opt-in since 1.20.7: it writes the raw reset link (a credential) to the
|
|
92
93
|
// server log. Without SMTP, admins issue links from the Users page instead.
|
|
@@ -59,10 +59,10 @@ const xenonScripts_1 = require("../interceptors/xenonScripts");
|
|
|
59
59
|
*
|
|
60
60
|
* Each answers `{ recorded: true }` when it wrote, or `{ recorded: false,
|
|
61
61
|
* message }` saying why nothing was written: no record of the session on this
|
|
62
|
-
* server (
|
|
63
|
-
* hub's sessions), an argument it can't use, or a failed write.
|
|
64
|
-
* fails the test's command. They are bookkeeping, and a test
|
|
65
|
-
* because the server it ran on
|
|
62
|
+
* server (it runs without `enableDashboard`, or it is a node, which keeps
|
|
63
|
+
* none for the hub's sessions), an argument it can't use, or a failed write.
|
|
64
|
+
* None of them fails the test's command. They are bookkeeping, and a test
|
|
65
|
+
* shouldn't die because the server it ran on records no sessions. Through 2.13.2
|
|
66
66
|
* setSessionName, setSessionStatus and debug failed the command there
|
|
67
67
|
* (Prisma's P2025, a foreign-key error), and addTag and captureEvidence
|
|
68
68
|
* answered `null` as if they had worked.
|
|
@@ -132,7 +132,7 @@ class DashboardCommands {
|
|
|
132
132
|
});
|
|
133
133
|
if (!row) {
|
|
134
134
|
answer = notRecorded(`This server keeps no record of session ${sessionId}, so nothing was saved. ` +
|
|
135
|
-
'Sessions are recorded
|
|
135
|
+
'Sessions are recorded by a server started with enableDashboard, never by a node.');
|
|
136
136
|
}
|
|
137
137
|
else {
|
|
138
138
|
switch (command) {
|