@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
|
@@ -52,16 +52,54 @@ const MODELS = [
|
|
|
52
52
|
'sessionMetric',
|
|
53
53
|
'selectorEvent',
|
|
54
54
|
];
|
|
55
|
+
let scratchCount = 0;
|
|
56
|
+
let template;
|
|
57
|
+
/**
|
|
58
|
+
* A database migrated once for the whole process, which each suite copies.
|
|
59
|
+
* Removed when the process exits.
|
|
60
|
+
*/
|
|
61
|
+
function migratedTemplate() {
|
|
62
|
+
if (template)
|
|
63
|
+
return template;
|
|
64
|
+
const file = path_1.default.join(os_1.default.tmpdir(), `xenon-scratch-template-${process.pid}-${Date.now()}.db`);
|
|
65
|
+
(0, child_process_1.execSync)('npx prisma migrate deploy', {
|
|
66
|
+
cwd: path_1.default.resolve(__dirname, '../..'),
|
|
67
|
+
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: `file:${file}` }),
|
|
68
|
+
stdio: 'pipe',
|
|
69
|
+
});
|
|
70
|
+
process.once('exit', () => {
|
|
71
|
+
for (const f of [file, `${file}-journal`]) {
|
|
72
|
+
try {
|
|
73
|
+
fs_1.default.unlinkSync(f);
|
|
74
|
+
}
|
|
75
|
+
catch (_a) {
|
|
76
|
+
// already gone
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
template = file;
|
|
81
|
+
return file;
|
|
82
|
+
}
|
|
55
83
|
/**
|
|
56
84
|
* Back the whole `prisma` wrapper with a scratch SQLite file for the enclosing
|
|
57
85
|
* `describe`, so the real queries (the device store, the session lifecycle,
|
|
58
86
|
* SessionOwnerResolver) run against a real database that is not the server's.
|
|
59
87
|
* Like useScratchPortLeases, for every model.
|
|
60
88
|
*
|
|
61
|
-
* Call it inside a `describe`, never at the top of a file.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
89
|
+
* Call it inside a `describe`, never at the top of a file. Each suite gets a
|
|
90
|
+
* fresh database: a copy of one migrated once per process (migrating takes
|
|
91
|
+
* seconds). The stubs are made before each test and removed after it, so a
|
|
92
|
+
* spec's own `sinon.restore()` can't leave the next test on the real
|
|
93
|
+
* database. Tests empty the tables they use.
|
|
94
|
+
*
|
|
95
|
+
* With `wholeSuite`, the stubs are made once, before the suite's own `before`
|
|
96
|
+
* hooks (call it first in the describe), and removed after the suite. Making
|
|
97
|
+
* them takes tens of milliseconds, which a suite of hundreds of short tests
|
|
98
|
+
* otherwise pays per test. Its `before` hooks can then seed the rows every
|
|
99
|
+
* test reads, and nothing needs deleting: the file goes with the suite. Only
|
|
100
|
+
* for a suite that doesn't stub or restore `prisma` itself. Its own `after`
|
|
101
|
+
* hooks run once the stubs are gone, so they must not touch `prisma`: it is
|
|
102
|
+
* the real database again by then.
|
|
65
103
|
*
|
|
66
104
|
* With `captureQueries`, `queries` holds the SQL each test ran, so a spec can
|
|
67
105
|
* ask SQLite how it reads a table (`EXPLAIN QUERY PLAN`).
|
|
@@ -72,13 +110,10 @@ function useScratchDatabase(options = {}) {
|
|
|
72
110
|
let dbPath = '';
|
|
73
111
|
before(function () {
|
|
74
112
|
this.timeout(90000);
|
|
75
|
-
|
|
113
|
+
scratchCount += 1;
|
|
114
|
+
dbPath = path_1.default.join(os_1.default.tmpdir(), `xenon-scratch-${process.pid}-${Date.now()}-${scratchCount}.db`);
|
|
115
|
+
fs_1.default.copyFileSync(migratedTemplate(), dbPath);
|
|
76
116
|
const url = `file:${dbPath}`;
|
|
77
|
-
(0, child_process_1.execSync)('npx prisma migrate deploy', {
|
|
78
|
-
cwd: path_1.default.resolve(__dirname, '../..'),
|
|
79
|
-
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
|
|
80
|
-
stdio: 'pipe',
|
|
81
|
-
});
|
|
82
117
|
if (options.captureQueries) {
|
|
83
118
|
const db = new client_1.PrismaClient({
|
|
84
119
|
datasources: { db: { url } },
|
|
@@ -90,9 +125,10 @@ function useScratchDatabase(options = {}) {
|
|
|
90
125
|
else {
|
|
91
126
|
ctx.db = new client_1.PrismaClient({ datasources: { db: { url } } });
|
|
92
127
|
}
|
|
128
|
+
if (options.wholeSuite)
|
|
129
|
+
stubPrisma();
|
|
93
130
|
});
|
|
94
|
-
|
|
95
|
-
ctx.queries.length = 0;
|
|
131
|
+
function stubPrisma() {
|
|
96
132
|
for (const model of MODELS) {
|
|
97
133
|
const wrapper = prisma_1.prisma[model];
|
|
98
134
|
const delegate = ctx.db[model];
|
|
@@ -102,8 +138,16 @@ function useScratchDatabase(options = {}) {
|
|
|
102
138
|
sandbox.stub(wrapper, name).callsFake((...args) => delegate[name](...args));
|
|
103
139
|
}
|
|
104
140
|
}
|
|
141
|
+
}
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
ctx.queries.length = 0;
|
|
144
|
+
if (!options.wholeSuite)
|
|
145
|
+
stubPrisma();
|
|
146
|
+
});
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
if (!options.wholeSuite)
|
|
149
|
+
sandbox.restore();
|
|
105
150
|
});
|
|
106
|
-
afterEach(() => sandbox.restore());
|
|
107
151
|
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
108
152
|
sandbox.restore();
|
|
109
153
|
if (ctx.db)
|
|
@@ -133,6 +133,6 @@ describe('Android Test', () => {
|
|
|
133
133
|
};
|
|
134
134
|
yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 1000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
|
|
135
135
|
.to.be.an('error')
|
|
136
|
-
.with.property('message', 'Device is busy or blocked
|
|
136
|
+
.with.property('message', 'Device is busy or blocked. Device request: {"platform":"android"}'));
|
|
137
137
|
}));
|
|
138
138
|
});
|
|
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const sinon_1 = __importDefault(require("sinon"));
|
|
20
20
|
// Static import (matches the passing integration-spec idiom, e.g. selector-health-endpoint.spec):
|
|
21
21
|
// a dynamic `await import('...routers/audit')` fails ESM resolution on the extensionless path
|
|
@@ -37,7 +37,7 @@ describe('POST /xenon/api/audit/events', () => {
|
|
|
37
37
|
app.use('/xenon/api/audit', (0, audit_1.makeRouter)({ eventLogService: eventLogSvc }));
|
|
38
38
|
});
|
|
39
39
|
it('202s on a valid batch of 2 events and appends each via EventLogService.appendSafe with type mcp_audit', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
-
const res = yield (0,
|
|
40
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
41
41
|
.post('/xenon/api/audit/events')
|
|
42
42
|
.send({
|
|
43
43
|
events: [
|
|
@@ -70,20 +70,20 @@ describe('POST /xenon/api/audit/events', () => {
|
|
|
70
70
|
(0, chai_1.expect)(secondArg.payload).to.deep.include({ sessionId: 'sess-1' });
|
|
71
71
|
}));
|
|
72
72
|
it('400s when events is missing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
-
const res = yield (0,
|
|
73
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send({});
|
|
74
74
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
75
75
|
(0, chai_1.expect)(res.body.error).to.equal('bad_request');
|
|
76
76
|
(0, chai_1.expect)(eventLogSvc.appendSafe.called).to.be.false;
|
|
77
77
|
}));
|
|
78
78
|
it('400s when events is not an array', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
-
const res = yield (0,
|
|
79
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
80
80
|
.post('/xenon/api/audit/events')
|
|
81
81
|
.send({ events: 'nope' });
|
|
82
82
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
83
83
|
(0, chai_1.expect)(res.body.error).to.equal('bad_request');
|
|
84
84
|
}));
|
|
85
85
|
it('400s when a batch event is malformed (missing tool)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
-
const res = yield (0,
|
|
86
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
87
87
|
.post('/xenon/api/audit/events')
|
|
88
88
|
.send({
|
|
89
89
|
events: [{ subject: 'agent-1', decision: 'allow', latencyMs: 10 }],
|
|
@@ -100,7 +100,7 @@ describe('POST /xenon/api/audit/events', () => {
|
|
|
100
100
|
decision: 'allow',
|
|
101
101
|
latencyMs: i,
|
|
102
102
|
}));
|
|
103
|
-
const res = yield (0,
|
|
103
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send({ events });
|
|
104
104
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
105
105
|
(0, chai_1.expect)(res.body.error).to.equal('bad_request');
|
|
106
106
|
(0, chai_1.expect)(eventLogSvc.appendSafe.called).to.be.false;
|
|
@@ -131,12 +131,12 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
|
|
|
131
131
|
});
|
|
132
132
|
it('401s when unauthenticated', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
133
|
const app = yield makeApp(eventLogSvc)();
|
|
134
|
-
const res = yield (0,
|
|
134
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/audit/events').send(validBody);
|
|
135
135
|
(0, chai_1.expect)(res.status).to.equal(401);
|
|
136
136
|
}));
|
|
137
137
|
it('403s for a role below MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
138
|
const app = yield makeApp(eventLogSvc)();
|
|
139
|
-
const res = yield (0,
|
|
139
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
140
140
|
.post('/xenon/api/audit/events')
|
|
141
141
|
.set('x-test-role', 'BOGUS')
|
|
142
142
|
.set('x-test-scopes', 'admin')
|
|
@@ -145,7 +145,7 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
|
|
|
145
145
|
}));
|
|
146
146
|
it('403s for MEMBER role without the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
147
|
const app = yield makeApp(eventLogSvc)();
|
|
148
|
-
const res = yield (0,
|
|
148
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
149
149
|
.post('/xenon/api/audit/events')
|
|
150
150
|
.set('x-test-role', 'MEMBER')
|
|
151
151
|
.set('x-test-scopes', 'devices')
|
|
@@ -155,7 +155,7 @@ describe('POST /xenon/api/audit/events — guard (router-level roleGuard(MEMBER)
|
|
|
155
155
|
}));
|
|
156
156
|
it('202s for MEMBER role with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
157
157
|
const app = yield makeApp(eventLogSvc)();
|
|
158
|
-
const res = yield (0,
|
|
158
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
159
159
|
.post('/xenon/api/audit/events')
|
|
160
160
|
.set('x-test-role', 'MEMBER')
|
|
161
161
|
.set('x-test-scopes', 'admin')
|
|
@@ -14,13 +14,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
|
-
const
|
|
17
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
18
18
|
const express_1 = __importDefault(require("express"));
|
|
19
19
|
const auth_1 = require("../../src/app/routers/auth");
|
|
20
20
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
21
|
-
const prisma_1 = require("../../src/prisma");
|
|
22
21
|
const typedi_1 = require("typedi");
|
|
23
22
|
const UserService_1 = require("../../src/services/UserService");
|
|
23
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
24
24
|
function buildApp() {
|
|
25
25
|
const app = (0, express_1.default)();
|
|
26
26
|
app.use(express_1.default.json());
|
|
@@ -32,10 +32,9 @@ function buildApp() {
|
|
|
32
32
|
}
|
|
33
33
|
describe('auth flow (integration)', function () {
|
|
34
34
|
this.timeout(30000);
|
|
35
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
35
36
|
let user;
|
|
36
37
|
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
37
|
-
yield prisma_1.prisma.userSession.deleteMany({ where: { user: { email: 'loginflow@xenon.local' } } });
|
|
38
|
-
yield prisma_1.prisma.user.deleteMany({ where: { email: 'loginflow@xenon.local' } });
|
|
39
38
|
user = yield typedi_1.Container.get(UserService_1.UserService).createUser({
|
|
40
39
|
email: 'loginflow@xenon.local',
|
|
41
40
|
name: 'Login Flow',
|
|
@@ -43,31 +42,27 @@ describe('auth flow (integration)', function () {
|
|
|
43
42
|
role: 'ADMIN',
|
|
44
43
|
});
|
|
45
44
|
}));
|
|
46
|
-
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
47
|
-
yield prisma_1.prisma.userSession.deleteMany({ where: { userId: user.id } });
|
|
48
|
-
yield prisma_1.prisma.user.delete({ where: { id: user.id } });
|
|
49
|
-
}));
|
|
50
45
|
it('login → cookie → /whoami → logout → 401', () => __awaiter(this, void 0, void 0, function* () {
|
|
51
46
|
var _a;
|
|
52
47
|
const app = buildApp();
|
|
53
|
-
const login = yield (0,
|
|
48
|
+
const login = yield (0, loopbackRequest_1.default)(app)
|
|
54
49
|
.post('/api/auth/login')
|
|
55
50
|
.send({ email: 'loginflow@xenon.local', password: 'flow-test-12' });
|
|
56
51
|
(0, chai_1.expect)(login.status).to.equal(204);
|
|
57
52
|
const cookie = (_a = login.headers['set-cookie']) === null || _a === void 0 ? void 0 : _a[0];
|
|
58
53
|
(0, chai_1.expect)(cookie).to.match(/^xenon_dashboard_session=/);
|
|
59
|
-
const me = yield (0,
|
|
54
|
+
const me = yield (0, loopbackRequest_1.default)(app).get('/api/whoami').set('Cookie', cookie);
|
|
60
55
|
(0, chai_1.expect)(me.status).to.equal(200);
|
|
61
56
|
(0, chai_1.expect)(me.body.userId).to.equal(user.id);
|
|
62
57
|
(0, chai_1.expect)(me.body.kind).to.equal('user-session');
|
|
63
|
-
const out = yield (0,
|
|
58
|
+
const out = yield (0, loopbackRequest_1.default)(app).post('/api/auth/logout').set('Cookie', cookie);
|
|
64
59
|
(0, chai_1.expect)(out.status).to.equal(204);
|
|
65
|
-
const me2 = yield (0,
|
|
60
|
+
const me2 = yield (0, loopbackRequest_1.default)(app).get('/api/whoami').set('Cookie', cookie);
|
|
66
61
|
(0, chai_1.expect)(me2.status).to.equal(401);
|
|
67
62
|
}));
|
|
68
63
|
it('login with wrong password returns generic 401 (no enumeration)', () => __awaiter(this, void 0, void 0, function* () {
|
|
69
64
|
const app = buildApp();
|
|
70
|
-
const login = yield (0,
|
|
65
|
+
const login = yield (0, loopbackRequest_1.default)(app)
|
|
71
66
|
.post('/api/auth/login')
|
|
72
67
|
.send({ email: 'loginflow@xenon.local', password: 'WRONG' });
|
|
73
68
|
(0, chai_1.expect)(login.status).to.equal(401);
|
|
@@ -75,7 +70,7 @@ describe('auth flow (integration)', function () {
|
|
|
75
70
|
}));
|
|
76
71
|
it('login with unknown email returns the same generic 401', () => __awaiter(this, void 0, void 0, function* () {
|
|
77
72
|
const app = buildApp();
|
|
78
|
-
const login = yield (0,
|
|
73
|
+
const login = yield (0, loopbackRequest_1.default)(app)
|
|
79
74
|
.post('/api/auth/login')
|
|
80
75
|
.send({ email: 'no-such@xenon.local', password: 'whatever' });
|
|
81
76
|
(0, chai_1.expect)(login.status).to.equal(401);
|
|
@@ -15,22 +15,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const auth_1 = require("../../src/app/routers/auth");
|
|
20
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
20
21
|
describe('login rate limit', function () {
|
|
21
22
|
this.timeout(15000);
|
|
23
|
+
// Each attempt looks the email address up.
|
|
24
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
22
25
|
it('5 bad logins from one IP → 6th returns 429 with Retry-After', () => __awaiter(this, void 0, void 0, function* () {
|
|
23
26
|
const app = (0, express_1.default)();
|
|
24
27
|
app.use(express_1.default.json());
|
|
25
28
|
app.use('/auth', (0, auth_1.authPublicRouter)());
|
|
26
29
|
for (let i = 0; i < 5; i++) {
|
|
27
|
-
const r = yield (0,
|
|
30
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
28
31
|
.post('/auth/login')
|
|
29
32
|
.set('X-Forwarded-For', '9.9.9.9')
|
|
30
33
|
.send({ email: 'no@no.com', password: 'no' });
|
|
31
34
|
(0, chai_1.expect)(r.status).to.equal(401);
|
|
32
35
|
}
|
|
33
|
-
const blocked = yield (0,
|
|
36
|
+
const blocked = yield (0, loopbackRequest_1.default)(app)
|
|
34
37
|
.post('/auth/login')
|
|
35
38
|
.set('X-Forwarded-For', '9.9.9.9')
|
|
36
39
|
.send({ email: 'no@no.com', password: 'no' });
|
|
@@ -48,7 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
require("reflect-metadata");
|
|
49
49
|
const chai_1 = require("chai");
|
|
50
50
|
const express_1 = __importDefault(require("express"));
|
|
51
|
-
const
|
|
51
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
52
52
|
const sinon_1 = __importDefault(require("sinon"));
|
|
53
53
|
const fs = __importStar(require("fs"));
|
|
54
54
|
const path = __importStar(require("path"));
|
|
@@ -57,11 +57,14 @@ const unzipper = __importStar(require("unzipper"));
|
|
|
57
57
|
const prisma_1 = require("../../src/prisma");
|
|
58
58
|
const config_1 = require("../../src/config");
|
|
59
59
|
const bug_report_1 = __importDefault(require("../../src/app/routers/bug-report"));
|
|
60
|
+
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
60
61
|
describe('POST /sessions/:id/bug-report (integration)', () => {
|
|
61
62
|
let tmpAssets;
|
|
63
|
+
let savedAssetsPath;
|
|
62
64
|
const fixtureMp4 = path.join(__dirname, 'fixtures/bug-report/recording.mp4');
|
|
63
65
|
beforeEach(() => {
|
|
64
66
|
tmpAssets = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-it-'));
|
|
67
|
+
savedAssetsPath = config_1.config.sessionAssetsPath;
|
|
65
68
|
config_1.config.sessionAssetsPath = tmpAssets;
|
|
66
69
|
const sessionDir = path.join(tmpAssets, 'sess-it', 'video');
|
|
67
70
|
fs.mkdirSync(sessionDir, { recursive: true });
|
|
@@ -69,10 +72,27 @@ describe('POST /sessions/:id/bug-report (integration)', () => {
|
|
|
69
72
|
});
|
|
70
73
|
afterEach(() => {
|
|
71
74
|
sinon_1.default.restore();
|
|
75
|
+
config_1.config.sessionAssetsPath = savedAssetsPath;
|
|
72
76
|
fs.rmSync(tmpAssets, { recursive: true, force: true });
|
|
73
77
|
});
|
|
78
|
+
/**
|
|
79
|
+
* The router sits behind authMiddleware in the API, and its roleGuard
|
|
80
|
+
* answers 401 to a request with no `req.auth` (27fd825, three days after
|
|
81
|
+
* this spec). Stand in for the middleware with an admin, as it sets one:
|
|
82
|
+
* no `teamIds`, so the session needs no Device row to be visible.
|
|
83
|
+
*/
|
|
74
84
|
function makeApp() {
|
|
75
85
|
const app = (0, express_1.default)();
|
|
86
|
+
app.use((req, _res, next) => {
|
|
87
|
+
req.auth = {
|
|
88
|
+
kind: 'user-session',
|
|
89
|
+
userId: 'usr_admin',
|
|
90
|
+
role: 'ADMIN',
|
|
91
|
+
scopes: (0, authMiddleware_1.scopesForRole)('ADMIN'),
|
|
92
|
+
rateLimit: 100,
|
|
93
|
+
};
|
|
94
|
+
next();
|
|
95
|
+
});
|
|
76
96
|
bug_report_1.default.register(app);
|
|
77
97
|
return app;
|
|
78
98
|
}
|
|
@@ -94,7 +114,7 @@ describe('POST /sessions/:id/bug-report (integration)', () => {
|
|
|
94
114
|
sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findMany').resolves([
|
|
95
115
|
{ createdAt: new Date(), title: 'click', response: 'fail' },
|
|
96
116
|
]);
|
|
97
|
-
const res = yield (0,
|
|
117
|
+
const res = yield (0, loopbackRequest_1.default)(makeApp())
|
|
98
118
|
.post('/sessions/sess-it/bug-report?mode=full')
|
|
99
119
|
.buffer(true)
|
|
100
120
|
.parse((r, cb) => {
|
|
@@ -15,22 +15,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const auth_1 = require("../../src/app/routers/auth");
|
|
20
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
20
21
|
describe('forgot-password rate limit', function () {
|
|
21
22
|
this.timeout(15000);
|
|
23
|
+
// Each attempt looks the email address up.
|
|
24
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
22
25
|
it('3 attempts from one IP -> 4th returns 429 with Retry-After', () => __awaiter(this, void 0, void 0, function* () {
|
|
23
26
|
const app = (0, express_1.default)();
|
|
24
27
|
app.use(express_1.default.json());
|
|
25
28
|
app.use('/auth', (0, auth_1.authPublicRouter)());
|
|
26
29
|
for (let i = 0; i < 3; i++) {
|
|
27
|
-
const r = yield (0,
|
|
30
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
28
31
|
.post('/auth/forgot-password')
|
|
29
32
|
.set('X-Forwarded-For', '7.7.7.7')
|
|
30
33
|
.send({ email: 'whatever@xenon.local' });
|
|
31
34
|
(0, chai_1.expect)(r.status).to.equal(204);
|
|
32
35
|
}
|
|
33
|
-
const blocked = yield (0,
|
|
36
|
+
const blocked = yield (0, loopbackRequest_1.default)(app)
|
|
34
37
|
.post('/auth/forgot-password')
|
|
35
38
|
.set('X-Forwarded-For', '7.7.7.7')
|
|
36
39
|
.send({ email: 'whatever@xenon.local' });
|
|
@@ -15,20 +15,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const auth_1 = require("../../src/app/routers/auth");
|
|
20
20
|
const typedi_1 = require("typedi");
|
|
21
21
|
const UserService_1 = require("../../src/services/UserService");
|
|
22
22
|
const PasswordResetService_1 = require("../../src/services/PasswordResetService");
|
|
23
23
|
const prisma_1 = require("../../src/prisma");
|
|
24
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
24
25
|
describe('forgot-password flow (integration)', function () {
|
|
25
26
|
this.timeout(30000);
|
|
27
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
26
28
|
let user;
|
|
27
29
|
const email = 'forgot-it@xenon.local';
|
|
28
30
|
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
29
|
-
yield prisma_1.prisma.passwordResetToken.deleteMany({ where: { user: { email } } });
|
|
30
|
-
yield prisma_1.prisma.userSession.deleteMany({ where: { user: { email } } });
|
|
31
|
-
yield prisma_1.prisma.user.deleteMany({ where: { email } });
|
|
32
31
|
user = yield typedi_1.Container.get(UserService_1.UserService).createUser({
|
|
33
32
|
email,
|
|
34
33
|
name: 'Forgot IT',
|
|
@@ -36,42 +35,41 @@ describe('forgot-password flow (integration)', function () {
|
|
|
36
35
|
role: 'ADMIN',
|
|
37
36
|
});
|
|
38
37
|
}));
|
|
39
|
-
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
40
|
-
yield prisma_1.prisma.passwordResetToken.deleteMany({ where: { userId: user.id } });
|
|
41
|
-
yield prisma_1.prisma.userSession.deleteMany({ where: { userId: user.id } });
|
|
42
|
-
yield prisma_1.prisma.user.delete({ where: { id: user.id } });
|
|
43
|
-
}));
|
|
44
38
|
function buildApp() {
|
|
45
39
|
const app = (0, express_1.default)();
|
|
46
40
|
app.use(express_1.default.json());
|
|
47
41
|
app.use('/auth', (0, auth_1.authPublicRouter)());
|
|
48
42
|
return app;
|
|
49
43
|
}
|
|
44
|
+
// A POST with the token in the body: since #265 the token stays out of
|
|
45
|
+
// request URLs, which the server log records. The old GET /check/:token is
|
|
46
|
+
// gone, so it answered 404 to every token, valid or not.
|
|
47
|
+
const check = (app, token) => (0, loopbackRequest_1.default)(app).post('/auth/reset-password/check').send({ token });
|
|
50
48
|
it('full flow: forgot -> DB token -> reset -> login with new password', () => __awaiter(this, void 0, void 0, function* () {
|
|
51
49
|
const app = buildApp();
|
|
52
50
|
// Generic 204 even with wrong email
|
|
53
|
-
const r1 = yield (0,
|
|
51
|
+
const r1 = yield (0, loopbackRequest_1.default)(app).post('/auth/forgot-password').send({ email: 'no@no.com' });
|
|
54
52
|
(0, chai_1.expect)(r1.status).to.equal(204);
|
|
55
|
-
const r2 = yield (0,
|
|
53
|
+
const r2 = yield (0, loopbackRequest_1.default)(app).post('/auth/forgot-password').send({ email });
|
|
56
54
|
(0, chai_1.expect)(r2.status).to.equal(204);
|
|
57
55
|
// Generate a token via the service directly so we have the raw value to reset with
|
|
58
56
|
const { raw } = yield typedi_1.Container.get(PasswordResetService_1.PasswordResetService).createToken(user.id);
|
|
59
|
-
const
|
|
60
|
-
(0, chai_1.expect)(
|
|
61
|
-
const reset = yield (0,
|
|
57
|
+
const valid = yield check(app, raw);
|
|
58
|
+
(0, chai_1.expect)(valid.status).to.equal(200);
|
|
59
|
+
const reset = yield (0, loopbackRequest_1.default)(app)
|
|
62
60
|
.post('/auth/reset-password')
|
|
63
61
|
.send({ token: raw, newPassword: 'NewPassw0rd1' });
|
|
64
62
|
(0, chai_1.expect)(reset.status).to.equal(204);
|
|
65
63
|
// Token now consumed
|
|
66
|
-
const recheck = yield (
|
|
64
|
+
const recheck = yield check(app, raw);
|
|
67
65
|
(0, chai_1.expect)(recheck.status).to.equal(404);
|
|
68
66
|
// Login with new password works
|
|
69
|
-
const login = yield (0,
|
|
67
|
+
const login = yield (0, loopbackRequest_1.default)(app)
|
|
70
68
|
.post('/auth/login')
|
|
71
69
|
.send({ email, password: 'NewPassw0rd1' });
|
|
72
70
|
(0, chai_1.expect)(login.status).to.equal(204);
|
|
73
71
|
// Login with old password fails
|
|
74
|
-
const oldLogin = yield (0,
|
|
72
|
+
const oldLogin = yield (0, loopbackRequest_1.default)(app)
|
|
75
73
|
.post('/auth/login')
|
|
76
74
|
.send({ email, password: 'old-password-1' });
|
|
77
75
|
(0, chai_1.expect)(oldLogin.status).to.equal(401);
|
|
@@ -83,7 +81,8 @@ describe('forgot-password flow (integration)', function () {
|
|
|
83
81
|
data: { expiresAt: new Date(Date.now() - 1000) },
|
|
84
82
|
});
|
|
85
83
|
const app = buildApp();
|
|
86
|
-
const r = yield (
|
|
84
|
+
const r = yield check(app, raw);
|
|
87
85
|
(0, chai_1.expect)(r.status).to.equal(404);
|
|
86
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'invalid or expired token' });
|
|
88
87
|
}));
|
|
89
88
|
});
|
|
@@ -145,7 +145,7 @@ describe('Max sessions CLI argument test', () => {
|
|
|
145
145
|
};
|
|
146
146
|
yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 6000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
|
|
147
147
|
.to.be.an('error')
|
|
148
|
-
.with.property('message', 'Device is busy or blocked
|
|
148
|
+
.with.property('message', 'Device is busy or blocked. Device request: {"platform":"ios","name":"iPhone","deviceType":"simulator"}'));
|
|
149
149
|
}));
|
|
150
150
|
});
|
|
151
151
|
describe('IOS Simulator Test', () => {
|
|
@@ -53,7 +53,7 @@ describe('IOS Test', () => {
|
|
|
53
53
|
};
|
|
54
54
|
yield (0, device_utils_1.allocateDeviceForSession)(capabilities, 6000, 1000, pluginArgs).catch((error) => (0, chai_1.expect)(error)
|
|
55
55
|
.to.be.an('error')
|
|
56
|
-
.with.property('message', 'No device matching request
|
|
56
|
+
.with.property('message', 'No device matching request. Device request: {"platform":"ios","name":"iPhone","deviceType":"real"}'));
|
|
57
57
|
}));
|
|
58
58
|
it('Should throw error if the IPA does not match with device type real', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
59
|
yield (0, device_utils_1.initializeStorage)();
|
|
@@ -48,8 +48,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
require("reflect-metadata");
|
|
49
49
|
const chai_1 = require("chai");
|
|
50
50
|
const express_1 = __importDefault(require("express"));
|
|
51
|
-
const
|
|
51
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
52
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
52
53
|
describe('legacy /reservation headers', () => {
|
|
54
|
+
// The route reads the phones' reservations from the database.
|
|
55
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
53
56
|
it('GET /reservation returns Deprecation + Sunset + Link headers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
57
|
const router = (yield Promise.resolve().then(() => __importStar(require('../../src/app/routers/reservation')))).default;
|
|
55
58
|
const app = (0, express_1.default)();
|
|
@@ -60,7 +63,7 @@ describe('legacy /reservation headers', () => {
|
|
|
60
63
|
next();
|
|
61
64
|
});
|
|
62
65
|
app.use('/reservation', router);
|
|
63
|
-
const res = yield (0,
|
|
66
|
+
const res = yield (0, loopbackRequest_1.default)(app).get('/reservation');
|
|
64
67
|
(0, chai_1.expect)(res.headers['deprecation']).to.equal('true');
|
|
65
68
|
(0, chai_1.expect)(res.headers['sunset']).to.match(/^\d{4}-\d{2}-\d{2}T/);
|
|
66
69
|
(0, chai_1.expect)(res.headers['link']).to.include('rel="alternate"');
|
|
@@ -13,15 +13,20 @@ require("reflect-metadata");
|
|
|
13
13
|
const chai_1 = require("chai");
|
|
14
14
|
const bootstrap_1 = require("../../src/services/identity/bootstrap");
|
|
15
15
|
const prisma_1 = require("../../src/prisma");
|
|
16
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
16
17
|
describe('bootstrapIdentity idempotency (integration)', function () {
|
|
17
18
|
this.timeout(30000);
|
|
19
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
20
|
+
// On a fresh database, so the first run has a super-admin to create. On a
|
|
21
|
+
// developer's populated one both runs returned at once and proved nothing.
|
|
18
22
|
it('running twice does not create duplicate users', () => __awaiter(this, void 0, void 0, function* () {
|
|
19
23
|
const before = yield prisma_1.prisma.user.count();
|
|
24
|
+
(0, chai_1.expect)(before, 'a fresh database').to.equal(0);
|
|
20
25
|
yield (0, bootstrap_1.bootstrapIdentity)();
|
|
21
26
|
const after1 = yield prisma_1.prisma.user.count();
|
|
27
|
+
(0, chai_1.expect)(after1, 'the bootstrap super-admin').to.equal(1);
|
|
22
28
|
yield (0, bootstrap_1.bootstrapIdentity)();
|
|
23
29
|
const after2 = yield prisma_1.prisma.user.count();
|
|
24
30
|
(0, chai_1.expect)(after2).to.equal(after1);
|
|
25
|
-
(0, chai_1.expect)(after1).to.be.greaterThanOrEqual(before);
|
|
26
31
|
}));
|
|
27
32
|
});
|
|
@@ -15,15 +15,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
20
20
|
const grid_1 = __importDefault(require("../../src/app/routers/grid"));
|
|
21
21
|
const typedi_1 = require("typedi");
|
|
22
22
|
const UserService_1 = require("../../src/services/UserService");
|
|
23
23
|
const ApiKeyService_1 = require("../../src/services/ApiKeyService");
|
|
24
|
-
const
|
|
24
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
25
25
|
describe('node pair auth — REST /register (integration)', function () {
|
|
26
26
|
this.timeout(60000);
|
|
27
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
27
28
|
let nodeUser;
|
|
28
29
|
let rawToken;
|
|
29
30
|
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -41,11 +42,6 @@ describe('node pair auth — REST /register (integration)', function () {
|
|
|
41
42
|
});
|
|
42
43
|
rawToken = tok.raw;
|
|
43
44
|
}));
|
|
44
|
-
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
45
|
-
yield prisma_1.prisma.apiKey.deleteMany({ where: { userId: nodeUser.id } });
|
|
46
|
-
yield prisma_1.prisma.userSession.deleteMany({ where: { userId: nodeUser.id } });
|
|
47
|
-
yield prisma_1.prisma.user.delete({ where: { id: nodeUser.id } });
|
|
48
|
-
}));
|
|
49
45
|
function buildApp() {
|
|
50
46
|
const app = (0, express_1.default)();
|
|
51
47
|
app.use(express_1.default.json());
|
|
@@ -54,7 +50,7 @@ describe('node pair auth — REST /register (integration)', function () {
|
|
|
54
50
|
return app;
|
|
55
51
|
}
|
|
56
52
|
it('pair (accessKey, token) → not 401/403', () => __awaiter(this, void 0, void 0, function* () {
|
|
57
|
-
const r = yield (0,
|
|
53
|
+
const r = yield (0, loopbackRequest_1.default)(buildApp())
|
|
58
54
|
.post('/register')
|
|
59
55
|
.query({ type: 'add' })
|
|
60
56
|
.set('x-xenon-access-key', nodeUser.accessKey)
|
|
@@ -64,7 +60,7 @@ describe('node pair auth — REST /register (integration)', function () {
|
|
|
64
60
|
(0, chai_1.expect)(r.status).to.not.equal(403);
|
|
65
61
|
}));
|
|
66
62
|
it('no auth headers → 401', () => __awaiter(this, void 0, void 0, function* () {
|
|
67
|
-
const r = yield (0,
|
|
63
|
+
const r = yield (0, loopbackRequest_1.default)(buildApp())
|
|
68
64
|
.post('/register')
|
|
69
65
|
.query({ type: 'add' })
|
|
70
66
|
.send([]);
|
|
@@ -48,7 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
require("reflect-metadata");
|
|
49
49
|
const chai_1 = require("chai");
|
|
50
50
|
const express_1 = __importDefault(require("express"));
|
|
51
|
-
const
|
|
51
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
52
52
|
const sinon_1 = __importDefault(require("sinon"));
|
|
53
53
|
describe('POST /xenon/api/ports/allocate', () => {
|
|
54
54
|
let app;
|
|
@@ -62,7 +62,7 @@ describe('POST /xenon/api/ports/allocate', () => {
|
|
|
62
62
|
app.use('/xenon/api/ports', router.makeRouter({ allocator: { allocate: allocateStub } }));
|
|
63
63
|
}));
|
|
64
64
|
it('returns 200 with the allocated ports', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
-
const res = yield (0,
|
|
65
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
66
66
|
.post('/xenon/api/ports/allocate')
|
|
67
67
|
.send({ udid: 'u1', host: 'h1', purposes: ['systemPort', 'chromedriverPort'], durationMs: 60000 });
|
|
68
68
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
@@ -70,14 +70,14 @@ describe('POST /xenon/api/ports/allocate', () => {
|
|
|
70
70
|
(0, chai_1.expect)(allocateStub.calledOnce).to.equal(true);
|
|
71
71
|
}));
|
|
72
72
|
it('returns 400 when purposes is missing or empty', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
-
const res = yield (0,
|
|
73
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
74
74
|
.post('/xenon/api/ports/allocate')
|
|
75
75
|
.send({ udid: 'u1', host: 'h1', durationMs: 60000 });
|
|
76
76
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
77
77
|
}));
|
|
78
78
|
it('returns 500 on allocator failure', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
79
|
allocateStub.rejects(new Error('boom'));
|
|
80
|
-
const res = yield (0,
|
|
80
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
81
81
|
.post('/xenon/api/ports/allocate')
|
|
82
82
|
.send({ udid: 'u1', host: 'h1', purposes: ['systemPort'], durationMs: 60000 });
|
|
83
83
|
(0, chai_1.expect)(res.status).to.equal(500);
|