@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
|
@@ -48,7 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
require("reflect-metadata");
|
|
49
49
|
const chai_1 = require("chai");
|
|
50
50
|
const express_1 = __importStar(require("express"));
|
|
51
|
-
const
|
|
51
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
52
52
|
describe('GET /xenon/api/sdk/version', () => {
|
|
53
53
|
it('returns pluginVersion + supports array', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
54
54
|
const register = (yield Promise.resolve().then(() => __importStar(require('../../src/app/routers/sdk-version')))).default;
|
|
@@ -61,7 +61,7 @@ describe('GET /xenon/api/sdk/version', () => {
|
|
|
61
61
|
const apiRouter = (0, express_1.Router)();
|
|
62
62
|
register(apiRouter);
|
|
63
63
|
app.use('/xenon/api', apiRouter);
|
|
64
|
-
const res = yield (0,
|
|
64
|
+
const res = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/sdk/version');
|
|
65
65
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
66
66
|
(0, chai_1.expect)(res.body.pluginVersion).to.be.a('string');
|
|
67
67
|
(0, chai_1.expect)(res.body.supports).to.include.members(['leases', 'ports', 'heartbeat']);
|
|
@@ -16,7 +16,7 @@ require("reflect-metadata");
|
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const sinon_1 = __importDefault(require("sinon"));
|
|
18
18
|
const express_1 = __importDefault(require("express"));
|
|
19
|
-
const
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
20
|
const dashboard_1 = require("../../src/app/routers/dashboard");
|
|
21
21
|
const roleGuard_1 = require("../../src/middleware/roleGuard");
|
|
22
22
|
const prisma_1 = require("../../src/prisma");
|
|
@@ -209,23 +209,23 @@ describe('GET /healing/selector-health — MEMBER guard (router-level)', () => {
|
|
|
209
209
|
});
|
|
210
210
|
afterEach(() => sinon_1.default.restore());
|
|
211
211
|
it('401s when unauthenticated', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
|
-
const r = yield (0,
|
|
212
|
+
const r = yield (0, loopbackRequest_1.default)(makeApp()).get('/xenon/api/healing/selector-health');
|
|
213
213
|
(0, chai_1.expect)(r.status).to.equal(401);
|
|
214
214
|
}));
|
|
215
215
|
it('403s for a role below MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
|
-
const r = yield (0,
|
|
216
|
+
const r = yield (0, loopbackRequest_1.default)(makeApp())
|
|
217
217
|
.get('/xenon/api/healing/selector-health')
|
|
218
218
|
.set('x-test-role', 'BOGUS');
|
|
219
219
|
(0, chai_1.expect)(r.status).to.equal(403);
|
|
220
220
|
}));
|
|
221
221
|
it('200s for MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
222
|
-
const r = yield (0,
|
|
222
|
+
const r = yield (0, loopbackRequest_1.default)(makeApp())
|
|
223
223
|
.get('/xenon/api/healing/selector-health')
|
|
224
224
|
.set('x-test-role', 'MEMBER');
|
|
225
225
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
226
226
|
}));
|
|
227
227
|
it('200s for ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
228
|
-
const r = yield (0,
|
|
228
|
+
const r = yield (0, loopbackRequest_1.default)(makeApp())
|
|
229
229
|
.get('/xenon/api/healing/selector-health')
|
|
230
230
|
.set('x-test-role', 'ADMIN');
|
|
231
231
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
@@ -8,28 +8,89 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
require("reflect-metadata");
|
|
13
16
|
const chai_1 = require("chai");
|
|
17
|
+
const child_process_1 = require("child_process");
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
14
21
|
const typedi_1 = require("typedi");
|
|
22
|
+
const client_1 = require("../../src/generated/client");
|
|
15
23
|
const UserService_1 = require("../../src/services/UserService");
|
|
16
24
|
const TeamService_1 = require("../../src/services/TeamService");
|
|
17
25
|
const prisma_1 = require("../../src/prisma");
|
|
26
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
27
|
+
const ROOT = path_1.default.resolve(__dirname, '../..');
|
|
28
|
+
const MIGRATIONS = path_1.default.join(ROOT, 'prisma', 'migrations');
|
|
29
|
+
/** The migration that backfills TeamMember from ApiKey.teamId. */
|
|
30
|
+
const BACKFILL = '20260430001747_phase_3_team_members';
|
|
31
|
+
/**
|
|
32
|
+
* `prisma migrate deploy` into `url` of the repository's migrations that
|
|
33
|
+
* `include` names, from a copy of the schema in `dir`. Run again with more
|
|
34
|
+
* of them, it applies only the ones not yet applied.
|
|
35
|
+
*/
|
|
36
|
+
function deploy(dir, url, include) {
|
|
37
|
+
const target = path_1.default.join(dir, 'migrations');
|
|
38
|
+
fs_1.default.mkdirSync(target, { recursive: true });
|
|
39
|
+
fs_1.default.copyFileSync(path_1.default.join(MIGRATIONS, 'migration_lock.toml'), path_1.default.join(target, 'migration_lock.toml'));
|
|
40
|
+
for (const name of fs_1.default.readdirSync(MIGRATIONS)) {
|
|
41
|
+
const sql = path_1.default.join(MIGRATIONS, name, 'migration.sql');
|
|
42
|
+
if (!include(name) || !fs_1.default.existsSync(sql))
|
|
43
|
+
continue;
|
|
44
|
+
fs_1.default.mkdirSync(path_1.default.join(target, name), { recursive: true });
|
|
45
|
+
fs_1.default.copyFileSync(sql, path_1.default.join(target, name, 'migration.sql'));
|
|
46
|
+
}
|
|
47
|
+
fs_1.default.copyFileSync(path_1.default.join(ROOT, 'prisma', 'schema.prisma'), path_1.default.join(dir, 'schema.prisma'));
|
|
48
|
+
(0, child_process_1.execSync)(`npx prisma migrate deploy --schema "${path_1.default.join(dir, 'schema.prisma')}"`, {
|
|
49
|
+
cwd: ROOT,
|
|
50
|
+
env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
|
|
51
|
+
stdio: 'pipe',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
18
54
|
describe('team-membership migration backfill (integration)', function () {
|
|
19
55
|
this.timeout(60000);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
57
|
+
// Through 2.14 this read the developer's own database, and returned early
|
|
58
|
+
// unless an old Legacy Admin happened to be in it. It runs the migration
|
|
59
|
+
// itself now, on a database migrated up to it and seeded as such a lab was.
|
|
60
|
+
it('backfills TeamMember from team keys, and gives Legacy Admin no membership', () => __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-team-backfill-'));
|
|
62
|
+
const url = `file:${path_1.default.join(dir, 'xenon.db')}`;
|
|
63
|
+
try {
|
|
64
|
+
deploy(dir, url, (name) => name < BACKFILL);
|
|
65
|
+
const before = new client_1.PrismaClient({ datasources: { db: { url } } });
|
|
66
|
+
try {
|
|
67
|
+
yield before.$executeRawUnsafe('INSERT INTO "Team" ("id", "name") VALUES (?, ?)', 'team-1', 'Team 1');
|
|
68
|
+
for (const [id, email] of [
|
|
69
|
+
['legacy', 'legacy-admin@xenon.local'],
|
|
70
|
+
['alice', 'alice@xenon.local'],
|
|
71
|
+
]) {
|
|
72
|
+
yield before.$executeRawUnsafe(`INSERT INTO "User" ("id", "email", "name", "passwordHash", "accessKey", "updatedAt")
|
|
73
|
+
VALUES (?, ?, ?, 'x', ?, CURRENT_TIMESTAMP)`, id, email, id, `ak-${id}`);
|
|
74
|
+
yield before.$executeRawUnsafe(`INSERT INTO "ApiKey" ("id", "name", "keyHash", "scopes", "teamId", "userId")
|
|
75
|
+
VALUES (?, ?, ?, 'devices', 'team-1', ?)`, `key-${id}`, id, `hash-${id}`, id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
yield before.$disconnect();
|
|
80
|
+
}
|
|
81
|
+
deploy(dir, url, () => true);
|
|
82
|
+
const after = new client_1.PrismaClient({ datasources: { db: { url } } });
|
|
83
|
+
try {
|
|
84
|
+
const members = yield after.teamMember.findMany({ select: { teamId: true, userId: true } });
|
|
85
|
+
(0, chai_1.expect)(members).to.deep.equal([{ teamId: 'team-1', userId: 'alice' }]);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
yield after.$disconnect();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
30
93
|
}
|
|
31
|
-
const polluted = yield prisma_1.prisma.teamMember.count({ where: { userId: legacy.id } });
|
|
32
|
-
(0, chai_1.expect)(polluted).to.equal(0);
|
|
33
94
|
}));
|
|
34
95
|
it('addMember / removeMember round-trip an arbitrary user', () => __awaiter(this, void 0, void 0, function* () {
|
|
35
96
|
const u = yield typedi_1.Container.get(UserService_1.UserService).createUser({
|
|
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const fs_1 = __importDefault(require("fs"));
|
|
20
20
|
const os_1 = __importDefault(require("os"));
|
|
21
21
|
const path_1 = __importDefault(require("path"));
|
|
@@ -29,17 +29,17 @@ const app_service_1 = require("../../src/dashboard/services/app-service");
|
|
|
29
29
|
const prisma_1 = require("../../src/prisma");
|
|
30
30
|
const seedUser_1 = require("../helpers/seedUser");
|
|
31
31
|
const container_registration_1 = require("../helpers/container-registration");
|
|
32
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
32
33
|
/**
|
|
33
34
|
* Uploaded apps follow the device team rule, run as real queries: the list
|
|
34
35
|
* builds a Prisma filter (`OR: [{ teamId: null }, { teamId: { in } }]`, with an
|
|
35
36
|
* empty `in` for a member in no team) that a stub would only echo back.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
* DATABASE_URL=file:/tmp/x.db npx prisma migrate deploy
|
|
39
|
-
* DATABASE_URL=file:/tmp/x.db npx mocha test/integration/team-visibility-apps.spec.ts
|
|
38
|
+
* On a scratch database of its own, so `npx mocha <file>` is enough.
|
|
40
39
|
*/
|
|
41
40
|
describe('team visibility on uploaded apps (integration)', function () {
|
|
42
41
|
this.timeout(60000);
|
|
42
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
43
43
|
let sa;
|
|
44
44
|
let alice;
|
|
45
45
|
let loner;
|
|
@@ -87,17 +87,10 @@ describe('team visibility on uploaded apps (integration)', function () {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
}));
|
|
90
|
-
after(() =>
|
|
91
|
-
yield prisma_1.prisma.app.deleteMany({ where: { id: { in: Object.values(A) } } });
|
|
92
|
-
yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
|
|
93
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
|
|
94
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
|
|
95
|
-
yield sa.cleanup();
|
|
96
|
-
yield alice.cleanup();
|
|
97
|
-
yield loner.cleanup();
|
|
90
|
+
after(() => {
|
|
98
91
|
restore();
|
|
99
92
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
100
|
-
})
|
|
93
|
+
});
|
|
101
94
|
function app() {
|
|
102
95
|
const a = (0, express_1.default)();
|
|
103
96
|
a.use(express_1.default.json());
|
|
@@ -106,13 +99,13 @@ describe('team visibility on uploaded apps (integration)', function () {
|
|
|
106
99
|
return a;
|
|
107
100
|
}
|
|
108
101
|
const ids = (cookie) => __awaiter(this, void 0, void 0, function* () {
|
|
109
|
-
return (yield (0,
|
|
102
|
+
return (yield (0, loopbackRequest_1.default)(app()).get('/apps').set('Cookie', cookie)).body
|
|
110
103
|
.map((r) => r.id)
|
|
111
104
|
.filter((id) => Object.values(A).includes(id));
|
|
112
105
|
});
|
|
113
106
|
const UNKNOWN = { error: 'App not found' };
|
|
114
107
|
it("lists a member's shared and own-team apps, with the team's name", () => __awaiter(this, void 0, void 0, function* () {
|
|
115
|
-
const res = yield (0,
|
|
108
|
+
const res = yield (0, loopbackRequest_1.default)(app()).get('/apps').set('Cookie', alice.cookie);
|
|
116
109
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
117
110
|
const mine = res.body.filter((r) => Object.values(A).includes(r.id));
|
|
118
111
|
(0, chai_1.expect)(mine.map((r) => r.id)).to.have.members([A.shared, A.a]);
|
|
@@ -127,20 +120,20 @@ describe('team visibility on uploaded apps (integration)', function () {
|
|
|
127
120
|
(0, chai_1.expect)(yield ids(sa.cookie)).to.have.members(Object.values(A));
|
|
128
121
|
}));
|
|
129
122
|
it("answers another team's app exactly as an unknown id", () => __awaiter(this, void 0, void 0, function* () {
|
|
130
|
-
const hidden = yield (0,
|
|
131
|
-
const unknown = yield (0,
|
|
123
|
+
const hidden = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.b}/download`).set('Cookie', alice.cookie);
|
|
124
|
+
const unknown = yield (0, loopbackRequest_1.default)(app())
|
|
132
125
|
.get(`/apps/nope-${stamp}/download`)
|
|
133
126
|
.set('Cookie', alice.cookie);
|
|
134
127
|
(0, chai_1.expect)(hidden.status).to.equal(404);
|
|
135
128
|
(0, chai_1.expect)(hidden.body).to.deep.equal(UNKNOWN);
|
|
136
129
|
(0, chai_1.expect)(unknown.status).to.equal(404);
|
|
137
130
|
(0, chai_1.expect)(unknown.body).to.deep.equal(UNKNOWN);
|
|
138
|
-
const own = yield (0,
|
|
131
|
+
const own = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.a}/download`).set('Cookie', alice.cookie);
|
|
139
132
|
(0, chai_1.expect)(own.status).to.equal(200);
|
|
140
133
|
}));
|
|
141
134
|
it('a download ticket serves its app once, with no login', () => __awaiter(this, void 0, void 0, function* () {
|
|
142
135
|
const t = yield typedi_1.Container.get(AppDownloadTicketService_1.AppDownloadTicketService).mint(A.b);
|
|
143
|
-
const first = yield (0,
|
|
136
|
+
const first = yield (0, loopbackRequest_1.default)(app())
|
|
144
137
|
.get(`/apps/${A.b}/download`)
|
|
145
138
|
.query({ ticket: t })
|
|
146
139
|
.buffer(true)
|
|
@@ -151,16 +144,16 @@ describe('team visibility on uploaded apps (integration)', function () {
|
|
|
151
144
|
});
|
|
152
145
|
(0, chai_1.expect)(first.status).to.equal(200);
|
|
153
146
|
(0, chai_1.expect)(Buffer.compare(first.body, bytesOf(A.b))).to.equal(0);
|
|
154
|
-
const again = yield (0,
|
|
147
|
+
const again = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.b}/download`).query({ ticket: t });
|
|
155
148
|
(0, chai_1.expect)(again.status).to.equal(401);
|
|
156
149
|
}));
|
|
157
150
|
it('moving an app to a team changes who sees it', () => __awaiter(this, void 0, void 0, function* () {
|
|
158
|
-
const put = (teamId) => (0,
|
|
151
|
+
const put = (teamId) => (0, loopbackRequest_1.default)(app()).put(`/apps/${A.b}/team`).set('Cookie', sa.cookie).send({ teamId });
|
|
159
152
|
(0, chai_1.expect)((yield put(teamA.id)).body).to.deep.equal({ ok: true, updated: 1 });
|
|
160
153
|
(0, chai_1.expect)(yield ids(alice.cookie)).to.include(A.b);
|
|
161
154
|
(0, chai_1.expect)((yield put(teamB.id)).status).to.equal(200);
|
|
162
155
|
(0, chai_1.expect)(yield ids(alice.cookie)).to.not.include(A.b);
|
|
163
|
-
const member = yield (0,
|
|
156
|
+
const member = yield (0, loopbackRequest_1.default)(app())
|
|
164
157
|
.put(`/apps/${A.b}/team`)
|
|
165
158
|
.set('Cookie', alice.cookie)
|
|
166
159
|
.send({ teamId: teamA.id });
|
|
@@ -16,7 +16,7 @@ require("reflect-metadata");
|
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
18
|
const cors_1 = __importDefault(require("cors"));
|
|
19
|
-
const
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
20
|
const fs_1 = __importDefault(require("fs"));
|
|
21
21
|
const os_1 = __importDefault(require("os"));
|
|
22
22
|
const path_1 = __importDefault(require("path"));
|
|
@@ -31,6 +31,10 @@ const prisma_1 = require("../../src/prisma");
|
|
|
31
31
|
const seedUser_1 = require("../helpers/seedUser");
|
|
32
32
|
const container_registration_1 = require("../helpers/container-registration");
|
|
33
33
|
const device_store_1 = require("../../src/data-service/device-store");
|
|
34
|
+
const db_1 = require("../../src/data-service/db");
|
|
35
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
36
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
37
|
+
const own_node_id_1 = require("../helpers/own-node-id");
|
|
34
38
|
/**
|
|
35
39
|
* Teams are a device boundary on /control, end to end: the real auth
|
|
36
40
|
* middleware computes req.auth.teamIds from TeamMember rows, and the real
|
|
@@ -88,23 +92,37 @@ const METHODS_BY_ACTION = CONTROL_ROUTES.reduce((acc, { method, action }) => {
|
|
|
88
92
|
}, new Map());
|
|
89
93
|
describe('team boundary on /control (integration)', function () {
|
|
90
94
|
this.timeout(60000);
|
|
95
|
+
// Hermetic, so it runs in `npm run test:all` and on its own alike: users,
|
|
96
|
+
// teams and sessions go to a scratch database, never ~/.cache/xenon/xenon.db,
|
|
97
|
+
// and the phones to the in-memory device store. Nothing here stubs `prisma`
|
|
98
|
+
// itself, so the scratch database stands in for the whole suite.
|
|
99
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
100
|
+
(0, loki_stores_1.useLokiStores)();
|
|
101
|
+
// The fixture phones are this server's own; see DEVICE_HOST.
|
|
102
|
+
(0, own_node_id_1.useOwnNodeId)();
|
|
91
103
|
let sa;
|
|
92
104
|
let alice;
|
|
93
|
-
let teamA;
|
|
94
|
-
let teamB;
|
|
95
105
|
let keyDir;
|
|
106
|
+
let deviceRows;
|
|
96
107
|
const restores = [];
|
|
97
108
|
const stamp = Date.now();
|
|
98
109
|
const SHARED_UDID = `tb-shared-${stamp}`;
|
|
99
110
|
const TEAM_A_UDID = `tb-team-a-${stamp}`;
|
|
100
111
|
const TEAM_B_UDID = `tb-team-b-${stamp}`;
|
|
101
|
-
//
|
|
102
|
-
//
|
|
112
|
+
// Team B's phone on a node. /control sends a node's phone's actions on to
|
|
113
|
+
// the node, or refuses them naming it (nodePhoneControl.ts), so the team
|
|
114
|
+
// guard has to have hidden it before that gate runs.
|
|
115
|
+
const TEAM_B_NODE_UDID = `tb-node-b-${stamp}`;
|
|
116
|
+
// The same length as the hidden udids, so an answer that echoes the path
|
|
117
|
+
// (Express's "Cannot GET …") has the same length for each.
|
|
103
118
|
const UNKNOWN_UDID = `tb-nobody-${stamp}`;
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
// Whose a phone is comes from its row: its nodeId, else its exact host
|
|
120
|
+
// (isOtherServersPhone). A row with neither of this server's is another
|
|
121
|
+
// server's phone, and /control sends its requests on to that host. So the
|
|
122
|
+
// fixtures carry this server's node id, and /control runs them here with
|
|
123
|
+
// the fake manager below. Port 1 has nothing listening, so a request sent
|
|
124
|
+
// on by mistake fails at once instead of reaching a real server here.
|
|
125
|
+
const DEVICE_HOST = 'http://127.0.0.1:1';
|
|
108
126
|
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
109
127
|
keyDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-team-boundary-'));
|
|
110
128
|
const keys = new JwtKeyService_1.JwtKeyService();
|
|
@@ -112,21 +130,28 @@ describe('team boundary on /control (integration)', function () {
|
|
|
112
130
|
restores.push((0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, device_managers_1.XenonManager));
|
|
113
131
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
|
|
114
132
|
typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return [new IOSDeviceManager()]; }) });
|
|
133
|
+
// The scratch database goes with the suite, and every row seeded here
|
|
134
|
+
// with it.
|
|
115
135
|
sa = yield (0, seedUser_1.seedUser)('SUPER_ADMIN', { name: 'TB SA' });
|
|
116
136
|
alice = yield (0, seedUser_1.seedUser)('MEMBER', { name: 'Alice (team A)' });
|
|
117
|
-
teamA = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-a-${stamp}`);
|
|
118
|
-
teamB = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-b-${stamp}`);
|
|
137
|
+
const teamA = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-a-${stamp}`);
|
|
138
|
+
const teamB = yield typedi_1.Container.get(TeamService_1.TeamService).create(`tb-team-b-${stamp}`);
|
|
119
139
|
yield typedi_1.Container.get(TeamService_1.TeamService).addMember(teamA.id, alice.user.id);
|
|
140
|
+
// The in-memory store is one collection for the whole process. Held here,
|
|
141
|
+
// so `after` removes these rows whatever useLokiStores has put back by then.
|
|
142
|
+
deviceRows = yield db_1.XenonDatabase.DeviceModel;
|
|
120
143
|
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
121
|
-
for (const [udid, teamId] of [
|
|
122
|
-
[SHARED_UDID, null],
|
|
123
|
-
[TEAM_A_UDID, teamA.id],
|
|
124
|
-
[TEAM_B_UDID, teamB.id],
|
|
144
|
+
for (const [udid, teamId, nodeId] of [
|
|
145
|
+
[SHARED_UDID, null, own_node_id_1.OWN_NODE_ID],
|
|
146
|
+
[TEAM_A_UDID, teamA.id, own_node_id_1.OWN_NODE_ID],
|
|
147
|
+
[TEAM_B_UDID, teamB.id, own_node_id_1.OWN_NODE_ID],
|
|
148
|
+
[TEAM_B_NODE_UDID, teamB.id, 'tb-another-node'],
|
|
125
149
|
]) {
|
|
126
150
|
const row = yield prisma_1.prisma.device.create({
|
|
127
151
|
data: {
|
|
128
152
|
udid,
|
|
129
153
|
host: DEVICE_HOST,
|
|
154
|
+
nodeId,
|
|
130
155
|
name: udid,
|
|
131
156
|
platform: 'ios',
|
|
132
157
|
// Known screen size, so stream/ticket's after-response size fetch
|
|
@@ -139,19 +164,14 @@ describe('team boundary on /control (integration)', function () {
|
|
|
139
164
|
yield store.addDevices([row]);
|
|
140
165
|
}
|
|
141
166
|
}));
|
|
142
|
-
after(() =>
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
after(() => {
|
|
168
|
+
deviceRows === null || deviceRows === void 0 ? void 0 : deviceRows.findAndRemove({
|
|
169
|
+
udid: { $in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID, TEAM_B_NODE_UDID] },
|
|
145
170
|
});
|
|
146
|
-
yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
|
|
147
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
|
|
148
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
|
|
149
|
-
yield sa.cleanup();
|
|
150
|
-
yield alice.cleanup();
|
|
151
171
|
for (const restore of restores.reverse())
|
|
152
172
|
restore();
|
|
153
173
|
fs_1.default.rmSync(keyDir, { recursive: true, force: true });
|
|
154
|
-
})
|
|
174
|
+
});
|
|
155
175
|
/**
|
|
156
176
|
* The /xenon/api stack as src/app/index.ts builds it, as far as /control
|
|
157
177
|
* goes: cors, auth, the control router, then `tail`.
|
|
@@ -181,8 +201,8 @@ describe('team boundary on /control (integration)', function () {
|
|
|
181
201
|
}
|
|
182
202
|
function send(who, method, udid, action, tail = 'api') {
|
|
183
203
|
const url = `/xenon/api/control/${encodeURIComponent(udid)}/${action}`;
|
|
184
|
-
const req = (0,
|
|
185
|
-
return req.set('Cookie', who.cookie)
|
|
204
|
+
const req = (0, loopbackRequest_1.default)(buildApp(tail))[method](url);
|
|
205
|
+
return req.set('Cookie', who.cookie);
|
|
186
206
|
}
|
|
187
207
|
/** What a caller can observe of an answer, with the requested udid masked out. */
|
|
188
208
|
function observed(res, udid) {
|
|
@@ -195,12 +215,12 @@ describe('team boundary on /control (integration)', function () {
|
|
|
195
215
|
allow: res.headers['allow'],
|
|
196
216
|
};
|
|
197
217
|
}
|
|
198
|
-
function expectSameAnswer(method, action, tail) {
|
|
218
|
+
function expectSameAnswer(udid, method, action, tail) {
|
|
199
219
|
return __awaiter(this, void 0, void 0, function* () {
|
|
200
220
|
var _a;
|
|
201
|
-
const hidden = yield send(alice, method,
|
|
221
|
+
const hidden = yield send(alice, method, udid, action, tail);
|
|
202
222
|
const unknown = yield send(alice, method, UNKNOWN_UDID, action, tail);
|
|
203
|
-
(0, chai_1.expect)(observed(hidden,
|
|
223
|
+
(0, chai_1.expect)(observed(hidden, udid)).to.deep.equal(observed(unknown, UNKNOWN_UDID));
|
|
204
224
|
// The guard's placeholder never reaches the caller, in a body or a header.
|
|
205
225
|
(0, chai_1.expect)((_a = hidden.text) !== null && _a !== void 0 ? _a : '', 'body').to.not.include(controlDevice_1.HIDDEN_DEVICE_UDID);
|
|
206
226
|
(0, chai_1.expect)(JSON.stringify(hidden.headers), 'headers').to.not.include(controlDevice_1.HIDDEN_DEVICE_UDID);
|
|
@@ -274,51 +294,58 @@ describe('team boundary on /control (integration)', function () {
|
|
|
274
294
|
// has this phone. Hold every request to that: another team's phone has to
|
|
275
295
|
// get the same status, body, content type, length and Allow header as a
|
|
276
296
|
// udid that does not exist, whether a route handles the request or not.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
'echo-url': 'a later layer that echoes req.url',
|
|
290
|
-
};
|
|
291
|
-
for (const tail of ['api', 'express', 'echo-url']) {
|
|
292
|
-
describe(`where no route handles the request (${TAIL_LABEL[tail]})`, () => {
|
|
293
|
-
for (const [method, action] of [
|
|
294
|
-
['get', 'bogus'],
|
|
295
|
-
['post', 'bogus'],
|
|
296
|
-
['post', 'stream/bogus'],
|
|
297
|
-
['get', ''],
|
|
298
|
-
]) {
|
|
299
|
-
it(`${method.toUpperCase()} ${action || '(no action)'}`, () => __awaiter(this, void 0, void 0, function* () {
|
|
300
|
-
yield expectSameAnswer(method, action, tail);
|
|
297
|
+
// On this server or on a node: a node's phone must not get the gate's
|
|
298
|
+
// forward or its refusal naming the node.
|
|
299
|
+
for (const [label, hiddenUdid] of [
|
|
300
|
+
["another team's phone", TEAM_B_UDID],
|
|
301
|
+
["another team's phone on a node", TEAM_B_NODE_UDID],
|
|
302
|
+
]) {
|
|
303
|
+
describe(`${label} answers exactly like an unknown udid`, () => {
|
|
304
|
+
describe('on every route', () => {
|
|
305
|
+
for (const { method, action } of CONTROL_ROUTES) {
|
|
306
|
+
it(`${method.toUpperCase()} ${action}`, () => __awaiter(this, void 0, void 0, function* () {
|
|
307
|
+
const res = yield expectSameAnswer(hiddenUdid, method, action, 'api');
|
|
308
|
+
(0, chai_1.expect)(res.status, 'status').to.equal(404);
|
|
301
309
|
}));
|
|
302
310
|
}
|
|
311
|
+
});
|
|
312
|
+
const TAIL_LABEL = {
|
|
313
|
+
api: "the API's JSON 404",
|
|
314
|
+
express: "Express's own 404",
|
|
315
|
+
'echo-url': 'a later layer that echoes req.url',
|
|
316
|
+
};
|
|
317
|
+
for (const tail of ['api', 'express', 'echo-url']) {
|
|
318
|
+
describe(`where no route handles the request (${TAIL_LABEL[tail]})`, () => {
|
|
319
|
+
for (const [method, action] of [
|
|
320
|
+
['get', 'bogus'],
|
|
321
|
+
['post', 'bogus'],
|
|
322
|
+
['post', 'stream/bogus'],
|
|
323
|
+
['get', ''],
|
|
324
|
+
]) {
|
|
325
|
+
it(`${method.toUpperCase()} ${action || '(no action)'}`, () => __awaiter(this, void 0, void 0, function* () {
|
|
326
|
+
yield expectSameAnswer(hiddenUdid, method, action, tail);
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
for (const [action, methods] of METHODS_BY_ACTION) {
|
|
330
|
+
const wrong = methods.includes('get') ? 'post' : 'get';
|
|
331
|
+
if (methods.includes(wrong))
|
|
332
|
+
continue;
|
|
333
|
+
it(`${wrong.toUpperCase()} ${action} (the wrong method)`, () => __awaiter(this, void 0, void 0, function* () {
|
|
334
|
+
yield expectSameAnswer(hiddenUdid, wrong, action, tail);
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
describe('OPTIONS', () => {
|
|
303
340
|
for (const [action, methods] of METHODS_BY_ACTION) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
341
|
+
it(`OPTIONS ${action}`, () => __awaiter(this, void 0, void 0, function* () {
|
|
342
|
+
const res = yield expectSameAnswer(hiddenUdid, 'options', action, 'api');
|
|
343
|
+
(0, chai_1.expect)(res.headers['allow'], 'allow').to.be.a('string');
|
|
344
|
+
for (const m of methods)
|
|
345
|
+
(0, chai_1.expect)(res.headers['allow']).to.include(m.toUpperCase());
|
|
309
346
|
}));
|
|
310
347
|
}
|
|
311
348
|
});
|
|
312
|
-
}
|
|
313
|
-
describe('OPTIONS', () => {
|
|
314
|
-
for (const [action, methods] of METHODS_BY_ACTION) {
|
|
315
|
-
it(`OPTIONS ${action}`, () => __awaiter(this, void 0, void 0, function* () {
|
|
316
|
-
const res = yield expectSameAnswer('options', action, 'api');
|
|
317
|
-
(0, chai_1.expect)(res.headers['allow'], 'allow').to.be.a('string');
|
|
318
|
-
for (const m of methods)
|
|
319
|
-
(0, chai_1.expect)(res.headers['allow']).to.include(m.toUpperCase());
|
|
320
|
-
}));
|
|
321
|
-
}
|
|
322
349
|
});
|
|
323
|
-
}
|
|
350
|
+
}
|
|
324
351
|
});
|
|
@@ -15,15 +15,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
20
20
|
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
21
21
|
const typedi_1 = require("typedi");
|
|
22
22
|
const TeamService_1 = require("../../src/services/TeamService");
|
|
23
23
|
const prisma_1 = require("../../src/prisma");
|
|
24
24
|
const seedUser_1 = require("../helpers/seedUser");
|
|
25
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
25
26
|
describe('team visibility on /dashboard reads (integration)', function () {
|
|
26
27
|
this.timeout(60000);
|
|
28
|
+
(0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
|
|
27
29
|
let sa;
|
|
28
30
|
let aliceMember;
|
|
29
31
|
let teamA;
|
|
@@ -94,24 +96,6 @@ describe('team visibility on /dashboard reads (integration)', function () {
|
|
|
94
96
|
data: Object.assign({ id: `sess-b-${stamp}`, device_udid: TEAM_B_UDID, build_id: buildTeamB.id }, baseSession),
|
|
95
97
|
});
|
|
96
98
|
}));
|
|
97
|
-
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
98
|
-
yield prisma_1.prisma.session.deleteMany({
|
|
99
|
-
where: { id: { in: [sessionShared.id, sessionTeamA.id, sessionTeamB.id] } },
|
|
100
|
-
});
|
|
101
|
-
yield prisma_1.prisma.build.deleteMany({
|
|
102
|
-
where: { id: { in: [buildShared.id, buildTeamA.id, buildTeamB.id] } },
|
|
103
|
-
});
|
|
104
|
-
yield prisma_1.prisma.device.deleteMany({
|
|
105
|
-
where: { udid: { in: [SHARED_UDID, TEAM_A_UDID, TEAM_B_UDID] } },
|
|
106
|
-
});
|
|
107
|
-
yield prisma_1.prisma.teamMember.deleteMany({
|
|
108
|
-
where: { teamId: { in: [teamA.id, teamB.id] } },
|
|
109
|
-
});
|
|
110
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
|
|
111
|
-
yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
|
|
112
|
-
yield sa.cleanup();
|
|
113
|
-
yield aliceMember.cleanup();
|
|
114
|
-
}));
|
|
115
99
|
function buildApp() {
|
|
116
100
|
const app = (0, express_1.default)();
|
|
117
101
|
app.use(express_1.default.json());
|
|
@@ -121,13 +105,13 @@ describe('team visibility on /dashboard reads (integration)', function () {
|
|
|
121
105
|
}
|
|
122
106
|
it('SA sees all three sessions; Alice sees only shared + team-A', () => __awaiter(this, void 0, void 0, function* () {
|
|
123
107
|
const app = buildApp();
|
|
124
|
-
const saList = yield (0,
|
|
108
|
+
const saList = yield (0, loopbackRequest_1.default)(app).get('/session').set('Cookie', sa.cookie);
|
|
125
109
|
(0, chai_1.expect)(saList.status).to.equal(200);
|
|
126
110
|
const saIds = saList.body.map((s) => s.id);
|
|
127
111
|
(0, chai_1.expect)(saIds).to.include(sessionShared.id);
|
|
128
112
|
(0, chai_1.expect)(saIds).to.include(sessionTeamA.id);
|
|
129
113
|
(0, chai_1.expect)(saIds).to.include(sessionTeamB.id);
|
|
130
|
-
const aliceList = yield (0,
|
|
114
|
+
const aliceList = yield (0, loopbackRequest_1.default)(app).get('/session').set('Cookie', aliceMember.cookie);
|
|
131
115
|
(0, chai_1.expect)(aliceList.status).to.equal(200);
|
|
132
116
|
const aliceIds = aliceList.body.map((s) => s.id);
|
|
133
117
|
(0, chai_1.expect)(aliceIds).to.include(sessionShared.id);
|
|
@@ -135,13 +119,13 @@ describe('team visibility on /dashboard reads (integration)', function () {
|
|
|
135
119
|
(0, chai_1.expect)(aliceIds).to.not.include(sessionTeamB.id);
|
|
136
120
|
}));
|
|
137
121
|
it('Alice GET /session/:teamB-id → 404', () => __awaiter(this, void 0, void 0, function* () {
|
|
138
|
-
const r = yield (0,
|
|
122
|
+
const r = yield (0, loopbackRequest_1.default)(buildApp())
|
|
139
123
|
.get(`/session/${sessionTeamB.id}`)
|
|
140
124
|
.set('Cookie', aliceMember.cookie);
|
|
141
125
|
(0, chai_1.expect)(r.status).to.equal(404);
|
|
142
126
|
}));
|
|
143
127
|
it('Alice GET /build → does not include team-B-only build', () => __awaiter(this, void 0, void 0, function* () {
|
|
144
|
-
const r = yield (0,
|
|
128
|
+
const r = yield (0, loopbackRequest_1.default)(buildApp()).get('/build').set('Cookie', aliceMember.cookie);
|
|
145
129
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
146
130
|
const ids = r.body.map((b) => b.id);
|
|
147
131
|
(0, chai_1.expect)(ids).to.include(buildShared.id);
|