@xenon-device-management/xenon 2.14.0 → 2.16.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 +44 -29
- package/lib/package.json +6 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/ai-settings-DU_Ls06P.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BgGZ8AvV.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/forgot-password-Bjmf49i6.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-DeD__6wa.js} +6 -6
- package/lib/public/assets/{index-DJGc1M2f.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/index-B0JeuZbg.js +15 -0
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-Cm0tmSoc.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/runbook-page-DEQ-DBOX.js +304 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +16 -12
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- 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 +12 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +31 -6
- 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 +12 -6
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +64 -204
- package/lib/src/dashboard/services/failure-analysis-service.js +118 -105
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- 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/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -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 +29 -3
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +6 -11
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- 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/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +256 -196
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- 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 +16 -6
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +8 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +21 -6
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +118 -44
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +380 -45
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- 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 +57 -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/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +358 -0
- package/lib/src/services/logcat/deviceLogBook.js +164 -0
- package/lib/src/services/logcat/iosDriverLog.js +178 -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/selector-health/SelectorVisibilityResolver.js +121 -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/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +4 -1
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/androidDevices.spec.js +1 -1
- 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/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- 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/selector-state-flow.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/XenonCapabilityManager.spec.js +23 -0
- 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-adb-retry.spec.js +112 -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/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +255 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +251 -0
- package/lib/test/unit/failure-analysis.spec.js +196 -0
- package/lib/test/unit/failure-categories.spec.js +130 -0
- package/lib/test/unit/failure-categorization.spec.js +393 -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 +74 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +243 -1
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -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/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- 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/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- 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/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +28 -19
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- 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-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +356 -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/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- 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/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -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 +6 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +16 -12
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- 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/index-BbKEXUFH.js +0 -219
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
2
18
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
19
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
20
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
21
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
22
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
23
|
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
8
41
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
42
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
43
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -19,9 +52,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
19
52
|
};
|
|
20
53
|
var SocketServer_1;
|
|
21
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.SocketServer = void 0;
|
|
55
|
+
exports.SocketServer = exports.SOCKET_RECHECK_INTERVAL_MS = void 0;
|
|
23
56
|
const socket_io_1 = require("socket.io");
|
|
24
57
|
const typedi_1 = require("typedi");
|
|
58
|
+
const jose = __importStar(require("jose"));
|
|
25
59
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
60
|
const config_1 = require("../config");
|
|
27
61
|
const ApiKeyService_1 = require("./ApiKeyService");
|
|
@@ -33,9 +67,40 @@ const SocketEvents_1 = require("../enums/SocketEvents");
|
|
|
33
67
|
const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
34
68
|
const DeviceTeamResolver_1 = require("./device-access/DeviceTeamResolver");
|
|
35
69
|
const upgradeRouter_1 = require("../app/ws/upgradeRouter");
|
|
70
|
+
const sessionCommandSummary_1 = require("../dashboard/sessionCommandSummary");
|
|
71
|
+
const SelectorVisibilityResolver_1 = require("./selector-health/SelectorVisibilityResolver");
|
|
72
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
36
73
|
/** socket.io's default path, which the dashboard and nodes connect to. */
|
|
37
74
|
const SOCKET_IO_PATH = '/socket.io';
|
|
75
|
+
/**
|
|
76
|
+
* How often every socket is checked again, for a change this server didn't
|
|
77
|
+
* make itself (another server sharing the database, a direct edit). Changes
|
|
78
|
+
* made through Xenon reach a socket at once (onIdentityChanged).
|
|
79
|
+
*/
|
|
80
|
+
exports.SOCKET_RECHECK_INTERVAL_MS = 60000;
|
|
81
|
+
/** setTimeout fires at once for a longer delay. */
|
|
82
|
+
const MAX_TIMER_MS = 2147483647;
|
|
83
|
+
/** How many sockets a sweep checks at once, so a large lab's sweep is no burst of queries. */
|
|
84
|
+
const SWEEP_BATCH = 10;
|
|
85
|
+
/**
|
|
86
|
+
* The credential no longer admits the socket: wrong, revoked, expired, or its
|
|
87
|
+
* user inactive or gone. Anything else thrown while checking means the check
|
|
88
|
+
* could not run (the database or signing key unavailable).
|
|
89
|
+
*/
|
|
90
|
+
class SocketRefused extends Error {
|
|
91
|
+
}
|
|
38
92
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
93
|
+
const EVENT_LOG_KEEPS = new Map([
|
|
94
|
+
[SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, null],
|
|
95
|
+
[SocketEvents_1.SocketEvents.SESSION_COMMAND, sessionCommandSummary_1.sessionCommandSummary],
|
|
96
|
+
]);
|
|
97
|
+
/** A row's expiresAt as epoch ms, or undefined for none. */
|
|
98
|
+
function msOf(at) {
|
|
99
|
+
if (!at)
|
|
100
|
+
return undefined;
|
|
101
|
+
const ms = new Date(at).getTime();
|
|
102
|
+
return Number.isNaN(ms) ? undefined : ms;
|
|
103
|
+
}
|
|
39
104
|
function readCookie(cookieHeader, name) {
|
|
40
105
|
if (!cookieHeader)
|
|
41
106
|
return undefined;
|
|
@@ -55,8 +120,23 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
55
120
|
this.nodes = new Map(); // socketId -> nodeHost
|
|
56
121
|
/** The tail of each phone's pending event deliveries (see emitToDashboardForDevices). */
|
|
57
122
|
this.deliveries = new Map();
|
|
123
|
+
/** The tail of each selector's pending event deliveries (see emitToDashboardForSelector). */
|
|
124
|
+
this.selectorDeliveries = new Map();
|
|
125
|
+
/** The timer that checks a socket again when its credential ends, by socket id. */
|
|
126
|
+
this.expiryTimers = new Map();
|
|
127
|
+
/**
|
|
128
|
+
* Each socket's checks, numbered as they start. A check's outcome is
|
|
129
|
+
* applied unless one that started after it has already applied its own.
|
|
130
|
+
*/
|
|
131
|
+
this.checks = new WeakMap();
|
|
132
|
+
/** The check of a socket whose handshake overlapped a change; it joins the dashboard room after it. */
|
|
133
|
+
this.connectChecks = new WeakMap();
|
|
134
|
+
/** Counts the identity changes, so a handshake that overlapped one checks again. */
|
|
135
|
+
this.identityEpoch = 0;
|
|
136
|
+
this.sweeping = false;
|
|
58
137
|
}
|
|
59
|
-
initialize(server) {
|
|
138
|
+
initialize(server, options = {}) {
|
|
139
|
+
var _a;
|
|
60
140
|
// socket.io (engine.io) takes its websocket transport by adding its own
|
|
61
141
|
// `upgrade` listener, which would also see every other upgrade and end
|
|
62
142
|
// the ones it doesn't own after 1 s. The upgrade router moves that
|
|
@@ -74,9 +154,12 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
74
154
|
}));
|
|
75
155
|
this.io.use((socket, next) => __awaiter(this, void 0, void 0, function* () {
|
|
76
156
|
try {
|
|
77
|
-
const
|
|
157
|
+
const epoch = this.identityEpoch;
|
|
158
|
+
const { identity, endsAt } = yield this.check(socket);
|
|
78
159
|
socket.data.identity = identity;
|
|
79
160
|
socket.data.principal = identity.principal;
|
|
161
|
+
socket.data.endsAt = endsAt;
|
|
162
|
+
socket.data.checkedEpoch = epoch;
|
|
80
163
|
next();
|
|
81
164
|
}
|
|
82
165
|
catch (err) {
|
|
@@ -88,6 +171,13 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
88
171
|
const socketId = socket.id;
|
|
89
172
|
const principal = socket.data.principal;
|
|
90
173
|
logger_1.default.info(`[SocketServer] New connection: ${socketId} (principal=${principal})`);
|
|
174
|
+
this.armExpiry(socket);
|
|
175
|
+
// A user changed while this handshake was being checked. The change
|
|
176
|
+
// found no socket of theirs to check yet, so this one may hold what was
|
|
177
|
+
// true before it.
|
|
178
|
+
if (socket.data.checkedEpoch !== this.identityEpoch) {
|
|
179
|
+
this.connectChecks.set(socket, this.recheck(socket));
|
|
180
|
+
}
|
|
91
181
|
// Protocol version handshake — still runs after auth so a logged-in
|
|
92
182
|
// client on the wrong protocol version still gets a clean disconnect.
|
|
93
183
|
socket.on(SocketEvents_1.SocketEvents.HANDSHAKE, (data) => {
|
|
@@ -112,16 +202,22 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
112
202
|
// Notify dashboard about new node
|
|
113
203
|
this.emitToDashboard(SocketEvents_1.SocketEvents.NODE_CONNECTED, { host });
|
|
114
204
|
});
|
|
115
|
-
socket.on(SocketEvents_1.SocketEvents.REGISTER_DASHBOARD, () => {
|
|
205
|
+
socket.on(SocketEvents_1.SocketEvents.REGISTER_DASHBOARD, () => __awaiter(this, void 0, void 0, function* () {
|
|
116
206
|
if (!SocketServer_1.canRegisterAs(principal, 'dashboard')) {
|
|
117
207
|
logger_1.default.warn(`[SocketServer] Rejected REGISTER_DASHBOARD from principal=${principal} on ${socketId}`);
|
|
118
208
|
socket.disconnect();
|
|
119
209
|
return;
|
|
120
210
|
}
|
|
211
|
+
// Its events wait for the identity the overlapping change gave it.
|
|
212
|
+
yield this.connectChecks.get(socket);
|
|
213
|
+
if (!socket.connected)
|
|
214
|
+
return;
|
|
121
215
|
logger_1.default.info(`[SocketServer] Dashboard client registered (Socket: ${socketId})`);
|
|
122
216
|
socket.join('dashboard');
|
|
123
|
-
});
|
|
217
|
+
}));
|
|
124
218
|
socket.on('disconnect', () => {
|
|
219
|
+
clearTimeout(this.expiryTimers.get(socketId));
|
|
220
|
+
this.expiryTimers.delete(socketId);
|
|
125
221
|
if (this.nodes.has(socketId)) {
|
|
126
222
|
const host = this.nodes.get(socketId);
|
|
127
223
|
logger_1.default.info(`[SocketServer] Node disconnected: ${host} (Socket: ${socketId})`);
|
|
@@ -133,8 +229,128 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
133
229
|
}
|
|
134
230
|
});
|
|
135
231
|
});
|
|
232
|
+
const stopListening = (0, identityChanges_1.onIdentityChanged)((userId) => this.recheckUser(userId));
|
|
233
|
+
const sweep = setInterval(() => void this.sweep(), (_a = options.recheckIntervalMs) !== null && _a !== void 0 ? _a : exports.SOCKET_RECHECK_INTERVAL_MS);
|
|
234
|
+
sweep.unref();
|
|
235
|
+
server.once('close', () => {
|
|
236
|
+
stopListening();
|
|
237
|
+
clearInterval(sweep);
|
|
238
|
+
});
|
|
136
239
|
logger_1.default.info('[SocketServer] WebSocket server initialized (auth enabled)');
|
|
137
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Checks every socket of `userId` again, on its own credential (see
|
|
243
|
+
* recheck). Settles once each has its new identity or is closed (or, if it
|
|
244
|
+
* could not be checked, is as it was).
|
|
245
|
+
*/
|
|
246
|
+
recheckUser(userId) {
|
|
247
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
248
|
+
this.identityEpoch += 1;
|
|
249
|
+
if (!this.io || config_1.config.authDisabled === true)
|
|
250
|
+
return;
|
|
251
|
+
const sockets = Array.from(this.io.sockets.sockets.values()).filter((socket) => { var _a, _b; return ((_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.userId) === userId; });
|
|
252
|
+
yield Promise.all(sockets.map((socket) => this.recheck(socket)));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Every socket again, for a change made where this server doesn't hear of
|
|
257
|
+
* it, SWEEP_BATCH at a time. One sweep at a time.
|
|
258
|
+
*/
|
|
259
|
+
sweep() {
|
|
260
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
261
|
+
if (this.sweeping || !this.io || config_1.config.authDisabled === true)
|
|
262
|
+
return;
|
|
263
|
+
this.sweeping = true;
|
|
264
|
+
try {
|
|
265
|
+
const sockets = Array.from(this.io.sockets.sockets.values());
|
|
266
|
+
for (let i = 0; i < sockets.length; i += SWEEP_BATCH) {
|
|
267
|
+
yield Promise.all(sockets.slice(i, i + SWEEP_BATCH).map((s) => this.recheck(s)));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
this.sweeping = false;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The handshake's check, again, on the socket's own credential, renewing
|
|
277
|
+
* nothing: a sign-in's TTL doesn't slide and a key's `lastUsedAt` doesn't
|
|
278
|
+
* move. Its outcome is applied unless a check that started after it has
|
|
279
|
+
* already applied its own. So once a check settles, the socket is at least
|
|
280
|
+
* as current as what it read: a change's check lands before REST answers,
|
|
281
|
+
* though a sweep's started meanwhile, and a newer check that could not run
|
|
282
|
+
* doesn't cancel an older refusal.
|
|
283
|
+
*
|
|
284
|
+
* - Admitted: the socket gets its new identity where it is, still
|
|
285
|
+
* connected, so a demoted admin's tab keeps its member's events.
|
|
286
|
+
* - Refused: its connection is closed (the transport, not
|
|
287
|
+
* `disconnect()`). The client reconnects as it does after any drop, and
|
|
288
|
+
* the handshake decides on the credential it has now: a fresh bearer
|
|
289
|
+
* token from its `auth` function (Xenon Studio, the documented client),
|
|
290
|
+
* or the browser's new cookie, is admitted; the refused one is refused,
|
|
291
|
+
* and socket.io-client doesn't try again after a refused handshake.
|
|
292
|
+
* `disconnect()` would have the client never reconnect at all
|
|
293
|
+
* (`io server disconnect`), so every token's `exp` would end a
|
|
294
|
+
* long-running client's events for good. Once closing, the socket is
|
|
295
|
+
* sent nothing more.
|
|
296
|
+
* - Could not check: the socket is kept as it was. Closing every
|
|
297
|
+
* dashboard over a database hiccup would have their reconnects refused
|
|
298
|
+
* too, leaving them all without live events.
|
|
299
|
+
*/
|
|
300
|
+
recheck(socket) {
|
|
301
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
302
|
+
var _a;
|
|
303
|
+
let state = this.checks.get(socket);
|
|
304
|
+
if (!state) {
|
|
305
|
+
state = { started: 0, applied: 0 };
|
|
306
|
+
this.checks.set(socket, state);
|
|
307
|
+
}
|
|
308
|
+
const checks = state;
|
|
309
|
+
const n = ++checks.started;
|
|
310
|
+
const current = () => n > checks.applied && socket.connected;
|
|
311
|
+
const before = socket.data.identity;
|
|
312
|
+
let checked;
|
|
313
|
+
try {
|
|
314
|
+
checked = yield this.check(socket, true);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
if (!current())
|
|
318
|
+
return;
|
|
319
|
+
if (err instanceof SocketRefused) {
|
|
320
|
+
checks.applied = n;
|
|
321
|
+
logger_1.default.info(`[SocketServer] Closing ${socket.id} (user ${before === null || before === void 0 ? void 0 : before.userId}): ${err.message}`);
|
|
322
|
+
socket.conn.close();
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
logger_1.default.warn(`[SocketServer] Could not check ${socket.id} (user ${before === null || before === void 0 ? void 0 : before.userId}) again, so it is kept as it was: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!current())
|
|
330
|
+
return;
|
|
331
|
+
checks.applied = n;
|
|
332
|
+
const { identity, endsAt } = checked;
|
|
333
|
+
socket.data.identity = identity;
|
|
334
|
+
socket.data.endsAt = endsAt;
|
|
335
|
+
this.armExpiry(socket);
|
|
336
|
+
if ((before === null || before === void 0 ? void 0 : before.role) !== identity.role ||
|
|
337
|
+
JSON.stringify(before === null || before === void 0 ? void 0 : before.teamIds) !== JSON.stringify(identity.teamIds)) {
|
|
338
|
+
logger_1.default.info(`[SocketServer] ${socket.id} (user ${identity.userId}) is now ${identity.role}, teams ${identity.teamIds ? `[${identity.teamIds.join(', ')}]` : 'all'}`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/** Checks the socket again when its credential ends by itself (`socket.data.endsAt`). */
|
|
343
|
+
armExpiry(socket) {
|
|
344
|
+
clearTimeout(this.expiryTimers.get(socket.id));
|
|
345
|
+
this.expiryTimers.delete(socket.id);
|
|
346
|
+
const endsAt = socket.data.endsAt;
|
|
347
|
+
if (endsAt === undefined || !socket.connected)
|
|
348
|
+
return;
|
|
349
|
+
const delay = Math.min(Math.max(endsAt - Date.now(), 0), MAX_TIMER_MS);
|
|
350
|
+
const timer = setTimeout(() => void this.recheck(socket), delay);
|
|
351
|
+
timer.unref();
|
|
352
|
+
this.expiryTimers.set(socket.id, timer);
|
|
353
|
+
}
|
|
138
354
|
// Role-based registration guard. A dashboard-authed socket can't join the
|
|
139
355
|
// 'nodes' broadcast room and a node-authed socket can't subscribe to
|
|
140
356
|
// dashboard events — keeps a stolen credential of one class from acting
|
|
@@ -146,13 +362,27 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
146
362
|
}
|
|
147
363
|
authenticate(socket) {
|
|
148
364
|
return __awaiter(this, void 0, void 0, function* () {
|
|
149
|
-
|
|
365
|
+
return (yield this.check(socket)).identity;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Who the socket's handshake credential says it is now, and when that
|
|
370
|
+
* credential ends by itself (`endsAt`, epoch ms; none for one that doesn't).
|
|
371
|
+
* Throws SocketRefused when the credential no longer admits it, anything
|
|
372
|
+
* else when it could not be checked. A `recheck` renews nothing (see
|
|
373
|
+
* recheck).
|
|
374
|
+
*/
|
|
375
|
+
check(socket_1) {
|
|
376
|
+
return __awaiter(this, arguments, void 0, function* (socket, recheck = false) {
|
|
377
|
+
var _a, _b, _c, _d, _e;
|
|
150
378
|
if (config_1.config.authDisabled === true) {
|
|
151
379
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
380
|
+
identity: {
|
|
381
|
+
principal: 'auth-disabled',
|
|
382
|
+
userId: 'auth-disabled',
|
|
383
|
+
role: 'SUPER_ADMIN',
|
|
384
|
+
teamIds: undefined,
|
|
385
|
+
},
|
|
156
386
|
};
|
|
157
387
|
}
|
|
158
388
|
const auth = (socket.handshake.auth || {});
|
|
@@ -166,58 +396,74 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
166
396
|
try {
|
|
167
397
|
payload = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(bearer, { audience: 'xenon-rest' });
|
|
168
398
|
}
|
|
169
|
-
catch (
|
|
170
|
-
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (err instanceof jose.errors.JOSEError)
|
|
401
|
+
throw new SocketRefused('invalid bearer token');
|
|
402
|
+
throw new Error(`bearer token could not be checked: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
171
403
|
}
|
|
404
|
+
// The socket closes when REST stops taking the token: verify() allows
|
|
405
|
+
// JWT_CLOCK_TOLERANCE_SEC past exp, and refuses it from then on.
|
|
406
|
+
const endsAt = typeof payload.exp === 'number'
|
|
407
|
+
? (payload.exp + JwtKeyService_1.JWT_CLOCK_TOLERANCE_SEC) * 1000
|
|
408
|
+
: undefined;
|
|
172
409
|
const owner = yield prisma_1.prisma.user.findUnique({
|
|
173
410
|
where: { id: String(payload.sub) },
|
|
174
411
|
select: { status: true, role: true },
|
|
175
412
|
});
|
|
176
413
|
if (!owner || owner.status !== 'ACTIVE')
|
|
177
|
-
throw new
|
|
414
|
+
throw new SocketRefused('inactive user');
|
|
178
415
|
// The team claim is read exactly as REST's bearer path reads it.
|
|
179
|
-
|
|
416
|
+
const identity = yield this.identify('dashboard', String(payload.sub), owner.role, (_b = payload.teamId) !== null && _b !== void 0 ? _b : null);
|
|
417
|
+
return { identity, endsAt };
|
|
180
418
|
}
|
|
181
419
|
// Node path: per-node (accessKey, token) pair. Resolves to a real
|
|
182
420
|
// ApiKey row owned by a real User; we additionally enforce that the
|
|
183
421
|
// User is ACTIVE so a disabled account can't keep a stolen key alive.
|
|
184
422
|
const pairKey = (typeof auth.accessKey === 'string' && auth.accessKey) ||
|
|
185
|
-
((
|
|
423
|
+
((_c = headers['x-xenon-access-key']) !== null && _c !== void 0 ? _c : '');
|
|
186
424
|
const pairTok = (typeof auth.token === 'string' && auth.token) ||
|
|
187
|
-
((
|
|
425
|
+
((_d = headers['x-xenon-token']) !== null && _d !== void 0 ? _d : '');
|
|
188
426
|
if (pairKey && pairTok) {
|
|
189
|
-
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair(pairKey, pairTok
|
|
427
|
+
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verifyPair(pairKey, pairTok, {
|
|
428
|
+
touch: !recheck,
|
|
429
|
+
});
|
|
190
430
|
if (!row)
|
|
191
|
-
throw new
|
|
431
|
+
throw new SocketRefused('invalid (accessKey, token) pair');
|
|
192
432
|
const owner = yield prisma_1.prisma.user.findUnique({
|
|
193
433
|
where: { id: row.userId },
|
|
194
434
|
select: { status: true, role: true },
|
|
195
435
|
});
|
|
196
436
|
if (!owner || owner.status !== 'ACTIVE')
|
|
197
|
-
throw new
|
|
198
|
-
|
|
437
|
+
throw new SocketRefused('inactive user');
|
|
438
|
+
const identity = yield this.identify('node', row.userId, owner.role, row.teamId);
|
|
439
|
+
return { identity, endsAt: msOf(row.expiresAt) };
|
|
199
440
|
}
|
|
200
441
|
// Dashboard path: the cookie. As in REST's authMiddleware, it is first a
|
|
201
442
|
// UserSession id (set by /auth/login), then a raw API key (the older
|
|
202
443
|
// /api-key-gate cookie). A session whose user is disabled is refused, not
|
|
203
444
|
// retried as a key.
|
|
204
|
-
const cookieValue = (
|
|
445
|
+
const cookieValue = (_e = readCookie(headers.cookie, SESSION_COOKIE)) !== null && _e !== void 0 ? _e : '';
|
|
205
446
|
if (!cookieValue) {
|
|
206
|
-
throw new
|
|
447
|
+
throw new SocketRefused('missing credentials (need (accessKey, token) pair or dashboard cookie)');
|
|
207
448
|
}
|
|
208
|
-
|
|
449
|
+
// A sign-in's expiresAt slides as REST uses it: checked again then, it
|
|
450
|
+
// has moved on and the timer is set again.
|
|
451
|
+
const session = yield typedi_1.Container.get(UserSessionService_1.UserSessionService).resolve(cookieValue, {
|
|
452
|
+
renew: !recheck,
|
|
453
|
+
});
|
|
209
454
|
if (session) {
|
|
210
455
|
const user = yield prisma_1.prisma.user.findUnique({
|
|
211
456
|
where: { id: session.userId },
|
|
212
457
|
select: { status: true, role: true },
|
|
213
458
|
});
|
|
214
459
|
if (!user || user.status !== 'ACTIVE')
|
|
215
|
-
throw new
|
|
216
|
-
|
|
460
|
+
throw new SocketRefused('inactive user');
|
|
461
|
+
const identity = yield this.identify('dashboard', session.userId, user.role, null);
|
|
462
|
+
return { identity, endsAt: msOf(session.expiresAt) };
|
|
217
463
|
}
|
|
218
|
-
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verify(cookieValue);
|
|
464
|
+
const row = yield typedi_1.Container.get(ApiKeyService_1.ApiKeyService).verify(cookieValue, { touch: !recheck });
|
|
219
465
|
if (!row) {
|
|
220
|
-
throw new
|
|
466
|
+
throw new SocketRefused('invalid or revoked dashboard session');
|
|
221
467
|
}
|
|
222
468
|
// The key owner's role decides the team scope, and an inactive owner is
|
|
223
469
|
// refused here as REST refuses them.
|
|
@@ -226,8 +472,9 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
226
472
|
select: { status: true, role: true },
|
|
227
473
|
});
|
|
228
474
|
if (!owner || owner.status !== 'ACTIVE')
|
|
229
|
-
throw new
|
|
230
|
-
|
|
475
|
+
throw new SocketRefused('inactive user');
|
|
476
|
+
const identity = yield this.identify('dashboard', row.userId, owner.role, row.teamId);
|
|
477
|
+
return { identity, endsAt: msOf(row.expiresAt) };
|
|
231
478
|
});
|
|
232
479
|
}
|
|
233
480
|
identify(principal, userId, role, apiKeyTeamId) {
|
|
@@ -237,17 +484,80 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
237
484
|
return { principal, userId, role: r, teamIds };
|
|
238
485
|
});
|
|
239
486
|
}
|
|
240
|
-
/**
|
|
487
|
+
/**
|
|
488
|
+
* Unscoped: every dashboard socket. Only for the hub's node events, which
|
|
489
|
+
* carry a node's address and nothing of a phone, a session or a selector.
|
|
490
|
+
*/
|
|
241
491
|
emitToDashboard(event, data) {
|
|
242
492
|
if (this.io) {
|
|
243
493
|
this.io.to('dashboard').emit(event, data);
|
|
244
494
|
}
|
|
245
|
-
|
|
495
|
+
this.logEvent(event, data);
|
|
496
|
+
}
|
|
497
|
+
/** Once per event, unscoped: whole, or what EVENT_LOG_KEEPS keeps of it. */
|
|
498
|
+
logEvent(event, data) {
|
|
499
|
+
const keep = EVENT_LOG_KEEPS.get(event);
|
|
500
|
+
if (keep === null)
|
|
501
|
+
return;
|
|
502
|
+
typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({
|
|
503
|
+
type: event,
|
|
504
|
+
payload: keep ? keep(data) : data,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* A Selector Health event (`selector_*`): each dashboard socket gets it only
|
|
509
|
+
* if its caller may see the selector, by REST's rule (a selector healed in a
|
|
510
|
+
* session the caller may see; SelectorVisibilityResolver). Admins get every
|
|
511
|
+
* one. The event log records it once, unscoped.
|
|
512
|
+
*
|
|
513
|
+
* Each selector's events are delivered through one chain, in the order they
|
|
514
|
+
* were emitted. With no scoped socket connected and nothing pending for the
|
|
515
|
+
* selector, it is the synchronous room broadcast, with no lookup. The
|
|
516
|
+
* returned promise settles once the event is delivered and never rejects.
|
|
517
|
+
*/
|
|
518
|
+
emitToDashboardForSelector(event, data, selector) {
|
|
519
|
+
var _a;
|
|
520
|
+
this.logEvent(event, data);
|
|
521
|
+
const key = JSON.stringify([selector.strategy, selector.selector]);
|
|
522
|
+
if (!this.hasScopedDashboard() && !this.selectorDeliveries.has(key)) {
|
|
523
|
+
(_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
|
|
524
|
+
return Promise.resolve();
|
|
525
|
+
}
|
|
526
|
+
return this.enqueue(event, [key], () => this.deliverForSelector(event, data, selector), this.selectorDeliveries);
|
|
527
|
+
}
|
|
528
|
+
deliverForSelector(event, data, selector) {
|
|
529
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
530
|
+
var _a;
|
|
531
|
+
// The scoped sockets may have left while this waited its turn.
|
|
532
|
+
if (!this.hasScopedDashboard()) {
|
|
533
|
+
(_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const resolver = typedi_1.Container.get(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
|
|
537
|
+
const sockets = this.dashboardSockets();
|
|
538
|
+
const asked = sockets.map((socket) => { var _a; return (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity; });
|
|
539
|
+
const allowed = yield Promise.all(sockets.map((socket) => {
|
|
540
|
+
var _a, _b;
|
|
541
|
+
const teamIds = teamIdsOf(socket);
|
|
542
|
+
if (teamIds === undefined)
|
|
543
|
+
return true;
|
|
544
|
+
const userId = (_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.userId;
|
|
545
|
+
return resolver.canSee(selector, { userId, teamIds });
|
|
546
|
+
}));
|
|
547
|
+
// A socket whose identity was replaced meanwhile (a role or team
|
|
548
|
+
// change) was not the one asked about: it gets nothing.
|
|
549
|
+
sockets.forEach((socket, i) => {
|
|
550
|
+
var _a;
|
|
551
|
+
if (allowed[i] && ((_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === asked[i])
|
|
552
|
+
socket.emit(event, data);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
246
555
|
}
|
|
247
556
|
/**
|
|
248
557
|
* A dashboard event about one or more phones: each dashboard socket gets it
|
|
249
558
|
* only if its caller can see the phone ({@link DeviceEventScope}), by the
|
|
250
|
-
* same team rule as REST. The event log records it once, unscoped
|
|
559
|
+
* same team rule as REST. The event log records it once, unscoped, as
|
|
560
|
+
* EVENT_LOG_KEEPS says.
|
|
251
561
|
*
|
|
252
562
|
* Each phone's events are delivered through one chain, in the order they
|
|
253
563
|
* were emitted, whatever their scope shape: one that waits on a team lookup
|
|
@@ -262,9 +572,9 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
262
572
|
*/
|
|
263
573
|
emitToDashboardForDevices(event, data, scope) {
|
|
264
574
|
var _a, _b;
|
|
265
|
-
|
|
575
|
+
this.logEvent(event, data);
|
|
266
576
|
const udids = 'udids' in scope ? scope.udids : [(_a = scope.udid) !== null && _a !== void 0 ? _a : ''];
|
|
267
|
-
if (
|
|
577
|
+
if (this.reachesEverySocket(scope) && udids.every((udid) => !this.deliveries.has(udid))) {
|
|
268
578
|
this.noteRow(scope);
|
|
269
579
|
(_b = this.io) === null || _b === void 0 ? void 0 : _b.to('dashboard').emit(event, data);
|
|
270
580
|
return Promise.resolve();
|
|
@@ -289,34 +599,53 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
289
599
|
hasScopedDashboard() {
|
|
290
600
|
return this.dashboardSockets().some((socket) => teamIdsOf(socket) !== undefined);
|
|
291
601
|
}
|
|
292
|
-
/** Runs `step` after every pending delivery for `
|
|
293
|
-
enqueue(event,
|
|
294
|
-
const before =
|
|
602
|
+
/** Runs `step` after every pending delivery for `keys` in `chains`, and makes it their new tail. */
|
|
603
|
+
enqueue(event, keys, step, chains = this.deliveries) {
|
|
604
|
+
const before = keys.map((key) => { var _a; return (_a = chains.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve(); });
|
|
295
605
|
const done = Promise.all(before)
|
|
296
606
|
.then(step)
|
|
297
607
|
.catch((err) => { var _a; return logger_1.default.warn(`[SocketServer] ${event} not delivered: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
|
|
298
|
-
for (const
|
|
299
|
-
|
|
608
|
+
for (const key of keys)
|
|
609
|
+
chains.set(key, done);
|
|
300
610
|
void done.then(() => {
|
|
301
|
-
for (const
|
|
302
|
-
if (
|
|
303
|
-
|
|
611
|
+
for (const key of keys) {
|
|
612
|
+
if (chains.get(key) === done)
|
|
613
|
+
chains.delete(key);
|
|
304
614
|
}
|
|
305
615
|
});
|
|
306
616
|
return done;
|
|
307
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Whether every dashboard socket gets an event of this scope whatever its
|
|
620
|
+
* phones are, so it can be the room broadcast with no lookup.
|
|
621
|
+
*/
|
|
622
|
+
reachesEverySocket(scope) {
|
|
623
|
+
return this.dashboardSockets().every((socket) => teamIdsOf(socket) === undefined && (!scope.adminOnly || isAdmin(socket)));
|
|
624
|
+
}
|
|
308
625
|
deliver(event, data, scope) {
|
|
309
626
|
return __awaiter(this, void 0, void 0, function* () {
|
|
310
627
|
var _a;
|
|
311
628
|
// The scoped sockets may have left while this waited its turn.
|
|
312
|
-
if (
|
|
629
|
+
if (this.reachesEverySocket(scope)) {
|
|
313
630
|
(_a = this.io) === null || _a === void 0 ? void 0 : _a.to('dashboard').emit(event, data);
|
|
314
631
|
return;
|
|
315
632
|
}
|
|
316
633
|
const resolver = typedi_1.Container.get(DeviceTeamResolver_1.DeviceTeamResolver);
|
|
634
|
+
// Read once the team is known, so a socket that joined meanwhile is included.
|
|
635
|
+
const recipients = () => this.dashboardSockets().filter((socket) => !scope.adminOnly || isAdmin(socket));
|
|
636
|
+
// An admin-only event goes to admins, who see every phone: no lookup, so
|
|
637
|
+
// a slow store holds no captured request back.
|
|
638
|
+
if (scope.adminOnly) {
|
|
639
|
+
const admins = recipients();
|
|
640
|
+
if (admins.every((socket) => teamIdsOf(socket) === undefined)) {
|
|
641
|
+
for (const socket of admins)
|
|
642
|
+
socket.emit(event, data);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
317
646
|
if ('udids' in scope) {
|
|
318
647
|
const teams = yield Promise.all(scope.udids.map((udid) => resolver.resolve(udid)));
|
|
319
|
-
for (const socket of
|
|
648
|
+
for (const socket of recipients()) {
|
|
320
649
|
const teamIds = teamIdsOf(socket);
|
|
321
650
|
if (teamIds === undefined) {
|
|
322
651
|
socket.emit(event, data);
|
|
@@ -332,7 +661,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
332
661
|
const team = scope.teamId !== undefined
|
|
333
662
|
? { known: true, teamId: scope.teamId }
|
|
334
663
|
: yield resolver.resolve(scope.udid);
|
|
335
|
-
for (const socket of
|
|
664
|
+
for (const socket of recipients()) {
|
|
336
665
|
if ((0, DeviceTeamResolver_1.canSeeDeviceTeam)(team, teamIdsOf(socket)))
|
|
337
666
|
socket.emit(event, data);
|
|
338
667
|
}
|
|
@@ -368,3 +697,9 @@ function teamIdsOf(socket) {
|
|
|
368
697
|
const identity = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity;
|
|
369
698
|
return identity ? identity.teamIds : [];
|
|
370
699
|
}
|
|
700
|
+
/** An admin by role, as `roleGuard('ADMIN')` decides. A socket with no identity is not one. */
|
|
701
|
+
function isAdmin(socket) {
|
|
702
|
+
var _a, _b;
|
|
703
|
+
const role = (_b = (_a = socket.data) === null || _a === void 0 ? void 0 : _a.identity) === null || _b === void 0 ? void 0 : _b.role;
|
|
704
|
+
return role === 'ADMIN' || role === 'SUPER_ADMIN';
|
|
705
|
+
}
|
|
@@ -22,6 +22,7 @@ exports.TeamService = void 0;
|
|
|
22
22
|
const typedi_1 = require("typedi");
|
|
23
23
|
const prisma_1 = require("../prisma");
|
|
24
24
|
const logger_1 = __importDefault(require("../logger"));
|
|
25
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
25
26
|
let TeamService = class TeamService {
|
|
26
27
|
constructor() {
|
|
27
28
|
this.log = logger_1.default.scope('Team');
|
|
@@ -101,7 +102,9 @@ let TeamService = class TeamService {
|
|
|
101
102
|
}
|
|
102
103
|
addMember(teamId, userId) {
|
|
103
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
104
|
-
|
|
105
|
+
const member = yield prisma_1.prisma.teamMember.create({ data: { teamId, userId } });
|
|
106
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
107
|
+
return member;
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
110
|
removeMember(teamId, userId) {
|
|
@@ -109,6 +112,7 @@ let TeamService = class TeamService {
|
|
|
109
112
|
yield prisma_1.prisma.teamMember.delete({
|
|
110
113
|
where: { teamId_userId: { teamId, userId } },
|
|
111
114
|
});
|
|
115
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
112
116
|
});
|
|
113
117
|
}
|
|
114
118
|
};
|