@xenon-device-management/xenon 2.0.0 → 2.2.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 +2 -1
- package/lib/package.json +3 -4
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
- package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
- package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/InternalHttpClient.js +5 -1
- package/lib/src/XenonCapabilityManager.js +15 -1
- package/lib/src/app/appiumBasePath.js +32 -0
- package/lib/src/app/index.js +12 -0
- package/lib/src/app/insertBeforeRoutes.js +24 -4
- package/lib/src/app/registerCommandAuth.js +152 -33
- package/lib/src/app/routers/control.js +11 -104
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/routers/nodePhoneControl.js +301 -0
- package/lib/src/app/ws/h264StreamWs.js +43 -12
- package/lib/src/app/ws/logcatWs.js +48 -14
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- package/lib/src/app/ws/upgradeRouter.js +215 -0
- package/lib/src/dashboard/event-manager.js +7 -6
- package/lib/src/data-service/device-service.js +152 -49
- package/lib/src/data-service/device-store.js +92 -10
- package/lib/src/data-service/deviceClaims.js +116 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +102 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
- package/lib/src/device-managers/HealthMonitorService.js +17 -2
- package/lib/src/device-managers/IOSDeviceManager.js +5 -1
- package/lib/src/device-managers/NodeDevices.js +7 -2
- package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
- package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
- package/lib/src/device-managers/android/previewHold.js +2 -2
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
- package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
- package/lib/src/device-managers/localDeviceHosts.js +86 -0
- package/lib/src/device-managers/ownDeviceRow.js +38 -0
- package/lib/src/device-utils.js +78 -24
- package/lib/src/gateway/createHandoff.js +73 -0
- package/lib/src/gateway/defaultGateway.js +80 -0
- package/lib/src/gateway/forwardToNode.js +147 -0
- package/lib/src/gateway/hubSessionToken.js +356 -0
- package/lib/src/gateway/internalCall.js +90 -0
- package/lib/src/gateway/nodeSessionStatus.js +140 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
- package/lib/src/gateway/sessionCreate.js +136 -0
- package/lib/src/gateway/sessionGateway.js +201 -0
- package/lib/src/gateway/sessionLocator.js +100 -0
- package/lib/src/generated/client/edge.js +7 -4
- package/lib/src/generated/client/index-browser.js +4 -1
- package/lib/src/generated/client/index.d.ts +112 -1
- package/lib/src/generated/client/index.js +7 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +9 -0
- package/lib/src/generated/client/wasm.js +4 -1
- package/lib/src/helpers/index.js +5 -1
- package/lib/src/logger.js +5 -0
- package/lib/src/middleware/authMiddleware.js +80 -0
- package/lib/src/middleware/commandAuth.js +31 -17
- package/lib/src/middleware/csrfMiddleware.js +3 -2
- package/lib/src/middleware/deviceAccessGuard.js +4 -1
- package/lib/src/middleware/sessionListingFilter.js +153 -0
- package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
- package/lib/src/plugin.js +27 -1
- package/lib/src/prisma.js +2 -1
- package/lib/src/services/DeviceReconciler.js +5 -2
- package/lib/src/services/OrphanSweeper.js +12 -19
- package/lib/src/services/PortAllocator.js +75 -16
- package/lib/src/services/ServerManager.js +63 -30
- package/lib/src/services/SessionLifecycleService.js +335 -76
- package/lib/src/services/ShutdownCoordinator.js +7 -1
- package/lib/src/services/SocketServer.js +11 -2
- package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
- package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
- package/lib/src/services/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
- package/lib/src/services/lease/LeaseService.js +19 -5
- package/lib/src/services/session/nodeCreateCaps.js +55 -0
- package/lib/src/services/session/sessionCredentials.js +7 -59
- package/lib/src/sessions/CloudSession.js +12 -0
- package/lib/src/sessions/LocalSession.js +52 -26
- package/lib/src/sessions/RemoteSession.js +116 -9
- package/lib/src/sessions/SessionManager.js +49 -6
- package/lib/src/sessions/appiumUmbrella.js +32 -0
- package/lib/test/helpers/appium-umbrella.js +84 -0
- package/lib/test/helpers/fake-device-table.js +97 -0
- package/lib/test/helpers/loki-stores.js +41 -0
- package/lib/test/helpers/loopbackRequest.js +77 -0
- package/lib/test/helpers/own-node-id.js +29 -0
- package/lib/test/helpers/scratch-database.js +100 -0
- package/lib/test/helpers/scratch-port-leases.js +119 -0
- package/lib/test/helpers/test-container.js +35 -27
- package/lib/test/integration/androidDevices.spec.js +5 -4
- package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
- package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
- package/lib/test/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
- package/lib/test/unit/DeviceModel.spec.js +9 -9
- package/lib/test/unit/HealingCascade.test.js +5 -1
- package/lib/test/unit/OrphanSweeper.test.js +17 -13
- package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
- package/lib/test/unit/StreamTicketService.spec.js +4 -1
- package/lib/test/unit/TokenEndpoint.spec.js +17 -3
- package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
- package/lib/test/unit/android-h264-service.spec.js +24 -0
- package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
- package/lib/test/unit/apps-team-visibility.spec.js +16 -16
- package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
- package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
- package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
- package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
- package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
- package/lib/test/unit/authMiddleware.test.js +3 -0
- package/lib/test/unit/bug-report/route.spec.js +9 -9
- package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
- package/lib/test/unit/clipboard-route.spec.js +22 -8
- package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
- package/lib/test/unit/command-auth.spec.js +31 -31
- package/lib/test/unit/control-device-lookup.spec.js +9 -9
- package/lib/test/unit/control-forward-decision.spec.js +115 -0
- package/lib/test/unit/control-lookup-failure.spec.js +3 -3
- package/lib/test/unit/create-error-message.spec.js +43 -0
- package/lib/test/unit/create-handoff.spec.js +86 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/device-access-guard.spec.js +66 -28
- package/lib/test/unit/device-service.spec.js +4 -0
- package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
- package/lib/test/unit/device-team-guard.spec.js +20 -20
- package/lib/test/unit/device-utils.spec.js +8 -0
- package/lib/test/unit/extract-team-cap.spec.js +65 -0
- package/lib/test/unit/grid-admin-only.spec.js +3 -3
- package/lib/test/unit/healing-tier-gate.spec.js +5 -1
- package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
- package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
- package/lib/test/unit/helpers.spec.js +27 -0
- package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
- package/lib/test/unit/hub-create-once.spec.js +190 -0
- package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
- package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +426 -0
- package/lib/test/unit/hub-node-same-host.spec.js +356 -0
- package/lib/test/unit/hub-register-removal.spec.js +165 -0
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
- package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
- package/lib/test/unit/hub-session-token.spec.js +265 -0
- package/lib/test/unit/insert-before-routes.spec.js +5 -5
- package/lib/test/unit/install-repository-app-team.spec.js +6 -2
- package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
- package/lib/test/unit/internal-call.spec.js +181 -0
- package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
- package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
- package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
- package/lib/test/unit/is-xenon-running.spec.js +69 -0
- package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
- package/lib/test/unit/lease/LeaseService.spec.js +28 -1
- package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
- package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
- package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
- package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
- package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
- package/lib/test/unit/logcat-ws.spec.js +12 -1
- package/lib/test/unit/loopback-request.spec.js +81 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
- package/lib/test/unit/node-create-caps.spec.js +54 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
- package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
- package/lib/test/unit/node-phone-control.spec.js +131 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/omni-vision.spec.js +7 -3
- package/lib/test/unit/plugin.spec.js +45 -4
- package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
- package/lib/test/unit/profile-router.test.js +11 -11
- package/lib/test/unit/queue-requester.spec.js +5 -0
- package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
- package/lib/test/unit/queue-visibility.spec.js +1 -1
- package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
- package/lib/test/unit/recording-instrumentation.spec.js +5 -0
- package/lib/test/unit/recording-source-ended.spec.js +5 -0
- package/lib/test/unit/recording-store.spec.js +9 -3
- package/lib/test/unit/recordings-library-routes.spec.js +51 -51
- package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
- package/lib/test/unit/remote-session-create.spec.js +130 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
- package/lib/test/unit/reset-link.test.js +14 -14
- package/lib/test/unit/session-app-resolution.spec.js +14 -10
- package/lib/test/unit/session-assets.spec.js +9 -9
- package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
- package/lib/test/unit/session-create-layer.spec.js +255 -0
- package/lib/test/unit/session-credential-strip.spec.js +25 -9
- package/lib/test/unit/session-credentials.spec.js +0 -47
- package/lib/test/unit/session-listing-auth.spec.js +345 -0
- package/lib/test/unit/session-locator.spec.js +250 -0
- package/lib/test/unit/session-node-calls.spec.js +174 -0
- package/lib/test/unit/session-owner-resolver.spec.js +107 -0
- package/lib/test/unit/session-recovery-routing.spec.js +118 -0
- package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
- package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
- package/lib/test/unit/socketServer.bearer.spec.js +3 -1
- package/lib/test/unit/stream-leave-route.spec.js +19 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
- package/lib/test/unit/stream-stop-recording.spec.js +17 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- package/lib/test/unit/upgrade-router.spec.js +446 -0
- package/lib/test/unit/users-router.test.js +11 -8
- package/lib/test/unit/verify-credential.spec.js +4 -1
- package/lib/test/unit/webdriver-info-route.spec.js +64 -0
- package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
- package/prisma/schema.prisma +9 -0
- package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
- package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
- package/lib/src/proxy/wd-command-proxy.js +0 -221
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const fs_1 = __importDefault(require("fs"));
|
|
52
|
+
const os_1 = __importDefault(require("os"));
|
|
53
|
+
const path_1 = __importDefault(require("path"));
|
|
54
|
+
const express_1 = __importDefault(require("express"));
|
|
55
|
+
const jose = __importStar(require("jose"));
|
|
56
|
+
const typedi_1 = require("typedi");
|
|
57
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
58
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
59
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
60
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
61
|
+
/**
|
|
62
|
+
* The hub's session token: what a node accepts, with its own per-command auth
|
|
63
|
+
* on, in place of the client's credentials the hub already checked. Signed by
|
|
64
|
+
* the hub's JwtKeyService, checked by the node against the hub's public keys.
|
|
65
|
+
*/
|
|
66
|
+
describe('hub session tokens', () => {
|
|
67
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
68
|
+
let dirs;
|
|
69
|
+
let restore;
|
|
70
|
+
let hubKeys;
|
|
71
|
+
let hubUrl;
|
|
72
|
+
function keyService() {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-hub-token-'));
|
|
75
|
+
dirs.push(dir);
|
|
76
|
+
const keys = new JwtKeyService_1.JwtKeyService();
|
|
77
|
+
yield keys.init(dir);
|
|
78
|
+
return keys;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
dirs = [];
|
|
83
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer);
|
|
84
|
+
hubKeys = yield keyService();
|
|
85
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
86
|
+
// The hub serves its keys the way the real route does.
|
|
87
|
+
const hub = (0, express_1.default)();
|
|
88
|
+
hub.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
89
|
+
const server = yield loopback.serve(hub);
|
|
90
|
+
hubUrl = `http://127.0.0.1:${server.address().port}`;
|
|
91
|
+
}));
|
|
92
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
93
|
+
sinon_1.default.restore();
|
|
94
|
+
restore();
|
|
95
|
+
yield loopback.closeAll();
|
|
96
|
+
for (const dir of dirs)
|
|
97
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
98
|
+
}));
|
|
99
|
+
describe('HubSessionTokenIssuer (hub)', () => {
|
|
100
|
+
it('signs a token for one session, audience xenon-node, for five minutes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
102
|
+
const token = yield issuer.tokenFor('s-1');
|
|
103
|
+
(0, chai_1.expect)(token).to.be.a('string');
|
|
104
|
+
const claims = jose.decodeJwt(token);
|
|
105
|
+
(0, chai_1.expect)(claims.aud).to.equal(hubSessionToken_1.HUB_TOKEN_AUDIENCE);
|
|
106
|
+
(0, chai_1.expect)(claims.sid).to.equal('s-1');
|
|
107
|
+
(0, chai_1.expect)(claims.exp - claims.iat).to.equal(hubSessionToken_1.HUB_TOKEN_TTL_SECONDS);
|
|
108
|
+
// The hub's own key verifies it.
|
|
109
|
+
yield hubKeys.verify(token, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE });
|
|
110
|
+
}));
|
|
111
|
+
it('reuses a session’s token until it is forgotten', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
113
|
+
const sign = sinon_1.default.spy(hubKeys, 'sign');
|
|
114
|
+
const first = yield issuer.tokenFor('s-1');
|
|
115
|
+
(0, chai_1.expect)(yield issuer.tokenFor('s-1')).to.equal(first);
|
|
116
|
+
(0, chai_1.expect)(sign.callCount).to.equal(1);
|
|
117
|
+
yield issuer.tokenFor('s-2');
|
|
118
|
+
(0, chai_1.expect)(sign.callCount).to.equal(2);
|
|
119
|
+
issuer.forget('s-1');
|
|
120
|
+
yield issuer.tokenFor('s-1');
|
|
121
|
+
(0, chai_1.expect)(sign.callCount).to.equal(3);
|
|
122
|
+
}));
|
|
123
|
+
it('answers null, and warns once, when this server cannot sign', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, new JwtKeyService_1.JwtKeyService()); // never initialised
|
|
125
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
126
|
+
const warn = sinon_1.default.stub(issuer.logger, 'warn');
|
|
127
|
+
(0, chai_1.expect)(yield issuer.tokenFor('s-1')).to.equal(null);
|
|
128
|
+
(0, chai_1.expect)(yield issuer.tokenFor('s-2')).to.equal(null);
|
|
129
|
+
(0, chai_1.expect)(warn.callCount).to.equal(1);
|
|
130
|
+
}));
|
|
131
|
+
it('is an audience /auth/token will not mint for a user', () => {
|
|
132
|
+
var _a, _b;
|
|
133
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../src/app/routers/auth.ts'), 'utf8');
|
|
134
|
+
const mintable = (_b = (_a = /MINTABLE_AUDIENCES = \[([^\]]*)\]/.exec(source)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
|
|
135
|
+
(0, chai_1.expect)(mintable).to.include('xenon-rest');
|
|
136
|
+
(0, chai_1.expect)(mintable).not.to.include(hubSessionToken_1.HUB_TOKEN_AUDIENCE);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('HubSessionTokenVerifier (node)', () => {
|
|
140
|
+
it('fetches the keys from the hub’s public JWKS route', () => {
|
|
141
|
+
(0, chai_1.expect)((0, hubSessionToken_1.hubJwksUrl)('http://hub:4724').toString()).to.equal('http://hub:4724/xenon/api/auth/jwks.json');
|
|
142
|
+
});
|
|
143
|
+
it('accepts the hub’s token for its session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
144
|
+
const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().tokenFor('s-1'));
|
|
145
|
+
(0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-1')).to.equal(true);
|
|
146
|
+
}));
|
|
147
|
+
it('refuses the hub’s token for another session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
148
|
+
const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().tokenFor('s-1'));
|
|
149
|
+
(0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-2')).to.equal(false);
|
|
150
|
+
}));
|
|
151
|
+
it('refuses an expired token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
152
|
+
const token = yield hubKeys.sign({ sid: 's-1' }, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE, ttlSeconds: -3600 });
|
|
153
|
+
(0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-1')).to.equal(false);
|
|
154
|
+
}));
|
|
155
|
+
it('refuses a token signed by another key, even under the hub’s key id', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
156
|
+
const other = yield keyService();
|
|
157
|
+
const foreign = yield other.sign({ sid: 's-1' }, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE, ttlSeconds: 300 });
|
|
158
|
+
const verifier = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
|
|
159
|
+
(0, chai_1.expect)(yield verifier.verify(foreign, 's-1')).to.equal(false);
|
|
160
|
+
const hubKid = hubKeys.jwks().keys[0].kid;
|
|
161
|
+
const { privateKey } = yield jose.generateKeyPair('RS256');
|
|
162
|
+
const forged = yield new jose.SignJWT({ sid: 's-1' })
|
|
163
|
+
.setProtectedHeader({ alg: 'RS256', kid: hubKid })
|
|
164
|
+
.setAudience(hubSessionToken_1.HUB_TOKEN_AUDIENCE)
|
|
165
|
+
.setIssuedAt()
|
|
166
|
+
.setExpirationTime('5m')
|
|
167
|
+
.sign(privateKey);
|
|
168
|
+
(0, chai_1.expect)(yield verifier.verify(forged, 's-1')).to.equal(false);
|
|
169
|
+
}));
|
|
170
|
+
it('refuses the hub’s token for another audience', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
const rest = yield hubKeys.sign({ sid: 's-1', sub: 'alice' }, { audience: 'xenon-rest', ttlSeconds: 300 });
|
|
172
|
+
(0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(rest, 's-1')).to.equal(false);
|
|
173
|
+
}));
|
|
174
|
+
it('refuses what is not a token at all', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
175
|
+
(0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify('not-a-jwt', 's-1')).to.equal(false);
|
|
176
|
+
}));
|
|
177
|
+
it('throws HubTokenUnavailableError when the hub’s keys cannot be fetched', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().tokenFor('s-1'));
|
|
179
|
+
yield loopback.closeAll();
|
|
180
|
+
let thrown;
|
|
181
|
+
try {
|
|
182
|
+
yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verify(token, 's-1');
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
thrown = err;
|
|
186
|
+
}
|
|
187
|
+
(0, chai_1.expect)(thrown).to.be.instanceOf(hubSessionToken_1.HubTokenUnavailableError);
|
|
188
|
+
}));
|
|
189
|
+
});
|
|
190
|
+
describe('the token for a create (hub → node)', () => {
|
|
191
|
+
const grant = { userId: 'alice', udid: 'phone-1', host: 'http://10.0.0.5:4725' };
|
|
192
|
+
it('names the owner, the phone and its node, audience xenon-node-create, for two minutes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(grant));
|
|
194
|
+
const claims = jose.decodeJwt(token);
|
|
195
|
+
(0, chai_1.expect)(claims.aud).to.equal(hubSessionToken_1.HUB_CREATE_AUDIENCE);
|
|
196
|
+
(0, chai_1.expect)(claims).to.include({ sub: 'alice', udid: 'phone-1', host: 'http://10.0.0.5:4725' });
|
|
197
|
+
(0, chai_1.expect)(claims.sid, 'no session yet').to.equal(undefined);
|
|
198
|
+
(0, chai_1.expect)(claims.exp - claims.iat).to.equal(hubSessionToken_1.HUB_CREATE_TTL_SECONDS);
|
|
199
|
+
}));
|
|
200
|
+
it('carries no owner for an unattributed create', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
201
|
+
const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(Object.assign(Object.assign({}, grant), { userId: null })));
|
|
202
|
+
(0, chai_1.expect)(jose.decodeJwt(token).sub).to.equal(undefined);
|
|
203
|
+
(0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verifyCreate(token)).to.deep.equal(Object.assign(Object.assign({}, grant), { userId: null }));
|
|
204
|
+
}));
|
|
205
|
+
it('is minted fresh for every create', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
207
|
+
const sign = sinon_1.default.spy(hubKeys, 'sign');
|
|
208
|
+
yield issuer.createTokenFor(grant);
|
|
209
|
+
yield issuer.createTokenFor(grant);
|
|
210
|
+
(0, chai_1.expect)(sign.callCount).to.equal(2);
|
|
211
|
+
}));
|
|
212
|
+
it('answers null when this server cannot sign', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
213
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, new JwtKeyService_1.JwtKeyService()); // never initialised
|
|
214
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
215
|
+
sinon_1.default.stub(issuer.logger, 'warn');
|
|
216
|
+
(0, chai_1.expect)(yield issuer.createTokenFor(grant)).to.equal(null);
|
|
217
|
+
}));
|
|
218
|
+
it('the node reads the grant back from it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(grant));
|
|
220
|
+
(0, chai_1.expect)(yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verifyCreate(token)).to.deep.equal(grant);
|
|
221
|
+
}));
|
|
222
|
+
it('is no command token, and a command token is no create token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
223
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
224
|
+
const verifier = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
|
|
225
|
+
const create = (yield issuer.createTokenFor(grant));
|
|
226
|
+
const command = (yield issuer.tokenFor('s-1'));
|
|
227
|
+
(0, chai_1.expect)(yield verifier.verify(create, 's-1')).to.equal(false);
|
|
228
|
+
(0, chai_1.expect)(yield verifier.verifyCreate(command)).to.equal(null);
|
|
229
|
+
}));
|
|
230
|
+
it('refuses a forged, an expired, or an incomplete one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
+
const verifier = new hubSessionToken_1.HubSessionTokenVerifier(hubUrl);
|
|
232
|
+
const forger = yield keyService();
|
|
233
|
+
const claims = { sub: 'alice', udid: 'phone-1', host: grant.host };
|
|
234
|
+
const tokens = [
|
|
235
|
+
yield forger.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
|
|
236
|
+
yield hubKeys.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: -3600 }),
|
|
237
|
+
yield hubKeys.sign({ sub: 'alice', host: grant.host }, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
|
|
238
|
+
yield hubKeys.sign({ sub: 'alice', udid: 'phone-1' }, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
|
|
239
|
+
yield hubKeys.sign(claims, { audience: 'xenon-rest', ttlSeconds: 120 }),
|
|
240
|
+
'not-a-jwt',
|
|
241
|
+
];
|
|
242
|
+
for (const token of tokens)
|
|
243
|
+
(0, chai_1.expect)(yield verifier.verifyCreate(token)).to.equal(null);
|
|
244
|
+
}));
|
|
245
|
+
it('throws HubTokenUnavailableError when the hub’s keys cannot be fetched', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
246
|
+
const token = (yield new hubSessionToken_1.HubSessionTokenIssuer().createTokenFor(grant));
|
|
247
|
+
yield loopback.closeAll();
|
|
248
|
+
let thrown;
|
|
249
|
+
try {
|
|
250
|
+
yield new hubSessionToken_1.HubSessionTokenVerifier(hubUrl).verifyCreate(token);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
thrown = err;
|
|
254
|
+
}
|
|
255
|
+
(0, chai_1.expect)(thrown).to.be.instanceOf(hubSessionToken_1.HubTokenUnavailableError);
|
|
256
|
+
}));
|
|
257
|
+
it('is an audience /auth/token will not mint for a user', () => {
|
|
258
|
+
var _a, _b;
|
|
259
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../src/app/routers/auth.ts'), 'utf8');
|
|
260
|
+
const mintable = (_b = (_a = /MINTABLE_AUDIENCES = \[([^\]]*)\]/.exec(source)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : '';
|
|
261
|
+
(0, chai_1.expect)(mintable).to.include('xenon-rest');
|
|
262
|
+
(0, chai_1.expect)(mintable).not.to.include(hubSessionToken_1.HUB_CREATE_AUDIENCE);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
const chai_1 = require("chai");
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const insertBeforeRoutes_1 = require("../../src/app/insertBeforeRoutes");
|
|
20
20
|
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
21
21
|
/**
|
|
@@ -80,7 +80,7 @@ describe('insertBeforeRoutes', () => {
|
|
|
80
80
|
next();
|
|
81
81
|
});
|
|
82
82
|
(0, chai_1.expect)(result.placed).to.equal(true);
|
|
83
|
-
yield (0,
|
|
83
|
+
yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
|
|
84
84
|
.get('/wd/hub/session/abc/url')
|
|
85
85
|
.expect(200);
|
|
86
86
|
(0, chai_1.expect)(ran).to.deep.equal(['appium-middleware', 'guard:abc', 'route:abc']);
|
|
@@ -90,7 +90,7 @@ describe('insertBeforeRoutes', () => {
|
|
|
90
90
|
(0, insertBeforeRoutes_1.insertBeforeRoutes)(app, '/wd/hub/session/:sessionId', (_req, res) => {
|
|
91
91
|
res.status(404).json({ refused: true });
|
|
92
92
|
});
|
|
93
|
-
const res = yield (0,
|
|
93
|
+
const res = yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
|
|
94
94
|
.get('/wd/hub/session/abc/url')
|
|
95
95
|
.expect(404);
|
|
96
96
|
(0, chai_1.expect)(res.body).to.deep.equal({ refused: true });
|
|
@@ -102,7 +102,7 @@ describe('insertBeforeRoutes', () => {
|
|
|
102
102
|
ran.push('guard');
|
|
103
103
|
next();
|
|
104
104
|
});
|
|
105
|
-
yield (0,
|
|
105
|
+
yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
|
|
106
106
|
.post('/wd/hub/session')
|
|
107
107
|
.send({})
|
|
108
108
|
.expect(200);
|
|
@@ -133,7 +133,7 @@ describe('insertBeforeRoutes', () => {
|
|
|
133
133
|
res.json({});
|
|
134
134
|
});
|
|
135
135
|
(0, chai_1.expect)(result.placed).to.equal(true);
|
|
136
|
-
yield (0,
|
|
136
|
+
yield (0, loopbackRequest_1.default)(yield loopback.serve(app))
|
|
137
137
|
.get('/wd/hub/session/abc/url')
|
|
138
138
|
.expect(200);
|
|
139
139
|
(0, chai_1.expect)(ran).to.deep.equal(['guard', 'route']);
|
|
@@ -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 typedi_1 = require("typedi");
|
|
21
21
|
const control_1 = __importDefault(require("../../src/app/routers/control"));
|
|
22
22
|
const device_store_1 = require("../../src/data-service/device-store");
|
|
@@ -25,6 +25,7 @@ const device_managers_1 = require("../../src/device-managers");
|
|
|
25
25
|
const app_service_1 = require("../../src/dashboard/services/app-service");
|
|
26
26
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
27
27
|
const container_registration_1 = require("../helpers/container-registration");
|
|
28
|
+
const own_node_id_1 = require("../helpers/own-node-id");
|
|
28
29
|
/**
|
|
29
30
|
* Installing an uploaded app onto a phone hands over its binary as surely as
|
|
30
31
|
* downloading it does, so POST /control/:udid/install-repository-app follows
|
|
@@ -63,6 +64,8 @@ function buildApp(teamIds) {
|
|
|
63
64
|
return app;
|
|
64
65
|
}
|
|
65
66
|
describe('POST /control/:udid/install-repository-app follows the app team rule', () => {
|
|
67
|
+
// The fake rows are this server's own phones.
|
|
68
|
+
(0, own_node_id_1.useOwnNodeId)();
|
|
66
69
|
let manager;
|
|
67
70
|
let restore;
|
|
68
71
|
beforeEach(() => {
|
|
@@ -74,6 +77,7 @@ describe('POST /control/:udid/install-repository-app follows the app team rule',
|
|
|
74
77
|
return ({
|
|
75
78
|
udid: UDID,
|
|
76
79
|
host: '127.0.0.1',
|
|
80
|
+
nodeId: own_node_id_1.OWN_NODE_ID,
|
|
77
81
|
platform: 'android',
|
|
78
82
|
busy: false,
|
|
79
83
|
session_id: null,
|
|
@@ -89,7 +93,7 @@ describe('POST /control/:udid/install-repository-app follows the app team rule',
|
|
|
89
93
|
sinon_1.default.restore();
|
|
90
94
|
restore();
|
|
91
95
|
});
|
|
92
|
-
const install = (teamIds, appId) => (0,
|
|
96
|
+
const install = (teamIds, appId) => (0, loopbackRequest_1.default)(buildApp(teamIds))
|
|
93
97
|
.post(`/xenon/api/control/${UDID}/install-repository-app`)
|
|
94
98
|
.send({ appId });
|
|
95
99
|
it("installs a member's team app and a shared one", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const chai_1 = require("chai");
|
|
7
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
4
8
|
const MitmProxyHost_1 = require("../../../src/services/interceptor/MitmProxyHost");
|
|
5
9
|
const MockEngine_1 = require("../../../src/services/interceptor/MockEngine");
|
|
6
10
|
function makeHost(mockEngine = new MockEngine_1.MockEngine()) {
|
|
@@ -251,16 +255,25 @@ describe('MitmProxyHost.handleProxyError ring edge cases', () => {
|
|
|
251
255
|
});
|
|
252
256
|
// Tied to RECENT_CONNECT_TTL_MS in the source. If that constant changes, update here.
|
|
253
257
|
it('honors the TTL boundary: entry exactly at the cutoff is still attributed', () => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
258
|
+
// Freeze the clock: the source reads Date.now() again, and one millisecond
|
|
259
|
+
// passing in between (it does, under the full suite's load) puts the entry
|
|
260
|
+
// past the cutoff.
|
|
261
|
+
const clock = sinon_1.default.useFakeTimers({ now: Date.now(), toFake: ['Date'] });
|
|
262
|
+
try {
|
|
263
|
+
const { host, emitted } = makeHost();
|
|
264
|
+
host.recentConnects.push({
|
|
265
|
+
host: 'edge.example.com',
|
|
266
|
+
// strict `>` in the source means equal-to-TTL is still attributable
|
|
267
|
+
ts: Date.now() - 30000,
|
|
268
|
+
consumed: false,
|
|
269
|
+
});
|
|
270
|
+
host.handleProxyError(null, new Error('x'), 'HTTPS_CLIENT_ERROR');
|
|
271
|
+
(0, chai_1.expect)(emitted).to.have.length(1);
|
|
272
|
+
(0, chai_1.expect)(emitted[0].host).to.equal('edge.example.com');
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
clock.restore();
|
|
276
|
+
}
|
|
264
277
|
});
|
|
265
278
|
it('honors the TTL boundary: entry one ms past the cutoff is skipped', () => {
|
|
266
279
|
const { host, emitted } = makeHost();
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
const chai_1 = require("chai");
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const express_1 = __importDefault(require("express"));
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
|
+
const commandCaller_1 = require("../../src/middleware/commandCaller");
|
|
21
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
22
|
+
const insertBeforeRoutes_1 = require("../../src/app/insertBeforeRoutes");
|
|
23
|
+
const internalCall_1 = require("../../src/gateway/internalCall");
|
|
24
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
25
|
+
/**
|
|
26
|
+
* Xenon's own loopback calls (`<basePath>/wd-internal/...`).
|
|
27
|
+
*
|
|
28
|
+
* A call that carries this process's secret header is Xenon's own: the
|
|
29
|
+
* `/wd-internal` marker is stripped and the call skips per-command auth. The
|
|
30
|
+
* path alone proves nothing, so without the secret a `/wd-internal` request
|
|
31
|
+
* is left exactly as it came and ends where any unknown route ends.
|
|
32
|
+
*/
|
|
33
|
+
function appiumExpress() {
|
|
34
|
+
const appiumDir = path_1.default.dirname(require.resolve('appium/package.json'));
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
36
|
+
return require(require.resolve('express', { paths: [appiumDir] }));
|
|
37
|
+
}
|
|
38
|
+
const shapes = [
|
|
39
|
+
['Express 4', () => (0, express_1.default)()],
|
|
40
|
+
['Express 5 (Appium 3)', () => appiumExpress()()],
|
|
41
|
+
];
|
|
42
|
+
// Appium's catch-all, in shape: every unknown route gets the same answer.
|
|
43
|
+
const UNKNOWN_ROUTE = {
|
|
44
|
+
value: { error: 'unknown command', message: 'no route', stacktrace: '' },
|
|
45
|
+
};
|
|
46
|
+
describe('internal calls (/wd-internal with the per-process secret)', () => {
|
|
47
|
+
describe('the secret', () => {
|
|
48
|
+
it('is sent by internalCallHeaders and recognised by hasInternalCallSecret', () => {
|
|
49
|
+
const headers = (0, internalCall_1.internalCallHeaders)();
|
|
50
|
+
(0, chai_1.expect)(Object.keys(headers)).to.deep.equal([internalCall_1.INTERNAL_CALL_HEADER]);
|
|
51
|
+
(0, chai_1.expect)(headers[internalCall_1.INTERNAL_CALL_HEADER]).to.have.length.greaterThan(30);
|
|
52
|
+
(0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)(headers)).to.equal(true);
|
|
53
|
+
});
|
|
54
|
+
it('is refused when absent, wrong, a different length, or repeated', () => {
|
|
55
|
+
const secret = (0, internalCall_1.internalCallHeaders)()[internalCall_1.INTERNAL_CALL_HEADER];
|
|
56
|
+
(0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({})).to.equal(false);
|
|
57
|
+
(0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({ [internalCall_1.INTERNAL_CALL_HEADER]: `${secret.slice(1)}x` })).to.equal(false);
|
|
58
|
+
(0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({ [internalCall_1.INTERNAL_CALL_HEADER]: 'short' })).to.equal(false);
|
|
59
|
+
(0, chai_1.expect)((0, internalCall_1.hasInternalCallSecret)({ [internalCall_1.INTERNAL_CALL_HEADER]: [secret, secret] })).to.equal(false);
|
|
60
|
+
});
|
|
61
|
+
it('builds the loopback base URL under the normalised base path', () => {
|
|
62
|
+
(0, chai_1.expect)((0, internalCall_1.internalCallBaseUrl)('0.0.0.0', 4723, 'wd/hub/')).to.equal('http://127.0.0.1:4723/wd/hub/wd-internal');
|
|
63
|
+
(0, chai_1.expect)((0, internalCall_1.internalCallBaseUrl)('10.0.0.5', 4723, '')).to.equal('http://10.0.0.5:4723/wd-internal');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
for (const [shapeName, makeExpress] of shapes) {
|
|
67
|
+
describe(shapeName, () => {
|
|
68
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
69
|
+
let ran;
|
|
70
|
+
let ownerOf;
|
|
71
|
+
let warnings;
|
|
72
|
+
afterEach(() => loopback.closeAll());
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
ran = [];
|
|
75
|
+
warnings = [];
|
|
76
|
+
ownerOf = sinon_1.default.stub().resolves('alice');
|
|
77
|
+
});
|
|
78
|
+
function lab() {
|
|
79
|
+
return __awaiter(this, arguments, void 0, function* (basePath = '/wd/hub') {
|
|
80
|
+
const app = makeExpress();
|
|
81
|
+
app.use(express_1.default.json());
|
|
82
|
+
const route = (name) => (req, res) => {
|
|
83
|
+
var _a;
|
|
84
|
+
ran.push({ name, url: req.originalUrl, secret: req.headers[internalCall_1.INTERNAL_CALL_HEADER] });
|
|
85
|
+
res.json({ value: (_a = req.params.sessionId) !== null && _a !== void 0 ? _a : null });
|
|
86
|
+
};
|
|
87
|
+
app.get(`${basePath}/session/:sessionId/url`, route('getUrl'));
|
|
88
|
+
app.get(`${basePath}/session/:sessionId/timeouts`, route('getTimeouts'));
|
|
89
|
+
app.post(`${basePath}/session`, route('createSession'));
|
|
90
|
+
const result = (0, registerCommandAuth_1.registerCommandAuth)(app, { basePath }, {
|
|
91
|
+
enabled: () => true,
|
|
92
|
+
authDisabled: () => false,
|
|
93
|
+
verifier: new commandCaller_1.CommandCallerVerifier({
|
|
94
|
+
verifyKeyPair: sinon_1.default.stub().resolves(null),
|
|
95
|
+
verifyBearer: sinon_1.default.stub().resolves(null),
|
|
96
|
+
}),
|
|
97
|
+
ownerOf,
|
|
98
|
+
logger: {
|
|
99
|
+
info: () => undefined,
|
|
100
|
+
warn: (m) => warnings.push(m),
|
|
101
|
+
error: () => undefined,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
(0, chai_1.expect)(result.placed).to.equal(true);
|
|
105
|
+
// Appium adds its catch-all after every updater.
|
|
106
|
+
app.use((_req, res) => res.status(404).json(UNKNOWN_ROUTE));
|
|
107
|
+
return { app: yield loopback.serve(app), raw: app };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
it('without the secret, answers a /wd-internal command as an unknown route', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
const { app } = yield lab();
|
|
112
|
+
const internal = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/wd-internal/session/s1/url');
|
|
113
|
+
const unknown = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/no-such-prefix/session/s1/url');
|
|
114
|
+
(0, chai_1.expect)(internal.status).to.equal(404);
|
|
115
|
+
(0, chai_1.expect)(internal.status).to.equal(unknown.status);
|
|
116
|
+
(0, chai_1.expect)(internal.text).to.equal(unknown.text);
|
|
117
|
+
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
118
|
+
}));
|
|
119
|
+
it('with a wrong secret, answers as an unknown route too', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
const { app } = yield lab();
|
|
121
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
122
|
+
.get('/wd/hub/wd-internal/session/s1/url')
|
|
123
|
+
.set(internalCall_1.INTERNAL_CALL_HEADER, 'not-the-secret');
|
|
124
|
+
(0, chai_1.expect)(res.status).to.equal(404);
|
|
125
|
+
(0, chai_1.expect)(res.body).to.deep.equal(UNKNOWN_ROUTE);
|
|
126
|
+
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
127
|
+
}));
|
|
128
|
+
it('with the secret, strips the marker and skips per-command auth', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
const { app } = yield lab();
|
|
130
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
131
|
+
.get('/wd/hub/wd-internal/session/s1/timeouts')
|
|
132
|
+
.set((0, internalCall_1.internalCallHeaders)());
|
|
133
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
134
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: 's1' });
|
|
135
|
+
// The route saw the canonical path and never the secret.
|
|
136
|
+
(0, chai_1.expect)(ran).to.deep.equal([
|
|
137
|
+
{ name: 'getTimeouts', url: '/wd/hub/session/s1/timeouts', secret: undefined },
|
|
138
|
+
]);
|
|
139
|
+
(0, chai_1.expect)(ownerOf.called).to.equal(false);
|
|
140
|
+
(0, chai_1.expect)(warnings).to.deep.equal([]);
|
|
141
|
+
}));
|
|
142
|
+
it('the same session command without the marker still needs credentials', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
|
+
const { app } = yield lab();
|
|
144
|
+
const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s1/url').set((0, internalCall_1.internalCallHeaders)());
|
|
145
|
+
(0, chai_1.expect)(res.status).to.equal(404);
|
|
146
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('invalid session id');
|
|
147
|
+
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
148
|
+
}));
|
|
149
|
+
it('works under an empty base path', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
const { app } = yield lab('');
|
|
151
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
152
|
+
.get('/wd-internal/session/s1/url')
|
|
153
|
+
.set((0, internalCall_1.internalCallHeaders)());
|
|
154
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
155
|
+
(0, chai_1.expect)(ran.map((r) => r.url)).to.deep.equal(['/session/s1/url']);
|
|
156
|
+
}));
|
|
157
|
+
it('recognises only the exact marker, never a look-alike', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
|
+
const { app } = yield lab();
|
|
159
|
+
for (const url of [
|
|
160
|
+
'/wd/hub/wd-internalx/session/s1/url',
|
|
161
|
+
'/WD/HUB/WD-INTERNAL/session/s1/url',
|
|
162
|
+
'/wd-internal/wd/hub/session/s1/url',
|
|
163
|
+
]) {
|
|
164
|
+
const res = yield (0, loopbackRequest_1.default)(app).get(url).set((0, internalCall_1.internalCallHeaders)());
|
|
165
|
+
(0, chai_1.expect)(res.status, url).to.equal(404);
|
|
166
|
+
}
|
|
167
|
+
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
168
|
+
}));
|
|
169
|
+
it('puts the internal-call layer ahead of the session layer, both ahead of the routes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
170
|
+
const { raw } = yield lab();
|
|
171
|
+
const stack = (0, insertBeforeRoutes_1.routerStackOf)(raw);
|
|
172
|
+
const firstRoute = stack.findIndex((layer) => !!layer.route);
|
|
173
|
+
const names = stack.slice(0, firstRoute).map((layer) => { var _a; return (_a = layer.handle) === null || _a === void 0 ? void 0 : _a.name; });
|
|
174
|
+
const internal = names.indexOf('xenonInternalCalls');
|
|
175
|
+
const gateway = names.indexOf('xenonSessionGateway');
|
|
176
|
+
(0, chai_1.expect)(internal, 'internal-call layer placed').to.be.greaterThan(-1);
|
|
177
|
+
(0, chai_1.expect)(gateway, 'session gateway placed').to.be.greaterThan(internal);
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|