@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,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const lokijs_1 = __importDefault(require("lokijs"));
|
|
18
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const LeaseOrphanSweeper_1 = require("../../../src/services/lease/LeaseOrphanSweeper");
|
|
20
|
+
const LeaseService_1 = require("../../../src/services/lease/LeaseService");
|
|
21
|
+
const leaseToken_1 = require("../../../src/services/lease/leaseToken");
|
|
22
|
+
const device_store_1 = require("../../../src/data-service/device-store");
|
|
23
|
+
const prisma_store_1 = require("../../../src/data-service/prisma-store");
|
|
24
|
+
const db_1 = require("../../../src/data-service/db");
|
|
25
|
+
const scratch_database_1 = require("../../helpers/scratch-database");
|
|
26
|
+
/**
|
|
27
|
+
* A lease locks its phone with `busy` alone. A session created on the lease
|
|
28
|
+
* claims the phone too (claimForSession). Ending the lease (a reap at
|
|
29
|
+
* expiresAt or on missed heartbeats, or its holder's release) wrote
|
|
30
|
+
* `busy: false` unconditionally, so a session that outlived its lease had
|
|
31
|
+
* its phone handed to another session mid-run.
|
|
32
|
+
*
|
|
33
|
+
* #375's rule (deviceClaims.ts, UNHELD) applies: clear `busy` only where
|
|
34
|
+
* nothing else holds the phone. The session's own release frees it later.
|
|
35
|
+
*
|
|
36
|
+
* Both stores run for real: Prisma on a scratch SQLite file, Loki on an
|
|
37
|
+
* in-memory collection.
|
|
38
|
+
*/
|
|
39
|
+
const UDID = 'leased-phone-1';
|
|
40
|
+
const HOST = 'http://127.0.0.1:4723';
|
|
41
|
+
const TOKEN = 'd'.repeat(64);
|
|
42
|
+
function lokiStore() {
|
|
43
|
+
// The factory only makes a Loki store under NODE_ENV=test; take it without
|
|
44
|
+
// keeping it as the process's store.
|
|
45
|
+
const factory = device_store_1.DeviceStoreFactory;
|
|
46
|
+
const saved = factory._deviceStore;
|
|
47
|
+
const savedEnv = process.env.NODE_ENV;
|
|
48
|
+
factory._deviceStore = undefined;
|
|
49
|
+
process.env.NODE_ENV = 'test';
|
|
50
|
+
try {
|
|
51
|
+
return device_store_1.DeviceStoreFactory.getStore();
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
process.env.NODE_ENV = savedEnv;
|
|
55
|
+
factory._deviceStore = saved;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
describe('ending a lease frees only a phone nothing else holds', () => {
|
|
59
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
60
|
+
let now;
|
|
61
|
+
function lease(over) {
|
|
62
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
63
|
+
yield scratch.db.lease.create({
|
|
64
|
+
data: Object.assign({ id: 'lse_held', tokenHash: (0, leaseToken_1.hashToken)(TOKEN), deviceUdid: UDID, deviceHost: HOST, actorId: 'usr_alice', status: 'active', heartbeatSeconds: 30, allocatedPorts: '{}', capabilityBag: '{}' }, over),
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const cases = [
|
|
69
|
+
{
|
|
70
|
+
name: 'PrismaDeviceStore (scratch SQLite)',
|
|
71
|
+
make: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
yield scratch.db.device.create({ data: { udid: UDID, host: HOST, busy: true } });
|
|
73
|
+
return {
|
|
74
|
+
store: new prisma_store_1.PrismaDeviceStore(),
|
|
75
|
+
busy: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
var _a;
|
|
77
|
+
const where = { udid_host: { udid: UDID, host: HOST } };
|
|
78
|
+
return (_a = (yield scratch.db.device.findUnique({ where }))) === null || _a === void 0 ? void 0 : _a.busy;
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'LokiDeviceStore (in memory)',
|
|
85
|
+
make: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
const devices = new lokijs_1.default('lease-reap-held-phone.json').addCollection('devices');
|
|
87
|
+
sinon_1.default.stub(db_1.XenonDatabase, 'DeviceModel').get(() => Promise.resolve(devices));
|
|
88
|
+
devices.insert({ udid: UDID, host: HOST, busy: true });
|
|
89
|
+
return {
|
|
90
|
+
store: lokiStore(),
|
|
91
|
+
busy: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = devices.findOne({ udid: UDID, host: HOST })) === null || _a === void 0 ? void 0 : _a.busy; }),
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
for (const { name, make } of cases) {
|
|
97
|
+
describe(name, () => {
|
|
98
|
+
let store;
|
|
99
|
+
let busy;
|
|
100
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
yield scratch.db.portLease.deleteMany({});
|
|
102
|
+
yield scratch.db.lease.deleteMany({});
|
|
103
|
+
yield scratch.db.device.deleteMany({});
|
|
104
|
+
now = Date.now();
|
|
105
|
+
({ store, busy } = yield make());
|
|
106
|
+
}));
|
|
107
|
+
afterEach(() => sinon_1.default.restore());
|
|
108
|
+
/** A session created on the lease: it claims the leased phone. */
|
|
109
|
+
function sessionOnLease() {
|
|
110
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
(0, chai_1.expect)(yield store.claimForSession(UDID, HOST, null, 'sess-1', {})).to.equal(true);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const reap = () => new LeaseOrphanSweeper_1.LeaseOrphanSweeper(scratch.db, store).sweep();
|
|
115
|
+
const ended = () => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = (yield scratch.db.lease.findUnique({ where: { id: 'lse_held' } }))) === null || _a === void 0 ? void 0 : _a.status; });
|
|
116
|
+
it('a reap at expiresAt leaves the phone busy while a session holds it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
yield lease({ expiresAt: now - 500, lastHeartbeatAt: now - 1000 });
|
|
118
|
+
yield sessionOnLease();
|
|
119
|
+
yield reap();
|
|
120
|
+
(0, chai_1.expect)(yield ended()).to.equal('expired');
|
|
121
|
+
(0, chai_1.expect)(yield busy(), "the session's phone").to.equal(true);
|
|
122
|
+
}));
|
|
123
|
+
it('a missed-heartbeat reap leaves the phone busy while a session holds it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
yield lease({ expiresAt: now + 60000, lastHeartbeatAt: now - 120000 });
|
|
125
|
+
yield sessionOnLease();
|
|
126
|
+
yield reap();
|
|
127
|
+
(0, chai_1.expect)(yield ended()).to.equal('expired');
|
|
128
|
+
(0, chai_1.expect)(yield busy()).to.equal(true);
|
|
129
|
+
}));
|
|
130
|
+
it('a reap frees the phone when nothing else holds it, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
131
|
+
yield lease({ expiresAt: now - 500, lastHeartbeatAt: now - 1000 });
|
|
132
|
+
yield reap();
|
|
133
|
+
(0, chai_1.expect)(yield ended()).to.equal('expired');
|
|
134
|
+
(0, chai_1.expect)(yield busy()).to.equal(false);
|
|
135
|
+
}));
|
|
136
|
+
it("once the session ends after the reap, the session's release frees the phone", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
yield lease({ expiresAt: now - 500, lastHeartbeatAt: now - 1000 });
|
|
138
|
+
yield sessionOnLease();
|
|
139
|
+
yield reap();
|
|
140
|
+
(0, chai_1.expect)(yield store.releaseClaim(UDID, HOST, { sessionId: 'sess-1' }, {})).to.equal(true);
|
|
141
|
+
(0, chai_1.expect)(yield busy()).to.equal(false);
|
|
142
|
+
}));
|
|
143
|
+
it("the holder's release leaves the phone busy while a session holds it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
144
|
+
yield lease({ expiresAt: now + 60000, lastHeartbeatAt: now });
|
|
145
|
+
yield sessionOnLease();
|
|
146
|
+
const noAuth = { nodePairAuth: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); }) };
|
|
147
|
+
const leases = new LeaseService_1.LeaseService(scratch.db, store, {}, noAuth);
|
|
148
|
+
yield leases.release('lse_held', TOKEN);
|
|
149
|
+
(0, chai_1.expect)(yield ended()).to.equal('released');
|
|
150
|
+
(0, chai_1.expect)(yield busy()).to.equal(true);
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const LeaseOrphanSweeper_1 = require("../../../src/services/lease/LeaseOrphanSweeper");
|
|
19
|
+
const scratch_database_1 = require("../../helpers/scratch-database");
|
|
20
|
+
/**
|
|
21
|
+
* A lease ends at expiresAt. Since #366 heartbeat and extend refuse a lease
|
|
22
|
+
* past it, but the sweeper reaped only on missed heartbeats, so the lease
|
|
23
|
+
* stayed 'active', its phone busy and out of allocation, for up to
|
|
24
|
+
* 3 x heartbeatSeconds (15 minutes at the 300 s maximum) after it ended.
|
|
25
|
+
*
|
|
26
|
+
* The sweeper runs its real queries against a scratch SQLite file.
|
|
27
|
+
*/
|
|
28
|
+
describe('LeaseOrphanSweeper: a lease ends at expiresAt (scratch DB)', () => {
|
|
29
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
30
|
+
let releaseLeaseLock;
|
|
31
|
+
let sweeper;
|
|
32
|
+
let now;
|
|
33
|
+
function lease(id, over) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
yield scratch.db.lease.create({
|
|
36
|
+
data: Object.assign({ id, tokenHash: `hash-${id}`, deviceUdid: `udid-${id}`, deviceHost: 'http://127.0.0.1:4723', actorId: 'usr_alice', status: 'active', heartbeatSeconds: 30, allocatedPorts: '{}', capabilityBag: '{}' }, over),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function portLease(port, leaseId) {
|
|
41
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
42
|
+
yield scratch.db.portLease.create({
|
|
43
|
+
data: {
|
|
44
|
+
port,
|
|
45
|
+
purpose: 'wdaLocalPort',
|
|
46
|
+
leasedToUdid: `udid-${leaseId}`,
|
|
47
|
+
leaseId,
|
|
48
|
+
leasedAt: now,
|
|
49
|
+
expiresAt: now + 3600000,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const statusOf = (id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = (yield scratch.db.lease.findUnique({ where: { id } }))) === null || _a === void 0 ? void 0 : _a.status; });
|
|
55
|
+
const portsOf = (leaseId) => __awaiter(void 0, void 0, void 0, function* () { return (yield scratch.db.portLease.findMany({ where: { leaseId } })).map((p) => p.port); });
|
|
56
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
yield scratch.db.portLease.deleteMany({});
|
|
58
|
+
yield scratch.db.lease.deleteMany({});
|
|
59
|
+
now = Date.now();
|
|
60
|
+
releaseLeaseLock = sinon_1.default.stub().resolves(true);
|
|
61
|
+
sweeper = new LeaseOrphanSweeper_1.LeaseOrphanSweeper(scratch.db, { releaseLeaseLock });
|
|
62
|
+
}));
|
|
63
|
+
it('reaps a lease past expiresAt whose client is still heartbeating, like a missed-heartbeat reap', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
64
|
+
yield lease('ended', { expiresAt: now - 500, lastHeartbeatAt: now - 1000 });
|
|
65
|
+
yield portLease(28301, 'ended');
|
|
66
|
+
yield sweeper.sweep();
|
|
67
|
+
(0, chai_1.expect)(yield statusOf('ended')).to.equal('expired');
|
|
68
|
+
(0, chai_1.expect)(yield portsOf('ended'), 'its ports are freed').to.deep.equal([]);
|
|
69
|
+
(0, chai_1.expect)(releaseLeaseLock.calledOnceWith('udid-ended', 'http://127.0.0.1:4723')).to.equal(true);
|
|
70
|
+
}));
|
|
71
|
+
it('leaves a heartbeating lease alone until expiresAt', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
yield lease('live', { expiresAt: now + 60000, lastHeartbeatAt: now - 1000 });
|
|
73
|
+
yield portLease(28302, 'live');
|
|
74
|
+
yield sweeper.sweep();
|
|
75
|
+
(0, chai_1.expect)(yield statusOf('live')).to.equal('active');
|
|
76
|
+
(0, chai_1.expect)(yield portsOf('live')).to.deep.equal([28302]);
|
|
77
|
+
(0, chai_1.expect)(releaseLeaseLock.called).to.equal(false);
|
|
78
|
+
}));
|
|
79
|
+
it('still reaps on missed heartbeats before expiresAt', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
80
|
+
yield lease('silent', { expiresAt: now + 60000, lastHeartbeatAt: now - 120000 });
|
|
81
|
+
yield sweeper.sweep();
|
|
82
|
+
(0, chai_1.expect)(yield statusOf('silent')).to.equal('expired');
|
|
83
|
+
(0, chai_1.expect)(releaseLeaseLock.calledOnceWith('udid-silent', 'http://127.0.0.1:4723')).to.equal(true);
|
|
84
|
+
}));
|
|
85
|
+
it('never touches a lease that was released', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
yield lease('gone', { expiresAt: now - 500, lastHeartbeatAt: now - 1000 });
|
|
87
|
+
yield scratch.db.lease.update({ where: { id: 'gone' }, data: { status: 'released' } });
|
|
88
|
+
yield sweeper.sweep();
|
|
89
|
+
(0, chai_1.expect)(yield statusOf('gone')).to.equal('released');
|
|
90
|
+
(0, chai_1.expect)(releaseLeaseLock.called).to.equal(false);
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
@@ -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 sdk_leases_1 = require("../../../src/app/routers/sdk-leases");
|
|
21
21
|
const LeaseService_1 = require("../../../src/services/lease/LeaseService");
|
|
22
22
|
const device_store_1 = require("../../../src/data-service/device-store");
|
|
@@ -120,7 +120,7 @@ describe('SDK leases respect the team boundary', () => {
|
|
|
120
120
|
next();
|
|
121
121
|
});
|
|
122
122
|
app.use('/sdk/leases', (0, sdk_leases_1.makeRouter)({ leaseService: svc }));
|
|
123
|
-
return (0,
|
|
123
|
+
return (0, loopbackRequest_1.default)(app)
|
|
124
124
|
.post('/sdk/leases')
|
|
125
125
|
.send({ filters: Object.assign({ platform: 'android', filterByHost: HOST }, filters) });
|
|
126
126
|
}
|
|
@@ -21,95 +21,273 @@ const yaml_1 = __importDefault(require("yaml"));
|
|
|
21
21
|
const secure_values_preprocessor_1 = require("@appium/logger/build/lib/secure-values-preprocessor");
|
|
22
22
|
const constants_1 = require("@appium/base-driver/build/lib/constants");
|
|
23
23
|
const leaseToken_1 = require("../../../src/services/lease/leaseToken");
|
|
24
|
-
// Appium logs
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// Appium logs every request body before any plugin sees it: the POST /session
|
|
25
|
+
// capabilities with their tokens, and the dashboard's login body with its
|
|
26
|
+
// password. Xenon can't strip them from those lines, so the documented Appium
|
|
27
|
+
// log filters have to. This runs the filters exactly as documented — the JSON
|
|
28
|
+
// for --log-filters and the YAML for a --config file, read straight out of the
|
|
28
29
|
// doc — through Appium's own preprocessor, over the forms Appium logs.
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
const ROOT = path_1.default.resolve(__dirname, '../../..');
|
|
31
|
+
const DOC = path_1.default.join(ROOT, 'docs/superpowers/specs/2026-05-14-server-side-lease-api-design.md');
|
|
32
|
+
const CHANGELOG = path_1.default.join(ROOT, 'CHANGELOG.md');
|
|
33
|
+
const HEADING = "#### Keeping tokens and passwords out of Appium's own log";
|
|
34
|
+
const REDACTED = '**REDACTED**';
|
|
35
|
+
// The text from `heading` to the next heading of any level.
|
|
36
|
+
function sectionAfter(text, heading) {
|
|
37
|
+
const start = text.indexOf(heading);
|
|
38
|
+
(0, chai_1.expect)(start, `${heading.trim()} found`).to.be.greaterThan(-1);
|
|
39
|
+
const rest = text.slice(start + heading.length);
|
|
40
|
+
const end = rest.search(/\n#+ /);
|
|
41
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
39
42
|
}
|
|
43
|
+
// Every ```language block in `text`, dedented by its fence's indent.
|
|
44
|
+
function fencedBlocks(text, language) {
|
|
45
|
+
const fence = new RegExp('^( *)```' + language + '\\n([\\s\\S]*?)\\n\\1```', 'gm');
|
|
46
|
+
return [...text.matchAll(fence)].map(([, indent, body]) => body
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((line) => (line.startsWith(indent) ? line.slice(indent.length) : line))
|
|
49
|
+
.join('\n'));
|
|
50
|
+
}
|
|
51
|
+
// The doc's blocks: the token rule, then the password rule.
|
|
52
|
+
const docBlocks = (language) => fencedBlocks(sectionAfter(fs_1.default.readFileSync(DOC, 'utf8'), HEADING), language);
|
|
53
|
+
const RULE_INDEX = { token: 0, password: 1 };
|
|
54
|
+
function docBlock(language, rule) {
|
|
55
|
+
const block = docBlocks(language)[RULE_INDEX[rule]];
|
|
56
|
+
(0, chai_1.expect)(block, `the ${rule} rule's ${language} block under ${HEADING}`).to.be.a('string');
|
|
57
|
+
return block;
|
|
58
|
+
}
|
|
59
|
+
const RULE_FORMS = [
|
|
60
|
+
['--log-filters JSON', 'json', (block) => JSON.parse(block)],
|
|
61
|
+
[
|
|
62
|
+
'--config YAML (server.log-filters)',
|
|
63
|
+
'yaml',
|
|
64
|
+
(block) => yaml_1.default.parse(block).server['log-filters'],
|
|
65
|
+
],
|
|
66
|
+
];
|
|
40
67
|
function filterFrom(rules) {
|
|
41
68
|
return __awaiter(this, void 0, void 0, function* () {
|
|
42
69
|
const pre = new secure_values_preprocessor_1.SecureValuesPreprocessor();
|
|
43
70
|
(0, chai_1.expect)(yield pre.loadRules(rules)).to.deep.equal([]);
|
|
71
|
+
(0, chai_1.expect)(pre.rules).to.have.length(rules.length);
|
|
44
72
|
return pre;
|
|
45
73
|
});
|
|
46
74
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
// Appium's own cut: _.truncate to MAX_LOG_BODY_LENGTH, "..." included.
|
|
76
|
+
const appiumCut = (body) => lodash_1.default.truncate(body, { length: constants_1.MAX_LOG_BODY_LENGTH });
|
|
77
|
+
/**
|
|
78
|
+
* `body(padding)` cut by Appium `into` characters inside `secret` (the secret
|
|
79
|
+
* as the body spells it). Returns the cut body and where the secret starts.
|
|
80
|
+
*/
|
|
81
|
+
function cutInside(body, secret, into) {
|
|
82
|
+
const at = body(0).indexOf(secret);
|
|
83
|
+
(0, chai_1.expect)(at).to.be.greaterThan(-1);
|
|
84
|
+
const start = constants_1.MAX_LOG_BODY_LENGTH - '...'.length - into;
|
|
85
|
+
const cut = appiumCut(body(start - at));
|
|
86
|
+
(0, chai_1.expect)(cut.slice(start)).to.equal(secret.slice(0, into) + '...');
|
|
87
|
+
return { cut, start };
|
|
88
|
+
}
|
|
89
|
+
// `raw` with `length` characters at `start` replaced by the redaction mark.
|
|
90
|
+
function redacted(raw, start, length) {
|
|
91
|
+
(0, chai_1.expect)(start).to.be.greaterThan(-1);
|
|
92
|
+
return { raw, expected: raw.slice(0, start) + REDACTED + raw.slice(start + length) };
|
|
93
|
+
}
|
|
94
|
+
// An [HTTP] request line: Appium logs the body, cut, after the method and URL.
|
|
95
|
+
const httpLine = (url, body) => `[HTTP] --> POST ${url} ${body}`;
|
|
96
|
+
// The same line as Appium colours it on a terminal.
|
|
97
|
+
const colouredHttpLine = (url, body) => `[HTTP] \u001b[37m-->\u001b[39m \u001b[37mPOST\u001b[39m \u001b[37m${url}\u001b[39m ` +
|
|
98
|
+
`\u001b[90m${body}\u001b[39m`;
|
|
99
|
+
// A line whose body Appium cut inside the secret: everything from the secret
|
|
100
|
+
// on, the cut's "..." included, is redacted, and nothing before it changes.
|
|
101
|
+
function cutLine(url, body, secret, into) {
|
|
102
|
+
const { cut, start } = cutInside(body, secret, into);
|
|
103
|
+
const raw = httpLine(url, cut);
|
|
104
|
+
const at = raw.length - cut.length + start;
|
|
105
|
+
return { raw, expected: raw.slice(0, at) + REDACTED };
|
|
106
|
+
}
|
|
107
|
+
const cutInto = (into) => `a body Appium cut ${into} character${into === 1 ? '' : 's'} into it`;
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Tokens: the API token, a session JWT and a lease token.
|
|
110
|
+
const ACCESS_KEY = 'xen_AbC123dEf456';
|
|
111
|
+
const TOKENS = [
|
|
112
|
+
{
|
|
113
|
+
key: 'token',
|
|
114
|
+
value: (0, leaseToken_1.generateToken)(),
|
|
115
|
+
options: { accessKey: ACCESS_KEY },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: 'sessionToken',
|
|
119
|
+
value: 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1In0.sig_-x',
|
|
120
|
+
options: {},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: 'leaseToken',
|
|
124
|
+
value: (0, leaseToken_1.generateToken)(),
|
|
125
|
+
options: { accessKey: ACCESS_KEY, leaseId: 'lse_1' },
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
function sessionRequest(options, padding = 0) {
|
|
129
|
+
return {
|
|
50
130
|
capabilities: {
|
|
51
131
|
alwaysMatch: {
|
|
52
132
|
platformName: 'Android',
|
|
53
133
|
'appium:padding': 'x'.repeat(padding),
|
|
54
|
-
'xe:options':
|
|
134
|
+
'xe:options': options,
|
|
55
135
|
},
|
|
56
136
|
firstMatch: [{}],
|
|
57
137
|
},
|
|
58
|
-
}
|
|
138
|
+
};
|
|
59
139
|
}
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
140
|
+
function tokenLines(key, token, others) {
|
|
141
|
+
const options = Object.assign(Object.assign({}, others), { [key]: token });
|
|
142
|
+
const body = (padding = 0) => JSON.stringify(sessionRequest(options, padding));
|
|
143
|
+
const once = (raw) => redacted(raw, raw.indexOf(token), token.length);
|
|
144
|
+
const lines = [
|
|
145
|
+
['the JSON body', () => once(body())],
|
|
146
|
+
['the [HTTP] request line', () => once(httpLine('/session', appiumCut(body())))],
|
|
147
|
+
['the coloured [HTTP] request line', () => once(colouredHttpLine('/session', body()))],
|
|
148
|
+
[
|
|
149
|
+
'the createSession args line',
|
|
150
|
+
() => once('Calling AppiumDriver.createSession() with args: ' +
|
|
151
|
+
appiumCut(JSON.stringify([null, null, sessionRequest(options).capabilities]))),
|
|
152
|
+
],
|
|
153
|
+
['util.inspect output', () => once(util_1.default.inspect(sessionRequest(options), { depth: 5 }))],
|
|
154
|
+
['doubly-escaped JSON', () => once(JSON.stringify(body()))],
|
|
155
|
+
];
|
|
156
|
+
for (const into of [1, 10, 30]) {
|
|
157
|
+
lines.push([cutInto(into), () => cutLine('/session', body, token, into)]);
|
|
158
|
+
}
|
|
159
|
+
return lines;
|
|
70
160
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Passwords: the dashboard's POST /xenon/api/auth/login body.
|
|
163
|
+
const LOGIN_URL = '/xenon/api/auth/login';
|
|
164
|
+
// A password can hold any character. Most of these would get past a rule that
|
|
165
|
+
// stops at the first quote, or make one that doesn't read escapes eat the rest
|
|
166
|
+
// of the line.
|
|
167
|
+
const PASSWORDS = [
|
|
168
|
+
'hunter2-Hunter2',
|
|
169
|
+
"it's mine", // util.inspect switches to "..."
|
|
170
|
+
'say "hi", } then', // escaped quotes and JSON punctuation
|
|
171
|
+
'it\'s "all" `three`', // util.inspect escapes its own quote
|
|
172
|
+
'ends in a backslash\\',
|
|
173
|
+
'","password":"decoy', // reads like the next field
|
|
174
|
+
"password: 'decoy", // reads like the util.inspect key
|
|
175
|
+
'my token: abc"def', // the token rule rewrites part of it first
|
|
176
|
+
'tab\tnew\nline\u001bescape', // control characters are escaped
|
|
177
|
+
'pässwörd 🔑 ok',
|
|
85
178
|
];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
179
|
+
// Long enough to cut 30 characters in, with an escape early on. No astral
|
|
180
|
+
// characters: lodash counts those as one when it cuts.
|
|
181
|
+
const LONG_PASSWORD = 'c"x\\y it\'s, }] "correct horse" `battery` staple ä\n';
|
|
182
|
+
function loginBody(password, padding = 0) {
|
|
183
|
+
// Padding the address moves the password, keeping the body's shape.
|
|
184
|
+
return { email: `${'x'.repeat(padding)}qa@example.com`, password };
|
|
185
|
+
}
|
|
186
|
+
function passwordLines(password) {
|
|
187
|
+
const body = (padding = 0) => JSON.stringify(loginBody(password, padding));
|
|
188
|
+
const json = JSON.stringify(password).slice(1, -1);
|
|
189
|
+
const inspected = util_1.default.inspect(password);
|
|
190
|
+
// Anchored on what follows the password: it is the body's last field.
|
|
191
|
+
const at = (raw, spelled, after) => redacted(raw, raw.lastIndexOf(spelled + after), spelled.length);
|
|
192
|
+
const lines = [
|
|
193
|
+
['the JSON body', () => at(body(), json, '"}')],
|
|
194
|
+
['the [HTTP] request line', () => at(httpLine(LOGIN_URL, appiumCut(body())), json, '"}')],
|
|
195
|
+
['the coloured [HTTP] request line', () => at(colouredHttpLine(LOGIN_URL, body()), json, '"}')],
|
|
196
|
+
[
|
|
197
|
+
'util.inspect output',
|
|
198
|
+
() => at(util_1.default.inspect(loginBody(password)), inspected.slice(1, -1), inspected[0]),
|
|
199
|
+
],
|
|
89
200
|
[
|
|
90
|
-
'
|
|
91
|
-
() =>
|
|
201
|
+
'doubly-escaped JSON',
|
|
202
|
+
() => at(JSON.stringify(body()), JSON.stringify(json).slice(1, -1), '\\"}"'),
|
|
92
203
|
],
|
|
93
204
|
];
|
|
94
|
-
|
|
205
|
+
return lines;
|
|
206
|
+
}
|
|
207
|
+
function passwordCutLines() {
|
|
208
|
+
const body = (padding = 0) => JSON.stringify(loginBody(LONG_PASSWORD, padding));
|
|
209
|
+
const json = JSON.stringify(LONG_PASSWORD).slice(1, -1);
|
|
210
|
+
return [1, 10, 30].map((into) => [
|
|
211
|
+
cutInto(into),
|
|
212
|
+
() => cutLine(LOGIN_URL, body, json, into),
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
describe("the documented Appium log filters keep tokens and passwords out of Appium's log", () => {
|
|
216
|
+
for (const [form, language, parse] of RULE_FORMS) {
|
|
95
217
|
describe(form, () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
218
|
+
const tokenRule = () => parse(docBlock(language, 'token'));
|
|
219
|
+
const passwordRule = () => parse(docBlock(language, 'password'));
|
|
220
|
+
// An operator may run the token rule alone (it shipped first) or both.
|
|
221
|
+
const withTokenRule = [
|
|
222
|
+
['the token rule', tokenRule],
|
|
223
|
+
['both rules', () => [...tokenRule(), ...passwordRule()]],
|
|
224
|
+
];
|
|
225
|
+
for (const { key, value, options } of TOKENS) {
|
|
226
|
+
describe(`a ${key}`, () => {
|
|
227
|
+
for (const [name, line] of tokenLines(key, value, options)) {
|
|
228
|
+
it(`is redacted from ${name}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
229
|
+
const { raw, expected } = line();
|
|
230
|
+
for (const [rules, load] of withTokenRule) {
|
|
231
|
+
const out = (yield filterFrom(load())).preprocess(raw);
|
|
232
|
+
// Only the token changes: the access key, the lease id and
|
|
233
|
+
// the rest of the line are kept.
|
|
234
|
+
(0, chai_1.expect)(out, rules).to.equal(expected);
|
|
235
|
+
(0, chai_1.expect)(out, rules).to.not.include(value.slice(0, 8));
|
|
236
|
+
(0, chai_1.expect)(out, rules).to.not.match(new RegExp(`${key}\\\\?["']?\\s*:\\s*\\\\?["']?${lodash_1.default.escapeRegExp(value[0])}`));
|
|
237
|
+
}
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
109
241
|
}
|
|
242
|
+
it('keeps the access key, which names a key without proving it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
243
|
+
const raw = JSON.stringify(sessionRequest({ accessKey: ACCESS_KEY }));
|
|
244
|
+
const pre = yield filterFrom([...tokenRule(), ...passwordRule()]);
|
|
245
|
+
(0, chai_1.expect)(pre.preprocess(raw)).to.equal(raw);
|
|
246
|
+
}));
|
|
247
|
+
describe('a password', () => {
|
|
248
|
+
for (const password of PASSWORDS) {
|
|
249
|
+
for (const [name, line] of passwordLines(password)) {
|
|
250
|
+
it(`${JSON.stringify(password)} is redacted from ${name}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
251
|
+
const { raw, expected } = line();
|
|
252
|
+
for (const [rules, load] of [
|
|
253
|
+
['the password rule', passwordRule],
|
|
254
|
+
['both rules', () => [...tokenRule(), ...passwordRule()]],
|
|
255
|
+
]) {
|
|
256
|
+
const out = (yield filterFrom(load())).preprocess(raw);
|
|
257
|
+
// Exactly the password goes: the address, the closing quote
|
|
258
|
+
// and everything after it stay.
|
|
259
|
+
(0, chai_1.expect)(out, rules).to.equal(expected);
|
|
260
|
+
(0, chai_1.expect)(out, rules).to.include('qa@example.com');
|
|
261
|
+
}
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [name, line] of passwordCutLines()) {
|
|
266
|
+
it(`is redacted from ${name}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
267
|
+
const { raw, expected } = line();
|
|
268
|
+
const out = (yield filterFrom([...tokenRule(), ...passwordRule()])).preprocess(raw);
|
|
269
|
+
(0, chai_1.expect)(out).to.equal(expected);
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
110
273
|
});
|
|
111
274
|
}
|
|
112
|
-
it('reads the same
|
|
113
|
-
(0, chai_1.expect)(
|
|
275
|
+
it('shows each rule once in each form, and reads the same in both', () => {
|
|
276
|
+
(0, chai_1.expect)(docBlocks('json')).to.have.length(2);
|
|
277
|
+
(0, chai_1.expect)(docBlocks('yaml')).to.have.length(2);
|
|
278
|
+
for (const rule of ['token', 'password']) {
|
|
279
|
+
(0, chai_1.expect)(yaml_1.default.parse(docBlock('yaml', rule)).server['log-filters'], rule).to.deep.equal(JSON.parse(docBlock('json', rule)));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
it('shows the token rule exactly as the 2.0.0 CHANGELOG publishes it', () => {
|
|
283
|
+
const changelog = fs_1.default.readFileSync(CHANGELOG, 'utf8');
|
|
284
|
+
const release = changelog.indexOf('\n## 2.0.0\n');
|
|
285
|
+
const entry = changelog.indexOf("**Keep tokens out of Appium's own request log.**", release);
|
|
286
|
+
(0, chai_1.expect)(release).to.be.greaterThan(-1);
|
|
287
|
+
(0, chai_1.expect)(entry).to.be.greaterThan(release);
|
|
288
|
+
for (const language of ['json', 'yaml']) {
|
|
289
|
+
const published = fencedBlocks(changelog.slice(entry), language)[0];
|
|
290
|
+
(0, chai_1.expect)(docBlock(language, 'token'), language).to.equal(published);
|
|
291
|
+
}
|
|
114
292
|
});
|
|
115
293
|
});
|