@xenon-device-management/xenon 1.29.0 → 2.0.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 +11 -2
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-Ne9IA8t9.js → AnnotationOverlay-BmTPCAds.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CwXbDjtR.js → ApiKeyGate-B5eB7f8B.js} +1 -1
- package/lib/public/assets/{BugReportButton-D8694Ndn.js → BugReportButton-DKYsOdGV.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-vqKa_a7b.js → DeviceMosaicView-9DUoZnQi.js} +1 -1
- package/lib/public/assets/{EmptyState-DqbEejWx.js → EmptyState-lvyNiXev.js} +1 -1
- package/lib/public/assets/{FieldGroup-BNKuUsHt.js → FieldGroup-BduMPM9K.js} +1 -1
- package/lib/public/assets/{FilterMenu-CLqoSOsz.js → FilterMenu-BSqX6I94.js} +1 -1
- package/lib/public/assets/{Menu-CU57vgsd.js → Menu-Ba5cT7jZ.js} +1 -1
- package/lib/public/assets/{Modal-Dt2whV22.js → Modal-CceoMY7D.js} +1 -1
- package/lib/public/assets/{RecordingPage-BipS3IoG.js → RecordingPage-6T9dOidc.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DORtXeH5.js → RecordingsPage-qj6FdZXR.js} +1 -1
- package/lib/public/assets/{SegmentedControl-IX6aSFFO.js → SegmentedControl-CSRuZ8TR.js} +1 -1
- package/lib/public/assets/{SettingCard-DDp2N5zA.js → SettingCard-Bp1kz4By.js} +1 -1
- package/lib/public/assets/{Table-CiaA4Dni.js → Table-LV7zFyZg.js} +1 -1
- package/lib/public/assets/{activity-DzhwJvJO.js → activity-D17TUloi.js} +1 -1
- package/lib/public/assets/{ai-settings-BllB4M52.js → ai-settings-qvbhY89P.js} +1 -1
- package/lib/public/assets/api-keys-BfpvdG7G.js +1 -0
- package/lib/public/assets/apps-DMhpZuKd.css +1 -0
- package/lib/public/assets/apps-Dbnzd0fW.js +26 -0
- package/lib/public/assets/{arrow-left-poHDsOnw.js → arrow-left-DD1VjtC7.js} +1 -1
- package/lib/public/assets/{arrow-right-Bt8ka8uH.js → arrow-right-D0pnM3ma.js} +1 -1
- package/lib/public/assets/{arrow-up-right-C7zjEfIX.js → arrow-up-right-BIC8JOV6.js} +1 -1
- package/lib/public/assets/{auth-shell-DqNP66nv.js → auth-shell-DWHPXSnI.js} +2 -2
- package/lib/public/assets/{builds-page-BmoSSMe1.js → builds-page-ClCNBUv_.js} +1 -1
- package/lib/public/assets/{button-DujkWRKD.js → button-xtAhYR-f.js} +1 -1
- package/lib/public/assets/{calendar-3xj_SfAt.js → calendar-CupwJ7Bm.js} +1 -1
- package/lib/public/assets/{check-DAtcemi7.js → check-CyuEoKkd.js} +1 -1
- package/lib/public/assets/{chevron-left-BODVavmZ.js → chevron-left-DgX-F9xK.js} +1 -1
- package/lib/public/assets/{chevron-right-BtzJa9CV.js → chevron-right-BKJgJnU9.js} +1 -1
- package/lib/public/assets/{circle-check-CNLy00dn.js → circle-check-CZdg8mMa.js} +1 -1
- package/lib/public/assets/{circle-x-DwlxOXJV.js → circle-x-CZH6t9z-.js} +1 -1
- package/lib/public/assets/{clock-CthJSgmT.js → clock-Cd-t7xpe.js} +1 -1
- package/lib/public/assets/{copy-o_vwnvDl.js → copy-C6zerX3I.js} +1 -1
- package/lib/public/assets/{device-explorer-BGwZQSit.js → device-explorer-B1pI6b_M.js} +2 -2
- package/lib/public/assets/device-explorer-DV8rWRxL.css +1 -0
- package/lib/public/assets/{download-CH7KYHQ6.js → download-x9WP7mcL.js} +1 -1
- package/lib/public/assets/{forgot-password-Fo0NoMuQ.js → forgot-password-B0TEH4N-.js} +1 -1
- package/lib/public/assets/{funnel-DLq7lVeF.js → funnel-Cb40MfFN.js} +1 -1
- package/lib/public/assets/{index-SVMaJ9rC.js → index-BR1wTIPC.js} +9 -9
- package/lib/public/assets/{index-tbOE1tW_.js → index-BvH_ekRy.js} +1 -1
- package/lib/public/assets/{index-Fa91L6sA.css → index-hymrhlEZ.css} +1 -1
- package/lib/public/assets/{input-Cn9aCi1V.js → input-DejcCiTI.js} +1 -1
- package/lib/public/assets/{keycodes-DVNcuy4F.js → keycodes-Mavx5iGg.js} +1 -1
- package/lib/public/assets/{layers-DFA4vIXT.js → layers-BKqYLiHE.js} +1 -1
- package/lib/public/assets/{lock-DbVuQMlE.js → lock-LFsFhc17.js} +1 -1
- package/lib/public/assets/{login-kvBEtLEI.js → login-x2pkzt-t.js} +1 -1
- package/lib/public/assets/{maintenance-settings-9MSQd7-h.js → maintenance-settings-CYafcoV-.js} +1 -1
- package/lib/public/assets/{monitor-C2wiQCzm.js → monitor-_7_uJ8d9.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BxOt4PTR.js → mouse-pointer-2-BfLuURKN.js} +1 -1
- package/lib/public/assets/{network-VOsv-B1C.js → network-Z_zBhpsD.js} +1 -1
- package/lib/public/assets/{overview-BxRv07jX.js → overview-BY5uv08S.js} +1 -1
- package/lib/public/assets/{page-header-DRYmsZ55.js → page-header-CP2UkykG.js} +1 -1
- package/lib/public/assets/{plus-BY8JP9c5.js → plus-BTD0dnQ0.js} +1 -1
- package/lib/public/assets/{profile-page-C0IMKJRO.js → profile-page-BhJZuCS7.js} +1 -1
- package/lib/public/assets/{reset-password-2OxcQdve.js → reset-password-CMN0SO81.js} +1 -1
- package/lib/public/assets/{runbook-page-ChzDYLnk.js → runbook-page-CBpPX9_5.js} +1 -1
- package/lib/public/assets/{select-DkLtE2T3.js → select-Uc9E5pye.js} +1 -1
- package/lib/public/assets/{selector-detail-page-9XKCgLLk.js → selector-detail-page-BSyBnRuU.js} +1 -1
- package/lib/public/assets/{selector-health-CZb7lL82.js → selector-health-DzqxNkzm.js} +1 -1
- package/lib/public/assets/{selector-health-page-CgNkXERq.js → selector-health-page-CyPLjoD1.js} +1 -1
- package/lib/public/assets/{session-detail-page-V68omLzd.js → session-detail-page-BZQZN1P-.js} +1 -1
- package/lib/public/assets/{settings-BTO5_h8Z.js → settings-CkSWiZq8.js} +1 -1
- package/lib/public/assets/{status-pill-outline-DteJiQpj.js → status-pill-outline-BFYni2qo.js} +1 -1
- package/lib/public/assets/{tablet-BvW2cqBO.js → tablet-BSd-xSZK.js} +1 -1
- package/lib/public/assets/teams-PpoEQz31.js +1 -0
- package/lib/public/assets/{trash-2-Ck4Hj9AT.js → trash-2-BorJv5_u.js} +1 -1
- package/lib/public/assets/{upload-DOZwUk1H.js → upload-C3i7sAZl.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BkBlX2qd.js → use-password-reset-mode-CeZQ3EsH.js} +1 -1
- package/lib/public/assets/{users-yoXht7X_.js → users-CEJ3fC78.js} +1 -1
- package/lib/public/assets/{users-CkggsUeX.js → users-Drd7ifJx.js} +1 -1
- package/lib/public/assets/{video-off-DZG2o_Gl.js → video-off-CNzn6bPE.js} +1 -1
- package/lib/public/assets/{webhook-settings-D31jQ3yC.js → webhook-settings-BFqX6QXV.js} +1 -1
- package/lib/public/assets/{zap-nAiJWOGv.js → zap-4yN8Jdfw.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +1 -1
- package/lib/src/XenonCapabilityManager.js +21 -50
- package/lib/src/app/index.js +14 -9
- package/lib/src/app/insertBeforeRoutes.js +57 -0
- package/lib/src/app/registerCommandAuth.js +67 -0
- package/lib/src/app/routers/apps.js +83 -5
- package/lib/src/app/routers/auth.js +1 -1
- package/lib/src/app/routers/bug-report.js +12 -0
- package/lib/src/app/routers/capabilities.js +4 -0
- package/lib/src/app/routers/control.js +6 -1
- package/lib/src/app/routers/dashboard.js +116 -66
- package/lib/src/app/routers/grid.js +38 -57
- package/lib/src/app/routers/recordings.js +3 -1
- package/lib/src/app/swagger-docs.js +7 -4
- package/lib/src/app/swagger.js +3 -0
- package/lib/src/dashboard/event-manager.js +18 -21
- package/lib/src/dashboard/services/app-service.js +24 -2
- package/lib/src/data-service/device-service.js +2 -1
- package/lib/src/data-service/queue-service.js +4 -3
- package/lib/src/device-utils.js +1 -1
- package/lib/src/generated/client/edge.js +5 -4
- package/lib/src/generated/client/index-browser.js +2 -1
- package/lib/src/generated/client/index.d.ts +413 -7
- package/lib/src/generated/client/index.js +5 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +7 -0
- package/lib/src/generated/client/wasm.js +2 -1
- package/lib/src/interceptors/CommandInterceptor.js +5 -5
- package/lib/src/middleware/authMiddleware.js +44 -36
- package/lib/src/middleware/commandAuth.js +146 -0
- package/lib/src/middleware/commandCaller.js +142 -0
- package/lib/src/middleware/verifyCredential.js +124 -0
- package/lib/src/services/ServerManager.js +7 -0
- package/lib/src/services/SessionLifecycleService.js +143 -73
- package/lib/src/services/TeamService.js +6 -3
- package/lib/src/services/device-access/appVisibility.js +27 -0
- package/lib/src/services/device-access/queueVisibility.js +118 -0
- package/lib/src/services/device-access/sessionVisibility.js +65 -0
- package/lib/src/services/healing/HealingOrchestrator.js +11 -3
- package/lib/src/services/lease/LeaseService.js +13 -4
- package/lib/src/services/lease/buildCapabilityBag.js +4 -1
- package/lib/src/services/lease/leaseSessionCaps.js +13 -69
- package/lib/src/services/recording/annotation-render.js +33 -1
- package/lib/src/services/recording/recordingSummary.js +6 -5
- package/lib/src/services/recording/recordingTiming.js +19 -5
- package/lib/src/services/session/appCapability.js +50 -0
- package/lib/src/services/session/sessionCredentials.js +105 -0
- package/lib/src/services/session/xenonOptions.js +71 -0
- package/lib/src/services/sessionTokenGate.js +19 -9
- package/lib/src/services/token/AppDownloadTicketService.js +73 -0
- package/lib/src/services/token/StreamTicketService.js +7 -17
- package/lib/src/services/token/singleUseLedger.js +41 -0
- package/lib/src/sessions/XenonSession.js +2 -2
- package/lib/test/helpers/loopbackServer.js +48 -0
- package/lib/test/integration/sdk-leases-router.spec.js +1 -1
- package/lib/test/integration/team-visibility-apps.spec.js +181 -0
- package/lib/test/integration/team-visibility-recordings.spec.js +4 -0
- package/lib/test/integration/team-visibility-sessions.spec.js +274 -0
- package/lib/test/unit/AppDownloadTicketService.spec.js +101 -0
- package/lib/test/unit/CapabilitiesRouter.spec.js +10 -0
- package/lib/test/unit/SessionTokenGate.spec.js +7 -2
- package/lib/test/unit/TeamService.test.js +22 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +11 -1
- package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +202 -0
- package/lib/test/unit/annotation-render.spec.js +90 -0
- package/lib/test/unit/apps-team-visibility.spec.js +305 -0
- package/lib/test/unit/authMiddleware.appTicket.spec.js +201 -0
- package/lib/test/unit/bug-report/route.spec.js +49 -2
- package/lib/test/unit/cliargs-admin-only.spec.js +182 -0
- package/lib/test/unit/command-auth-appium-server.spec.js +184 -0
- package/lib/test/unit/command-auth.spec.js +410 -0
- package/lib/test/unit/command-caller.spec.js +254 -0
- package/lib/test/unit/event-manager-regression-hook.spec.js +7 -1
- package/lib/test/unit/extract-access-key-token-pair.test.js +47 -4
- package/lib/test/unit/grid-admin-only.spec.js +55 -0
- package/lib/test/unit/healing-tier-gate.spec.js +24 -1
- package/lib/test/unit/insert-before-routes.spec.js +167 -0
- package/lib/test/unit/install-repository-app-team.spec.js +116 -0
- package/lib/test/unit/lease/LeaseService.spec.js +3 -1
- package/lib/test/unit/lease/allocate-lease-branch.spec.js +2 -2
- package/lib/test/unit/lease/buildCapabilityBag.spec.js +3 -2
- package/lib/test/unit/lease/lease-override.spec.js +1 -1
- package/lib/test/unit/lease/lease-session-ownership.spec.js +4 -4
- package/lib/test/unit/lease/lease-team-visibility.spec.js +1 -1
- package/lib/test/unit/lease/lease-token-log-filter.spec.js +1 -1
- package/lib/test/unit/lease/lease-token-oracle.spec.js +207 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +42 -26
- package/lib/test/unit/logcat-ws.spec.js +7 -4
- package/lib/test/unit/queue-requester.spec.js +211 -0
- package/lib/test/unit/queue-team-scope-routes.spec.js +209 -0
- package/lib/test/unit/queue-visibility.spec.js +179 -0
- package/lib/test/unit/recording-summary.spec.js +22 -0
- package/lib/test/unit/recording-timing.spec.js +27 -0
- package/lib/test/unit/recordings-library-routes.spec.js +25 -0
- package/lib/test/unit/session-app-resolution.spec.js +331 -0
- package/lib/test/unit/session-assets.spec.js +167 -0
- package/lib/test/unit/session-attribution-wiring.spec.js +134 -24
- package/lib/test/unit/session-credential-strip.spec.js +277 -0
- package/lib/test/unit/session-credentials.spec.js +149 -0
- package/lib/test/unit/session-identity.spec.js +2 -2
- package/lib/test/unit/session-visibility.spec.js +109 -0
- package/lib/test/unit/stored-session-capabilities.spec.js +54 -0
- package/lib/test/unit/verify-credential.spec.js +138 -0
- package/lib/test/unit/xenon-options.spec.js +95 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260928120000_app_team/migration.sql +26 -0
- package/prisma/schema.prisma +7 -0
- package/schema.json +1 -1
- package/lib/public/assets/api-keys-bjXg97iu.js +0 -1
- package/lib/public/assets/apps-CQEjt22I.js +0 -21
- package/lib/public/assets/apps-DJMD5JOy.css +0 -1
- package/lib/public/assets/device-explorer-DTH5oU54.css +0 -1
- package/lib/public/assets/teams-8Qo7HmUv.js +0 -1
|
@@ -61,11 +61,26 @@ const roleGuard_1 = require("../../middleware/roleGuard");
|
|
|
61
61
|
const build_export_1 = __importDefault(require("./build-export"));
|
|
62
62
|
const NotificationService_1 = require("../../services/NotificationService");
|
|
63
63
|
const SelectorStateService_1 = require("../../services/SelectorStateService");
|
|
64
|
-
const
|
|
64
|
+
const path_1 = __importDefault(require("path"));
|
|
65
|
+
const config_1 = require("../../config");
|
|
66
|
+
const sessionVisibility_1 = require("../../services/device-access/sessionVisibility");
|
|
65
67
|
const HealEtalonService_1 = require("../../services/healing/HealEtalonService");
|
|
66
68
|
const logger_1 = __importDefault(require("../../logger"));
|
|
67
69
|
const MJPEG_PROXY_CACHE = new Map();
|
|
68
|
-
|
|
70
|
+
const authOf = (request) => request.auth;
|
|
71
|
+
/**
|
|
72
|
+
* Only the healed logs of sessions the caller may see (visibleSessionWhere),
|
|
73
|
+
* as a SessionLog `where` fragment; empty for an admin.
|
|
74
|
+
*/
|
|
75
|
+
function visibleLogScope(request) {
|
|
76
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
77
|
+
const where = yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request));
|
|
78
|
+
return where ? { session: { is: where } } : {};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Session guard, on every /session/:sessionId route: the session must exist
|
|
82
|
+
// and be one the caller may see (canSeeSession). Another team's session gets
|
|
83
|
+
// exactly the unknown-id answer, so it can't be told apart from none.
|
|
69
84
|
function isValidSession(request, response, next) {
|
|
70
85
|
return __awaiter(this, void 0, void 0, function* () {
|
|
71
86
|
const sessionId = request.params.sessionId;
|
|
@@ -74,7 +89,7 @@ function isValidSession(request, response, next) {
|
|
|
74
89
|
id: sessionId,
|
|
75
90
|
},
|
|
76
91
|
});
|
|
77
|
-
if (!session) {
|
|
92
|
+
if (!session || !(yield (0, sessionVisibility_1.canSeeSession)(session, authOf(request)))) {
|
|
78
93
|
return response.status(404).send({
|
|
79
94
|
error: true,
|
|
80
95
|
message: `Session with id ${sessionId} not found`,
|
|
@@ -108,50 +123,38 @@ function getSessions(request, response) {
|
|
|
108
123
|
{ tags: { contains: query } },
|
|
109
124
|
];
|
|
110
125
|
}
|
|
126
|
+
// The team rule is part of the query, so `take` counts the caller's sessions.
|
|
127
|
+
const visible = yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request));
|
|
111
128
|
const sessions = yield prisma_1.prisma.session.findMany({
|
|
112
129
|
orderBy: {
|
|
113
130
|
createdAt: 'desc',
|
|
114
131
|
},
|
|
115
|
-
where,
|
|
132
|
+
where: visible ? { AND: [where, visible] } : where,
|
|
116
133
|
take: 500,
|
|
117
134
|
});
|
|
118
|
-
|
|
119
|
-
const auth = request.auth;
|
|
120
|
-
const visible = yield (0, device_service_1.filterRowsByVisibleDevice)(sessions, auth === null || auth === void 0 ? void 0 : auth.teamIds, 'device_udid');
|
|
121
|
-
return response.status(200).json(visible);
|
|
135
|
+
return response.status(200).json(sessions);
|
|
122
136
|
});
|
|
123
137
|
}
|
|
124
138
|
function getBuilds(request, response) {
|
|
125
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
140
|
+
// A build is listed if the caller may see at least one of its sessions, and
|
|
141
|
+
// its counts are over those sessions only: builds are shared by name, so
|
|
142
|
+
// one build can hold several teams' sessions. Admins get every session.
|
|
143
|
+
const visible = yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request));
|
|
126
144
|
const builds = yield prisma_1.prisma.build.findMany({
|
|
127
145
|
orderBy: {
|
|
128
146
|
createdAt: 'desc',
|
|
129
147
|
},
|
|
130
148
|
include: {
|
|
131
|
-
|
|
132
|
-
select: { sessions: true },
|
|
133
|
-
},
|
|
134
|
-
sessions: {
|
|
135
|
-
select: {
|
|
149
|
+
sessions: Object.assign(Object.assign({}, (visible ? { where: visible } : {})), { select: {
|
|
136
150
|
status: true,
|
|
137
151
|
device_udid: true,
|
|
138
|
-
},
|
|
139
|
-
},
|
|
152
|
+
} }),
|
|
140
153
|
},
|
|
141
154
|
});
|
|
142
|
-
|
|
143
|
-
// device the caller can see. Admins (teamIds undefined) skip the filter.
|
|
144
|
-
const auth = request.auth;
|
|
145
|
-
let visibleBuilds = builds;
|
|
146
|
-
if ((auth === null || auth === void 0 ? void 0 : auth.teamIds) !== undefined) {
|
|
147
|
-
const filtered = yield Promise.all(builds.map((b) => __awaiter(this, void 0, void 0, function* () {
|
|
148
|
-
const visibleSessions = yield (0, device_service_1.filterRowsByVisibleDevice)(b.sessions, auth.teamIds, 'device_udid');
|
|
149
|
-
return { build: b, hasVisible: visibleSessions.length > 0 };
|
|
150
|
-
})));
|
|
151
|
-
visibleBuilds = filtered.filter((x) => x.hasVisible).map((x) => x.build);
|
|
152
|
-
}
|
|
155
|
+
const visibleBuilds = visible ? builds.filter((b) => b.sessions.length > 0) : builds;
|
|
153
156
|
// Principal formatting: Add a flat summary object for the frontend
|
|
154
|
-
const formattedBuilds = visibleBuilds.map((b) => (Object.assign(Object.assign({}, b), { sessionCount: b.
|
|
157
|
+
const formattedBuilds = visibleBuilds.map((b) => (Object.assign(Object.assign({}, b), { _count: { sessions: b.sessions.length }, sessionCount: b.sessions.length, passedCount: b.sessions.filter((s) => ['success', 'passed'].includes(s.status)).length, failedCount: b.sessions.filter((s) => s.status === 'failed').length, runningCount: b.sessions.filter((s) => s.status === 'running').length, sessions: undefined })));
|
|
155
158
|
return response.status(200).json(formattedBuilds);
|
|
156
159
|
});
|
|
157
160
|
}
|
|
@@ -161,21 +164,10 @@ function getSessionById(request, response) {
|
|
|
161
164
|
const session = yield prisma_1.prisma.session.findUnique({
|
|
162
165
|
where: { id: sessionId },
|
|
163
166
|
});
|
|
167
|
+
// isValidSession has already refused an unknown or hidden session.
|
|
164
168
|
if (!session) {
|
|
165
169
|
return response.status(404).json({ error: true, message: 'Session not found' });
|
|
166
170
|
}
|
|
167
|
-
// Phase 4A: 404 if the session's device is not visible to the caller.
|
|
168
|
-
const auth = request.auth;
|
|
169
|
-
if ((auth === null || auth === void 0 ? void 0 : auth.teamIds) !== undefined) {
|
|
170
|
-
const dev = yield prisma_1.prisma.device.findFirst({
|
|
171
|
-
where: { udid: session.device_udid },
|
|
172
|
-
select: { teamId: true },
|
|
173
|
-
});
|
|
174
|
-
const visible = dev && (dev.teamId === null || auth.teamIds.includes(dev.teamId));
|
|
175
|
-
if (!visible) {
|
|
176
|
-
return response.status(404).json({ error: true, message: 'Session not found' });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
171
|
return response.status(200).json(session);
|
|
180
172
|
});
|
|
181
173
|
}
|
|
@@ -233,9 +225,11 @@ function getRecentHealingEvents(request, response) {
|
|
|
233
225
|
const limitRaw = parseInt(request.query.limit || '50', 10);
|
|
234
226
|
const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 200) : 50;
|
|
235
227
|
const sessionId = request.query.sessionId || undefined;
|
|
228
|
+
// Another team's session, even by ?sessionId=, reads as one with no heals.
|
|
229
|
+
const scope = yield visibleLogScope(request);
|
|
236
230
|
const [rows, todayCount] = yield Promise.all([
|
|
237
231
|
prisma_1.prisma.sessionLog.findMany({
|
|
238
|
-
where: Object.assign({ is_healed: true }, (sessionId ? { session_id: sessionId } : {})),
|
|
232
|
+
where: Object.assign(Object.assign({ is_healed: true }, (sessionId ? { session_id: sessionId } : {})), scope),
|
|
239
233
|
orderBy: { createdAt: 'desc' },
|
|
240
234
|
take: limit,
|
|
241
235
|
include: {
|
|
@@ -250,7 +244,7 @@ function getRecentHealingEvents(request, response) {
|
|
|
250
244
|
},
|
|
251
245
|
}),
|
|
252
246
|
prisma_1.prisma.sessionLog.count({
|
|
253
|
-
where: { is_healed: true, createdAt: { gte: startOfTodayUtc() } },
|
|
247
|
+
where: Object.assign({ is_healed: true, createdAt: { gte: startOfTodayUtc() } }, scope),
|
|
254
248
|
}),
|
|
255
249
|
]);
|
|
256
250
|
const events = rows.map((r) => {
|
|
@@ -309,13 +303,12 @@ function getHealingSummary(request, response) {
|
|
|
309
303
|
since.setDate(since.getDate() - windowDays);
|
|
310
304
|
const priorSince = new Date(since);
|
|
311
305
|
priorSince.setDate(priorSince.getDate() - windowDays);
|
|
306
|
+
// Heal counts are over the caller's sessions. resolvedCount and pendingCount
|
|
307
|
+
// come from SelectorState, which has no team, so they stay fleet-wide.
|
|
308
|
+
const scope = yield visibleLogScope(request);
|
|
312
309
|
const [currentRows, priorRows] = yield Promise.all([
|
|
313
310
|
prisma_1.prisma.sessionLog.findMany({
|
|
314
|
-
where: {
|
|
315
|
-
is_healed: true,
|
|
316
|
-
createdAt: { gte: since, lte: now },
|
|
317
|
-
original_selector: { not: null },
|
|
318
|
-
},
|
|
311
|
+
where: Object.assign({ is_healed: true, createdAt: { gte: since, lte: now }, original_selector: { not: null } }, scope),
|
|
319
312
|
select: {
|
|
320
313
|
session_id: true,
|
|
321
314
|
original_selector: true,
|
|
@@ -323,11 +316,7 @@ function getHealingSummary(request, response) {
|
|
|
323
316
|
},
|
|
324
317
|
}),
|
|
325
318
|
prisma_1.prisma.sessionLog.findMany({
|
|
326
|
-
where: {
|
|
327
|
-
is_healed: true,
|
|
328
|
-
createdAt: { gte: priorSince, lt: since },
|
|
329
|
-
original_selector: { not: null },
|
|
330
|
-
},
|
|
319
|
+
where: Object.assign({ is_healed: true, createdAt: { gte: priorSince, lt: since }, original_selector: { not: null } }, scope),
|
|
331
320
|
select: {
|
|
332
321
|
session_id: true,
|
|
333
322
|
original_selector: true,
|
|
@@ -385,7 +374,14 @@ function getHealingHotspots(request, response) {
|
|
|
385
374
|
? request.query.platform
|
|
386
375
|
: null;
|
|
387
376
|
const status = (_a = request.query.status) !== null && _a !== void 0 ? _a : 'active';
|
|
388
|
-
const agg = yield aggregateHotspots({
|
|
377
|
+
const agg = yield aggregateHotspots({
|
|
378
|
+
windowDays,
|
|
379
|
+
limit,
|
|
380
|
+
tier,
|
|
381
|
+
platform,
|
|
382
|
+
status,
|
|
383
|
+
sessionWhere: yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request)),
|
|
384
|
+
});
|
|
389
385
|
return response.status(200).json({
|
|
390
386
|
windowDays,
|
|
391
387
|
totalScanned: agg.totalScanned,
|
|
@@ -428,9 +424,18 @@ function aggregateHotspots(opts) {
|
|
|
428
424
|
};
|
|
429
425
|
if (opts.tier)
|
|
430
426
|
where.healing_tier = opts.tier;
|
|
427
|
+
// The platform and build filter, and the caller's sessions, under AND: the
|
|
428
|
+
// caller's rule is an OR, and a spread would let one OR overwrite another.
|
|
429
|
+
const sessionFilters = [];
|
|
431
430
|
if (opts.platform || opts.buildId) {
|
|
432
|
-
|
|
431
|
+
sessionFilters.push(Object.assign(Object.assign({}, (opts.platform ? { device_platform: opts.platform } : {})), (opts.buildId ? { build_id: opts.buildId } : {})));
|
|
433
432
|
}
|
|
433
|
+
if (opts.sessionWhere)
|
|
434
|
+
sessionFilters.push(opts.sessionWhere);
|
|
435
|
+
if (sessionFilters.length === 1)
|
|
436
|
+
where.session = sessionFilters[0];
|
|
437
|
+
else if (sessionFilters.length > 1)
|
|
438
|
+
where.session = { AND: sessionFilters };
|
|
434
439
|
const rows = yield prisma_1.prisma.sessionLog.findMany({
|
|
435
440
|
where,
|
|
436
441
|
select: {
|
|
@@ -529,6 +534,11 @@ function aggregateHotspots(opts) {
|
|
|
529
534
|
// then merge them into each hotspot. The default `status=active` filter
|
|
530
535
|
// hides muted/pending/resolved selectors so the live list, CI gate, and
|
|
531
536
|
// webhook digest all see only "still actively breaking" selectors.
|
|
537
|
+
//
|
|
538
|
+
// The state is looked up only for selectors already in this (scoped) list,
|
|
539
|
+
// but the row is lab-wide and not scoped: `status`, `regression_count`,
|
|
540
|
+
// `clean_builds_count`, `last_event_at`, `resolved_at` and the
|
|
541
|
+
// `*_by_api_key` ids move with any team's heals, builds and admin actions.
|
|
532
542
|
let stateMap = new Map();
|
|
533
543
|
if (topHotspots.length > 0) {
|
|
534
544
|
const states = yield prisma_1.prisma.selectorState.findMany({
|
|
@@ -584,6 +594,7 @@ function getHealingViolations(request, response) {
|
|
|
584
594
|
platform,
|
|
585
595
|
buildId,
|
|
586
596
|
minHealCount,
|
|
597
|
+
sessionWhere: yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request)),
|
|
587
598
|
});
|
|
588
599
|
return response.status(200).json({
|
|
589
600
|
windowDays,
|
|
@@ -610,6 +621,8 @@ function sendHealingDigest(request, response) {
|
|
|
610
621
|
const minHealCount = Number.isFinite(minHealCountRaw)
|
|
611
622
|
? Math.min(Math.max(minHealCountRaw, 1), 1000)
|
|
612
623
|
: 2;
|
|
624
|
+
// Every team's heals, on purpose: admin-only, and the digest goes to the
|
|
625
|
+
// lab's webhooks rather than back to one caller.
|
|
613
626
|
const agg = yield aggregateHotspots({ windowDays, limit, minHealCount });
|
|
614
627
|
const payload = {
|
|
615
628
|
windowDays,
|
|
@@ -652,12 +665,10 @@ function getHealingSelectorDetail(request, response) {
|
|
|
652
665
|
const windowDays = parseWindowDays(request.query.windowDays);
|
|
653
666
|
const since = new Date();
|
|
654
667
|
since.setDate(since.getDate() - windowDays);
|
|
668
|
+
// Only the caller's sessions: a selector healed only on phones they can't
|
|
669
|
+
// see reads as one with no heals.
|
|
655
670
|
const rows = yield prisma_1.prisma.sessionLog.findMany({
|
|
656
|
-
where: {
|
|
657
|
-
is_healed: true,
|
|
658
|
-
original_selector: value,
|
|
659
|
-
createdAt: { gte: since },
|
|
660
|
-
},
|
|
671
|
+
where: Object.assign({ is_healed: true, original_selector: value, createdAt: { gte: since } }, (yield visibleLogScope(request))),
|
|
661
672
|
orderBy: { createdAt: 'desc' },
|
|
662
673
|
take: 1000,
|
|
663
674
|
include: {
|
|
@@ -774,6 +785,7 @@ function getSelectorHealth(request, response) {
|
|
|
774
785
|
windowDays: 365,
|
|
775
786
|
limit: 1000,
|
|
776
787
|
status: 'all',
|
|
788
|
+
sessionWhere: yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request)),
|
|
777
789
|
});
|
|
778
790
|
let rows = agg.hotspots;
|
|
779
791
|
if (selectorFilter) {
|
|
@@ -796,7 +808,8 @@ function getSelectorHealth(request, response) {
|
|
|
796
808
|
// LocatorEtalon.selector is unique, so a lookup by selector alone
|
|
797
809
|
// already pins the row; the strategy check guards against treating a
|
|
798
810
|
// stale/unrelated etalon (selector reused under a different strategy)
|
|
799
|
-
// as a match for this hotspot's (selector, strategy) tuple.
|
|
811
|
+
// as a match for this hotspot's (selector, strategy) tuple. Shared, not
|
|
812
|
+
// scoped: one etalon per selector, refreshed by any team's session.
|
|
800
813
|
const etalon = yield etalonService.getSignature(h.originalSelector);
|
|
801
814
|
if (etalon && etalon.strategy === h.originalStrategy) {
|
|
802
815
|
out.etalonAge = now - etalon.lastSeen;
|
|
@@ -859,6 +872,11 @@ function postSelectorStateAction(request, response) {
|
|
|
859
872
|
// hotspot aggregator) so muted-but-no-recent-heal entries still surface.
|
|
860
873
|
// Each row is enriched with `last_healed_at` from the most recent healed
|
|
861
874
|
// SessionLog row for that tuple (null if never healed).
|
|
875
|
+
//
|
|
876
|
+
// The mute is lab-wide state, so every caller gets the same rows and
|
|
877
|
+
// `muted_at`, `muted_by_api_key` and `regression_count`. `last_healed_at` is
|
|
878
|
+
// heal data, so it is the latest heal among the sessions the caller may see:
|
|
879
|
+
// null for a selector only another team has healed.
|
|
862
880
|
function getMutedSelectors(request, response) {
|
|
863
881
|
return __awaiter(this, void 0, void 0, function* () {
|
|
864
882
|
var _a, _b;
|
|
@@ -871,13 +889,10 @@ function getMutedSelectors(request, response) {
|
|
|
871
889
|
skip: offset,
|
|
872
890
|
});
|
|
873
891
|
const total = yield prisma_1.prisma.selectorState.count({ where: { status: 'muted' } });
|
|
892
|
+
const scope = yield visibleLogScope(request);
|
|
874
893
|
const enriched = yield Promise.all(muted.map((s) => __awaiter(this, void 0, void 0, function* () {
|
|
875
894
|
const last = yield prisma_1.prisma.sessionLog.findFirst({
|
|
876
|
-
where: {
|
|
877
|
-
original_strategy: s.original_strategy,
|
|
878
|
-
original_selector: s.original_selector,
|
|
879
|
-
is_healed: true,
|
|
880
|
-
},
|
|
895
|
+
where: Object.assign({ original_strategy: s.original_strategy, original_selector: s.original_selector, is_healed: true }, scope),
|
|
881
896
|
orderBy: { createdAt: 'desc' },
|
|
882
897
|
select: { createdAt: true },
|
|
883
898
|
});
|
|
@@ -896,7 +911,8 @@ function getMutedSelectors(request, response) {
|
|
|
896
911
|
// Single-tuple state lookup. Strategy and value arrive URL-encoded since
|
|
897
912
|
// XPath selectors contain `/` and `[`. Returns `{ state: null }` when no
|
|
898
913
|
// row exists (intentional — null is meaningful: the selector is implicitly
|
|
899
|
-
// active).
|
|
914
|
+
// active). The raw SelectorState row only, with no heal-derived field, so it
|
|
915
|
+
// answers the same for every caller: the row is lab-wide state.
|
|
900
916
|
function getSelectorStateByTuple(request, response) {
|
|
901
917
|
return __awaiter(this, void 0, void 0, function* () {
|
|
902
918
|
var _a, _b;
|
|
@@ -913,6 +929,34 @@ function getSelectorStateByTuple(request, response) {
|
|
|
913
929
|
return response.json({ state: row });
|
|
914
930
|
});
|
|
915
931
|
}
|
|
932
|
+
/** The session folders the dashboard shows (asset-manager.ts). Interceptor captures and recordings have their own rules. */
|
|
933
|
+
const SESSION_ASSET_KINDS = new Set(['screenshots', 'video', 'performance']);
|
|
934
|
+
/** One file name, no path and no leading dot. */
|
|
935
|
+
const SESSION_ASSET_FILE = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
936
|
+
/**
|
|
937
|
+
* A session's stored screenshot, video or performance trace, stored as
|
|
938
|
+
* `<sessionId>/<kind>/<file>` under sessionAssetsPath. Behind isValidSession,
|
|
939
|
+
* so the caller must be able to see the session. Supports Range, for video
|
|
940
|
+
* seeking. This replaced an express.static mount outside the API's login.
|
|
941
|
+
*/
|
|
942
|
+
function getSessionAsset(request, response) {
|
|
943
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
944
|
+
const { sessionId, kind, file } = request.params;
|
|
945
|
+
const notFound = () => response.status(404).json({ error: true, message: 'Session asset not found' });
|
|
946
|
+
if (!SESSION_ASSET_KINDS.has(kind) || !SESSION_ASSET_FILE.test(file))
|
|
947
|
+
return notFound();
|
|
948
|
+
response.sendFile(file, {
|
|
949
|
+
root: path_1.default.join(config_1.config.sessionAssetsPath, sessionId, kind),
|
|
950
|
+
headers: { 'Cache-Control': 'private, max-age=300' },
|
|
951
|
+
}, (err) => {
|
|
952
|
+
if (!err || response.headersSent)
|
|
953
|
+
return;
|
|
954
|
+
if (err.status === 416 || err.statusCode === 416)
|
|
955
|
+
return response.status(416).end();
|
|
956
|
+
notFound();
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
}
|
|
916
960
|
function getProfilingData(request, response) {
|
|
917
961
|
return __awaiter(this, void 0, void 0, function* () {
|
|
918
962
|
const sessionId = request.params.sessionId;
|
|
@@ -1038,6 +1082,9 @@ function register(router) {
|
|
|
1038
1082
|
router.get('/session/:sessionId/logs/device', getDeviceLogs);
|
|
1039
1083
|
router.get('/session/:sessionId/logs/debug', getDebugLogs);
|
|
1040
1084
|
router.get('/session/:sessionId/profiling', getProfilingData);
|
|
1085
|
+
router.get('/session/:sessionId/asset/:kind/:file', getSessionAsset);
|
|
1086
|
+
// The healing reads count only heals from sessions the caller may see
|
|
1087
|
+
// (visibleSessionWhere); the digest below counts every team's.
|
|
1041
1088
|
router.get('/healing/events', getRecentHealingEvents);
|
|
1042
1089
|
router.get('/healing/summary', getHealingSummary);
|
|
1043
1090
|
router.get('/healing/hotspots', getHealingHotspots);
|
|
@@ -1049,7 +1096,10 @@ function register(router) {
|
|
|
1049
1096
|
router.post('/healing/digest/send', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), sendHealingDigest);
|
|
1050
1097
|
// SelectorState lifecycle: state mutations require admin (they affect what
|
|
1051
1098
|
// shows up in the live hotspot list, the CI gate, and the digest); the two
|
|
1052
|
-
// reads inherit the existing dashboard auth.
|
|
1099
|
+
// reads inherit the existing dashboard auth. Both reads stay global, not
|
|
1100
|
+
// team-scoped: a selector's mute or fix is one lab-wide row with no team
|
|
1101
|
+
// column, shared by every team whose tests use that selector. The one
|
|
1102
|
+
// heal-derived field, the muted list's `last_healed_at`, is the caller's.
|
|
1053
1103
|
router.post('/healing/selector/state', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), postSelectorStateAction);
|
|
1054
1104
|
router.get('/healing/state/muted', getMutedSelectors);
|
|
1055
1105
|
router.get('/healing/state/:strategy/:value', getSelectorStateByTuple);
|
|
@@ -50,9 +50,10 @@ const queue_service_1 = require("../../data-service/queue-service");
|
|
|
50
50
|
const InternalHttpClient_1 = require("../../InternalHttpClient");
|
|
51
51
|
const lodash_1 = __importDefault(require("lodash"));
|
|
52
52
|
const device_service_1 = require("../../data-service/device-service");
|
|
53
|
+
const queueVisibility_1 = require("../../services/device-access/queueVisibility");
|
|
53
54
|
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
54
55
|
const roleGuard_1 = require("../../middleware/roleGuard");
|
|
55
|
-
const logger_1 =
|
|
56
|
+
const logger_1 = __importStar(require("../../logger"));
|
|
56
57
|
const device_managers_1 = require("../../device-managers");
|
|
57
58
|
const typedi_1 = require("typedi");
|
|
58
59
|
const prisma_1 = require("../../prisma");
|
|
@@ -204,38 +205,22 @@ function unBlockDevice(request, response) {
|
|
|
204
205
|
response.status(200).send({ success: true });
|
|
205
206
|
});
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
-
// from the Session model — they may carry `appium:udid` if the test runner
|
|
209
|
-
// targeted a specific device, but most queue items are platform-only.
|
|
210
|
-
// Strategy: rows with `appium:udid` get visibility-filtered against that
|
|
211
|
-
// udid; rows without one pass through (the capability could still match any
|
|
212
|
-
// device the caller can see, and there's no row-level ownership to enforce).
|
|
213
|
-
function filterPendingByVisibleDevice(rows, teamIds) {
|
|
214
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
215
|
-
if (teamIds === undefined)
|
|
216
|
-
return rows;
|
|
217
|
-
const targeted = rows.filter((r) => typeof r['appium:udid'] === 'string' && r['appium:udid']);
|
|
218
|
-
const untargeted = rows.filter((r) => !(typeof r['appium:udid'] === 'string' && r['appium:udid']));
|
|
219
|
-
if (targeted.length === 0)
|
|
220
|
-
return untargeted;
|
|
221
|
-
const visibleTargeted = yield (0, device_service_1.filterRowsByVisibleDevice)(targeted, teamIds, 'appium:udid');
|
|
222
|
-
return [...untargeted, ...visibleTargeted];
|
|
223
|
-
});
|
|
224
|
-
}
|
|
208
|
+
const queueCaller = (request) => request.auth;
|
|
225
209
|
function getQueuedSessionLength(request, response) {
|
|
226
210
|
return __awaiter(this, void 0, void 0, function* () {
|
|
227
|
-
const auth = request.auth;
|
|
228
211
|
const all = yield pendingStore.getAllPendingSessions();
|
|
229
|
-
|
|
230
|
-
response.json(visible.length);
|
|
212
|
+
response.json(all.length);
|
|
231
213
|
});
|
|
232
214
|
}
|
|
233
215
|
function getQueuedSessionRequests(request, response) {
|
|
234
216
|
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
-
const auth = request.auth;
|
|
236
217
|
const all = yield pendingStore.getAllPendingSessions();
|
|
237
|
-
const visible = yield
|
|
238
|
-
|
|
218
|
+
const { visible } = yield (0, queueVisibility_1.partitionPendingForCaller)(all, queueCaller(request));
|
|
219
|
+
// A row is the capabilities the client sent. createSession takes the
|
|
220
|
+
// credentials out of xe:options before writing it, but a row queued by an
|
|
221
|
+
// older server can still hold the key and token that signed it. Those are
|
|
222
|
+
// nobody else's to read, a teammate's or an admin's.
|
|
223
|
+
response.json(visible.map((row) => (0, logger_1.redactSecrets)((0, queueVisibility_1.withoutRequester)(row))));
|
|
239
224
|
});
|
|
240
225
|
}
|
|
241
226
|
function getNodes(request, response) {
|
|
@@ -260,39 +245,32 @@ function getNodes(request, response) {
|
|
|
260
245
|
}
|
|
261
246
|
function getQueueStatusById(request, response) {
|
|
262
247
|
return __awaiter(this, void 0, void 0, function* () {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const targetUdid = target && typeof target['appium:udid'] === 'string' ? target['appium:udid'] : undefined;
|
|
275
|
-
if (targetUdid) {
|
|
276
|
-
const dev = yield prisma_1.prisma.device.findFirst({
|
|
277
|
-
where: { udid: targetUdid },
|
|
278
|
-
select: { teamId: true },
|
|
279
|
-
});
|
|
280
|
-
const visible = dev && (dev.teamId === null || auth.teamIds.includes(dev.teamId));
|
|
281
|
-
if (!visible) {
|
|
282
|
-
return response.status(404).json({ error: 'Pending session not found' });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
248
|
+
const notFound = () => response.status(404).json({ error: 'Pending session not found' });
|
|
249
|
+
// A request the caller may not see answers exactly as an unknown id.
|
|
250
|
+
const caller = queueCaller(request);
|
|
251
|
+
if ((caller === null || caller === void 0 ? void 0 : caller.teamIds) !== undefined) {
|
|
252
|
+
const all = yield pendingStore.getAllPendingSessions();
|
|
253
|
+
const target = all.find((s) => s.capability_id === request.params.capability_id);
|
|
254
|
+
if (!target)
|
|
255
|
+
return notFound();
|
|
256
|
+
const { visible } = yield (0, queueVisibility_1.partitionPendingForCaller)([target], caller);
|
|
257
|
+
if (visible.length === 0)
|
|
258
|
+
return notFound();
|
|
285
259
|
}
|
|
260
|
+
const status = yield typedi_1.Container.get(queue_service_1.QueueService).getQueueStatus(request.params.capability_id);
|
|
261
|
+
if (!status)
|
|
262
|
+
return notFound();
|
|
286
263
|
response.json(status);
|
|
287
264
|
});
|
|
288
265
|
}
|
|
289
266
|
function getQueueSummary(request, response) {
|
|
290
267
|
return __awaiter(this, void 0, void 0, function* () {
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
268
|
+
// Counts only. `total` is the whole queue, like /queue/length; `otherCount`
|
|
269
|
+
// is how many of those the caller doesn't get in detail from /queue.
|
|
270
|
+
const all = yield pendingStore.getAllPendingSessions();
|
|
271
|
+
const { otherCount } = yield (0, queueVisibility_1.partitionPendingForCaller)(all, queueCaller(request));
|
|
272
|
+
const summary = yield typedi_1.Container.get(queue_service_1.QueueService).getQueueSummary(all);
|
|
273
|
+
response.json(Object.assign(Object.assign({}, summary), { otherCount }));
|
|
296
274
|
});
|
|
297
275
|
}
|
|
298
276
|
function nodeAdbStatusOnOtherHost(currentHost, request, response) {
|
|
@@ -478,12 +456,15 @@ function register(router, pluginArgs) {
|
|
|
478
456
|
router.get('/queue/status/:capability_id', getQueueStatusById);
|
|
479
457
|
router.get('/queue/summary', getQueueSummary);
|
|
480
458
|
router.get('/sessions/active', getActiveSessions);
|
|
481
|
-
// debugging / observability
|
|
482
|
-
|
|
483
|
-
//
|
|
459
|
+
// debugging / observability. Admin-only, like /processes: the log holds
|
|
460
|
+
// every internal HTTP call, including control proxied to other nodes'
|
|
461
|
+
// phones (with what was typed) and forwarded new sessions (their ids).
|
|
462
|
+
router.get('/logs/requests', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), getRequestLogs);
|
|
463
|
+
// node related routes. The status reads list every attached phone, so
|
|
464
|
+
// they are admin-only too.
|
|
484
465
|
router.get('/node', getNodes);
|
|
485
|
-
router.get('/node/status', nodeAdbStatusOnThisHost);
|
|
486
|
-
router.get('/node/:host/status', lodash_1.default.curry(nodeAdbStatusOnOtherHost)(pluginArgs.bindHostOrIp));
|
|
466
|
+
router.get('/node/status', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), nodeAdbStatusOnThisHost);
|
|
467
|
+
router.get('/node/:host/status', (0, roleGuard_1.roleGuard)('ADMIN'), (0, scopeGuard_1.scopeGuard)(['admin']), lodash_1.default.curry(nodeAdbStatusOnOtherHost)(pluginArgs.bindHostOrIp));
|
|
487
468
|
// node status
|
|
488
469
|
router.get('/status', (request, response) => {
|
|
489
470
|
response.json({
|
|
@@ -464,7 +464,9 @@ router.get('/recordings/active', (req, res) => __awaiter(void 0, void 0, void 0,
|
|
|
464
464
|
var _a;
|
|
465
465
|
try {
|
|
466
466
|
const actor = (0, actor_1.resolveActor)(req);
|
|
467
|
-
|
|
467
|
+
// A group the caller holds a phone of can hold phones they can't see (an
|
|
468
|
+
// admin added them, or a phone moved team): leave those out, and their marks.
|
|
469
|
+
const rows = yield visibleRows(req, yield typedi_1.Container.get(recording_store_1.RecordingStore).listActiveWithMarks());
|
|
468
470
|
const locks = new Map();
|
|
469
471
|
for (const udid of new Set(rows.map((r) => r.device_udid))) {
|
|
470
472
|
const d = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
|
|
@@ -414,9 +414,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
414
414
|
* summary: Read the plugin CLI arguments in effect
|
|
415
415
|
* description: |
|
|
416
416
|
* Returns the resolved plugin arguments (host, hub URL, platform, intervals, etc.)
|
|
417
|
-
* this process was started with.
|
|
418
|
-
* redacted
|
|
419
|
-
*
|
|
417
|
+
* this process was started with. Requires the ADMIN role and `admin` scope.
|
|
418
|
+
* Values are redacted the way the logger redacts them: any key that looks
|
|
419
|
+
* secret (database URL, API key, token, password, auth…) reads
|
|
420
|
+
* `***REDACTED***`, and so do provider-key-shaped strings.
|
|
420
421
|
* tags: [Health & Ops]
|
|
421
422
|
* responses:
|
|
422
423
|
* 200:
|
|
@@ -425,6 +426,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
425
426
|
* application/json:
|
|
426
427
|
* schema: { type: object, additionalProperties: true }
|
|
427
428
|
* 401: { $ref: '#/components/responses/Unauthorized' }
|
|
429
|
+
* 403: { $ref: '#/components/responses/Forbidden' }
|
|
428
430
|
* 429: { $ref: '#/components/responses/RateLimited' }
|
|
429
431
|
*/
|
|
430
432
|
// =============================================================================
|
|
@@ -1107,7 +1109,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
1107
1109
|
* description: |
|
|
1108
1110
|
* Returns the queue position and ETA for a pending session keyed by the
|
|
1109
1111
|
* capability id you submitted. 404 if the request has already been
|
|
1110
|
-
* allocated or expired
|
|
1112
|
+
* allocated or expired, or is one the caller can't see in
|
|
1113
|
+
* `GET /api/queue`: the same answer as for an unknown id.
|
|
1111
1114
|
* tags: [Grid]
|
|
1112
1115
|
* parameters:
|
|
1113
1116
|
* - in: path
|
package/lib/src/app/swagger.js
CHANGED
|
@@ -34,6 +34,9 @@ Xenon's high-performance API provides programmatic access to the core orchestrat
|
|
|
34
34
|
### Authentication
|
|
35
35
|
Every endpoint authenticates with the per-user \`(x-xenon-access-key, x-xenon-token)\` pair. Each token carries one or more scopes — \`read\`, \`sessions\`, \`devices\`, or \`admin\` (admin always satisfies any scope check). Mint tokens at \`/profile\` → **API Tokens**; the access key is shown at the top of that page and is rotatable. Browser dashboards may use the \`xenon_dashboard_session\` cookie set by \`POST /api/auth/login\`.
|
|
36
36
|
|
|
37
|
+
### Appium sessions
|
|
38
|
+
Appium's own \`POST /session\` is not under this API. A session authenticates with capabilities in \`xe:options\`, Xenon's capability namespace: \`accessKey\` + \`token\` (a token with the \`sessions\` scope), or a \`sessionToken\` minted by \`POST /api/auth/token\`. A leased device is named by \`leaseId\` + \`leaseToken\`, which the lease-create response's \`appiumCapabilities\` already carry. \`xenon:options\` is read as an alias (\`xe:options\` wins field by field); \`df:options\` is not read. Xenon removes the credentials before the driver or any stored record sees them.
|
|
39
|
+
|
|
37
40
|
### Rate limiting
|
|
38
41
|
Every authenticated response carries \`X-RateLimit-Limit\`, \`X-RateLimit-Remaining\`, and \`X-RateLimit-Reset\` headers. Requests beyond the configured budget receive \`429 Too Many Requests\`.
|
|
39
42
|
`,
|
|
@@ -52,9 +52,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
52
52
|
};
|
|
53
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
54
|
exports.DASHBORD_EVENT_MANAGER = exports.DashboardEventManager = void 0;
|
|
55
|
+
exports.storedSessionCapabilities = storedSessionCapabilities;
|
|
55
56
|
const SessionManager_1 = require("../sessions/SessionManager");
|
|
56
57
|
const prisma_1 = require("../prisma");
|
|
57
|
-
const logger_1 =
|
|
58
|
+
const logger_1 = __importStar(require("../logger"));
|
|
58
59
|
const session_service_1 = require("./services/session-service");
|
|
59
60
|
const XenonCapabilityManager_1 = require("../XenonCapabilityManager");
|
|
60
61
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -80,6 +81,21 @@ const types_1 = require("../services/healing/types");
|
|
|
80
81
|
const SelectorStateService_1 = require("../services/SelectorStateService");
|
|
81
82
|
const recording_store_1 = require("../services/recording/recording-store");
|
|
82
83
|
const typedi_2 = require("typedi");
|
|
84
|
+
/**
|
|
85
|
+
* What a Session row keeps of the capabilities the driver returned. The row
|
|
86
|
+
* is served to the dashboard and the session API, so any secret-named value
|
|
87
|
+
* is redacted. createSession already takes Xenon's own credentials out of
|
|
88
|
+
* xe:options and xenon:options before the driver sees them; this also covers
|
|
89
|
+
* a token a client put where Xenon doesn't read it, which the driver hands
|
|
90
|
+
* back in its response. The response itself is not changed.
|
|
91
|
+
*/
|
|
92
|
+
function storedSessionCapabilities(sessionResponse) {
|
|
93
|
+
const redacted = (0, logger_1.redactSecrets)(sessionResponse);
|
|
94
|
+
return {
|
|
95
|
+
desired_capabilities: JSON.stringify(redacted.desired || {}),
|
|
96
|
+
session_capabilities: JSON.stringify(lodash_1.default.omit(redacted, 'desired')),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
83
99
|
let DashboardEventManager = class DashboardEventManager {
|
|
84
100
|
constructor() {
|
|
85
101
|
// private SCREENSHOT_FOR_COMMANDS = ['click', 'setUrl', 'setValue', 'performActions'];
|
|
@@ -133,26 +149,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
133
149
|
const sessionResponse = lodash_1.default.assign({}, session.getCapabilities());
|
|
134
150
|
const tracingService = typedi_1.Container.get(TracingService_1.TracingService);
|
|
135
151
|
const traceId = tracingService.getTraceId(session.getId());
|
|
136
|
-
const createData = {
|
|
137
|
-
id: session.getId(),
|
|
138
|
-
build: build.id ? { connect: { id: build.id } } : undefined,
|
|
139
|
-
name: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined,
|
|
140
|
-
desired_capabilities: JSON.stringify(sessionResponse.desired || {}),
|
|
141
|
-
session_capabilities: JSON.stringify(lodash_1.default.omit(sessionResponse, 'desired')),
|
|
142
|
-
node_id: device.nodeId || '',
|
|
143
|
-
has_live_video: session.getLiveVideoUrl() !== null,
|
|
144
|
-
video_recording_enabled: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true,
|
|
145
|
-
is_profiling_available: is_profiling_available || isIosProfilingStarted,
|
|
146
|
-
device_info: device_info ? JSON.stringify(device_info) : null,
|
|
147
|
-
device_udid: device.udid || '',
|
|
148
|
-
device_platform: device.platform || '',
|
|
149
|
-
device_version: device.sdk || '',
|
|
150
|
-
device_name: device.name,
|
|
151
|
-
trace_id: traceId,
|
|
152
|
-
status: 'running', // Principal Polish: Set status explicitly
|
|
153
|
-
api_key_id: (_a = session.apiKeyId) !== null && _a !== void 0 ? _a : null,
|
|
154
|
-
user_id: (_b = session.userId) !== null && _b !== void 0 ? _b : null,
|
|
155
|
-
};
|
|
152
|
+
const createData = Object.assign(Object.assign({ id: session.getId(), build: build.id ? { connect: { id: build.id } } : undefined, name: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.SESSION_NAME] || undefined }, storedSessionCapabilities(sessionResponse)), { node_id: device.nodeId || '', has_live_video: session.getLiveVideoUrl() !== null, video_recording_enabled: capabilities[XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING] === true, is_profiling_available: is_profiling_available || isIosProfilingStarted, device_info: device_info ? JSON.stringify(device_info) : null, device_udid: device.udid || '', device_platform: device.platform || '', device_version: device.sdk || '', device_name: device.name, trace_id: traceId, status: 'running', api_key_id: (_a = session.apiKeyId) !== null && _a !== void 0 ? _a : null, user_id: (_b = session.userId) !== null && _b !== void 0 ? _b : null });
|
|
156
153
|
yield prisma_1.prisma.session.create({
|
|
157
154
|
data: createData,
|
|
158
155
|
});
|