@xenon-device-management/xenon 2.0.0 → 2.1.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 +1 -2
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
- package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
- package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.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 +57 -13
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/ws/h264StreamWs.js +13 -8
- package/lib/src/app/ws/logcatWs.js +12 -7
- 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 +81 -10
- package/lib/src/data-service/deviceClaims.js +98 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +94 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
- 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 +3 -3
- package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
- 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 +158 -38
- 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/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 +50 -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/lease/LeaseOrphanSweeper.js +32 -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/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/unit/AndroidDeviceManager.spec.js +8 -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 +6 -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 +18 -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 +28 -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 +291 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +401 -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 +2 -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 +92 -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 +4 -1
- package/lib/test/unit/loopback-request.spec.js +81 -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/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 +15 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
- package/lib/test/unit/stream-stop-recording.spec.js +13 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
- 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 +1 -2
- 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,356 @@
|
|
|
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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
42
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
43
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
44
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
45
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
46
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
47
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
51
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
52
|
+
};
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.HubSessionTokenVerifier = exports.HubTokenUnavailableError = exports.HubSessionTokenIssuer = exports.HUB_CONTROL_TTL_SECONDS = exports.HUB_CONTROL_AUDIENCE = exports.HUB_CREATE_TTL_SECONDS = exports.HUB_CREATE_AUDIENCE = exports.HUB_TOKEN_TTL_SECONDS = exports.HUB_TOKEN_AUDIENCE = exports.HUB_TOKEN_HEADER = void 0;
|
|
55
|
+
exports.hubJwksUrl = hubJwksUrl;
|
|
56
|
+
const crypto_1 = require("crypto");
|
|
57
|
+
const typedi_1 = require("typedi");
|
|
58
|
+
const jose = __importStar(require("jose"));
|
|
59
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
60
|
+
const JwtKeyService_1 = require("../services/token/JwtKeyService");
|
|
61
|
+
const singleUseLedger_1 = require("../services/token/singleUseLedger");
|
|
62
|
+
/**
|
|
63
|
+
* The credential a hub's calls to a node carry.
|
|
64
|
+
*
|
|
65
|
+
* Auth is enforced at the hub: it checks the client's credentials, then
|
|
66
|
+
* forwards the command to the node that runs the session. Each instance has
|
|
67
|
+
* its own database, so the node cannot check the client's key itself. It
|
|
68
|
+
* trusts the hub instead: every call the hub makes about a session (forwarded
|
|
69
|
+
* commands, and its own screenshot, source, recording and heartbeat calls)
|
|
70
|
+
* carries a short-lived RS256 token the hub signs for that one session. The
|
|
71
|
+
* node verifies it against the hub's public keys (`/xenon/api/auth/jwks.json`)
|
|
72
|
+
* when its own per-command auth is on.
|
|
73
|
+
*
|
|
74
|
+
* The audience is `xenon-node`, which /auth/token will not mint, and the token
|
|
75
|
+
* names its session (`sid`), so a token for one session opens no other.
|
|
76
|
+
*
|
|
77
|
+
* A create has no session yet, so it carries a token of its own kind:
|
|
78
|
+
* audience `xenon-node-create`, naming the owner the hub verified (`sub`,
|
|
79
|
+
* absent when the create is unattributed), the phone the hub allocated
|
|
80
|
+
* (`udid`) and that phone's node (`host`, the phone row's host). A separate
|
|
81
|
+
* audience rather than a claim on the `xenon-node` token, so neither kind can
|
|
82
|
+
* be taken for the other whatever claims a verifier reads: jose refuses the
|
|
83
|
+
* other kind's audience before any claim is looked at. The node treats a valid
|
|
84
|
+
* one as the session's credential (sessionCreate.ts).
|
|
85
|
+
*
|
|
86
|
+
* A hub's own device-control call to a node's phone (`/xenon/api/control/
|
|
87
|
+
* <udid>/tap`, swipe, text, keyevent, touchAndHold, which the hub forwards to
|
|
88
|
+
* the phone's node) carries a third kind: audience `xenon-node-control`, one
|
|
89
|
+
* minute, naming the hub user it acts for (`sub`), whether they are an admin
|
|
90
|
+
* (`adm`), the phone (`udid`) and its node (`host`). The hub checked the
|
|
91
|
+
* caller, the team rule and ownership before forwarding. The node accepts it
|
|
92
|
+
* only for `/control` on that phone (authMiddleware), then runs its own
|
|
93
|
+
* ownership guard with that user.
|
|
94
|
+
*
|
|
95
|
+
* A create token is single-use. The hub sends a create once (it is not safe
|
|
96
|
+
* to repeat), and each token has its own id (`jti`), which the node's
|
|
97
|
+
* verifier remembers until the token expires: the same token again is
|
|
98
|
+
* refused. A token with no id, from a hub before this rule, is accepted as
|
|
99
|
+
* before, without that check.
|
|
100
|
+
*/
|
|
101
|
+
exports.HUB_TOKEN_HEADER = 'x-xenon-hub-token';
|
|
102
|
+
exports.HUB_TOKEN_AUDIENCE = 'xenon-node';
|
|
103
|
+
exports.HUB_TOKEN_TTL_SECONDS = 300;
|
|
104
|
+
exports.HUB_CREATE_AUDIENCE = 'xenon-node-create';
|
|
105
|
+
/**
|
|
106
|
+
* Long enough for the create to reach the node, which checks the token as the
|
|
107
|
+
* request arrives; short enough that a copied token is soon worthless. It
|
|
108
|
+
* opens one phone, on one node, once.
|
|
109
|
+
*/
|
|
110
|
+
exports.HUB_CREATE_TTL_SECONDS = 120;
|
|
111
|
+
exports.HUB_CONTROL_AUDIENCE = 'xenon-node-control';
|
|
112
|
+
/** A forwarded tap is sent at once; a minute covers clock skew and a slow node. */
|
|
113
|
+
exports.HUB_CONTROL_TTL_SECONDS = 60;
|
|
114
|
+
/** A cached token is re-minted once it has less than this left. */
|
|
115
|
+
const RENEW_BEFORE_EXPIRY_MS = 60000;
|
|
116
|
+
const MAX_CACHED_TOKENS = 1000;
|
|
117
|
+
/** Clock skew tolerated between hub and node, as JwtKeyService.verify allows. */
|
|
118
|
+
const CLOCK_TOLERANCE_SECONDS = 60;
|
|
119
|
+
/** Hub side: mints a session's token (reused while it lasts), a create's and a control call's (fresh each time). */
|
|
120
|
+
let HubSessionTokenIssuer = class HubSessionTokenIssuer {
|
|
121
|
+
constructor() {
|
|
122
|
+
this.logger = logger_1.default.scope('HubSessionToken');
|
|
123
|
+
this.cache = new Map();
|
|
124
|
+
this.signer = () => typedi_1.Container.get(JwtKeyService_1.JwtKeyService);
|
|
125
|
+
this.warned = false;
|
|
126
|
+
}
|
|
127
|
+
/** For tests: sign with this instead of the server's JwtKeyService. */
|
|
128
|
+
useSigner(signer) {
|
|
129
|
+
this.signer = () => signer;
|
|
130
|
+
this.cache.clear();
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The token for a session, or null when this server cannot sign (its JWT
|
|
134
|
+
* key failed to load). A node with per-command auth off needs none, so the
|
|
135
|
+
* call still goes out; one with it on refuses it, and the warning says why.
|
|
136
|
+
*/
|
|
137
|
+
tokenFor(sessionId) {
|
|
138
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
139
|
+
if (!sessionId)
|
|
140
|
+
return null;
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const hit = this.cache.get(sessionId);
|
|
143
|
+
if (hit && hit.renewAt > now)
|
|
144
|
+
return hit.token;
|
|
145
|
+
let token;
|
|
146
|
+
try {
|
|
147
|
+
token = yield this.signer().sign({ sid: sessionId }, { audience: exports.HUB_TOKEN_AUDIENCE, ttlSeconds: exports.HUB_TOKEN_TTL_SECONDS });
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
this.warnCannotSign(err);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
this.cache.delete(sessionId);
|
|
154
|
+
if (this.cache.size >= MAX_CACHED_TOKENS) {
|
|
155
|
+
const oldest = this.cache.keys().next().value;
|
|
156
|
+
this.cache.delete(oldest);
|
|
157
|
+
}
|
|
158
|
+
this.cache.set(sessionId, {
|
|
159
|
+
token,
|
|
160
|
+
renewAt: now + exports.HUB_TOKEN_TTL_SECONDS * 1000 - RENEW_BEFORE_EXPIRY_MS,
|
|
161
|
+
});
|
|
162
|
+
return token;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
forget(sessionId) {
|
|
166
|
+
this.cache.delete(sessionId);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The token for a create the hub forwards to the phone's node, or null when
|
|
170
|
+
* this server cannot sign. Minted fresh for each create, never cached, with
|
|
171
|
+
* an id of its own: the node takes it once.
|
|
172
|
+
*/
|
|
173
|
+
createTokenFor(grant) {
|
|
174
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
const claims = { udid: grant.udid, host: grant.host };
|
|
176
|
+
if (grant.userId)
|
|
177
|
+
claims.sub = grant.userId;
|
|
178
|
+
try {
|
|
179
|
+
return yield this.signer().sign(claims, {
|
|
180
|
+
audience: exports.HUB_CREATE_AUDIENCE,
|
|
181
|
+
ttlSeconds: exports.HUB_CREATE_TTL_SECONDS,
|
|
182
|
+
jti: (0, crypto_1.randomUUID)(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
this.warnCannotSign(err);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The token for a device-control call the hub forwards to the phone's
|
|
193
|
+
* node, or null when this server cannot sign. Fresh for each call.
|
|
194
|
+
*/
|
|
195
|
+
controlTokenFor(grant) {
|
|
196
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
197
|
+
try {
|
|
198
|
+
return yield this.signer().sign({ sub: grant.userId, adm: grant.isAdmin, udid: grant.udid, host: grant.host }, { audience: exports.HUB_CONTROL_AUDIENCE, ttlSeconds: exports.HUB_CONTROL_TTL_SECONDS });
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
this.warnCannotSign(err);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
warnCannotSign(err) {
|
|
207
|
+
var _a;
|
|
208
|
+
if (this.warned)
|
|
209
|
+
return;
|
|
210
|
+
this.warned = true;
|
|
211
|
+
this.logger.warn(`Cannot sign session tokens for nodes (${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}). Calls to nodes go ` +
|
|
212
|
+
'without one, and a node with XENON_REQUIRE_COMMAND_AUTH or ' +
|
|
213
|
+
'XENON_REQUIRE_SESSION_TOKEN on will refuse them.');
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
exports.HubSessionTokenIssuer = HubSessionTokenIssuer;
|
|
217
|
+
exports.HubSessionTokenIssuer = HubSessionTokenIssuer = __decorate([
|
|
218
|
+
(0, typedi_1.Service)()
|
|
219
|
+
], HubSessionTokenIssuer);
|
|
220
|
+
/** The hub's public keys, which JwtKeyService serves without a login. */
|
|
221
|
+
function hubJwksUrl(hub) {
|
|
222
|
+
return new URL('/xenon/api/auth/jwks.json', hub);
|
|
223
|
+
}
|
|
224
|
+
/** The node could not check a hub token (the hub's keys could not be fetched). */
|
|
225
|
+
class HubTokenUnavailableError extends Error {
|
|
226
|
+
}
|
|
227
|
+
exports.HubTokenUnavailableError = HubTokenUnavailableError;
|
|
228
|
+
// jose's codes for a token that was checked and is not good: forged, expired,
|
|
229
|
+
// for another audience, malformed, or signed by a key the hub does not have.
|
|
230
|
+
// Anything else (the key set could not be fetched, or was not a key set) means
|
|
231
|
+
// the check did not run.
|
|
232
|
+
const VERDICT_CODES = new Set([
|
|
233
|
+
'ERR_JWKS_NO_MATCHING_KEY',
|
|
234
|
+
'ERR_JWKS_MULTIPLE_MATCHING_KEYS',
|
|
235
|
+
'ERR_JOSE_ALG_NOT_ALLOWED',
|
|
236
|
+
'ERR_JOSE_NOT_SUPPORTED',
|
|
237
|
+
]);
|
|
238
|
+
function isVerdict(err) {
|
|
239
|
+
const code = err === null || err === void 0 ? void 0 : err.code;
|
|
240
|
+
if (typeof code !== 'string')
|
|
241
|
+
return false;
|
|
242
|
+
return code.startsWith('ERR_JWT_') || code.startsWith('ERR_JWS_') || VERDICT_CODES.has(code);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Node side: is this a token the hub signed for this session?
|
|
246
|
+
*
|
|
247
|
+
* Resolves false for any token that is not (forged, expired, another
|
|
248
|
+
* session's, another audience's), and throws HubTokenUnavailableError when
|
|
249
|
+
* the hub's keys cannot be fetched, so the caller can answer 503 rather than
|
|
250
|
+
* pretend the token was checked. jose caches the key set, refetching it when
|
|
251
|
+
* a token names a key it does not have (a hub that rotated its key).
|
|
252
|
+
*/
|
|
253
|
+
class HubSessionTokenVerifier {
|
|
254
|
+
constructor(hubUrl, keys) {
|
|
255
|
+
/** The create tokens already taken, by id, until each one expires. */
|
|
256
|
+
this.usedCreates = new singleUseLedger_1.SingleUseLedger(exports.HUB_CREATE_TTL_SECONDS);
|
|
257
|
+
this.keys =
|
|
258
|
+
keys !== null && keys !== void 0 ? keys : jose.createRemoteJWKSet(hubJwksUrl(hubUrl), {
|
|
259
|
+
timeoutDuration: 5000,
|
|
260
|
+
cooldownDuration: 30000,
|
|
261
|
+
cacheMaxAge: 10 * 60000,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
verify(token, sessionId) {
|
|
265
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
266
|
+
var _a;
|
|
267
|
+
try {
|
|
268
|
+
const { payload } = yield jose.jwtVerify(token, this.keys, {
|
|
269
|
+
audience: exports.HUB_TOKEN_AUDIENCE,
|
|
270
|
+
algorithms: ['RS256'],
|
|
271
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
272
|
+
});
|
|
273
|
+
return typeof payload.sid === 'string' && payload.sid === sessionId;
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
if (isVerdict(err))
|
|
277
|
+
return false;
|
|
278
|
+
throw new HubTokenUnavailableError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The grant in a hub's create token, or null for any token that is not one
|
|
284
|
+
* (forged, expired, another audience's, missing the phone or its node, or
|
|
285
|
+
* already taken: a create token is taken once). Throws
|
|
286
|
+
* HubTokenUnavailableError when the hub's keys cannot be fetched.
|
|
287
|
+
*/
|
|
288
|
+
verifyCreate(token) {
|
|
289
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
290
|
+
var _a;
|
|
291
|
+
let payload;
|
|
292
|
+
try {
|
|
293
|
+
({ payload } = yield jose.jwtVerify(token, this.keys, {
|
|
294
|
+
audience: exports.HUB_CREATE_AUDIENCE,
|
|
295
|
+
algorithms: ['RS256'],
|
|
296
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
if (isVerdict(err))
|
|
301
|
+
return null;
|
|
302
|
+
throw new HubTokenUnavailableError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
|
|
303
|
+
}
|
|
304
|
+
const nonEmpty = (value) => typeof value === 'string' && value.length > 0;
|
|
305
|
+
if (!nonEmpty(payload.udid) || !nonEmpty(payload.host))
|
|
306
|
+
return null;
|
|
307
|
+
if (nonEmpty(payload.jti)) {
|
|
308
|
+
try {
|
|
309
|
+
this.usedCreates.consume(payload);
|
|
310
|
+
}
|
|
311
|
+
catch (_b) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
userId: nonEmpty(payload.sub) ? payload.sub : null,
|
|
317
|
+
udid: payload.udid,
|
|
318
|
+
host: payload.host,
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The grant in a hub's control token, or null for any token that is not
|
|
324
|
+
* one (forged, expired, another audience's, missing the user, the phone or
|
|
325
|
+
* its node). Throws HubTokenUnavailableError when the hub's keys cannot be
|
|
326
|
+
* fetched.
|
|
327
|
+
*/
|
|
328
|
+
verifyControl(token) {
|
|
329
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
330
|
+
var _a;
|
|
331
|
+
let payload;
|
|
332
|
+
try {
|
|
333
|
+
({ payload } = yield jose.jwtVerify(token, this.keys, {
|
|
334
|
+
audience: exports.HUB_CONTROL_AUDIENCE,
|
|
335
|
+
algorithms: ['RS256'],
|
|
336
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
if (isVerdict(err))
|
|
341
|
+
return null;
|
|
342
|
+
throw new HubTokenUnavailableError((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
|
|
343
|
+
}
|
|
344
|
+
const nonEmpty = (value) => typeof value === 'string' && value.length > 0;
|
|
345
|
+
if (!nonEmpty(payload.sub) || !nonEmpty(payload.udid) || !nonEmpty(payload.host))
|
|
346
|
+
return null;
|
|
347
|
+
return {
|
|
348
|
+
userId: payload.sub,
|
|
349
|
+
isAdmin: payload.adm === true,
|
|
350
|
+
udid: payload.udid,
|
|
351
|
+
host: payload.host,
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
exports.HubSessionTokenVerifier = HubSessionTokenVerifier;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INTERNAL_CALL_MARKER = exports.INTERNAL_CALL_HEADER = void 0;
|
|
4
|
+
exports.internalCallHeaders = internalCallHeaders;
|
|
5
|
+
exports.hasInternalCallSecret = hasInternalCallSecret;
|
|
6
|
+
exports.isInternalCall = isInternalCall;
|
|
7
|
+
exports.internalCallBaseUrl = internalCallBaseUrl;
|
|
8
|
+
exports.createInternalCallLayer = createInternalCallLayer;
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
10
|
+
const appiumBasePath_1 = require("../app/appiumBasePath");
|
|
11
|
+
/**
|
|
12
|
+
* Xenon's own loopback calls to its Appium server.
|
|
13
|
+
*
|
|
14
|
+
* A LocalSession asks its own server for a few things over HTTP (a page
|
|
15
|
+
* source or a screen recording when the in-process driver call fails). Those
|
|
16
|
+
* go to `<basePath>/wd-internal/session/<id>/...`, carrying this header with
|
|
17
|
+
* a secret made once per process. The session gateway recognises such a call
|
|
18
|
+
* only when both are present: it strips `/wd-internal` so Appium's own route
|
|
19
|
+
* answers, and lets the call skip per-command auth, since it is Xenon asking
|
|
20
|
+
* about a session it already runs.
|
|
21
|
+
*
|
|
22
|
+
* The path proves nothing on its own. Without the secret a `/wd-internal`
|
|
23
|
+
* request is left exactly as it came, matches no route, and gets Appium's
|
|
24
|
+
* unknown-route answer. Before this, the path alone was the marker, which on a
|
|
25
|
+
* server with per-command auth on was a way around it.
|
|
26
|
+
*
|
|
27
|
+
* The secret never leaves the process: it is sent only to this server's own
|
|
28
|
+
* address, with any HTTP proxy bypassed, and the gateway removes the header
|
|
29
|
+
* before anything after it sees the request.
|
|
30
|
+
*/
|
|
31
|
+
exports.INTERNAL_CALL_HEADER = 'x-xenon-internal';
|
|
32
|
+
exports.INTERNAL_CALL_MARKER = '/wd-internal';
|
|
33
|
+
const SECRET = Buffer.from((0, crypto_1.randomBytes)(32).toString('base64url'));
|
|
34
|
+
const INTERNAL = Symbol('xenon.internalCall');
|
|
35
|
+
/** The headers a loopback call to this process's own server carries. */
|
|
36
|
+
function internalCallHeaders() {
|
|
37
|
+
return { [exports.INTERNAL_CALL_HEADER]: SECRET.toString() };
|
|
38
|
+
}
|
|
39
|
+
/** Whether these headers carry this process's secret. Constant-time. */
|
|
40
|
+
function hasInternalCallSecret(headers) {
|
|
41
|
+
const presented = headers[exports.INTERNAL_CALL_HEADER];
|
|
42
|
+
if (typeof presented !== 'string')
|
|
43
|
+
return false;
|
|
44
|
+
const candidate = Buffer.from(presented);
|
|
45
|
+
return candidate.length === SECRET.length && (0, crypto_1.timingSafeEqual)(candidate, SECRET);
|
|
46
|
+
}
|
|
47
|
+
/** Whether the internal-call layer accepted this request as Xenon's own. */
|
|
48
|
+
function isInternalCall(req) {
|
|
49
|
+
return !!req && req[INTERNAL] === true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Where a local session's loopback calls go: this server, under its base
|
|
53
|
+
* path, marked `/wd-internal`. A wildcard bind address is reached on 127.0.0.1.
|
|
54
|
+
*/
|
|
55
|
+
function internalCallBaseUrl(address, port, basePath) {
|
|
56
|
+
const wildcard = !address || address === '0.0.0.0' || address === '::';
|
|
57
|
+
const host = wildcard ? '127.0.0.1' : address.includes(':') ? `[${address}]` : address;
|
|
58
|
+
return `http://${host}:${port}${(0, appiumBasePath_1.normalizeBasePath)(basePath)}${exports.INTERNAL_CALL_MARKER}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The layer that recognises internal calls. Path-less, because Express puts a
|
|
62
|
+
* mount path back onto `req.url` when a mounted layer calls `next()`, which
|
|
63
|
+
* would undo the strip. The marker is compared exactly as Xenon writes it:
|
|
64
|
+
* any other spelling is not an internal call and falls through untouched.
|
|
65
|
+
*/
|
|
66
|
+
function createInternalCallLayer(basePath) {
|
|
67
|
+
const base = (0, appiumBasePath_1.normalizeBasePath)(basePath);
|
|
68
|
+
const prefix = `${base}${exports.INTERNAL_CALL_MARKER}`;
|
|
69
|
+
/** Accept the call as Xenon's own, rewriting it in place; false leaves it untouched. */
|
|
70
|
+
function accept(req) {
|
|
71
|
+
const url = req.url;
|
|
72
|
+
const rest = url.startsWith(prefix) ? url.slice(prefix.length) : undefined;
|
|
73
|
+
if (rest === undefined || (rest !== '' && rest[0] !== '/' && rest[0] !== '?'))
|
|
74
|
+
return false;
|
|
75
|
+
if (!hasInternalCallSecret(req.headers))
|
|
76
|
+
return false;
|
|
77
|
+
delete req.headers[exports.INTERNAL_CALL_HEADER];
|
|
78
|
+
req[INTERNAL] = true;
|
|
79
|
+
req.url = `${base}${rest}` || '/';
|
|
80
|
+
req.originalUrl = req.url;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
// One call to next() for every outcome: Appium's 404 carries a stack trace,
|
|
84
|
+
// and a second call site would make a refused /wd-internal request's answer
|
|
85
|
+
// differ from any other unknown route's by that one frame.
|
|
86
|
+
return function xenonInternalCalls(req, _res, next) {
|
|
87
|
+
accept(req);
|
|
88
|
+
next();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.NodeSessionProbeSupport = exports.UNSUPPORTED_RECHECK_MS = exports.NODE_SESSION_STATUS_HEADER = exports.NODE_SESSION_STATUS_PATH = exports.NODE_SESSION_STATUS_ROUTE = void 0;
|
|
13
|
+
exports.nodeSessionStatusHandler = nodeSessionStatusHandler;
|
|
14
|
+
exports.registerNodeSessionStatus = registerNodeSessionStatus;
|
|
15
|
+
const typedi_1 = require("typedi");
|
|
16
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
17
|
+
const config_1 = require("../config");
|
|
18
|
+
const commandAuth_1 = require("../middleware/commandAuth");
|
|
19
|
+
const appiumUmbrella_1 = require("../sessions/appiumUmbrella");
|
|
20
|
+
const hubSessionToken_1 = require("./hubSessionToken");
|
|
21
|
+
/**
|
|
22
|
+
* "Does session X still exist?", asked by a hub of a node without touching it.
|
|
23
|
+
*
|
|
24
|
+
* A hub checks each session it routes to a node every ~30 s
|
|
25
|
+
* (SessionHeartbeatService, RemoteSession.checkHealth). It used to ask with a
|
|
26
|
+
* WebDriver command (`GET <basePath>/session/<id>/timeouts`), and any command
|
|
27
|
+
* restarts the node driver's new-command timeout and Xenon's idle clock there,
|
|
28
|
+
* so a session its client had abandoned, and its phone, stayed alive for as
|
|
29
|
+
* long as the hub did. A node's own sessions were fixed the same way in 2.1
|
|
30
|
+
* (LocalSession.checkHealth asks the umbrella).
|
|
31
|
+
*
|
|
32
|
+
* A node answers `GET /xenon/api/node/sessions/<id>` from Appium's umbrella
|
|
33
|
+
* (`sessionExists`), which runs no command: `200 { value: { sessionId,
|
|
34
|
+
* exists } }`. It is outside the login, like `/xenon/api/webdriver`: the hub
|
|
35
|
+
* holds no credentials for the node. It asks what a command to that session
|
|
36
|
+
* asks, never more, so it is never stricter than the probe it replaces. With
|
|
37
|
+
* per-command auth on (XENON_REQUIRE_COMMAND_AUTH, auth enabled) it needs the
|
|
38
|
+
* hub's session token for that session (`x-xenon-hub-token`, audience
|
|
39
|
+
* `xenon-node`, claim `sid`), the one the hub sends with every call about it:
|
|
40
|
+
* any other caller gets the unknown-session answer, and a JWKS that can't be
|
|
41
|
+
* fetched is `503`. With it off a command needs no credential, so neither does
|
|
42
|
+
* this. Otherwise a hub that can't sign (its JWT key failed to load) would
|
|
43
|
+
* see every node session as gone, and end them, where its command probe
|
|
44
|
+
* worked.
|
|
45
|
+
*
|
|
46
|
+
* Every answer carries NODE_SESSION_STATUS_HEADER, so a hub can tell a node
|
|
47
|
+
* that has this route from one that doesn't (an older Xenon, whose answer is a
|
|
48
|
+
* 404 or a 401 from its login). For those the hub keeps the old WebDriver
|
|
49
|
+
* probe and says so once per node (NodeSessionProbeSupport).
|
|
50
|
+
*/
|
|
51
|
+
exports.NODE_SESSION_STATUS_ROUTE = '/node/sessions/:sessionId';
|
|
52
|
+
/** The route under a server's origin, for a hub building the URL. */
|
|
53
|
+
exports.NODE_SESSION_STATUS_PATH = '/xenon/api/node/sessions';
|
|
54
|
+
exports.NODE_SESSION_STATUS_HEADER = 'x-xenon-node-sessions';
|
|
55
|
+
/** How long a hub keeps using the old probe for a node without the route before asking again. */
|
|
56
|
+
exports.UNSUPPORTED_RECHECK_MS = 10 * 60000;
|
|
57
|
+
function nodeSessionStatusHandler(deps) {
|
|
58
|
+
return (req, res) => {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
res.setHeader(exports.NODE_SESSION_STATUS_HEADER, '1');
|
|
61
|
+
const sessionId = String((_b = (_a = req.params) === null || _a === void 0 ? void 0 : _a.sessionId) !== null && _b !== void 0 ? _b : '');
|
|
62
|
+
const answer = () => res.status(200).json({ value: { sessionId, exists: deps.hasSession(sessionId) } });
|
|
63
|
+
if (!deps.enforced())
|
|
64
|
+
return answer();
|
|
65
|
+
const presented = req.headers[hubSessionToken_1.HUB_TOKEN_HEADER];
|
|
66
|
+
const token = typeof presented === 'string' ? presented : undefined;
|
|
67
|
+
if (token === undefined) {
|
|
68
|
+
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
deps.hubTokens
|
|
72
|
+
.verify(token, sessionId)
|
|
73
|
+
.then((valid) => {
|
|
74
|
+
if (valid)
|
|
75
|
+
return answer();
|
|
76
|
+
deps.logger.warn(`Session status for ${sessionId} refused: the hub token is not valid for it`);
|
|
77
|
+
res.status(commandAuth_1.UNKNOWN_SESSION_STATUS).json(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
78
|
+
})
|
|
79
|
+
.catch((error) => {
|
|
80
|
+
deps.logger.error(`Session status for ${sessionId} unavailable: hub token check failed: ${(0, commandAuth_1.summarize)(error)}`);
|
|
81
|
+
if (!res.headersSent) {
|
|
82
|
+
res.status(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_STATUS).json(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A node's route, on the `/xenon/api` router ahead of its login. Only a node
|
|
89
|
+
* (a server with `hub`) has it; a hub or a standalone server answers it as an
|
|
90
|
+
* unknown API route.
|
|
91
|
+
*/
|
|
92
|
+
function registerNodeSessionStatus(router, pluginArgs) {
|
|
93
|
+
if (pluginArgs.hub === undefined)
|
|
94
|
+
return;
|
|
95
|
+
router.get(exports.NODE_SESSION_STATUS_ROUTE, nodeSessionStatusHandler({
|
|
96
|
+
hubTokens: new hubSessionToken_1.HubSessionTokenVerifier(pluginArgs.hub),
|
|
97
|
+
enforced: () => (0, commandAuth_1.commandAuthEnabled)() && config_1.config.authDisabled !== true,
|
|
98
|
+
hasSession: (sessionId) => typedi_1.Container.get(appiumUmbrella_1.AppiumUmbrella).hasSession(sessionId),
|
|
99
|
+
logger: logger_1.default.scope('NodeSessionStatus'),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Hub side: which nodes don't have the route. Such a node is probed the old
|
|
104
|
+
* way, with a WebDriver command, and the hub logs that once for it. It is
|
|
105
|
+
* asked again after UNSUPPORTED_RECHECK_MS, so a node upgraded in place is
|
|
106
|
+
* picked up without restarting the hub.
|
|
107
|
+
*/
|
|
108
|
+
let NodeSessionProbeSupport = class NodeSessionProbeSupport {
|
|
109
|
+
constructor() {
|
|
110
|
+
this.logger = logger_1.default.scope('RemoteSession');
|
|
111
|
+
this.now = () => Date.now();
|
|
112
|
+
this.unsupportedUntil = new Map();
|
|
113
|
+
this.warned = new Set();
|
|
114
|
+
}
|
|
115
|
+
/** Whether to ask this node its route (true unless it recently had none). */
|
|
116
|
+
shouldAsk(origin) {
|
|
117
|
+
const until = this.unsupportedUntil.get(origin);
|
|
118
|
+
if (until === undefined)
|
|
119
|
+
return true;
|
|
120
|
+
if (until > this.now())
|
|
121
|
+
return false;
|
|
122
|
+
this.unsupportedUntil.delete(origin);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/** The node answered without the route: use the WebDriver probe for it for a while. */
|
|
126
|
+
unsupported(origin, status) {
|
|
127
|
+
this.unsupportedUntil.set(origin, this.now() + exports.UNSUPPORTED_RECHECK_MS);
|
|
128
|
+
if (this.warned.has(origin))
|
|
129
|
+
return;
|
|
130
|
+
this.warned.add(origin);
|
|
131
|
+
this.logger.warn(`Node ${origin} has no ${exports.NODE_SESSION_STATUS_PATH} route (answered ${status !== null && status !== void 0 ? status : 'nothing'}); ` +
|
|
132
|
+
'an older Xenon. Its sessions are checked with a WebDriver command, which restarts ' +
|
|
133
|
+
'their new-command timeout and idle clock there, so an abandoned session is kept ' +
|
|
134
|
+
'alive. Upgrade the node.');
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
exports.NodeSessionProbeSupport = NodeSessionProbeSupport;
|
|
138
|
+
exports.NodeSessionProbeSupport = NodeSessionProbeSupport = __decorate([
|
|
139
|
+
(0, typedi_1.Service)()
|
|
140
|
+
], NodeSessionProbeSupport);
|