@xenon-device-management/xenon 2.13.2 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-jNQHb2IR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-BjGymyf3.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/h264Stream-C-OtmIuk.js +26 -0
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BOxe43p1.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/maintenance-settings-t7qr3fvX.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-DvCSv25z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-Cw_g218H.js} +2 -2
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/settings-C3uyGhyc.js +6 -0
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/webhook-settings-BU-C9lBf.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +23 -25
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +73 -20
- package/lib/src/app/openapi/grid.yaml +31 -7
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +165 -39
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +20 -2
- package/lib/src/app/routers/dashboard.js +69 -26
- package/lib/src/app/routers/grid.js +37 -13
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +51 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +88 -67
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +76 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +169 -15
- package/lib/src/data-service/web-config-service.js +50 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +114 -40
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +18 -3
- package/lib/src/generated/client/index-browser.js +15 -0
- package/lib/src/generated/client/index.d.ts +2086 -379
- package/lib/src/generated/client/index.js +18 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +38 -7
- package/lib/src/generated/client/wasm.js +15 -0
- package/lib/src/interceptors/CommandInterceptor.js +271 -188
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- 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 +23 -4
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +281 -32
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +7 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +105 -9
- package/lib/src/services/SessionLifecycleService.js +133 -75
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +85 -71
- 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/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -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 +132 -95
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- 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/recording/concurrency-gate.js +12 -3
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +58 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/apple-family.spec.js +36 -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/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +219 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -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/max-sessions.spec.js +240 -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/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +159 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +6 -8
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +80 -0
- package/lib/test/unit/self-healing-setting.spec.js +273 -0
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +264 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +119 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +98 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +5 -5
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +38 -7
- package/schema.json +23 -25
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/BugReportButton-BoGhlI7F.js +0 -15
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/ai-settings-Bce5SJob.js +0 -21
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/forgot-password-DnudqFFo.js +0 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/runbook-page-DJTVCJX7.js +0 -124
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
21
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
22
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
23
|
+
const CommandInterceptor_1 = require("../../src/interceptors/CommandInterceptor");
|
|
24
|
+
const HealingOrchestrator_1 = require("../../src/services/healing/HealingOrchestrator");
|
|
25
|
+
const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
|
|
26
|
+
const web_config_service_1 = require("../../src/data-service/web-config-service");
|
|
27
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
28
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
29
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
30
|
+
const logger_1 = __importDefault(require("../../src/logger"));
|
|
31
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
32
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
33
|
+
/**
|
|
34
|
+
* The Settings page's "AI self-healing" toggle used to do nothing: POST /config
|
|
35
|
+
* stored only the fields its table knew, GET /config never sent the switch back
|
|
36
|
+
* (so the page always showed Enabled), and CommandInterceptor read the copy of
|
|
37
|
+
* the plugin options the server started with, which nothing updates.
|
|
38
|
+
*
|
|
39
|
+
* It follows the other lab settings now: a value saved in the dashboard wins
|
|
40
|
+
* over the startup option, which wins over the default, and applies to the very
|
|
41
|
+
* next command without a restart. The interceptor runs on every command, so it
|
|
42
|
+
* reads an in-memory value (SelfHealingSwitch), never the database.
|
|
43
|
+
*/
|
|
44
|
+
describe('the AI self-healing switch', () => {
|
|
45
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
46
|
+
let context;
|
|
47
|
+
let startedWith;
|
|
48
|
+
let restoreContainer;
|
|
49
|
+
let healingSwitch;
|
|
50
|
+
const startup = (over = {}) => {
|
|
51
|
+
context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), over);
|
|
52
|
+
return context.pluginArgs;
|
|
53
|
+
};
|
|
54
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
56
|
+
startedWith = context.pluginArgs;
|
|
57
|
+
restoreContainer = (0, container_registration_1.saveRegistrations)(HealingOrchestrator_1.HealingOrchestrator, SelfHealingSwitch_1.SelfHealingSwitch);
|
|
58
|
+
// A switch of its own, as at boot, so no saved value leaks in from another spec.
|
|
59
|
+
typedi_1.Container.set(SelfHealingSwitch_1.SelfHealingSwitch, new SelfHealingSwitch_1.SelfHealingSwitch());
|
|
60
|
+
healingSwitch = typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch);
|
|
61
|
+
yield scratch.db.webConfig.deleteMany({});
|
|
62
|
+
}));
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
context.pluginArgs = startedWith;
|
|
65
|
+
restoreContainer();
|
|
66
|
+
sinon_1.default.restore();
|
|
67
|
+
});
|
|
68
|
+
function app() {
|
|
69
|
+
const a = (0, express_1.default)();
|
|
70
|
+
a.use(express_1.default.json());
|
|
71
|
+
a.use((req, _res, next) => {
|
|
72
|
+
req.auth = {
|
|
73
|
+
kind: 'user-session',
|
|
74
|
+
userId: 'usr_x',
|
|
75
|
+
role: 'SUPER_ADMIN',
|
|
76
|
+
scopes: 'admin,devices,sessions,read',
|
|
77
|
+
rateLimit: 1000,
|
|
78
|
+
};
|
|
79
|
+
next();
|
|
80
|
+
});
|
|
81
|
+
dashboard_1.default.register(a);
|
|
82
|
+
return a;
|
|
83
|
+
}
|
|
84
|
+
const save = (body) => (0, loopbackRequest_1.default)(app()).post('/config').send(body).timeout(5000);
|
|
85
|
+
const read = () => (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
86
|
+
describe('as the Settings page saves and reads it', () => {
|
|
87
|
+
it('is kept: what POST /config saves, GET /config sends back', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
startup();
|
|
89
|
+
const saved = yield save({ enableSelfHealing: false });
|
|
90
|
+
(0, chai_1.expect)(saved.status).to.equal(200);
|
|
91
|
+
const shown = yield read();
|
|
92
|
+
(0, chai_1.expect)(shown.status).to.equal(200);
|
|
93
|
+
(0, chai_1.expect)(shown.body.enableSelfHealing).to.equal(false);
|
|
94
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
|
|
95
|
+
enableSelfHealing: false,
|
|
96
|
+
});
|
|
97
|
+
}));
|
|
98
|
+
it('can be turned back on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
startup();
|
|
100
|
+
yield save({ enableSelfHealing: false });
|
|
101
|
+
yield save({ enableSelfHealing: true });
|
|
102
|
+
(0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(true);
|
|
103
|
+
}));
|
|
104
|
+
it('is on, and says so, when nothing was saved and the server was started with nothing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
startup();
|
|
106
|
+
const shown = yield read();
|
|
107
|
+
(0, chai_1.expect)(shown.body.enableSelfHealing).to.equal(true);
|
|
108
|
+
(0, chai_1.expect)(shown.body.defaults.enableSelfHealing).to.equal(true);
|
|
109
|
+
}));
|
|
110
|
+
it('shows the option the server started with when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
startup({ enableSelfHealing: false });
|
|
112
|
+
(0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(false);
|
|
113
|
+
}));
|
|
114
|
+
it('shows a saved value over the option the server started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
startup({ enableSelfHealing: false });
|
|
116
|
+
yield save({ enableSelfHealing: true });
|
|
117
|
+
(0, chai_1.expect)((yield read()).body.enableSelfHealing).to.equal(true);
|
|
118
|
+
}));
|
|
119
|
+
it('is saved on its own, without touching the other settings', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
startup();
|
|
121
|
+
yield save({ buildCleanupDays: 7 });
|
|
122
|
+
yield save({ enableSelfHealing: false });
|
|
123
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({
|
|
124
|
+
buildCleanupDays: 7,
|
|
125
|
+
enableSelfHealing: false,
|
|
126
|
+
});
|
|
127
|
+
}));
|
|
128
|
+
for (const bad of ['false', 'no', 0, 1, null, {}, []]) {
|
|
129
|
+
it(`refuses ${JSON.stringify(bad)} with a 400 naming the setting, and saves nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
startup();
|
|
131
|
+
const refused = yield save({ enableSelfHealing: bad, buildCleanupDays: 9 });
|
|
132
|
+
(0, chai_1.expect)(refused.status).to.equal(400);
|
|
133
|
+
(0, chai_1.expect)(refused.body).to.include({ error: 'invalid_setting', field: 'enableSelfHealing' });
|
|
134
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(web_config_service_1.WebConfigService).getConfig()).to.deep.equal({});
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
describe('on a running interceptor', () => {
|
|
139
|
+
const noSuchElement = () => Object.assign(new Error('NoSuchElement: An element could not be located'), {
|
|
140
|
+
name: 'NoSuchElementError',
|
|
141
|
+
});
|
|
142
|
+
let attemptHealing;
|
|
143
|
+
let pluginArgs;
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
pluginArgs = startup();
|
|
146
|
+
attemptHealing = sinon_1.default.stub().resolves(null);
|
|
147
|
+
typedi_1.Container.set(HealingOrchestrator_1.HealingOrchestrator, { attemptHealing });
|
|
148
|
+
});
|
|
149
|
+
/** A findElement the driver answers with NoSuchElement, as a broken selector does. */
|
|
150
|
+
function findMissing(sessionId) {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
let thrown;
|
|
153
|
+
try {
|
|
154
|
+
yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(this, void 0, void 0, function* () {
|
|
155
|
+
throw noSuchElement();
|
|
156
|
+
}), { sessionId }, 'findElement', ['xpath', '//android.widget.Button[@text="Gone"]', sessionId], pluginArgs, false);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
thrown = err;
|
|
160
|
+
}
|
|
161
|
+
return thrown;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
it('heals a missing element while the switch is on, as it always did', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
165
|
+
const thrown = yield findMissing('sess-on');
|
|
166
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
167
|
+
// Nothing healed it, so the test still sees its own NoSuchElement.
|
|
168
|
+
(0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
|
|
169
|
+
}));
|
|
170
|
+
it('stops healing for the next command once off is saved, with no restart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
yield findMissing('sess-1');
|
|
172
|
+
(0, chai_1.expect)(attemptHealing.callCount).to.equal(1);
|
|
173
|
+
(0, chai_1.expect)((yield save({ enableSelfHealing: false })).status).to.equal(200);
|
|
174
|
+
const thrown = yield findMissing('sess-1');
|
|
175
|
+
(0, chai_1.expect)(attemptHealing.callCount).to.equal(1);
|
|
176
|
+
// The failure reaches the test untouched, which is the point of turning it off.
|
|
177
|
+
(0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.name).to.equal('NoSuchElementError');
|
|
178
|
+
}));
|
|
179
|
+
it('heals again once on is saved after off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
yield save({ enableSelfHealing: false });
|
|
181
|
+
yield findMissing('sess-2');
|
|
182
|
+
(0, chai_1.expect)(attemptHealing.called).to.equal(false);
|
|
183
|
+
yield save({ enableSelfHealing: true });
|
|
184
|
+
yield findMissing('sess-2');
|
|
185
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
186
|
+
}));
|
|
187
|
+
it('does not read the database for a command', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
|
+
yield save({ enableSelfHealing: false });
|
|
189
|
+
const reads = sinon_1.default.spy(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig');
|
|
190
|
+
yield findMissing('sess-3');
|
|
191
|
+
yield findMissing('sess-3');
|
|
192
|
+
(0, chai_1.expect)(reads.called).to.equal(false);
|
|
193
|
+
}));
|
|
194
|
+
it('follows the startup option when nothing is saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
pluginArgs = startup({ enableSelfHealing: false });
|
|
196
|
+
yield findMissing('sess-4');
|
|
197
|
+
(0, chai_1.expect)(attemptHealing.called).to.equal(false);
|
|
198
|
+
}));
|
|
199
|
+
it('lets a saved on beat a startup off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
200
|
+
pluginArgs = startup({ enableSelfHealing: false });
|
|
201
|
+
yield save({ enableSelfHealing: true });
|
|
202
|
+
yield findMissing('sess-5');
|
|
203
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
204
|
+
}));
|
|
205
|
+
it("still hands a session's own healing tiers to the healing it runs", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
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
|
+
yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
|
|
211
|
+
.handle(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
|
+
throw noSuchElement();
|
|
213
|
+
}), driver, 'findElement', ['id', 'gone', 'sess-6'], pluginArgs, false)
|
|
214
|
+
.catch(() => undefined);
|
|
215
|
+
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
216
|
+
(0, chai_1.expect)(attemptHealing.firstCall.args).to.deep.equal(['sess-6', driver, 'id', 'gone', [1, 2]]);
|
|
217
|
+
}));
|
|
218
|
+
describe('and what it learns from a found element', () => {
|
|
219
|
+
let learn;
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
|
|
222
|
+
learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning').resolves();
|
|
223
|
+
});
|
|
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);
|
|
226
|
+
it('learns the selector while the switch is on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
227
|
+
yield afterFind();
|
|
228
|
+
(0, chai_1.expect)(learn.calledOnce).to.equal(true);
|
|
229
|
+
}));
|
|
230
|
+
it('learns nothing once off is saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
+
yield save({ enableSelfHealing: false });
|
|
232
|
+
yield afterFind();
|
|
233
|
+
(0, chai_1.expect)(learn.called).to.equal(false);
|
|
234
|
+
}));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
describe('at boot', () => {
|
|
238
|
+
it('is the saved value, over the startup option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
239
|
+
const pluginArgs = startup({ enableSelfHealing: true });
|
|
240
|
+
yield scratch.db.webConfig.create({
|
|
241
|
+
data: { id: 'enableSelfHealing', name: 'enableSelfHealing', value: 'false' },
|
|
242
|
+
});
|
|
243
|
+
yield healingSwitch.load();
|
|
244
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(pluginArgs)).to.equal(false);
|
|
245
|
+
}));
|
|
246
|
+
it('is the startup option when nothing is saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
247
|
+
yield healingSwitch.load();
|
|
248
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(startup({ enableSelfHealing: false }))).to.equal(false);
|
|
249
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(startup({ enableSelfHealing: true }))).to.equal(true);
|
|
250
|
+
}));
|
|
251
|
+
it('is on when neither was set', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
252
|
+
yield healingSwitch.load();
|
|
253
|
+
(0, chai_1.expect)(healingSwitch.isEnabled({})).to.equal(true);
|
|
254
|
+
}));
|
|
255
|
+
it('is the startup option, and says so once, when the saved value cannot be read', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
256
|
+
const warn = sinon_1.default.stub(logger_1.default, 'warn');
|
|
257
|
+
sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig').rejects(new Error('db is down'));
|
|
258
|
+
yield healingSwitch.load();
|
|
259
|
+
const off = startup({ enableSelfHealing: false });
|
|
260
|
+
const on = startup({ enableSelfHealing: true });
|
|
261
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(off)).to.equal(false);
|
|
262
|
+
(0, chai_1.expect)(healingSwitch.isEnabled(on)).to.equal(true);
|
|
263
|
+
// However many commands follow, it is said once, at the load.
|
|
264
|
+
(0, chai_1.expect)(warn.calledOnce).to.equal(true);
|
|
265
|
+
(0, chai_1.expect)(String(warn.firstCall.args[0])).to.match(/self-healing/i);
|
|
266
|
+
(0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('db is down');
|
|
267
|
+
}));
|
|
268
|
+
it('uses the startup option until it has loaded', () => {
|
|
269
|
+
(0, chai_1.expect)(healingSwitch.isEnabled({ enableSelfHealing: false })).to.equal(false);
|
|
270
|
+
(0, chai_1.expect)(healingSwitch.isEnabled({ enableSelfHealing: true })).to.equal(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -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* () {
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const typedi_1 = require("typedi");
|
|
22
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
23
|
+
const ApiKeyService_1 = require("../../src/services/ApiKeyService");
|
|
24
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
25
|
+
const UserService_1 = require("../../src/services/UserService");
|
|
26
|
+
const config_1 = require("../../src/config");
|
|
27
|
+
const prisma_1 = require("../../src/prisma");
|
|
28
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
29
|
+
const sessionCredentials_1 = require("../../src/services/session/sessionCredentials");
|
|
30
|
+
/**
|
|
31
|
+
* Session create judges the credentials it is given by REST's rule: a key
|
|
32
|
+
* pair or session token says who the caller is only while that user exists
|
|
33
|
+
* and is ACTIVE. Through 2.14 the pair was checked against the key alone
|
|
34
|
+
* (ApiKeyService.verifyPair) and the session token against its signature
|
|
35
|
+
* alone, so a user set to Inactive, or deleted, kept creating sessions as
|
|
36
|
+
* themselves, past XENON_REQUIRE_SESSION_TOKEN too.
|
|
37
|
+
*
|
|
38
|
+
* Such credentials now count as wrong ones: with the gate on the session is
|
|
39
|
+
* refused, with it off it runs with no owner, like any session presenting
|
|
40
|
+
* credentials that don't check out.
|
|
41
|
+
*
|
|
42
|
+
* A session token also has to carry the `sessions` scope, as a key pair
|
|
43
|
+
* must: POST /auth/token gave one to any credential, a read-only one
|
|
44
|
+
* included.
|
|
45
|
+
*/
|
|
46
|
+
// authorizeSessionRequest is private: the single place a create's identity is
|
|
47
|
+
// decided. It is handed the credentials createSession took out of the caps.
|
|
48
|
+
const invoke = (svc, caps) => svc.authorizeSessionRequest(caps, (0, sessionCredentials_1.takeSessionCredentials)(caps));
|
|
49
|
+
const capsWith = (options) => ({
|
|
50
|
+
alwaysMatch: { 'xe:options': options },
|
|
51
|
+
firstMatch: [{}],
|
|
52
|
+
});
|
|
53
|
+
function refusalOf(promise) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
var _a;
|
|
56
|
+
try {
|
|
57
|
+
yield promise;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
describe('session create: the credentials of an inactive or deleted user', () => {
|
|
66
|
+
let svc;
|
|
67
|
+
let dir;
|
|
68
|
+
let keys;
|
|
69
|
+
let restore;
|
|
70
|
+
let authDisabledBefore;
|
|
71
|
+
let gateBefore;
|
|
72
|
+
let users;
|
|
73
|
+
let findById;
|
|
74
|
+
const keyRow = { id: 'key_alice', userId: 'usr_alice', scopes: 'sessions', teamId: null };
|
|
75
|
+
const alice = () => users.usr_alice;
|
|
76
|
+
const pair = () => capsWith({ accessKey: 'xen_alice', token: 'tk' });
|
|
77
|
+
const token = (claims = { sub: 'usr_alice', scopes: 'sessions' }) => keys.sign(claims, { audience: 'xenon-session', ttlSeconds: 600 });
|
|
78
|
+
const gate = (on) => {
|
|
79
|
+
if (on)
|
|
80
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = 'true';
|
|
81
|
+
else
|
|
82
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
83
|
+
};
|
|
84
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
authDisabledBefore = config_1.config.authDisabled;
|
|
86
|
+
config_1.config.authDisabled = false;
|
|
87
|
+
gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
88
|
+
restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
|
|
89
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-create-owner-'));
|
|
90
|
+
keys = new JwtKeyService_1.JwtKeyService();
|
|
91
|
+
yield keys.init(dir);
|
|
92
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
|
|
93
|
+
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
|
|
94
|
+
verifyPair: sinon_1.default.stub().resolves(keyRow),
|
|
95
|
+
hasScope: (row, req) => {
|
|
96
|
+
const owned = row.scopes.split(',');
|
|
97
|
+
return owned.includes('admin') || req.some((s) => owned.includes(s));
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
users = { usr_alice: { id: 'usr_alice', role: 'MEMBER', status: 'ACTIVE' } };
|
|
101
|
+
findById = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = users[id]) !== null && _a !== void 0 ? _a : null; }));
|
|
102
|
+
typedi_1.Container.set(UserService_1.UserService, { findById });
|
|
103
|
+
sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
104
|
+
svc = new SessionLifecycleService_1.SessionLifecycleService();
|
|
105
|
+
}));
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
config_1.config.authDisabled = authDisabledBefore;
|
|
108
|
+
if (gateBefore === undefined)
|
|
109
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
110
|
+
else
|
|
111
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
|
|
112
|
+
sinon_1.default.restore();
|
|
113
|
+
restore();
|
|
114
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
describe('with XENON_REQUIRE_SESSION_TOKEN on', () => {
|
|
117
|
+
beforeEach(() => gate(true));
|
|
118
|
+
it("refuses an Inactive user's access key and token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
alice().status = 'INACTIVE';
|
|
120
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
|
|
121
|
+
}));
|
|
122
|
+
it("refuses a deleted user's key, whose row outlived them", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
users.usr_alice = undefined;
|
|
124
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
|
|
125
|
+
}));
|
|
126
|
+
it("refuses an Inactive user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
127
|
+
alice().status = 'INACTIVE';
|
|
128
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
129
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
|
|
130
|
+
}));
|
|
131
|
+
it("refuses a deleted user's session token, which is still within its lifetime", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
+
users.usr_alice = undefined;
|
|
133
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
134
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
|
|
135
|
+
}));
|
|
136
|
+
it("still admits an Active user's key and token, and their session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
const byPair = yield invoke(svc, pair());
|
|
138
|
+
(0, chai_1.expect)(byPair).to.include({ apiKeyId: 'key_alice', userId: 'usr_alice' });
|
|
139
|
+
const byToken = yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
140
|
+
(0, chai_1.expect)(byToken).to.include({ apiKeyId: null, userId: 'usr_alice' });
|
|
141
|
+
}));
|
|
142
|
+
it('looks the owner up once, though the gate and attribution both verify the token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
|
+
yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
144
|
+
(0, chai_1.expect)(findById.callCount).to.equal(1);
|
|
145
|
+
}));
|
|
146
|
+
});
|
|
147
|
+
describe('with XENON_REQUIRE_SESSION_TOKEN off', () => {
|
|
148
|
+
beforeEach(() => gate(false));
|
|
149
|
+
it("gives no owner to a session made with an Inactive user's key", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
alice().status = 'INACTIVE';
|
|
151
|
+
const res = yield invoke(svc, pair());
|
|
152
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
153
|
+
}));
|
|
154
|
+
it("gives no owner to a session made with a deleted user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
|
+
users.usr_alice = undefined;
|
|
156
|
+
const res = yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
157
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
158
|
+
}));
|
|
159
|
+
it('looks an Active owner up once for a key and token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
const res = yield invoke(svc, pair());
|
|
161
|
+
yield res.leaseAccess();
|
|
162
|
+
(0, chai_1.expect)(res.userId).to.equal('usr_alice');
|
|
163
|
+
(0, chai_1.expect)(findById.callCount).to.equal(1);
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
describe('a session token needs the `sessions` scope', () => {
|
|
167
|
+
for (const on of [false, true]) {
|
|
168
|
+
describe(`(gate ${on ? 'on' : 'off'})`, () => {
|
|
169
|
+
beforeEach(() => gate(on));
|
|
170
|
+
it('refuses one minted for a read-only credential', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
const caps = capsWith({
|
|
172
|
+
sessionToken: yield token({ sub: 'usr_alice', scopes: 'devices' }),
|
|
173
|
+
});
|
|
174
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/sessions` scope/);
|
|
175
|
+
}));
|
|
176
|
+
it('refuses one minted before session tokens carried their scopes, saying so', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
177
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice' }) });
|
|
178
|
+
const refusal = yield refusalOf(invoke(svc, caps));
|
|
179
|
+
(0, chai_1.expect)(refusal).to.match(/carries no `sessions` scope/);
|
|
180
|
+
(0, chai_1.expect)(refusal).to.match(/minted by Xenon 2\.14 or earlier carry none/);
|
|
181
|
+
(0, chai_1.expect)(refusal).to.match(/mint a new one/);
|
|
182
|
+
}));
|
|
183
|
+
it('admits one with `sessions` or `admin`', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
+
for (const scopes of ['sessions', 'admin', 'devices,sessions']) {
|
|
185
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
|
|
186
|
+
(0, chai_1.expect)((yield invoke(svc, caps)).userId).to.equal('usr_alice');
|
|
187
|
+
}
|
|
188
|
+
}));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Taking over a lease someone else created is an ownership override: a
|
|
193
|
+
// SUPER_ADMIN, or a credential with the `admin` scope. A session token was
|
|
194
|
+
// judged by its user's role alone, so an ADMIN's key without the admin
|
|
195
|
+
// scope, which may not override, minted a session token that could.
|
|
196
|
+
describe('a session token overrides a lease only as the credential that minted it could', () => {
|
|
197
|
+
beforeEach(() => gate(false));
|
|
198
|
+
const overrideWith = (role, scopes) => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
|
+
alice().role = role;
|
|
200
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
|
|
201
|
+
return (yield (yield invoke(svc, caps)).leaseAccess()).canOverride;
|
|
202
|
+
});
|
|
203
|
+
it("is refused for an ADMIN's token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
204
|
+
(0, chai_1.expect)(yield overrideWith('ADMIN', 'sessions')).to.equal(false);
|
|
205
|
+
}));
|
|
206
|
+
it("is allowed for an ADMIN's or SUPER_ADMIN's token minted with the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
207
|
+
(0, chai_1.expect)(yield overrideWith('ADMIN', 'admin,sessions')).to.equal(true);
|
|
208
|
+
(0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'admin,sessions')).to.equal(true);
|
|
209
|
+
}));
|
|
210
|
+
// As the SUPER_ADMIN's key pair and Bearer token override.
|
|
211
|
+
it("is allowed for a SUPER_ADMIN's token minted without it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
|
+
(0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'sessions')).to.equal(true);
|
|
213
|
+
}));
|
|
214
|
+
// The scope is fixed when the token is minted; the role is read now. An
|
|
215
|
+
// admin demoted after minting overrides no more.
|
|
216
|
+
it('is refused once the admin who minted it is a member', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
|
+
(0, chai_1.expect)(yield overrideWith('MEMBER', 'admin,sessions')).to.equal(false);
|
|
218
|
+
}));
|
|
219
|
+
it("is refused for a member's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
220
|
+
(0, chai_1.expect)(yield overrideWith('MEMBER', 'sessions')).to.equal(false);
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
// The owner lookup that fails is not a token that doesn't check out: the
|
|
224
|
+
// create is refused, as for a key whose owner can't be looked up. It ran
|
|
225
|
+
// with no owner (gate off) or was refused as "invalid or expired" (gate on).
|
|
226
|
+
describe('a session token whose owner cannot be looked up', () => {
|
|
227
|
+
for (const on of [false, true]) {
|
|
228
|
+
it(`refuses the create (gate ${on ? 'on' : 'off'})`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
229
|
+
gate(on);
|
|
230
|
+
findById.rejects(new Error('db down'));
|
|
231
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
232
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.equal('db down');
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
it('still counts a token that does not check out as none (gate off)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
236
|
+
gate(false);
|
|
237
|
+
const res = yield invoke(svc, capsWith({ sessionToken: 'not-a-jwt' }));
|
|
238
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
239
|
+
(0, chai_1.expect)(findById.called).to.equal(false);
|
|
240
|
+
}));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -146,7 +146,7 @@ describe('createSession — credentials never reach the driver or storage', () =
|
|
|
146
146
|
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
147
|
if (!t.startsWith('jwt:'))
|
|
148
148
|
throw new Error('bad token');
|
|
149
|
-
return { sub: t.slice('jwt:'.length), teamId: null };
|
|
149
|
+
return { sub: t.slice('jwt:'.length), teamId: null, scopes: 'sessions' };
|
|
150
150
|
}),
|
|
151
151
|
});
|
|
152
152
|
// The owner's teams: none, and not read from the developer's database.
|