@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
|
@@ -53,7 +53,7 @@ describe('SelectorVerificationJob catches a heal the live check missed', functio
|
|
|
53
53
|
yield scratch.db.selectorEvent.deleteMany();
|
|
54
54
|
yield scratch.db.selectorState.deleteMany();
|
|
55
55
|
yield scratch.db.sessionLog.deleteMany();
|
|
56
|
-
socket = {
|
|
56
|
+
socket = { emitToDashboardForSelector: sinon_1.default.stub().resolves() };
|
|
57
57
|
}));
|
|
58
58
|
const find = (build, selector, healed, at) => scratch.db.sessionLog.create({
|
|
59
59
|
data: {
|
|
@@ -105,7 +105,7 @@ describe('SelectorVerificationJob catches a heal the live check missed', functio
|
|
|
105
105
|
(0, chai_1.expect)(row).to.include({ status: 'active', regression_count: 1, clean_builds_count: 0 });
|
|
106
106
|
(0, chai_1.expect)(row.fixed_at).to.equal(null);
|
|
107
107
|
(0, chai_1.expect)(yield actionsOf('//pending/broke')).to.deep.equal(['broke_again']);
|
|
108
|
-
(0, chai_1.expect)(socket.
|
|
108
|
+
(0, chai_1.expect)(socket.emitToDashboardForSelector.firstCall.args[0]).to.equal('selector_regressed');
|
|
109
109
|
}));
|
|
110
110
|
it('sends a fixed selector back to fix', () => __awaiter(this, void 0, void 0, function* () {
|
|
111
111
|
yield state('//resolved/broke', 'resolved');
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const chai_1 = require("chai");
|
|
14
|
+
const typedi_1 = require("typedi");
|
|
15
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
16
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
17
|
+
const selector_health_fixture_1 = require("../helpers/selector-health-fixture");
|
|
18
|
+
const SelectorVisibilityResolver_1 = require("../../src/services/selector-health/SelectorVisibilityResolver");
|
|
19
|
+
const KEY = { strategy: 'xpath', selector: '//x' };
|
|
20
|
+
const MEMBER_A = { userId: 'u-a', teamIds: ['team-a'] };
|
|
21
|
+
describe('SelectorVisibilityResolver: who may see a selector, for its live events', () => {
|
|
22
|
+
it('TypeDI builds it, as the socket server gets it (Container.get)', () => {
|
|
23
|
+
// A function-typed constructor parameter would make Container.get throw.
|
|
24
|
+
const restore = (0, container_registration_1.saveRegistrations)(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
|
|
25
|
+
try {
|
|
26
|
+
(0, chai_1.expect)(typedi_1.Container.get(SelectorVisibilityResolver_1.SelectorVisibilityResolver)).to.be.instanceOf(SelectorVisibilityResolver_1.SelectorVisibilityResolver);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
restore();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
describe('caching', () => {
|
|
33
|
+
let clock;
|
|
34
|
+
let scopes;
|
|
35
|
+
let checks;
|
|
36
|
+
let visible;
|
|
37
|
+
function resolver(over = {}) {
|
|
38
|
+
return new SelectorVisibilityResolver_1.SelectorVisibilityResolver(Object.assign({ sessionScope: (viewer) => __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
var _a;
|
|
40
|
+
scopes.push((_a = viewer.userId) !== null && _a !== void 0 ? _a : '');
|
|
41
|
+
return { OR: [{ device_udid: { in: viewer.teamIds } }] };
|
|
42
|
+
}), healedIn: (key) => __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
checks.push(key.selector);
|
|
44
|
+
return visible;
|
|
45
|
+
}), now: () => clock }, over));
|
|
46
|
+
}
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
clock = 1000000;
|
|
49
|
+
scopes = [];
|
|
50
|
+
checks = [];
|
|
51
|
+
visible = true;
|
|
52
|
+
});
|
|
53
|
+
it('asks once per viewer and selector while the answer is fresh, then again', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
|
+
const r = resolver();
|
|
55
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
56
|
+
visible = false;
|
|
57
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A), 'still the cached answer').to.equal(true);
|
|
58
|
+
(0, chai_1.expect)(checks).to.have.length(1);
|
|
59
|
+
clock += SelectorVisibilityResolver_1.SELECTOR_VISIBILITY_TTL_MS;
|
|
60
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A), 'asked again after the TTL').to.equal(false);
|
|
61
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
62
|
+
}));
|
|
63
|
+
it('concurrent questions share one lookup', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
64
|
+
const r = resolver();
|
|
65
|
+
const answers = yield Promise.all([
|
|
66
|
+
r.canSee(KEY, MEMBER_A),
|
|
67
|
+
r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a'] }),
|
|
68
|
+
r.canSee(KEY, MEMBER_A),
|
|
69
|
+
]);
|
|
70
|
+
(0, chai_1.expect)(answers).to.deep.equal([true, true, true]);
|
|
71
|
+
(0, chai_1.expect)(checks).to.have.length(1);
|
|
72
|
+
(0, chai_1.expect)(scopes).to.have.length(1);
|
|
73
|
+
}));
|
|
74
|
+
it("a viewer's sessions are read once for all their selectors", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
const r = resolver();
|
|
76
|
+
yield r.canSee(KEY, MEMBER_A);
|
|
77
|
+
yield r.canSee({ strategy: 'id', selector: 'other' }, MEMBER_A);
|
|
78
|
+
(0, chai_1.expect)(scopes).to.deep.equal(['u-a']);
|
|
79
|
+
(0, chai_1.expect)(checks).to.deep.equal(['//x', 'other']);
|
|
80
|
+
}));
|
|
81
|
+
it('the same teams in another order are the same viewer; another user or team is not', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
const r = resolver();
|
|
83
|
+
yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a', 'team-b'] });
|
|
84
|
+
yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-b', 'team-a'] });
|
|
85
|
+
(0, chai_1.expect)(checks, 'team order').to.have.length(1);
|
|
86
|
+
yield r.canSee(KEY, { userId: 'u-b', teamIds: ['team-a', 'team-b'] });
|
|
87
|
+
yield r.canSee(KEY, { userId: 'u-a', teamIds: ['team-a'] });
|
|
88
|
+
(0, chai_1.expect)(checks, 'another user, other teams').to.have.length(3);
|
|
89
|
+
}));
|
|
90
|
+
it('a selector with no strategy is its own selector, not one with the same text', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
const r = resolver();
|
|
92
|
+
yield r.canSee({ strategy: '', selector: '//x' }, MEMBER_A);
|
|
93
|
+
yield r.canSee({ strategy: 'xpath', selector: '//x' }, MEMBER_A);
|
|
94
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
95
|
+
}));
|
|
96
|
+
it('a lookup that fails answers no, and is asked again next time', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
+
let fail = true;
|
|
98
|
+
const r = resolver({
|
|
99
|
+
healedIn: (key) => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
|
+
checks.push(key.selector);
|
|
101
|
+
if (fail)
|
|
102
|
+
throw new Error('database gone');
|
|
103
|
+
return true;
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
|
|
107
|
+
fail = false;
|
|
108
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
109
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
110
|
+
}));
|
|
111
|
+
it("a viewer's sessions that can't be read answer no, and are read again next time", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
let fail = true;
|
|
113
|
+
const r = resolver({
|
|
114
|
+
sessionScope: (viewer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
var _a;
|
|
116
|
+
scopes.push((_a = viewer.userId) !== null && _a !== void 0 ? _a : '');
|
|
117
|
+
if (fail)
|
|
118
|
+
throw new Error('database gone');
|
|
119
|
+
return undefined;
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
|
|
123
|
+
fail = false;
|
|
124
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
125
|
+
(0, chai_1.expect)(scopes).to.have.length(2);
|
|
126
|
+
}));
|
|
127
|
+
it('a lookup that hangs answers no after the timeout, and is asked again next time', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
let hang = true;
|
|
129
|
+
const r = resolver({
|
|
130
|
+
healedIn: (key) => {
|
|
131
|
+
checks.push(key.selector);
|
|
132
|
+
return hang ? new Promise(() => undefined) : Promise.resolve(true);
|
|
133
|
+
},
|
|
134
|
+
lookupTimeoutMs: 20,
|
|
135
|
+
});
|
|
136
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(false);
|
|
137
|
+
hang = false;
|
|
138
|
+
(0, chai_1.expect)(yield r.canSee(KEY, MEMBER_A)).to.equal(true);
|
|
139
|
+
(0, chai_1.expect)(checks).to.have.length(2);
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
describe("REST's rule, on a real database (Selector Health's own fixture)", () => {
|
|
143
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
144
|
+
const UNPLUGGED = 'sh-phone-unplugged';
|
|
145
|
+
before(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
yield (0, selector_health_fixture_1.seedSelectorHealth)(scratch.db);
|
|
147
|
+
// Priya's session on a phone that has since gone: no Device row.
|
|
148
|
+
yield scratch.db.session.create({
|
|
149
|
+
data: {
|
|
150
|
+
id: 'sh-s-unplugged',
|
|
151
|
+
device_udid: UNPLUGGED,
|
|
152
|
+
device_name: 'Gone',
|
|
153
|
+
device_platform: 'android',
|
|
154
|
+
desired_capabilities: '{}',
|
|
155
|
+
session_capabilities: '{}',
|
|
156
|
+
node_id: 'localhost',
|
|
157
|
+
has_live_video: false,
|
|
158
|
+
device_version: '14',
|
|
159
|
+
status: 'passed',
|
|
160
|
+
user_id: selector_health_fixture_1.USER.priya,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
yield scratch.db.sessionLog.create({
|
|
164
|
+
data: {
|
|
165
|
+
session_id: 'sh-s-unplugged',
|
|
166
|
+
url: '/element',
|
|
167
|
+
method: 'POST',
|
|
168
|
+
title: 'findElement',
|
|
169
|
+
response: '{}',
|
|
170
|
+
command_name: 'findElement',
|
|
171
|
+
is_healed: true,
|
|
172
|
+
original_strategy: 'xpath',
|
|
173
|
+
original_selector: '//unplugged/only',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}));
|
|
177
|
+
const xpath = (selector) => ({ strategy: 'xpath', selector });
|
|
178
|
+
const teamA = { userId: selector_health_fixture_1.USER.priya, teamIds: [selector_health_fixture_1.TEAM.a] };
|
|
179
|
+
const teamB = { userId: selector_health_fixture_1.USER.alex, teamIds: [selector_health_fixture_1.TEAM.b] };
|
|
180
|
+
const noTeam = { userId: 'sh-u-nobody', teamIds: [] };
|
|
181
|
+
it("a team's member sees the selectors healed on their team's phone or a shared one, not another team's", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
183
|
+
(0, chai_1.expect)(yield r.canSee({ strategy: 'id', selector: selector_health_fixture_1.SEL.warm }, teamA), 'team A phone').to.equal(true);
|
|
184
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.hot), teamA), 'shared phone').to.equal(true);
|
|
185
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.bOnly), teamA), "team B's phone").to.equal(false);
|
|
186
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.bOnly), teamB)).to.equal(true);
|
|
187
|
+
(0, chai_1.expect)(yield r.canSee({ strategy: 'id', selector: selector_health_fixture_1.SEL.warm }, teamB)).to.equal(false);
|
|
188
|
+
}));
|
|
189
|
+
it('a member of no team sees only what healed on a shared phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
190
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
191
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.hot), noTeam)).to.equal(true);
|
|
192
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.muted), noTeam)).to.equal(false);
|
|
193
|
+
(0, chai_1.expect)(yield r.canSee(xpath(selector_health_fixture_1.SEL.mutedB), noTeam)).to.equal(false);
|
|
194
|
+
}));
|
|
195
|
+
it('a heal recorded with no strategy is strategy ""', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
196
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
197
|
+
(0, chai_1.expect)(yield r.canSee({ strategy: '', selector: selector_health_fixture_1.SEL.legacy }, noTeam)).to.equal(true);
|
|
198
|
+
}));
|
|
199
|
+
it("a phone that has gone leaves its sessions' selectors to their owner", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
200
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
201
|
+
(0, chai_1.expect)(yield r.canSee(xpath('//unplugged/only'), teamA), 'Priya owns it').to.equal(true);
|
|
202
|
+
(0, chai_1.expect)(yield r.canSee(xpath('//unplugged/only'), teamB)).to.equal(false);
|
|
203
|
+
}));
|
|
204
|
+
it('an unknown selector is seen by no one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
205
|
+
const r = new SelectorVisibilityResolver_1.SelectorVisibilityResolver();
|
|
206
|
+
(0, chai_1.expect)(yield r.canSee(xpath('//never/healed'), teamA)).to.equal(false);
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -20,14 +20,17 @@ const plugin_1 = require("../../src/plugin");
|
|
|
20
20
|
const ServerManager_1 = require("../../src/services/ServerManager");
|
|
21
21
|
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
22
|
const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
|
|
23
|
+
const aiEngineSettings_1 = require("../../src/services/settings/aiEngineSettings");
|
|
23
24
|
const logger_1 = __importDefault(require("../../src/logger"));
|
|
24
25
|
/**
|
|
25
|
-
* The self-healing value saved on the Settings page
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* The self-healing value saved on the Settings page, and the AI engine page's
|
|
27
|
+
* provider, model and base URL, are read once at boot, so a restart doesn't put
|
|
28
|
+
* the server back on the options it was started with. They have to be loaded
|
|
29
|
+
* once the database is ready (migrations ran), after the startup options were
|
|
30
|
+
* applied (the AI engine's are what a cleared value goes back to), and before
|
|
31
|
+
* the first route can answer or the first command arrive.
|
|
29
32
|
*/
|
|
30
|
-
describe('loading the saved self-healing
|
|
33
|
+
describe('loading the saved self-healing and AI engine settings, at boot', () => {
|
|
31
34
|
let statics;
|
|
32
35
|
beforeEach(() => {
|
|
33
36
|
statics = {
|
|
@@ -57,6 +60,7 @@ describe('loading the saved self-healing setting, at boot', () => {
|
|
|
57
60
|
// The real one kills every go-ios process on the machine.
|
|
58
61
|
sinon_1.default.stub(boot, 'reapLeftoverGoIos').callsFake(step('reap'));
|
|
59
62
|
sinon_1.default.stub(SelfHealingSwitch_1.SelfHealingSwitch.prototype, 'load').callsFake(step('self-healing setting'));
|
|
63
|
+
sinon_1.default.stub(aiEngineSettings_1.AiEngineSettings.prototype, 'load').callsFake(step('AI engine settings'));
|
|
60
64
|
sinon_1.default.stub(boot, 'registerRoutes').callsFake(() => {
|
|
61
65
|
steps.push('routes');
|
|
62
66
|
throw new Error('boot stopped by the test');
|
|
@@ -69,6 +73,7 @@ describe('loading the saved self-healing setting, at boot', () => {
|
|
|
69
73
|
'database and migrations',
|
|
70
74
|
'reap',
|
|
71
75
|
'self-healing setting',
|
|
76
|
+
'AI engine settings',
|
|
72
77
|
'routes',
|
|
73
78
|
]);
|
|
74
79
|
}));
|
|
@@ -203,10 +203,10 @@ describe('the AI self-healing switch', () => {
|
|
|
203
203
|
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
204
204
|
}));
|
|
205
205
|
it("still hands a session's own healing tiers to the healing it runs", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
206
|
+
// Read from the session's driver, which every session has: SESSION_MANAGER
|
|
207
|
+
// holds a local one only with the dashboard on, and holds none here.
|
|
208
|
+
const driver = { sessionId: 'sess-6', caps: { 'xe:options': { healingTiers: [1, 2] } } };
|
|
209
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('sess-6')).to.equal(undefined);
|
|
210
210
|
yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
|
|
211
211
|
.handle(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
212
|
throw noSuchElement();
|
|
@@ -219,9 +219,10 @@ describe('the AI self-healing switch', () => {
|
|
|
219
219
|
let learn;
|
|
220
220
|
beforeEach(() => {
|
|
221
221
|
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
|
|
222
|
-
learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning');
|
|
222
|
+
learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning').resolves();
|
|
223
223
|
});
|
|
224
|
-
|
|
224
|
+
/** A findElement the driver answers with its element. */
|
|
225
|
+
const afterFind = () => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return ({ ELEMENT: 'el-1' }); }), { sessionId: 'sess-7' }, 'findElement', ['id', 'there', 'sess-7'], pluginArgs, false);
|
|
225
226
|
it('learns the selector while the switch is on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
227
|
yield afterFind();
|
|
227
228
|
(0, chai_1.expect)(learn.calledOnce).to.equal(true);
|
|
@@ -145,7 +145,11 @@ describe('createSession — an app named by id follows the team rule', () => {
|
|
|
145
145
|
return owned.includes('admin') || req.some((s) => owned.includes(s));
|
|
146
146
|
},
|
|
147
147
|
});
|
|
148
|
-
|
|
148
|
+
// Every key's owner is an ACTIVE member: a key names its owner only while
|
|
149
|
+
// they are. The admin key is unscoped by its admin scope.
|
|
150
|
+
typedi_1.Container.set(UserService_1.UserService, {
|
|
151
|
+
findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
|
|
152
|
+
});
|
|
149
153
|
sinon_1.default.stub(app_service_1.APP_SERVICE, 'getAppById').callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = APPS[id]) !== null && _a !== void 0 ? _a : null; }));
|
|
150
154
|
const device = { udid: 'u1', host: 'h1', platform: 'android', nodeId: 'node-hub' };
|
|
151
155
|
allocate = sinon_1.default.stub(deviceUtils, 'allocateDeviceForSession').resolves(device);
|
|
@@ -41,10 +41,12 @@ describe('authorizeSessionRequest — identity', () => {
|
|
|
41
41
|
authDisabledBefore = config_1.config.authDisabled;
|
|
42
42
|
config_1.config.authDisabled = false;
|
|
43
43
|
restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// the
|
|
47
|
-
typedi_1.Container.set(UserService_1.UserService, {
|
|
44
|
+
// Every id is an ACTIVE member in no team: a credential names its owner
|
|
45
|
+
// only while they are (session-create-credential-owner.spec.ts covers
|
|
46
|
+
// the other cases). The real lookups would read the developer's database.
|
|
47
|
+
typedi_1.Container.set(UserService_1.UserService, {
|
|
48
|
+
findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
|
|
49
|
+
});
|
|
48
50
|
sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
49
51
|
svc = new SessionLifecycleService_1.SessionLifecycleService();
|
|
50
52
|
});
|
|
@@ -105,7 +107,7 @@ describe('authorizeSessionRequest — identity', () => {
|
|
|
105
107
|
hasScope: () => false,
|
|
106
108
|
});
|
|
107
109
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
|
|
108
|
-
verify: sinon_1.default.stub().resolves({ sub: 'usr_carol' }),
|
|
110
|
+
verify: sinon_1.default.stub().resolves({ sub: 'usr_carol', scopes: 'sessions' }),
|
|
109
111
|
});
|
|
110
112
|
const res = yield invoke(svc, capsWith({ 'xe:options': { sessionToken: 'tok' } }));
|
|
111
113
|
(0, chai_1.expect)(res.apiKeyId).to.equal(null);
|
|
@@ -156,13 +158,14 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
156
158
|
findUser = sinon_1.default.stub().resolves(owner);
|
|
157
159
|
typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
|
|
158
160
|
};
|
|
159
|
-
|
|
161
|
+
// A session token carries the scopes of the credential that minted it.
|
|
162
|
+
const withSessionToken = (owner, teamId = null, scopes = 'sessions') => {
|
|
160
163
|
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
|
|
161
164
|
verifyPair: sinon_1.default.stub().resolves(null),
|
|
162
165
|
hasScope: () => false,
|
|
163
166
|
});
|
|
164
167
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
|
|
165
|
-
verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId }),
|
|
168
|
+
verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId, scopes }),
|
|
166
169
|
});
|
|
167
170
|
findUser = sinon_1.default.stub().resolves(owner);
|
|
168
171
|
typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
|
|
@@ -208,9 +211,19 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
208
211
|
withKey(keyRow('admin'), user('SUPER_ADMIN', 'INACTIVE'));
|
|
209
212
|
(0, chai_1.expect)((yield access(pairCaps())).canOverride).to.equal(false);
|
|
210
213
|
}));
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
// By the scopes it was minted with, as a key is judged. By its user's role
|
|
215
|
+
// alone, an ADMIN's key without the admin scope minted itself an override.
|
|
216
|
+
it("follows resolveActor's rule on a session token's user's role now", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
|
+
withSessionToken(user('ADMIN'), null, 'admin,sessions');
|
|
218
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
|
|
219
|
+
withSessionToken(user('ADMIN'), null, 'sessions');
|
|
220
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
|
|
221
|
+
withSessionToken(user('SUPER_ADMIN'), null, 'admin,sessions');
|
|
222
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
|
|
223
|
+
withSessionToken(user('SUPER_ADMIN'), null, 'sessions');
|
|
213
224
|
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
|
|
225
|
+
withSessionToken(user('MEMBER'), null, 'admin,sessions');
|
|
226
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
|
|
214
227
|
withSessionToken(user('MEMBER'));
|
|
215
228
|
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
|
|
216
229
|
}));
|
|
@@ -223,9 +236,24 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
223
236
|
(0, chai_1.expect)(res.scoped).to.equal(false);
|
|
224
237
|
(0, chai_1.expect)((yield res.leaseAccess()).canOverride).to.equal(false);
|
|
225
238
|
}));
|
|
226
|
-
|
|
239
|
+
// The owner is looked up when the credential is checked: a key names them
|
|
240
|
+
// only while they are ACTIVE, so a lookup that fails refuses the session
|
|
241
|
+
// rather than run it as someone who may have been switched off.
|
|
242
|
+
it('refuses the session when the owner lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
227
243
|
withKey(keyRow('admin'), null);
|
|
228
244
|
typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
|
|
245
|
+
let error;
|
|
246
|
+
try {
|
|
247
|
+
yield invoke(svc, pairCaps());
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
error = e;
|
|
251
|
+
}
|
|
252
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal('db down');
|
|
253
|
+
}));
|
|
254
|
+
it('is false, and the teams fall back to the shared pool, when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
+
withKey(keyRow('sessions'), user('MEMBER'));
|
|
256
|
+
teamRows.rejects(new Error('db down'));
|
|
229
257
|
(0, chai_1.expect)(yield access(pairCaps())).to.deep.equal({ canOverride: false, teamIds: [] });
|
|
230
258
|
}));
|
|
231
259
|
it('is true when auth is disabled, where every caller is an admin', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -331,9 +359,9 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
331
359
|
}
|
|
332
360
|
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("xe:options.team 'team_c' is not allowed for this API key");
|
|
333
361
|
}));
|
|
334
|
-
it('falls back to the shared pool when the lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
335
|
-
withKey(keyRow('sessions'),
|
|
336
|
-
|
|
362
|
+
it('falls back to the shared pool when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
363
|
+
withKey(keyRow('sessions'), user('MEMBER'));
|
|
364
|
+
teamRows.rejects(new Error('db down'));
|
|
337
365
|
(0, chai_1.expect)(yield scope(pairCaps())).to.deep.equal({ callerTeamIds: [], scoped: true });
|
|
338
366
|
}));
|
|
339
367
|
it('stays unscoped, with no lookup, for a session with no credentials', () => __awaiter(void 0, void 0, void 0, function* () {
|