@xenon-device-management/xenon 1.3.0 → 1.4.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 +17 -0
- package/lib/package.json +1 -1
- package/lib/public/assets/{Layouts-D6IPfwoe.js → Layouts-BV_fQEKa.js} +1 -1
- package/lib/public/assets/{ai-settings-CflyFKan.js → ai-settings-BnoeJplt.js} +2 -2
- package/lib/public/assets/api-keys-TLWTJYrE.js +1 -0
- package/lib/public/assets/{apps-Da4dvQ1J.js → apps-BGCcB8_p.js} +2 -2
- package/lib/public/assets/arrow-left-B6-SH02F.js +6 -0
- package/lib/public/assets/{badge-BNR9umdu.js → badge-BtQKmQsj.js} +1 -1
- package/lib/public/assets/button-CNLKazSa.js +16 -0
- package/lib/public/assets/{calendar-fehdBtun.js → calendar-CPNkf0zl.js} +1 -1
- package/lib/public/assets/{clock-DrpxSvCL.js → clock-C8ZaLIQA.js} +1 -1
- package/lib/public/assets/copy-DCAmOyY9.js +11 -0
- package/lib/public/assets/{cpu-tuyMVZ4I.js → cpu-D3PzrmhN.js} +1 -1
- package/lib/public/assets/{device-explorer-DOfRH3zm.js → device-explorer-BfFe3bRe.js} +36 -41
- package/lib/public/assets/{index-BaTiUCeH.js → index-Do3u3FMx.js} +38 -28
- package/lib/public/assets/{lock-C6CoqSr2.js → lock-DNzAbx6Q.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CM2oC7-i.js → maintenance-settings-yHAKnfz_.js} +2 -2
- package/lib/public/assets/{mouse-pointer-2-CXdnjXIg.js → mouse-pointer-2-B0KBsfLG.js} +1 -1
- package/lib/public/assets/{plus-B4B1Hukt.js → plus-DecbehZh.js} +1 -1
- package/lib/public/assets/{session-dashboard-B5OPMTz5.js → session-dashboard-CJvDLc6D.js} +17 -22
- package/lib/public/assets/settings-DjTQTjSq.js +1 -0
- package/lib/public/assets/shield-alert-Br1Ebl1G.js +6 -0
- package/lib/public/assets/teams-LILlQka3.js +1 -0
- package/lib/public/assets/{trash-2-NJMZJ2Ol.js → trash-2-DEafUZlN.js} +1 -1
- package/lib/public/assets/{useSocket-Ct2wo7P2.js → useSocket-Dzv_TTsO.js} +1 -1
- package/lib/public/assets/{webhook-settings-Cz35-QJ7.js → webhook-settings-CVwvtwI7.js} +1 -1
- package/lib/public/assets/{zap-CssSMAN5.js → zap-DCsIYdwr.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/XenonCapabilityManager.js +30 -0
- package/lib/src/app/index.js +3 -0
- package/lib/src/app/routers/apikeys.js +5 -2
- package/lib/src/app/routers/grid.js +46 -2
- package/lib/src/app/routers/teams.js +66 -0
- package/lib/src/dashboard/event-manager.js +2 -0
- package/lib/src/data-service/device-store.js +12 -0
- package/lib/src/data-service/prisma-store.js +7 -0
- package/lib/src/device-utils.js +7 -1
- package/lib/src/generated/client/edge.js +18 -7
- package/lib/src/generated/client/index-browser.js +15 -4
- package/lib/src/generated/client/index.d.ts +2613 -391
- package/lib/src/generated/client/index.js +18 -7
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +18 -0
- package/lib/src/generated/client/wasm.js +15 -4
- package/lib/src/logger.js +17 -0
- package/lib/src/middleware/apiKeyMiddleware.js +22 -2
- package/lib/src/services/ApiKeyService.js +20 -1
- package/lib/src/services/ServerManager.js +10 -3
- package/lib/src/services/SessionLifecycleService.js +53 -4
- package/lib/src/services/TeamService.js +100 -0
- package/lib/src/sessions/XenonSession.js +3 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260424033547_add_teams/migration.sql +101 -0
- package/prisma/schema.prisma +18 -0
- package/lib/public/assets/button-hZFV1ypT.js +0 -26
- package/lib/public/assets/settings-BTHP7fj3.js +0 -1
- /package/lib/public/assets/{Layouts-DPMls9vh.css → settings-DPMls9vh.css} +0 -0
|
@@ -14,6 +14,7 @@ const typedi_1 = require("typedi");
|
|
|
14
14
|
const ApiKeyService_1 = require("../services/ApiKeyService");
|
|
15
15
|
const config_1 = require("../config");
|
|
16
16
|
const SESSION_COOKIE = 'xenon_dashboard_session';
|
|
17
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
17
18
|
function readSessionCookie(req) {
|
|
18
19
|
// cookie-parser isn't mounted globally, so parse the header inline.
|
|
19
20
|
const header = req.headers.cookie;
|
|
@@ -31,11 +32,14 @@ function readSessionCookie(req) {
|
|
|
31
32
|
}
|
|
32
33
|
function apiKeyMiddleware(req, res, next) {
|
|
33
34
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
var _a;
|
|
34
36
|
if (config_1.config.authDisabled === true) {
|
|
35
37
|
req.apiKey = { id: 'auth-disabled', scopes: 'admin', rateLimit: 100000 };
|
|
36
38
|
return next();
|
|
37
39
|
}
|
|
38
|
-
const
|
|
40
|
+
const headerKey = req.headers['x-xenon-api-key'];
|
|
41
|
+
const cookieKey = headerKey ? undefined : readSessionCookie(req);
|
|
42
|
+
const raw = headerKey || cookieKey;
|
|
39
43
|
if (!raw) {
|
|
40
44
|
return res.status(401).json({ error: 'missing API key' });
|
|
41
45
|
}
|
|
@@ -43,7 +47,23 @@ function apiKeyMiddleware(req, res, next) {
|
|
|
43
47
|
if (!row) {
|
|
44
48
|
return res.status(401).json({ error: 'invalid or revoked API key' });
|
|
45
49
|
}
|
|
46
|
-
|
|
50
|
+
// Slide the dashboard session forward on every authenticated hit so active
|
|
51
|
+
// users don't get kicked out 24h after their original login.
|
|
52
|
+
if (cookieKey) {
|
|
53
|
+
const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https';
|
|
54
|
+
res.cookie(SESSION_COOKIE, raw, {
|
|
55
|
+
httpOnly: true,
|
|
56
|
+
secure: isSecure,
|
|
57
|
+
sameSite: 'strict',
|
|
58
|
+
maxAge: SESSION_TTL_MS,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
req.apiKey = {
|
|
62
|
+
id: row.id,
|
|
63
|
+
scopes: row.scopes,
|
|
64
|
+
rateLimit: row.rateLimit,
|
|
65
|
+
teamId: (_a = row.teamId) !== null && _a !== void 0 ? _a : null,
|
|
66
|
+
};
|
|
47
67
|
next();
|
|
48
68
|
});
|
|
49
69
|
}
|
|
@@ -77,7 +77,7 @@ let ApiKeyService = class ApiKeyService {
|
|
|
77
77
|
}
|
|
78
78
|
create(params) {
|
|
79
79
|
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
-
var _a;
|
|
80
|
+
var _a, _b;
|
|
81
81
|
const raw = this.generateRaw();
|
|
82
82
|
const row = yield prisma_1.prisma.apiKey.create({
|
|
83
83
|
data: {
|
|
@@ -85,6 +85,7 @@ let ApiKeyService = class ApiKeyService {
|
|
|
85
85
|
keyHash: this.hash(raw),
|
|
86
86
|
scopes: params.scopes.join(','),
|
|
87
87
|
rateLimit: (_a = params.rateLimit) !== null && _a !== void 0 ? _a : 300,
|
|
88
|
+
teamId: (_b = params.teamId) !== null && _b !== void 0 ? _b : null,
|
|
88
89
|
},
|
|
89
90
|
});
|
|
90
91
|
return { id: row.id, raw };
|
|
@@ -95,6 +96,24 @@ let ApiKeyService = class ApiKeyService {
|
|
|
95
96
|
yield prisma_1.prisma.apiKey.update({ where: { id }, data: { revokedAt: new Date() } });
|
|
96
97
|
});
|
|
97
98
|
}
|
|
99
|
+
list() {
|
|
100
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
return prisma_1.prisma.apiKey.findMany({
|
|
102
|
+
where: { revokedAt: null },
|
|
103
|
+
select: {
|
|
104
|
+
id: true,
|
|
105
|
+
name: true,
|
|
106
|
+
scopes: true,
|
|
107
|
+
rateLimit: true,
|
|
108
|
+
createdAt: true,
|
|
109
|
+
lastUsedAt: true,
|
|
110
|
+
teamId: true,
|
|
111
|
+
role: true,
|
|
112
|
+
},
|
|
113
|
+
orderBy: { createdAt: 'desc' },
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
98
117
|
};
|
|
99
118
|
exports.ApiKeyService = ApiKeyService;
|
|
100
119
|
exports.ApiKeyService = ApiKeyService = __decorate([
|
|
@@ -97,7 +97,7 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
97
97
|
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
98
98
|
context.setContext(pluginArgs, cliArgs.port, nodeId, cliArgs.basePath || '');
|
|
99
99
|
yield this.syncDatabaseAndAIConfig(pluginArgs);
|
|
100
|
-
yield this.initializeCoreSubsystems(pluginArgs);
|
|
100
|
+
yield this.initializeCoreSubsystems(pluginArgs, cliArgs.port);
|
|
101
101
|
this.registerRoutes(expressApp, cliArgs, pluginArgs);
|
|
102
102
|
yield this.bootEmulators(pluginArgs);
|
|
103
103
|
this.registerDependenciesInContainer(pluginArgs, cliArgs, nodeId);
|
|
@@ -194,7 +194,7 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
194
194
|
}
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
|
-
initializeCoreSubsystems(pluginArgs) {
|
|
197
|
+
initializeCoreSubsystems(pluginArgs, port) {
|
|
198
198
|
return __awaiter(this, void 0, void 0, function* () {
|
|
199
199
|
yield (0, device_utils_1.initializeStorage)();
|
|
200
200
|
const { runMigrations } = yield Promise.resolve().then(() => __importStar(require('../scripts/run-migrations')));
|
|
@@ -202,7 +202,14 @@ let ServerManager = ServerManager_1 = class ServerManager {
|
|
|
202
202
|
yield device_store_1.DeviceStoreFactory.getStore().clearStorage();
|
|
203
203
|
const { ApiKeyService } = yield Promise.resolve().then(() => __importStar(require('./ApiKeyService')));
|
|
204
204
|
const { config: xenonConfig } = yield Promise.resolve().then(() => __importStar(require('../config')));
|
|
205
|
-
yield typedi_1.Container.get(ApiKeyService).bootstrapIfEmpty(xenonConfig.bootstrapKeyPath);
|
|
205
|
+
const raw = yield typedi_1.Container.get(ApiKeyService).bootstrapIfEmpty(xenonConfig.bootstrapKeyPath);
|
|
206
|
+
if (raw) {
|
|
207
|
+
logger_1.default.bootstrapKeyBanner({
|
|
208
|
+
dashboardUrl: `http://${pluginArgs.bindHostOrIp}:${port}/xenon/`,
|
|
209
|
+
key: raw,
|
|
210
|
+
keyFilePath: xenonConfig.bootstrapKeyPath,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
206
213
|
});
|
|
207
214
|
}
|
|
208
215
|
registerRoutes(expressApp, cliArgs, pluginArgs) {
|
|
@@ -81,6 +81,7 @@ const session_service_1 = require("../dashboard/services/session-service");
|
|
|
81
81
|
const SessionStatus_1 = require("../types/SessionStatus");
|
|
82
82
|
const SessionType_1 = __importDefault(require("../enums/SessionType"));
|
|
83
83
|
const async_lock_1 = __importDefault(require("async-lock"));
|
|
84
|
+
const base_driver_1 = require("@appium/base-driver");
|
|
84
85
|
const commandsQueueGuard = new async_lock_1.default();
|
|
85
86
|
// Serializes concurrent deleteSession cleanup for the same sessionId so
|
|
86
87
|
// that onSessionStopped events don't fire twice if two clients race.
|
|
@@ -99,6 +100,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
99
100
|
this.logger.warn('Rejecting new session: hub is draining for shutdown');
|
|
100
101
|
throw new Error('Hub is shutting down; please retry against a different node');
|
|
101
102
|
}
|
|
103
|
+
const authResult = yield this.authorizeSessionRequest(caps);
|
|
102
104
|
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
103
105
|
const pluginArgs = context.pluginArgs;
|
|
104
106
|
this.logger.debug(`📱 pluginArgs: ${JSON.stringify(pluginArgs)}`);
|
|
@@ -145,7 +147,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
145
147
|
this.logger.debug(`📱 Acquiring lock: ${lockName}`);
|
|
146
148
|
const device = yield commandsQueueGuard.acquire(lockName, () => __awaiter(this, void 0, void 0, function* () {
|
|
147
149
|
try {
|
|
148
|
-
return yield (0, device_utils_1.allocateDeviceForSession)(caps, pluginArgs.deviceAvailabilityTimeoutMs, pluginArgs.deviceAvailabilityQueryIntervalMs, pluginArgs);
|
|
150
|
+
return yield (0, device_utils_1.allocateDeviceForSession)(caps, pluginArgs.deviceAvailabilityTimeoutMs, pluginArgs.deviceAvailabilityQueryIntervalMs, pluginArgs, authResult.scoped ? authResult.callerTeamId : undefined);
|
|
149
151
|
}
|
|
150
152
|
catch (err) {
|
|
151
153
|
yield (0, pending_sessions_service_1.removePendingSession)(pendingSessionId);
|
|
@@ -171,7 +173,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
171
173
|
this.logger.debug(`📱 Removing pending session with capability_id: ${pendingSessionId}`);
|
|
172
174
|
yield (0, pending_sessions_service_1.removePendingSession)(pendingSessionId);
|
|
173
175
|
if (this.isCreateSessionResponseInternal(session)) {
|
|
174
|
-
yield this.finalizeSession(session, device, caps, driver, isRemoteOrCloudSession);
|
|
176
|
+
yield this.finalizeSession(session, device, caps, driver, isRemoteOrCloudSession, authResult.apiKeyId);
|
|
175
177
|
}
|
|
176
178
|
else {
|
|
177
179
|
yield this.handleSessionFailure(session, device, isRemoteOrCloudSession);
|
|
@@ -179,6 +181,52 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
179
181
|
return session;
|
|
180
182
|
});
|
|
181
183
|
}
|
|
184
|
+
// Verify a WebDriver session request against an API key supplied via the
|
|
185
|
+
// `xenon:accessKey` capability. Today this is best-effort: if no cap is
|
|
186
|
+
// supplied we log a warning and let the request through to avoid breaking
|
|
187
|
+
// pre-existing test clients. A supplied key that is invalid / revoked / has
|
|
188
|
+
// insufficient scope is rejected. Respects the global `authDisabled` flag.
|
|
189
|
+
//
|
|
190
|
+
// Returns the caller's { apiKeyId, callerTeamId } so allocation can filter
|
|
191
|
+
// devices by team. `apiKeyId` is null when auth is disabled or no key was
|
|
192
|
+
// presented (back-compat path); `callerTeamId` is null when the caller has
|
|
193
|
+
// no team (sees the shared pool only).
|
|
194
|
+
authorizeSessionRequest(caps) {
|
|
195
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
196
|
+
var _a;
|
|
197
|
+
const { config: xenonConfig } = yield Promise.resolve().then(() => __importStar(require('../config')));
|
|
198
|
+
// `scoped` tells the allocator whether to restrict device candidates by
|
|
199
|
+
// team. False = see everything (admin, authDisabled, back-compat path);
|
|
200
|
+
// true = filter to { null, callerTeamId } where callerTeamId may itself
|
|
201
|
+
// be null (meaning "shared pool only").
|
|
202
|
+
if (xenonConfig.authDisabled === true) {
|
|
203
|
+
return { apiKeyId: null, callerTeamId: null, scoped: false };
|
|
204
|
+
}
|
|
205
|
+
const raw = (0, XenonCapabilityManager_1.extractAccessKeyCap)(caps);
|
|
206
|
+
if (!raw) {
|
|
207
|
+
this.logger.warn('Session created without xenon:accessKey capability. Add `xenon:accessKey` to your Appium capabilities to authenticate this session.');
|
|
208
|
+
return { apiKeyId: null, callerTeamId: null, scoped: false };
|
|
209
|
+
}
|
|
210
|
+
const { ApiKeyService } = yield Promise.resolve().then(() => __importStar(require('./ApiKeyService')));
|
|
211
|
+
const svc = typedi_1.Container.get(ApiKeyService);
|
|
212
|
+
const row = yield svc.verify(raw);
|
|
213
|
+
if (!row || !svc.hasScope(row, ['sessions'])) {
|
|
214
|
+
this.logger.error('Rejecting session: xenon:accessKey is invalid, revoked, or lacks the `sessions` scope');
|
|
215
|
+
throw new base_driver_1.errors.InvalidArgumentError('xenon:accessKey is invalid, revoked, or lacks the `sessions` scope');
|
|
216
|
+
}
|
|
217
|
+
const isAdmin = svc.hasScope(row, ['admin']);
|
|
218
|
+
const callerTeamId = (_a = row.teamId) !== null && _a !== void 0 ? _a : null;
|
|
219
|
+
const requestedTeam = (0, XenonCapabilityManager_1.extractTeamCap)(caps);
|
|
220
|
+
if (requestedTeam) {
|
|
221
|
+
if (!isAdmin && requestedTeam !== callerTeamId) {
|
|
222
|
+
this.logger.error(`Rejecting session: xenon:team=${requestedTeam} but caller key is bound to team ${callerTeamId !== null && callerTeamId !== void 0 ? callerTeamId : '(none)'}`);
|
|
223
|
+
throw new base_driver_1.errors.InvalidArgumentError(`xenon:team '${requestedTeam}' is not allowed for this API key`);
|
|
224
|
+
}
|
|
225
|
+
return { apiKeyId: row.id, callerTeamId: requestedTeam, scoped: !isAdmin };
|
|
226
|
+
}
|
|
227
|
+
return { apiKeyId: row.id, callerTeamId, scoped: !isAdmin };
|
|
228
|
+
});
|
|
229
|
+
}
|
|
182
230
|
handleLocalWDAProvisioning(device, caps) {
|
|
183
231
|
return __awaiter(this, void 0, void 0, function* () {
|
|
184
232
|
if (device.platform === 'ios' && device.realDevice) {
|
|
@@ -252,8 +300,8 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
252
300
|
delete caps.firstMatch[0]['appium:mjpegServerPort'];
|
|
253
301
|
}
|
|
254
302
|
}
|
|
255
|
-
finalizeSession(
|
|
256
|
-
return __awaiter(this,
|
|
303
|
+
finalizeSession(session_1, device_1, caps_1, driver_1, isRemote_1) {
|
|
304
|
+
return __awaiter(this, arguments, void 0, function* (session, device, caps, driver, isRemote, apiKeyId = null) {
|
|
257
305
|
var _a;
|
|
258
306
|
const sessionId = session.value[0];
|
|
259
307
|
const sessionResponse = session.value[1];
|
|
@@ -284,6 +332,7 @@ let SessionLifecycleService = class SessionLifecycleService {
|
|
|
284
332
|
}
|
|
285
333
|
const freshDevice = yield this.getFreshDevice(device);
|
|
286
334
|
const sessionInstance = this.createSessionInstance(sessionId, freshDevice, sessionResponse, xenonCapabilities, driver);
|
|
335
|
+
sessionInstance.apiKeyId = apiKeyId;
|
|
287
336
|
yield this.applyPostSessionLogic(sessionInstance, xenonCapabilities, freshDevice);
|
|
288
337
|
});
|
|
289
338
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.TeamService = void 0;
|
|
22
|
+
const typedi_1 = require("typedi");
|
|
23
|
+
const prisma_1 = require("../prisma");
|
|
24
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
25
|
+
let TeamService = class TeamService {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.log = logger_1.default.scope('Team');
|
|
28
|
+
}
|
|
29
|
+
list() {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
const rows = yield prisma_1.prisma.team.findMany({
|
|
32
|
+
orderBy: { createdAt: 'asc' },
|
|
33
|
+
include: {
|
|
34
|
+
_count: { select: { devices: true, apiKeys: true } },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return rows.map((t) => ({
|
|
38
|
+
id: t.id,
|
|
39
|
+
name: t.name,
|
|
40
|
+
createdAt: t.createdAt,
|
|
41
|
+
deviceCount: t._count.devices,
|
|
42
|
+
memberCount: t._count.apiKeys,
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
create(name) {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const trimmed = name.trim();
|
|
49
|
+
if (!trimmed)
|
|
50
|
+
throw new Error('name required');
|
|
51
|
+
return prisma_1.prisma.team.create({ data: { name: trimmed } });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
delete(id) {
|
|
55
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
const [devices, apiKeys] = yield Promise.all([
|
|
57
|
+
prisma_1.prisma.device.count({ where: { teamId: id } }),
|
|
58
|
+
prisma_1.prisma.apiKey.count({ where: { teamId: id, revokedAt: null } }),
|
|
59
|
+
]);
|
|
60
|
+
if (devices > 0 || apiKeys > 0) {
|
|
61
|
+
throw new Error(`Team still has ${devices} device(s) and ${apiKeys} active member(s). Reassign them before deleting.`);
|
|
62
|
+
}
|
|
63
|
+
// Clear the teamId on revoked keys so the FK doesn't block the delete.
|
|
64
|
+
yield prisma_1.prisma.apiKey.updateMany({ where: { teamId: id }, data: { teamId: null } });
|
|
65
|
+
yield prisma_1.prisma.team.delete({ where: { id } });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
listMembers(teamId) {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
+
return prisma_1.prisma.apiKey.findMany({
|
|
71
|
+
where: { teamId, revokedAt: null },
|
|
72
|
+
select: { id: true, name: true, role: true, scopes: true, createdAt: true },
|
|
73
|
+
orderBy: { createdAt: 'asc' },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
addMember(teamId, apiKeyId, role) {
|
|
78
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
if (role !== 'admin' && role !== 'member')
|
|
80
|
+
throw new Error('invalid role');
|
|
81
|
+
yield prisma_1.prisma.apiKey.update({
|
|
82
|
+
where: { id: apiKeyId },
|
|
83
|
+
data: { teamId, role },
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
removeMember(teamId, apiKeyId) {
|
|
88
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
89
|
+
// Clear the team link rather than delete the key — scopes still apply.
|
|
90
|
+
yield prisma_1.prisma.apiKey.updateMany({
|
|
91
|
+
where: { id: apiKeyId, teamId },
|
|
92
|
+
data: { teamId: null, role: 'member' },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
exports.TeamService = TeamService;
|
|
98
|
+
exports.TeamService = TeamService = __decorate([
|
|
99
|
+
(0, typedi_1.Service)()
|
|
100
|
+
], TeamService);
|
|
@@ -22,6 +22,9 @@ class XenonSession {
|
|
|
22
22
|
this.options = options;
|
|
23
23
|
this.isStopping = false;
|
|
24
24
|
this.healthState = SessionHealthState.HEALTHY;
|
|
25
|
+
// Phase 2 audit: which API key created this session (null when auth is
|
|
26
|
+
// disabled or the client didn't present xenon:accessKey).
|
|
27
|
+
this.apiKeyId = null;
|
|
25
28
|
this.sessionId = options.sessionId;
|
|
26
29
|
this.xenonOption = options.xenonOption;
|
|
27
30
|
}
|
package/lib/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/auth.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/processes.ts","../src/app/routers/reservation.ts","../src/app/routers/webhook.ts","../src/auth/nodeSecret.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/index.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/apiKeyMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/nodeSecretMiddleware.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EventBus.ts","../src/services/InspectorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TracingService.ts","../src/services/VideoPipelineService.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/cliArgs.js","../test/integration/testHelpers.js","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/OrphanSweeper.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/apiKeyMiddleware.test.ts","../test/unit/commands.spec.js","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/helpers.spec.ts","../test/unit/nodeSecretMiddleware.test.ts","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js"],"version":"5.9.3"}
|
|
1
|
+
{"root":["../src/InternalHttpClient.ts","../src/PluginContext.ts","../src/XenonCapabilityManager.ts","../src/chromeUtils.ts","../src/config.ts","../src/device-utils.ts","../src/index.ts","../src/logger.ts","../src/plugin.ts","../src/prisma.ts","../src/app/index.ts","../src/app/swagger-docs.ts","../src/app/swagger.ts","../src/app/routers/apikeys.ts","../src/app/routers/apps.ts","../src/app/routers/auth.ts","../src/app/routers/config.ts","../src/app/routers/control.ts","../src/app/routers/dashboard.ts","../src/app/routers/grid.ts","../src/app/routers/processes.ts","../src/app/routers/reservation.ts","../src/app/routers/teams.ts","../src/app/routers/webhook.ts","../src/auth/nodeSecret.ts","../src/commands/handle.ts","../src/commands/index.ts","../src/dashboard/asset-manager.ts","../src/dashboard/commands.ts","../src/dashboard/event-manager.ts","../src/dashboard/services/app-service.ts","../src/dashboard/services/failure-analysis-service.ts","../src/dashboard/services/session-service.ts","../src/data-service/CircuitBreaker.ts","../src/data-service/config-service.ts","../src/data-service/db.ts","../src/data-service/device-service.ts","../src/data-service/device-store.interface.ts","../src/data-service/device-store.ts","../src/data-service/pending-sessions-service.ts","../src/data-service/pluginArgs.ts","../src/data-service/prisma-service.ts","../src/data-service/prisma-store.ts","../src/data-service/queue-service.ts","../src/data-service/web-config-service.ts","../src/device-managers/AndroidDeviceManager.ts","../src/device-managers/ChromeDriverManager.ts","../src/device-managers/HealthMonitorService.ts","../src/device-managers/IOSDeviceManager.ts","../src/device-managers/NodeDevices.ts","../src/device-managers/iOSTracker.ts","../src/device-managers/index.ts","../src/device-managers/android/AndroidStreamService.ts","../src/device-managers/android/DeviceLockManager.ts","../src/device-managers/cloud/CapabilityManager.ts","../src/device-managers/cloud/Devices.ts","../src/device-managers/ios/IOSDiscoveryService.ts","../src/device-managers/ios/IOSStreamService.ts","../src/device-managers/ios/WDAClient.ts","../src/enums/Capabilities.ts","../src/enums/Cloud.ts","../src/enums/Platform.ts","../src/enums/SessionType.ts","../src/enums/SocketEvents.ts","../src/generated/client/default.d.ts","../src/generated/client/default.js","../src/generated/client/edge.d.ts","../src/generated/client/edge.js","../src/generated/client/index-browser.js","../src/generated/client/index.d.ts","../src/generated/client/index.js","../src/generated/client/wasm.d.ts","../src/generated/client/wasm.js","../src/generated/client/runtime/edge-esm.js","../src/generated/client/runtime/edge.js","../src/generated/client/runtime/index-browser.d.ts","../src/generated/client/runtime/index-browser.js","../src/generated/client/runtime/library.d.ts","../src/generated/client/runtime/library.js","../src/generated/client/runtime/react-native.js","../src/generated/client/runtime/wasm.js","../src/helpers/UniversalMjpegProxy.ts","../src/helpers/index.ts","../src/interceptors/CommandInterceptor.ts","../src/interfaces/ICloudManager.ts","../src/interfaces/IDevice.ts","../src/interfaces/IDeviceFilterOptions.ts","../src/interfaces/IDeviceManager.ts","../src/interfaces/IOptions.ts","../src/interfaces/IPluginArgs.ts","../src/interfaces/ISessionCapability.ts","../src/logging/sessionContext.ts","../src/middleware/apiKeyMiddleware.ts","../src/middleware/csrfMiddleware.ts","../src/middleware/nodeSecretMiddleware.ts","../src/middleware/rateLimitMiddleware.ts","../src/middleware/scopeGuard.ts","../src/profiling/AndroidAppProfiler.ts","../src/proxy/wd-command-proxy.ts","../src/scripts/generate-database-migration.ts","../src/scripts/initialize-database.ts","../src/scripts/install-go-ios.ts","../src/scripts/prepare-prisma.ts","../src/scripts/run-migrations.ts","../src/services/AICommandService.ts","../src/services/AIService.ts","../src/services/ApiKeyService.ts","../src/services/CircuitBreaker.ts","../src/services/CleanupService.ts","../src/services/DeviceReconciler.ts","../src/services/EventBus.ts","../src/services/InspectorService.ts","../src/services/MetricsService.ts","../src/services/NetworkConditioningService.ts","../src/services/NotificationService.ts","../src/services/OrphanSweeper.ts","../src/services/PortAllocator.ts","../src/services/ProcessMetricsService.ts","../src/services/ProcessRegistry.ts","../src/services/RequestLogService.ts","../src/services/ResourceIsolationService.ts","../src/services/SecurityService.ts","../src/services/ServerManager.ts","../src/services/SessionHeartbeatService.ts","../src/services/SessionLifecycleService.ts","../src/services/ShutdownCoordinator.ts","../src/services/SocketClient.ts","../src/services/SocketServer.ts","../src/services/TeamService.ts","../src/services/TracingService.ts","../src/services/VideoPipelineService.ts","../src/services/healing/FuzzyXmlHealingProvider.ts","../src/services/healing/HealEtalonService.ts","../src/services/healing/HealedLocatorGenerator.ts","../src/services/healing/HealingMetrics.ts","../src/services/healing/HealingOrchestrator.ts","../src/services/healing/LlmHealingProvider.ts","../src/services/healing/OcrHealingProvider.ts","../src/services/healing/ResilioTreeHealingProvider.ts","../src/services/healing/VisualAiHealingProvider.ts","../src/services/healing/types.ts","../src/services/omni-vision/OmniVisionService.ts","../src/services/omni-vision/VisionAssertionService.ts","../src/sessions/CloudSession.ts","../src/sessions/LocalSession.ts","../src/sessions/RemoteSession.ts","../src/sessions/SessionManager.ts","../src/sessions/XenonSession.ts","../src/types/CLIArgs.ts","../src/types/CloudArgs.ts","../src/types/CloudSchema.ts","../src/types/DeviceType.ts","../src/types/DeviceUpdate.ts","../src/types/IOSDevice.ts","../src/types/Platform.ts","../src/types/SessionStatus.ts","../src/types/express.d.ts","../src/types/healing.d.ts","../src/validators/CapabilityValidator.ts","../test/e2e/e2ehelper.ts","../test/e2e/plugin-harness.ts","../test/e2e/plugin.spec.ts","../test/e2e/startup-verification.spec.ts","../test/e2e/telemetry_verification.spec.ts","../test/e2e/video-recording-test.spec.js","../test/e2e/android/conf.spec.js","../test/e2e/android/conf2.spec.js","../test/e2e/android/conf3.spec.js","../test/e2e/hubnode/forward-request.spec.ts","../test/e2e/hubnode/hubnode.spec.ts","../test/e2e/ios/conf1.spec.js","../test/e2e/ios/conf2.spec.js","../test/helpers/test-container.ts","../test/integration/androidDevices.spec.ts","../test/integration/cliArgs.js","../test/integration/testHelpers.js","../test/integration/ios/01iOSSimulator.spec.ts","../test/integration/ios/02iOSDevices.spec.ts","../test/unit/AndroidDeviceManager.spec.ts","../test/unit/ApiKeyService.test.ts","../test/unit/ChromeDriverManager.spec.js","../test/unit/CleanupService.spec.ts","../test/unit/DeviceModel.spec.ts","../test/unit/FuzzyXmlHealingProvider.test.ts","../test/unit/GetAdbOriginal.js","../test/unit/HealingCascade.test.ts","../test/unit/IOSDeviceManager.spec.js","../test/unit/OrphanSweeper.test.ts","../test/unit/PortAllocator.test.ts","../test/unit/ProcessRegistry.test.ts","../test/unit/RemoteIOs.spec.js","../test/unit/ResilioTreeHealingProvider.test.ts","../test/unit/SessionHeartbeatWrite.test.ts","../test/unit/apiKeyMiddleware.test.ts","../test/unit/commands.spec.js","../test/unit/config.spec.js","../test/unit/device-service.spec.ts","../test/unit/device-utils.spec.ts","../test/unit/helpers.spec.ts","../test/unit/nodeSecretMiddleware.test.ts","../test/unit/omni-vision.spec.ts","../test/unit/plugin.spec.ts","../test/unit/prisma.spec.ts","../test/unit/rateLimitMiddleware.test.ts","../test/unit/fixtures/device.config.js","../test/unit/fixtures/devices.js"],"version":"5.9.3"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "Session" ADD COLUMN "api_key_id" TEXT;
|
|
3
|
+
|
|
4
|
+
-- CreateTable
|
|
5
|
+
CREATE TABLE "Team" (
|
|
6
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
7
|
+
"name" TEXT NOT NULL,
|
|
8
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- RedefineTables
|
|
12
|
+
PRAGMA defer_foreign_keys=ON;
|
|
13
|
+
PRAGMA foreign_keys=OFF;
|
|
14
|
+
CREATE TABLE "new_ApiKey" (
|
|
15
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
16
|
+
"name" TEXT NOT NULL,
|
|
17
|
+
"keyHash" TEXT NOT NULL,
|
|
18
|
+
"scopes" TEXT NOT NULL,
|
|
19
|
+
"rateLimit" INTEGER NOT NULL DEFAULT 300,
|
|
20
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
21
|
+
"revokedAt" DATETIME,
|
|
22
|
+
"lastUsedAt" DATETIME,
|
|
23
|
+
"teamId" TEXT,
|
|
24
|
+
"role" TEXT NOT NULL DEFAULT 'member',
|
|
25
|
+
CONSTRAINT "ApiKey_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
26
|
+
);
|
|
27
|
+
INSERT INTO "new_ApiKey" ("createdAt", "id", "keyHash", "lastUsedAt", "name", "rateLimit", "revokedAt", "scopes") SELECT "createdAt", "id", "keyHash", "lastUsedAt", "name", "rateLimit", "revokedAt", "scopes" FROM "ApiKey";
|
|
28
|
+
DROP TABLE "ApiKey";
|
|
29
|
+
ALTER TABLE "new_ApiKey" RENAME TO "ApiKey";
|
|
30
|
+
CREATE UNIQUE INDEX "ApiKey_keyHash_key" ON "ApiKey"("keyHash");
|
|
31
|
+
CREATE INDEX "ApiKey_teamId_idx" ON "ApiKey"("teamId");
|
|
32
|
+
CREATE TABLE "new_Device" (
|
|
33
|
+
"udid" TEXT NOT NULL,
|
|
34
|
+
"host" TEXT NOT NULL,
|
|
35
|
+
"systemPort" INTEGER,
|
|
36
|
+
"proxyPort" INTEGER,
|
|
37
|
+
"proxyHost" TEXT,
|
|
38
|
+
"wdaLocalPort" INTEGER,
|
|
39
|
+
"name" TEXT DEFAULT 'unknown',
|
|
40
|
+
"state" TEXT DEFAULT 'available',
|
|
41
|
+
"sdk" TEXT DEFAULT 'unknown',
|
|
42
|
+
"platform" TEXT DEFAULT 'unknown',
|
|
43
|
+
"deviceType" TEXT DEFAULT 'real',
|
|
44
|
+
"busy" BOOLEAN DEFAULT false,
|
|
45
|
+
"userBlocked" BOOLEAN DEFAULT false,
|
|
46
|
+
"realDevice" BOOLEAN DEFAULT true,
|
|
47
|
+
"session_id" TEXT,
|
|
48
|
+
"offline" BOOLEAN DEFAULT false,
|
|
49
|
+
"mjpegServerPort" INTEGER,
|
|
50
|
+
"lastCmdExecutedAt" REAL,
|
|
51
|
+
"totalUtilizationTimeMilliSec" REAL NOT NULL DEFAULT 0,
|
|
52
|
+
"sessionStartTime" REAL NOT NULL DEFAULT 0,
|
|
53
|
+
"newCommandTimeout" INTEGER,
|
|
54
|
+
"cloud" TEXT,
|
|
55
|
+
"derivedDataPath" TEXT,
|
|
56
|
+
"chromeDriverPath" TEXT,
|
|
57
|
+
"capability" TEXT,
|
|
58
|
+
"adbRemoteHost" TEXT,
|
|
59
|
+
"adbPort" INTEGER,
|
|
60
|
+
"nodeId" TEXT,
|
|
61
|
+
"screenWidth" TEXT,
|
|
62
|
+
"screenHeight" TEXT,
|
|
63
|
+
"dashboard_link" TEXT,
|
|
64
|
+
"total_session_count" INTEGER DEFAULT 0,
|
|
65
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
66
|
+
"updatedAt" DATETIME NOT NULL,
|
|
67
|
+
"healthCheckError" TEXT,
|
|
68
|
+
"healthStatus" TEXT DEFAULT 'Healthy',
|
|
69
|
+
"lastHealthCheckAt" REAL,
|
|
70
|
+
"batteryLevel" INTEGER,
|
|
71
|
+
"reservationReason" TEXT,
|
|
72
|
+
"reservedBy" TEXT,
|
|
73
|
+
"reservedUntil" REAL,
|
|
74
|
+
"storageFree" TEXT,
|
|
75
|
+
"tags" TEXT,
|
|
76
|
+
"thermalStatus" TEXT,
|
|
77
|
+
"sessionProgress" TEXT DEFAULT '',
|
|
78
|
+
"totalHealedCount" INTEGER DEFAULT 0,
|
|
79
|
+
"ip" TEXT DEFAULT '',
|
|
80
|
+
"cpuArchitecture" TEXT,
|
|
81
|
+
"owning_session_id" TEXT,
|
|
82
|
+
"locked_at" REAL,
|
|
83
|
+
"teamId" TEXT,
|
|
84
|
+
|
|
85
|
+
PRIMARY KEY ("udid", "host"),
|
|
86
|
+
CONSTRAINT "Device_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
|
87
|
+
);
|
|
88
|
+
INSERT INTO "new_Device" ("adbPort", "adbRemoteHost", "batteryLevel", "busy", "capability", "chromeDriverPath", "cloud", "cpuArchitecture", "createdAt", "dashboard_link", "derivedDataPath", "deviceType", "healthCheckError", "healthStatus", "host", "ip", "lastCmdExecutedAt", "lastHealthCheckAt", "locked_at", "mjpegServerPort", "name", "newCommandTimeout", "nodeId", "offline", "owning_session_id", "platform", "proxyHost", "proxyPort", "realDevice", "reservationReason", "reservedBy", "reservedUntil", "screenHeight", "screenWidth", "sdk", "sessionProgress", "sessionStartTime", "session_id", "state", "storageFree", "systemPort", "tags", "thermalStatus", "totalHealedCount", "totalUtilizationTimeMilliSec", "total_session_count", "udid", "updatedAt", "userBlocked", "wdaLocalPort") SELECT "adbPort", "adbRemoteHost", "batteryLevel", "busy", "capability", "chromeDriverPath", "cloud", "cpuArchitecture", "createdAt", "dashboard_link", "derivedDataPath", "deviceType", "healthCheckError", "healthStatus", "host", "ip", "lastCmdExecutedAt", "lastHealthCheckAt", "locked_at", "mjpegServerPort", "name", "newCommandTimeout", "nodeId", "offline", "owning_session_id", "platform", "proxyHost", "proxyPort", "realDevice", "reservationReason", "reservedBy", "reservedUntil", "screenHeight", "screenWidth", "sdk", "sessionProgress", "sessionStartTime", "session_id", "state", "storageFree", "systemPort", "tags", "thermalStatus", "totalHealedCount", "totalUtilizationTimeMilliSec", "total_session_count", "udid", "updatedAt", "userBlocked", "wdaLocalPort" FROM "Device";
|
|
89
|
+
DROP TABLE "Device";
|
|
90
|
+
ALTER TABLE "new_Device" RENAME TO "Device";
|
|
91
|
+
CREATE INDEX "Device_owning_session_id_idx" ON "Device"("owning_session_id");
|
|
92
|
+
CREATE INDEX "Device_teamId_idx" ON "Device"("teamId");
|
|
93
|
+
PRAGMA foreign_keys=ON;
|
|
94
|
+
PRAGMA defer_foreign_keys=OFF;
|
|
95
|
+
|
|
96
|
+
-- CreateIndex
|
|
97
|
+
CREATE UNIQUE INDEX "Team_name_key" ON "Team"("name");
|
|
98
|
+
|
|
99
|
+
-- CreateIndex
|
|
100
|
+
CREATE INDEX "Session_api_key_id_idx" ON "Session"("api_key_id");
|
|
101
|
+
|
package/prisma/schema.prisma
CHANGED
|
@@ -47,6 +47,7 @@ model Session {
|
|
|
47
47
|
last_heartbeat_at DateTime?
|
|
48
48
|
heartbeat_pid Int?
|
|
49
49
|
heartbeat_host String?
|
|
50
|
+
api_key_id String?
|
|
50
51
|
Log Log[]
|
|
51
52
|
Profiling Profiling[]
|
|
52
53
|
build Build? @relation(fields: [build_id], references: [id])
|
|
@@ -56,6 +57,7 @@ model Session {
|
|
|
56
57
|
@@index([build_id])
|
|
57
58
|
@@index([device_udid])
|
|
58
59
|
@@index([device_platform, status])
|
|
60
|
+
@@index([api_key_id])
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
model SessionLog {
|
|
@@ -175,9 +177,12 @@ model Device {
|
|
|
175
177
|
cpuArchitecture String?
|
|
176
178
|
owningSessionId String? @map("owning_session_id")
|
|
177
179
|
lockedAt Float? @map("locked_at")
|
|
180
|
+
teamId String?
|
|
181
|
+
team Team? @relation(fields: [teamId], references: [id])
|
|
178
182
|
|
|
179
183
|
@@id([udid, host])
|
|
180
184
|
@@index([owningSessionId])
|
|
185
|
+
@@index([teamId])
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
model PendingSession {
|
|
@@ -245,4 +250,17 @@ model ApiKey {
|
|
|
245
250
|
createdAt DateTime @default(now())
|
|
246
251
|
revokedAt DateTime?
|
|
247
252
|
lastUsedAt DateTime?
|
|
253
|
+
teamId String?
|
|
254
|
+
role String @default("member")
|
|
255
|
+
team Team? @relation(fields: [teamId], references: [id])
|
|
256
|
+
|
|
257
|
+
@@index([teamId])
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
model Team {
|
|
261
|
+
id String @id @default(uuid())
|
|
262
|
+
name String @unique
|
|
263
|
+
createdAt DateTime @default(now())
|
|
264
|
+
devices Device[]
|
|
265
|
+
apiKeys ApiKey[]
|
|
248
266
|
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import{c as t,r as c,j as i}from"./index-BaTiUCeH.js";import{c as r,a as u}from"./badge-BNR9umdu.js";/**
|
|
2
|
-
* @license lucide-react v0.555.0 - ISC
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the ISC license.
|
|
5
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
-
*/const d=[["path",{d:"M12 6.528V3a1 1 0 0 1 1-1h0",key:"11qiee"}],["path",{d:"M18.237 21A15 15 0 0 0 22 11a6 6 0 0 0-10-4.472A6 6 0 0 0 2 11a15.1 15.1 0 0 0 3.763 10 3 3 0 0 0 3.648.648 5.5 5.5 0 0 1 5.178 0A3 3 0 0 0 18.237 21",key:"110c12"}]],f=t("apple",d);/**
|
|
7
|
-
* @license lucide-react v0.555.0 - ISC
|
|
8
|
-
*
|
|
9
|
-
* This source code is licensed under the ISC license.
|
|
10
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
-
*/const p=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],x=t("check",p);/**
|
|
12
|
-
* @license lucide-react v0.555.0 - ISC
|
|
13
|
-
*
|
|
14
|
-
* This source code is licensed under the ISC license.
|
|
15
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
16
|
-
*/const l=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],_=t("copy",l);/**
|
|
17
|
-
* @license lucide-react v0.555.0 - ISC
|
|
18
|
-
*
|
|
19
|
-
* This source code is licensed under the ISC license.
|
|
20
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
21
|
-
*/const h=[["path",{d:"M12 19h8",key:"baeox8"}],["path",{d:"m4 17 6-6-6-6",key:"1yngyt"}]],g=t("terminal",h);/**
|
|
22
|
-
* @license lucide-react v0.555.0 - ISC
|
|
23
|
-
*
|
|
24
|
-
* This source code is licensed under the ISC license.
|
|
25
|
-
* See the LICENSE file in the root directory of this source tree.
|
|
26
|
-
*/const y=[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]],M=t("upload",y),b=u("button-base",{variants:{variant:{default:"button-variant-default",outline:"button-variant-outline",ghost:"button-variant-ghost",link:"button-variant-link",destructive:"button-variant-destructive"},size:{default:"button-default",sm:"button-sm",lg:"button-lg",icon:"button-icon"}},defaultVariants:{variant:"default",size:"default"}}),k=c.forwardRef(({className:a,variant:o,size:n,...e},s)=>i.jsx("button",{className:r(b({variant:o,size:n,className:a})),ref:s,...e}));k.displayName="Button";export{f as A,k as B,x as C,g as T,M as U,_ as a};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as i,j as e,g as j,l as S,B as C,i as y,T as N}from"./index-BaTiUCeH.js";import{X as c}from"./index-C1DBaoSh.js";import{A as b}from"./Layouts-D6IPfwoe.js";import{C as k}from"./clock-DrpxSvCL.js";import{C as I}from"./calendar-fehdBtun.js";import{M}from"./mouse-pointer-2-CXdnjXIg.js";const B=()=>{const[a,t]=i.useState({healthCheckIntervalMs:3e4,healthCheckSchedule:"",enableSelfHealing:!0}),[g,d]=i.useState(!0),[m,h]=i.useState(!1),[l,n]=i.useState(null);i.useEffect(()=>{o()},[]);const o=async()=>{d(!0);try{const s=await c.getGlobalConfig();t({healthCheckIntervalMs:s.healthCheckIntervalMs||3e4,healthCheckSchedule:s.healthCheckSchedule||"",enableSelfHealing:s.enableSelfHealing!==void 0?s.enableSelfHealing:!0})}catch(s){console.error("Failed to load settings",s),n({type:"error",message:"Failed to access infrastructure parameters."})}finally{d(!1)}},u=async(s=a)=>{h(!0),n(null);try{await c.updateGlobalConfig(s),n({type:"success",message:"Infrastructure parameters synchronized across fleet."}),setTimeout(()=>n(null),5e3)}catch(r){console.error("Failed to save settings",r),n({type:"error",message:"Synchronization failed. Check network integrity."})}finally{h(!1)}},v=async()=>{const s={healthCheckIntervalMs:3e4,healthCheckSchedule:"",enableSelfHealing:!0};t(s),await u(s);try{await c.resetMetrics()}catch(r){console.error("Failed to reset metrics",r)}},p=[{label:"Battery Saver (2 AM)",value:"0 2 * * *"},{label:"Standard (Hourly)",value:"0 * * * *"},{label:"Operational Coverage (30m)",value:"*/30 * * * *"},{label:"High Performance (10m)",value:"*/10 * * * *"},{label:"Disable Schedule",value:""}],f=s=>s?s.split(" ").filter(x=>x!=="").length!==5?"Invalid Cron format (needs 5 parts)":s==="0 2 * * *"?"Quiet hours: Daily at 2:00 AM":s==="0 * * * *"?"Standard rotation: Start of every hour":s==="*/30 * * * *"?"Balanced: Every 30 minutes":s==="*/10 * * * *"?"Intensive: Every 10 minutes":`Custom orchestration: ${s}`:"Using Idle Health Frequency (Continuous/Passive mode).";return g?e.jsxs("div",{className:"settings-loading",children:[e.jsx(j,{className:"animate-spin",size:32}),e.jsx("span",{children:"Synchronizing Global Infrastructure..."})]}):e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsx("div",{className:"scanline",style:{position:"absolute",inset:0,pointerEvents:"none",opacity:.05,zIndex:1001}}),e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{className:"settings-title-group",children:[e.jsx(S,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:"Infrastructure Control"})]}),e.jsx("p",{className:"settings-subtitle",children:"Manage core farm parameters, heartbeat frequency, and maintenance orchestrations across the global registry."})]}),e.jsxs("div",{className:"settings-content",children:[e.jsxs("div",{className:"settings-grid",children:[e.jsxs("div",{className:"setting-card stagger-1",children:[e.jsxs("div",{className:"setting-card-header",children:[e.jsx(k,{size:16}),e.jsx("h4",{children:"Idle Health Frequency"})]}),e.jsx("p",{className:"section-description-dense",children:"Frequency of passive health pings when the system is in idle state."}),e.jsx("div",{className:"setting-field",children:e.jsxs("div",{className:"input-group",children:[e.jsx("input",{type:"number",value:a.healthCheckIntervalMs,onChange:s=>t({...a,healthCheckIntervalMs:parseInt(s.target.value)}),min:5e3,step:5e3}),e.jsx("span",{className:"code-font",children:"MS"})]})}),e.jsx("div",{className:"setting-hint-clean",children:"Minimum safe value: 5000ms. Note: This frequency is overridden when a schedule is active."})]}),e.jsxs("div",{className:"setting-card stagger-2",children:[e.jsxs("div",{className:"setting-card-header",children:[e.jsx(I,{size:16}),e.jsx("h4",{children:"Deep Diagnostic Schedule"})]}),e.jsx("p",{className:"section-description-dense",children:"Execute intensive reliability bursts (WDA restarts, Cache purges) using standardized Cron syntax."}),e.jsx("div",{className:"setting-field",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{type:"text",placeholder:"e.g. 0 * * * * (At internal min 0)",value:a.healthCheckSchedule,onChange:s=>t({...a,healthCheckSchedule:s.target.value})})})}),e.jsxs("div",{className:"cron-preview",children:[e.jsx("span",{className:"preview-label",children:"Active Logic:"}),e.jsx("span",{className:"preview-value",children:f(a.healthCheckSchedule)})]}),e.jsxs("div",{className:"cron-presets",children:[e.jsxs("div",{className:"presets-label",children:[e.jsx(M,{size:12}),e.jsx("span",{children:"Intent-Based Presets:"})]}),e.jsx("div",{className:"presets-grid",children:p.map(s=>e.jsx("button",{className:`preset-chip ${a.healthCheckSchedule===s.value?"active":""}`,onClick:()=>t({...a,healthCheckSchedule:s.value}),children:s.label},s.label))})]})]}),e.jsxs("div",{className:"setting-card stagger-3",children:[e.jsxs("div",{className:"setting-card-header",children:[e.jsx(C,{size:16}),e.jsx("h4",{children:"AI Self-Healing"})]}),e.jsx("p",{className:"section-description-dense",children:"Automatically intercept and recover from failing locators using Xenon's 5-tier strategy."}),e.jsxs("div",{className:"toggle-group",children:[e.jsxs("label",{className:"switch",children:[e.jsx("input",{type:"checkbox",checked:a.enableSelfHealing,onChange:s=>t({...a,enableSelfHealing:s.target.checked})}),e.jsx("span",{className:"slider round"})]}),e.jsx("span",{className:"toggle-label",children:a.enableSelfHealing?"ENABLED":"DISABLED"})]}),e.jsx("div",{className:"setting-hint-clean",children:"When enabled, Xenon will attempt to find elements via Fuzzy XML, OCR, Visual AI, and LLM before failing a test."})]})]}),l&&e.jsxs("div",{className:`status-banner ${l.type}`,children:[l.type==="success"?e.jsx(y,{size:18}):e.jsx(N,{size:18}),e.jsx("span",{children:l.message})]})]}),e.jsx(b,{onSave:u,onDiscard:o,onRestoreDefaults:v,isSaving:m,saveLabel:"Save Configuration"})]})};export{B as Settings};
|
|
File without changes
|