@xenon-device-management/xenon 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
- package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +5 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +49 -61
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +20 -2
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/interceptors/CommandInterceptor.js +243 -181
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +8 -3
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/OrphanSweeper.js +2 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +86 -26
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +55 -72
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +5 -5
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -23,6 +23,7 @@ const typedi_1 = require("typedi");
|
|
|
23
23
|
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
24
24
|
const config_1 = require("../config");
|
|
25
25
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
|
+
const passwordResetLink_1 = require("./passwordResetLink");
|
|
26
27
|
let EmailService = class EmailService {
|
|
27
28
|
constructor() {
|
|
28
29
|
this.log = logger_1.default.scope('Email');
|
|
@@ -55,6 +56,34 @@ let EmailService = class EmailService {
|
|
|
55
56
|
'over the account for the link lifetime. Configure XENON_SMTP_URL, or unset this and ' +
|
|
56
57
|
'issue links from the dashboard Users page.');
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Startup notice about XENON_PUBLIC_URL, the address links Xenon hands out
|
|
61
|
+
* are built from (never the request's Host, which the asker chooses):
|
|
62
|
+
*
|
|
63
|
+
* - set but refused (not an http(s) server address): whatever the mail
|
|
64
|
+
* setup, since it also decides each device's dashboard_link. The value is
|
|
65
|
+
* not logged: a user name and password in it is one reason to refuse it.
|
|
66
|
+
* - unset while Xenon could send reset links: the sign-in page's "forgot
|
|
67
|
+
* password" then sends people to an administrator, and an administrator's
|
|
68
|
+
* reset link is handed back to them rather than emailed.
|
|
69
|
+
*/
|
|
70
|
+
warnAboutPublicUrl() {
|
|
71
|
+
var _a;
|
|
72
|
+
if (((_a = config_1.config.publicUrl) === null || _a === void 0 ? void 0 : _a.trim()) && (0, passwordResetLink_1.resetLinkBase)() === null) {
|
|
73
|
+
this.warnLog('XENON_PUBLIC_URL is set but is not an http(s) server address such as ' +
|
|
74
|
+
'https://xenon.example.com or http://lab-mac:4723 (a trailing /xenon is fine; any ' +
|
|
75
|
+
'other path, a query, a fragment or a user name is not), so Xenon ignores it: it ' +
|
|
76
|
+
'emails no password-reset links, and device dashboard links are paths on this server.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!this.canDeliver() || (0, passwordResetLink_1.resetLinkBase)() !== null)
|
|
80
|
+
return;
|
|
81
|
+
this.warnLog("XENON_PUBLIC_URL is not set to this server's address, so Xenon sends no password-reset " +
|
|
82
|
+
'links: "Forgot password" asks people to contact an administrator, and the Users ' +
|
|
83
|
+
'page hands an administrator the link instead of emailing it. Set XENON_PUBLIC_URL to ' +
|
|
84
|
+
'the http(s) address people reach this server at, such as https://xenon.example.com ' +
|
|
85
|
+
'or http://lab-mac:4723 (a trailing /xenon is fine; any other path is not).');
|
|
86
|
+
}
|
|
58
87
|
send(mail) {
|
|
59
88
|
return __awaiter(this, void 0, void 0, function* () {
|
|
60
89
|
const smtpUrl = config_1.config.smtpUrl;
|
|
@@ -26,6 +26,7 @@ const event_manager_1 = require("../dashboard/event-manager");
|
|
|
26
26
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
27
27
|
const device_service_1 = require("../data-service/device-service");
|
|
28
28
|
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
29
|
+
const sessionMemory_1 = require("../sessions/sessionMemory");
|
|
29
30
|
let OrphanSweeper = class OrphanSweeper {
|
|
30
31
|
constructor() {
|
|
31
32
|
this.log = logger_1.default.scope('OrphanSweeper');
|
|
@@ -74,6 +75,7 @@ let OrphanSweeper = class OrphanSweeper {
|
|
|
74
75
|
// back before the phone is released; for a previous process's it is
|
|
75
76
|
// the ledger's, at boot (PhoneNetworkRestore).
|
|
76
77
|
yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(s.id, 'heartbeat timeout');
|
|
78
|
+
(0, sessionMemory_1.forgetSessionMemory)(s.id);
|
|
77
79
|
// This session's claim only, not every row with its udid: the phone
|
|
78
80
|
// may be another session's by now (deviceClaims.ts).
|
|
79
81
|
yield (0, device_service_1.releaseSessionDevices)(s.id);
|
|
@@ -32,8 +32,11 @@ const selectorKeys_1 = require("./selector-health/selectorKeys");
|
|
|
32
32
|
const log = logger_1.default.scope('SelectorVerification');
|
|
33
33
|
/**
|
|
34
34
|
* Threshold of distinct clean build_ids required to promote a Pending
|
|
35
|
-
* SelectorState row to Resolved. "Clean" means
|
|
36
|
-
*
|
|
35
|
+
* SelectorState row to Resolved. "Clean" means the selector was found without
|
|
36
|
+
* healing in the build: a findElement of the (strategy, selector) tuple that
|
|
37
|
+
* answered an element, or a findElements that answered at least one, and no
|
|
38
|
+
* heal of it. Through 2.14 a find that failed outright counted too, so a
|
|
39
|
+
* selector that was never found again was verified as fixed.
|
|
37
40
|
*/
|
|
38
41
|
const CLEAN_BUILDS_TO_RESOLVE = 3;
|
|
39
42
|
/**
|
|
@@ -127,12 +130,20 @@ let SelectorVerificationJob = class SelectorVerificationJob {
|
|
|
127
130
|
// that ran under a CI build (build_id IS NOT NULL); ad-hoc local runs
|
|
128
131
|
// intentionally don't move verification forward.
|
|
129
132
|
//
|
|
133
|
+
// A find that found the selector is one that didn't fail (`is_error`,
|
|
134
|
+
// the dashboard's record of an error answer) and didn't answer an empty
|
|
135
|
+
// list (`{"value":[]`, findElements finding nothing). The command log
|
|
136
|
+
// keeps the answer as JSON.stringify wrote it, or as the node sent it.
|
|
137
|
+
//
|
|
130
138
|
// Identifier names (PascalCase tables, camelCase createdAt) match the
|
|
131
139
|
// actual SQLite schema generated by Prisma — see
|
|
132
140
|
// `sqlite3 prisma/dev.db ".schema SessionLog"`.
|
|
133
141
|
const heals = yield this.prisma.$queryRaw `
|
|
134
142
|
SELECT s."build_id" AS build_id,
|
|
135
|
-
MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed
|
|
143
|
+
MAX(CASE WHEN sl."is_healed" THEN 1 ELSE 0 END) AS healed,
|
|
144
|
+
MAX(CASE WHEN NOT sl."is_healed" AND NOT sl."is_error"
|
|
145
|
+
AND sl."response" NOT LIKE '{"value":[]%'
|
|
146
|
+
THEN 1 ELSE 0 END) AS found
|
|
136
147
|
FROM "SessionLog" sl
|
|
137
148
|
INNER JOIN "Session" s ON sl."session_id" = s."id"
|
|
138
149
|
WHERE sl."original_strategy" = ${row.original_strategy}
|
|
@@ -142,7 +153,7 @@ let SelectorVerificationJob = class SelectorVerificationJob {
|
|
|
142
153
|
AND s."build_id" IS NOT NULL
|
|
143
154
|
GROUP BY s."build_id"
|
|
144
155
|
`;
|
|
145
|
-
const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0).length;
|
|
156
|
+
const cleanBuildCount = heals.filter((r) => Number(r.healed) === 0 && Number(r.found) === 1).length;
|
|
146
157
|
if (cleanBuildCount >= CLEAN_BUILDS_TO_RESOLVE) {
|
|
147
158
|
yield this.promoteToResolved(row);
|
|
148
159
|
}
|
|
@@ -79,6 +79,7 @@ const IPluginArgs_1 = require("../interfaces/IPluginArgs");
|
|
|
79
79
|
const config_service_1 = require("../data-service/config-service");
|
|
80
80
|
const PluginContext_1 = require("../PluginContext");
|
|
81
81
|
const SelfHealingSwitch_1 = require("./settings/SelfHealingSwitch");
|
|
82
|
+
const aiEngineSettings_1 = require("./settings/aiEngineSettings");
|
|
82
83
|
const device_store_1 = require("../data-service/device-store");
|
|
83
84
|
const device_utils_1 = require("../device-utils");
|
|
84
85
|
const localDeviceHosts_1 = require("../device-managers/localDeviceHosts");
|
|
@@ -140,6 +141,9 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
140
141
|
// The Settings page's AI self-healing toggle, which the command interceptor
|
|
141
142
|
// reads from memory at every command: load it before a command can arrive.
|
|
142
143
|
yield typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).load();
|
|
144
|
+
// The AI engine page's provider, model and base URL, over the startup
|
|
145
|
+
// options applied above (what a cleared value goes back to).
|
|
146
|
+
yield typedi_1.Container.get(aiEngineSettings_1.AiEngineSettings).load();
|
|
143
147
|
this.registerRoutes(expressApp, httpServer, cliArgs, pluginArgs);
|
|
144
148
|
yield this.bootEmulators(pluginArgs);
|
|
145
149
|
this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
|
|
@@ -397,6 +401,7 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
397
401
|
const { bootstrapIdentity } = yield Promise.resolve().then(() => __importStar(require('./identity/bootstrap')));
|
|
398
402
|
yield bootstrapIdentity();
|
|
399
403
|
typedi_1.Container.get(EmailService_1.EmailService).warnIfLogFallbackEnabled();
|
|
404
|
+
typedi_1.Container.get(EmailService_1.EmailService).warnAboutPublicUrl();
|
|
400
405
|
const { startUserSessionCleanupCron } = yield Promise.resolve().then(() => __importStar(require('./identity/sessionCleanupCron')));
|
|
401
406
|
startUserSessionCleanupCron();
|
|
402
407
|
// Hub-issued JWT signing key (REST/MCP tokens, stream tickets) — key
|
|
@@ -445,7 +450,12 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
445
450
|
});
|
|
446
451
|
}
|
|
447
452
|
registerRoutes(expressApp, httpServer, cliArgs, pluginArgs) {
|
|
448
|
-
expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs
|
|
453
|
+
expressApp.use('/xenon', (0, app_1.createRouter)(pluginArgs, {
|
|
454
|
+
address: cliArgs.address,
|
|
455
|
+
port: cliArgs.port,
|
|
456
|
+
// Appium serves HTTPS with both of these (base-driver's server()).
|
|
457
|
+
tls: !!(cliArgs.sslCertificatePath && cliArgs.sslKeyPath),
|
|
458
|
+
}));
|
|
449
459
|
// The session gateway goes in front of Appium's routes: Xenon's own
|
|
450
460
|
// /wd-internal calls (with the per-process secret), per-command auth
|
|
451
461
|
// (XENON_REQUIRE_COMMAND_AUTH), then, on a hub, forwarding of the sessions
|
|
@@ -69,7 +69,6 @@ const device_utils_1 = require("../device-utils");
|
|
|
69
69
|
const TracingService_1 = require("./TracingService");
|
|
70
70
|
const PortAllocator_1 = require("./PortAllocator");
|
|
71
71
|
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
72
|
-
const JwtKeyService_1 = require("./token/JwtKeyService");
|
|
73
72
|
const sessionIdentity_1 = require("./session/sessionIdentity");
|
|
74
73
|
const leaseSessionCaps_1 = require("./lease/leaseSessionCaps");
|
|
75
74
|
const sessionCredentials_1 = require("./session/sessionCredentials");
|
|
@@ -80,6 +79,7 @@ const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
|
80
79
|
const queueVisibility_1 = require("./device-access/queueVisibility");
|
|
81
80
|
const appVisibility_1 = require("./device-access/appVisibility");
|
|
82
81
|
const LiveSessionOwners_1 = require("./device-access/LiveSessionOwners");
|
|
82
|
+
const sessionMemory_1 = require("../sessions/sessionMemory");
|
|
83
83
|
const SessionMetricsService_1 = require("./metrics/SessionMetricsService");
|
|
84
84
|
const appCapability_1 = require("./session/appCapability");
|
|
85
85
|
const CircuitBreaker_1 = require("../data-service/CircuitBreaker");
|
|
@@ -100,6 +100,21 @@ const async_lock_1 = __importDefault(require("async-lock"));
|
|
|
100
100
|
const base_driver_1 = require("@appium/base-driver");
|
|
101
101
|
const PhoneNetworkRestore_1 = require("./network/PhoneNetworkRestore");
|
|
102
102
|
const interceptorOptions_1 = require("./interceptor/interceptorOptions");
|
|
103
|
+
/**
|
|
104
|
+
* A session token admits a session only with the `sessions` scope, as a key
|
|
105
|
+
* pair does. Tokens minted before this check carry no `scopes` claim, and
|
|
106
|
+
* POST /auth/token gave one to any credential, a read-only one included:
|
|
107
|
+
* they are refused too.
|
|
108
|
+
*/
|
|
109
|
+
function sessionTokenGrantsSessions(scopes) {
|
|
110
|
+
if (typeof scopes !== 'string')
|
|
111
|
+
return false;
|
|
112
|
+
const owned = scopes.split(',').map((s) => s.trim());
|
|
113
|
+
return owned.includes('sessions') || owned.includes('admin');
|
|
114
|
+
}
|
|
115
|
+
const SESSION_TOKEN_LACKS_SCOPE = 'session rejected: xe:options.sessionToken carries no `sessions` scope; tokens minted by ' +
|
|
116
|
+
'Xenon 2.14 or earlier carry none — mint a new one with POST /xenon/api/auth/token ' +
|
|
117
|
+
'({"audience":"xenon-mcp"}) using a credential that has the `sessions` scope.';
|
|
103
118
|
/**
|
|
104
119
|
* How long the hub waits for a node (or cloud provider) to create a session.
|
|
105
120
|
* A create is sent once (InternalRequestConfig `retry: false`): the node may
|
|
@@ -263,6 +278,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
263
278
|
apiKeyId: authResult.apiKeyId,
|
|
264
279
|
userId: authResult.userId,
|
|
265
280
|
appDownload,
|
|
281
|
+
allocatedAt: Date.now(),
|
|
266
282
|
};
|
|
267
283
|
// The grant opens one phone, on this node. A token taken to another node
|
|
268
284
|
// (an emulator's udid repeats across machines), or naming a phone this
|
|
@@ -391,7 +407,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
391
407
|
this.logger.debug(`📱 Removing pending session with capability_id: ${allocation.pendingSessionId}`);
|
|
392
408
|
yield (0, pending_sessions_service_1.removePendingSession)(allocation.pendingSessionId);
|
|
393
409
|
if (this.isCreateSessionResponseInternal(session)) {
|
|
394
|
-
yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId);
|
|
410
|
+
yield this.finalizeSession(session, device, caps, driver, remote, allocation.apiKeyId, allocation.userId, allocation.allocatedAt);
|
|
395
411
|
}
|
|
396
412
|
else {
|
|
397
413
|
yield this.handleSessionFailure(session, device, remote);
|
|
@@ -505,9 +521,11 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
505
521
|
// took out of `xe:options` (or its `xenon:options` alias): an access-key +
|
|
506
522
|
// token pair, or a session token. Today this is best-effort: if none is
|
|
507
523
|
// supplied we log a warning and let the request through to avoid breaking
|
|
508
|
-
// pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on.
|
|
509
|
-
//
|
|
510
|
-
//
|
|
524
|
+
// pre-existing test clients, unless XENON_REQUIRE_SESSION_TOKEN is on.
|
|
525
|
+
// Credentials that don't verify — a wrong, revoked or expired key or token,
|
|
526
|
+
// or one whose user is deleted or not ACTIVE — count as none. Credentials
|
|
527
|
+
// that verify without the `sessions` scope are rejected. Respects the
|
|
528
|
+
// global `authDisabled` flag.
|
|
511
529
|
//
|
|
512
530
|
// Returns the caller's { apiKeyId, callerTeamIds } so allocation can filter
|
|
513
531
|
// devices, and app resolution uploaded apps, by team. `apiKeyId` is null
|
|
@@ -549,23 +567,47 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
549
567
|
}
|
|
550
568
|
const { ApiKeyService } = yield Promise.resolve().then(() => __importStar(require('./ApiKeyService')));
|
|
551
569
|
const svc = typedi_1.Container.get(ApiKeyService);
|
|
570
|
+
const { verifyKeyPairCredential, verifySessionTokenCredential, isWrongSessionToken } = yield Promise.resolve().then(() => __importStar(require('../middleware/verifyCredential')));
|
|
552
571
|
// xe:options.{accessKey, token} — the only key-pair credential shape.
|
|
572
|
+
// Checked by REST's rule (verifyKeyPairCredential): a live key whose owner
|
|
573
|
+
// exists and is ACTIVE. The key alone (verifyPair) let a user set to
|
|
574
|
+
// Inactive, or deleted, keep creating sessions as themselves. The owner
|
|
575
|
+
// found here is the one lookup the session's scope and lease use.
|
|
553
576
|
const pair = credentials.pair;
|
|
554
|
-
const
|
|
577
|
+
const verifiedPair = pair ? yield verifyKeyPairCredential(pair.accessKey, pair.token) : null;
|
|
578
|
+
// The session token, verified once for both the gate and attribution, by
|
|
579
|
+
// REST's rule too: a live signature and an ACTIVE subject, looked up now.
|
|
580
|
+
let tokenCheck;
|
|
581
|
+
const verifyToken = (t) => {
|
|
582
|
+
if (!tokenCheck)
|
|
583
|
+
tokenCheck = verifySessionTokenCredential(t);
|
|
584
|
+
return tokenCheck;
|
|
585
|
+
};
|
|
586
|
+
// Both readers below treat any failure as a token that doesn't check out:
|
|
587
|
+
// the gate refuses it as "invalid or expired", attribution ignores it. A
|
|
588
|
+
// check that could not run (the database or the signing key unavailable)
|
|
589
|
+
// is not that: it refuses the create here, as a key whose owner can't be
|
|
590
|
+
// looked up does, rather than run the session with no owner.
|
|
591
|
+
if (!verifiedPair && credentials.sessionToken) {
|
|
592
|
+
yield verifyToken(credentials.sessionToken).catch((err) => {
|
|
593
|
+
if (!isWrongSessionToken(err))
|
|
594
|
+
throw err;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
555
597
|
const { assertSessionTokenGate, sessionTokenGateEnabled } = yield Promise.resolve().then(() => __importStar(require('./sessionTokenGate')));
|
|
556
598
|
try {
|
|
557
599
|
yield assertSessionTokenGate({
|
|
558
600
|
enabled: sessionTokenGateEnabled(),
|
|
559
|
-
hasValidKeyPair: !!
|
|
601
|
+
hasValidKeyPair: !!verifiedPair,
|
|
560
602
|
token: credentials.sessionToken,
|
|
561
|
-
verify:
|
|
603
|
+
verify: verifyToken,
|
|
562
604
|
});
|
|
563
605
|
}
|
|
564
606
|
catch (err) {
|
|
565
607
|
this.logger.error(`❌ ${err.message}`);
|
|
566
608
|
throw new base_driver_1.errors.InvalidArgumentError(err.message);
|
|
567
609
|
}
|
|
568
|
-
if (!
|
|
610
|
+
if (!verifiedPair) {
|
|
569
611
|
// No key pair. An xe:options.sessionToken still identifies the caller,
|
|
570
612
|
// so read it for attribution even when the gate is off — otherwise the
|
|
571
613
|
// ownership guard fails closed on a session whose owner we actually know.
|
|
@@ -573,23 +615,36 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
573
615
|
// The token's teamId claim narrows it the way a key's team does; kept
|
|
574
616
|
// from the one verify rather than verifying again.
|
|
575
617
|
let tokenTeamId = null;
|
|
618
|
+
let tokenScopes;
|
|
619
|
+
let tokenOwner;
|
|
576
620
|
const identity = yield (0, sessionIdentity_1.resolveSessionIdentity)({
|
|
577
621
|
row: null,
|
|
578
622
|
sessionToken: credentials.sessionToken,
|
|
579
623
|
verify: (t) => __awaiter(this, void 0, void 0, function* () {
|
|
580
|
-
const payload = yield
|
|
581
|
-
audience: 'xenon-session',
|
|
582
|
-
});
|
|
624
|
+
const { payload, user } = yield verifyToken(t);
|
|
583
625
|
tokenTeamId = typeof (payload === null || payload === void 0 ? void 0 : payload.teamId) === 'string' ? payload.teamId : null;
|
|
626
|
+
tokenScopes = payload === null || payload === void 0 ? void 0 : payload.scopes;
|
|
627
|
+
tokenOwner = user;
|
|
584
628
|
return payload;
|
|
585
629
|
}),
|
|
586
630
|
});
|
|
631
|
+
// A token that names its owner may create sessions only with the
|
|
632
|
+
// `sessions` scope, as a key pair may: refused, like a key without it.
|
|
633
|
+
if (identity.userId && !sessionTokenGrantsSessions(tokenScopes)) {
|
|
634
|
+
this.logger.error('Rejecting session: xe:options.sessionToken lacks the `sessions` scope');
|
|
635
|
+
throw new base_driver_1.errors.InvalidArgumentError(SESSION_TOKEN_LACKS_SCOPE);
|
|
636
|
+
}
|
|
587
637
|
if (!identity.userId) {
|
|
588
638
|
this.logger.warn('Session created without valid credentials. Pass `xe:options.accessKey` + ' +
|
|
589
639
|
'`xe:options.token`, or `xe:options.sessionToken`.');
|
|
590
640
|
}
|
|
591
|
-
const leaseAccess = this.leaseAccessFor(identity.userId
|
|
592
|
-
? {
|
|
641
|
+
const leaseAccess = this.leaseAccessFor(identity.userId && tokenOwner
|
|
642
|
+
? {
|
|
643
|
+
kind: 'session-token',
|
|
644
|
+
scopes: String(tokenScopes),
|
|
645
|
+
user: tokenOwner,
|
|
646
|
+
teamId: tokenTeamId,
|
|
647
|
+
}
|
|
593
648
|
: { kind: 'none' });
|
|
594
649
|
// A token's user is scoped as REST scopes them. No credentials at all
|
|
595
650
|
// stays unscoped (the back-compat path), with no lookup.
|
|
@@ -599,6 +654,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
599
654
|
teamId: identity.userId ? tokenTeamId : null,
|
|
600
655
|
} });
|
|
601
656
|
}
|
|
657
|
+
const { row, user: owner } = verifiedPair;
|
|
602
658
|
if (!svc.hasScope(row, ['sessions'])) {
|
|
603
659
|
this.logger.error('Rejecting session: credentials lack the `sessions` scope');
|
|
604
660
|
throw new base_driver_1.errors.InvalidArgumentError('credentials are invalid, revoked, or lack the `sessions` scope');
|
|
@@ -607,7 +663,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
607
663
|
const leaseAccess = this.leaseAccessFor({
|
|
608
664
|
kind: 'api-key',
|
|
609
665
|
scopes: row.scopes,
|
|
610
|
-
|
|
666
|
+
user: owner,
|
|
611
667
|
teamId: (_a = row.teamId) !== null && _a !== void 0 ? _a : null,
|
|
612
668
|
});
|
|
613
669
|
// The phones and apps this session may use: REST's rule
|
|
@@ -642,7 +698,8 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
642
698
|
// How a session that names a lease is judged, looked up at most once and
|
|
643
699
|
// only when asked. Who may override follows canOverrideLease; which phones
|
|
644
700
|
// it can see follows REST's computeTeamIds. Both need the owner's live role,
|
|
645
|
-
// as authMiddleware reads it for every REST request
|
|
701
|
+
// as authMiddleware reads it for every REST request: the owner the
|
|
702
|
+
// credential check looked up a moment ago, ACTIVE then.
|
|
646
703
|
leaseAccessFor(credential) {
|
|
647
704
|
let looked;
|
|
648
705
|
return () => {
|
|
@@ -659,24 +716,24 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
659
716
|
if (credential.kind === 'none') {
|
|
660
717
|
return { canOverride: (0, leaseOverride_1.canOverrideLease)({ kind: 'none' }), teamIds: undefined };
|
|
661
718
|
}
|
|
719
|
+
const { user } = credential;
|
|
662
720
|
try {
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const role = (user === null || user === void 0 ? void 0 : user.role) === 'SUPER_ADMIN' || (user === null || user === void 0 ? void 0 : user.role) === 'ADMIN' ? user.role : 'MEMBER';
|
|
721
|
+
const canOverride = (0, leaseOverride_1.canOverrideLease)({
|
|
722
|
+
kind: credential.kind,
|
|
723
|
+
scopes: credential.scopes,
|
|
724
|
+
user,
|
|
725
|
+
});
|
|
726
|
+
const role = user.role === 'SUPER_ADMIN' || user.role === 'ADMIN' ? user.role : 'MEMBER';
|
|
670
727
|
const teamIds = yield (0, callerTeamIds_1.computeTeamIds)({
|
|
671
728
|
role,
|
|
672
|
-
userId:
|
|
729
|
+
userId: user.id,
|
|
673
730
|
apiKeyTeamId: credential.teamId,
|
|
674
731
|
});
|
|
675
732
|
return { canOverride, teamIds };
|
|
676
733
|
}
|
|
677
734
|
catch (err) {
|
|
678
735
|
// Fail closed: no override, and the shared pool only.
|
|
679
|
-
this.logger.warn(`Could not look up how user ${
|
|
736
|
+
this.logger.warn(`Could not look up how user ${user.id} may use a lease: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
680
737
|
return { canOverride: false, teamIds: [] };
|
|
681
738
|
}
|
|
682
739
|
});
|
|
@@ -790,7 +847,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
790
847
|
}
|
|
791
848
|
}
|
|
792
849
|
finalizeSession(session_1, device_1, caps_1, driver_1, isRemote_1) {
|
|
793
|
-
return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null) {
|
|
850
|
+
return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null, userId = null, allocatedAt) {
|
|
794
851
|
var _a;
|
|
795
852
|
const sessionId = session.value[0];
|
|
796
853
|
const sessionResponse = session.value[1];
|
|
@@ -826,6 +883,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
826
883
|
const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver, webDriverUrl);
|
|
827
884
|
sessionInstance.apiKeyId = apiKeyId;
|
|
828
885
|
sessionInstance.userId = userId;
|
|
886
|
+
sessionInstance.allocatedAt = allocatedAt;
|
|
829
887
|
// A session this server drives: its owner is known while it runs, row or
|
|
830
888
|
// not (a node writes none for the hub's sessions; LiveSessionOwners).
|
|
831
889
|
if (sessionInstance instanceof LocalSession_1.LocalSession) {
|
|
@@ -1148,6 +1206,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1148
1206
|
finally {
|
|
1149
1207
|
if (sessionId) {
|
|
1150
1208
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1209
|
+
(0, sessionMemory_1.forgetSessionMemory)(sessionId);
|
|
1151
1210
|
// Before the lock's "still in memory?" check, which a dashboard-off
|
|
1152
1211
|
// node fails. On a node this ends the figures it holds for its hub;
|
|
1153
1212
|
// on a hub it collects a node session's last ones. Idempotent.
|
|
@@ -1263,6 +1322,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
1263
1322
|
}
|
|
1264
1323
|
SessionManager_1.SESSION_MANAGER.removeSession(sessionId);
|
|
1265
1324
|
typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
|
|
1325
|
+
(0, sessionMemory_1.forgetSessionMemory)(sessionId);
|
|
1266
1326
|
yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
|
|
1267
1327
|
}));
|
|
1268
1328
|
});
|
|
@@ -33,9 +33,14 @@ const SocketEvents_1 = require("../enums/SocketEvents");
|
|
|
33
33
|
const callerTeamIds_1 = require("./device-access/callerTeamIds");
|
|
34
34
|
const DeviceTeamResolver_1 = require("./device-access/DeviceTeamResolver");
|
|
35
35
|
const upgradeRouter_1 = require("../app/ws/upgradeRouter");
|
|
36
|
+
const sessionCommandSummary_1 = require("../dashboard/sessionCommandSummary");
|
|
36
37
|
/** socket.io's default path, which the dashboard and nodes connect to. */
|
|
37
38
|
const SOCKET_IO_PATH = '/socket.io';
|
|
38
39
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
40
|
+
const EVENT_LOG_KEEPS = new Map([
|
|
41
|
+
[SocketEvents_1.SocketEvents.INTERCEPTOR_REQUEST, null],
|
|
42
|
+
[SocketEvents_1.SocketEvents.SESSION_COMMAND, sessionCommandSummary_1.sessionCommandSummary],
|
|
43
|
+
]);
|
|
39
44
|
function readCookie(cookieHeader, name) {
|
|
40
45
|
if (!cookieHeader)
|
|
41
46
|
return undefined;
|
|
@@ -242,12 +247,23 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
242
247
|
if (this.io) {
|
|
243
248
|
this.io.to('dashboard').emit(event, data);
|
|
244
249
|
}
|
|
245
|
-
|
|
250
|
+
this.logEvent(event, data);
|
|
251
|
+
}
|
|
252
|
+
/** Once per event, unscoped: whole, or what EVENT_LOG_KEEPS keeps of it. */
|
|
253
|
+
logEvent(event, data) {
|
|
254
|
+
const keep = EVENT_LOG_KEEPS.get(event);
|
|
255
|
+
if (keep === null)
|
|
256
|
+
return;
|
|
257
|
+
typedi_1.Container.get(EventLogService_1.EventLogService).appendSafe({
|
|
258
|
+
type: event,
|
|
259
|
+
payload: keep ? keep(data) : data,
|
|
260
|
+
});
|
|
246
261
|
}
|
|
247
262
|
/**
|
|
248
263
|
* A dashboard event about one or more phones: each dashboard socket gets it
|
|
249
264
|
* only if its caller can see the phone ({@link DeviceEventScope}), by the
|
|
250
|
-
* same team rule as REST. The event log records it once, unscoped
|
|
265
|
+
* same team rule as REST. The event log records it once, unscoped, as
|
|
266
|
+
* EVENT_LOG_KEEPS says.
|
|
251
267
|
*
|
|
252
268
|
* Each phone's events are delivered through one chain, in the order they
|
|
253
269
|
* were emitted, whatever their scope shape: one that waits on a team lookup
|
|
@@ -262,7 +278,7 @@ let SocketServer = SocketServer_1 = class SocketServer {
|
|
|
262
278
|
*/
|
|
263
279
|
emitToDashboardForDevices(event, data, scope) {
|
|
264
280
|
var _a, _b;
|
|
265
|
-
|
|
281
|
+
this.logEvent(event, data);
|
|
266
282
|
const udids = 'udids' in scope ? scope.udids : [(_a = scope.udid) !== null && _a !== void 0 ? _a : ''];
|
|
267
283
|
if (!this.hasScopedDashboard() && udids.every((udid) => !this.deliveries.has(udid))) {
|
|
268
284
|
this.noteRow(scope);
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.sliceVideo = sliceVideo;
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const ffmpegPath_1 = require("../../helpers/ffmpegPath");
|
|
8
9
|
const logger_1 = __importDefault(require("../../logger"));
|
|
9
10
|
function sliceVideo(inputPath, startSec, endSec, outPath, spawnFn = child_process_1.spawn) {
|
|
10
11
|
return new Promise((resolve) => {
|
|
@@ -16,7 +17,9 @@ function sliceVideo(inputPath, startSec, endSec, outPath, spawnFn = child_proces
|
|
|
16
17
|
'-c', 'copy',
|
|
17
18
|
outPath,
|
|
18
19
|
];
|
|
19
|
-
|
|
20
|
+
// Xenon's own ffmpeg, as everywhere else: a bare name isn't found by a
|
|
21
|
+
// server the Mac app started, and finds a system ffmpeg first otherwise.
|
|
22
|
+
const proc = spawnFn((0, ffmpegPath_1.resolveFfmpegPath)(), args);
|
|
20
23
|
let stderr = '';
|
|
21
24
|
if (proc.stderr) {
|
|
22
25
|
proc.stderr.on('data', (chunk) => {
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.canOverrideLease = canOverrideLease;
|
|
4
4
|
function canOverrideLease(credential) {
|
|
5
|
-
var _a, _b;
|
|
6
5
|
switch (credential.kind) {
|
|
7
6
|
case 'auth-disabled':
|
|
8
7
|
return true;
|
|
9
|
-
case 'api-key':
|
|
8
|
+
case 'api-key':
|
|
9
|
+
case 'session-token': {
|
|
10
10
|
if (!credential.user)
|
|
11
11
|
return false;
|
|
12
12
|
const scopes = new Set(credential.scopes.split(',').map((s) => s.trim()));
|
|
13
|
-
|
|
13
|
+
const { role } = credential.user;
|
|
14
|
+
if (credential.kind === 'session-token') {
|
|
15
|
+
return role === 'SUPER_ADMIN' || (role === 'ADMIN' && scopes.has('admin'));
|
|
16
|
+
}
|
|
17
|
+
return scopes.has('admin') || role === 'SUPER_ADMIN';
|
|
14
18
|
}
|
|
15
|
-
case 'session-token':
|
|
16
|
-
return ((_a = credential.user) === null || _a === void 0 ? void 0 : _a.role) === 'SUPER_ADMIN' || ((_b = credential.user) === null || _b === void 0 ? void 0 : _b.role) === 'ADMIN';
|
|
17
19
|
default:
|
|
18
20
|
return false;
|
|
19
21
|
}
|
|
@@ -20,6 +20,9 @@ const xpath_1 = require("xpath");
|
|
|
20
20
|
const typedi_1 = require("typedi");
|
|
21
21
|
const logger_1 = __importDefault(require("../../logger"));
|
|
22
22
|
const HealedLocatorGenerator_1 = require("./HealedLocatorGenerator");
|
|
23
|
+
const fingerprintIdentity_1 = require("./fingerprintIdentity");
|
|
24
|
+
/** What an element says in words, for telling it apart once its identifier differs. */
|
|
25
|
+
const TEXTS = ['text', 'label', 'content-desc'];
|
|
23
26
|
class FuzzyXmlHealingProvider {
|
|
24
27
|
constructor(etalonService) {
|
|
25
28
|
this.etalonService = etalonService;
|
|
@@ -41,6 +44,12 @@ class FuzzyXmlHealingProvider {
|
|
|
41
44
|
if (this.etalonService) {
|
|
42
45
|
etalon = yield this.etalonService.getSignature(context.selector);
|
|
43
46
|
}
|
|
47
|
+
// A fingerprint with only a rect and a type (every learnt one through
|
|
48
|
+
// 2.14) would heal to whatever element of that type is at that spot.
|
|
49
|
+
if (etalon && !(0, fingerprintIdentity_1.hasIdentity)(etalon.attributes)) {
|
|
50
|
+
this.logger.info('The baseline signature says nothing about which element it is; matching without it.');
|
|
51
|
+
etalon = null;
|
|
52
|
+
}
|
|
44
53
|
const keywords = this.extractKeywords(context.selector);
|
|
45
54
|
if (keywords.length === 0 && !etalon)
|
|
46
55
|
return null;
|
|
@@ -135,6 +144,9 @@ class FuzzyXmlHealingProvider {
|
|
|
135
144
|
if (etalonTag !== nodeTag && etalonTag !== 'xcuielementtypeany' && etalonTag !== 'unknown') {
|
|
136
145
|
return 0; // Immediate rejection — wrong element type
|
|
137
146
|
}
|
|
147
|
+
// Position can't heal to an element whose identifier says it's another one.
|
|
148
|
+
if (this.contradictsIdentifier(node, etalon))
|
|
149
|
+
return 0;
|
|
138
150
|
// Tag matches perfectly
|
|
139
151
|
totalWeight += 1.0;
|
|
140
152
|
totalScore += 1.0;
|
|
@@ -260,6 +272,45 @@ class FuzzyXmlHealingProvider {
|
|
|
260
272
|
}
|
|
261
273
|
return finalScore;
|
|
262
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Whether `node` is, by its identifier, another element than the
|
|
277
|
+
* fingerprint's: the fingerprint has an identifier (an Android resource-id,
|
|
278
|
+
* compared without its package, or an iOS accessibility identifier), `node`
|
|
279
|
+
* has another one (sameIdentifier), and no text of it (text, label,
|
|
280
|
+
* content-desc) reads the same as the fingerprint's (sameText). Its
|
|
281
|
+
* position then counts for nothing.
|
|
282
|
+
*
|
|
283
|
+
* A renamed id that kept its text or description still heals, and so does
|
|
284
|
+
* a renamed text that kept its id. An element without an identifier, where
|
|
285
|
+
* the fingerprint has one, is judged by its text alone: an Android element
|
|
286
|
+
* with no resource-id, or an iOS one, whose name is then its label. Only
|
|
287
|
+
* where neither has one does position decide, as through 2.14.
|
|
288
|
+
* What this stops is the button that took the old one's place: "Cancel
|
|
289
|
+
* order" (cancel) where "Pay now" (pay) was, "Log out" where "Log in" was.
|
|
290
|
+
*/
|
|
291
|
+
contradictsIdentifier(node, etalon) {
|
|
292
|
+
const identifier = (0, fingerprintIdentity_1.stableIdentifierOf)(etalon.attributes);
|
|
293
|
+
if (!identifier)
|
|
294
|
+
return false;
|
|
295
|
+
// Another identifier, or none where the fingerprint has one, and the
|
|
296
|
+
// text decides. iOS gives an element without one its label as name.
|
|
297
|
+
const theirs = this.getAttrValue(node, identifier.attribute);
|
|
298
|
+
if ((0, fingerprintIdentity_1.meaningful)(theirs)) {
|
|
299
|
+
const [mine, other] = identifier.attribute === 'resource-id'
|
|
300
|
+
? [(0, fingerprintIdentity_1.resourceIdName)(identifier.value), (0, fingerprintIdentity_1.resourceIdName)(theirs)]
|
|
301
|
+
: [identifier.value, theirs];
|
|
302
|
+
if ((0, fingerprintIdentity_1.sameIdentifier)(mine, other))
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
const texts = TEXTS.map((name) => etalon.attributes[name]).filter(fingerprintIdentity_1.meaningful);
|
|
306
|
+
if (texts.length === 0)
|
|
307
|
+
return true;
|
|
308
|
+
const nodeTexts = [
|
|
309
|
+
node.textContent,
|
|
310
|
+
...TEXTS.map((name) => this.getAttrValue(node, name)),
|
|
311
|
+
].filter(fingerprintIdentity_1.meaningful);
|
|
312
|
+
return !nodeTexts.some((t) => texts.some((w) => (0, fingerprintIdentity_1.sameText)(t, w)));
|
|
313
|
+
}
|
|
263
314
|
getAttrValue(node, name) {
|
|
264
315
|
if (typeof node.getAttribute === 'function') {
|
|
265
316
|
const val = node.getAttribute(name);
|
|
@@ -37,7 +37,9 @@ let HealEtalonService = class HealEtalonService {
|
|
|
37
37
|
return __awaiter(this, void 0, void 0, function* () {
|
|
38
38
|
try {
|
|
39
39
|
const attributes = {};
|
|
40
|
-
// Extract identifying attributes
|
|
40
|
+
// Extract identifying attributes. Not `value`: on a text field it is
|
|
41
|
+
// what the test typed, an email or a name, which says nothing about
|
|
42
|
+
// which field it is.
|
|
41
43
|
const nodeAttrs = node.attributes || [];
|
|
42
44
|
const anchorNames = [
|
|
43
45
|
'content-desc',
|
|
@@ -48,7 +50,6 @@ let HealEtalonService = class HealEtalonService {
|
|
|
48
50
|
'hint',
|
|
49
51
|
'label',
|
|
50
52
|
'accessibility-id',
|
|
51
|
-
'value',
|
|
52
53
|
'x',
|
|
53
54
|
'y',
|
|
54
55
|
'width',
|