@xenon-device-management/xenon 1.7.2 → 1.7.3

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.
Files changed (38) hide show
  1. package/lib/package.json +2 -1
  2. package/lib/src/app/index.js +6 -0
  3. package/lib/src/app/routers/auth.js +30 -0
  4. package/lib/src/app/routers/capabilities.js +25 -0
  5. package/lib/src/app/routers/control.js +14 -0
  6. package/lib/src/app/routers/projects.js +55 -0
  7. package/lib/src/generated/client/edge.js +22 -4
  8. package/lib/src/generated/client/index-browser.js +19 -1
  9. package/lib/src/generated/client/index.d.ts +2376 -158
  10. package/lib/src/generated/client/index.js +22 -4
  11. package/lib/src/generated/client/package.json +1 -1
  12. package/lib/src/generated/client/schema.prisma +21 -0
  13. package/lib/src/generated/client/wasm.js +19 -1
  14. package/lib/src/middleware/authMiddleware.js +64 -3
  15. package/lib/src/prisma.js +1 -1
  16. package/lib/src/services/EventLogService.js +81 -0
  17. package/lib/src/services/ServerManager.js +36 -0
  18. package/lib/src/services/SocketServer.js +2 -0
  19. package/lib/src/services/artifacts/ArtifactStore.js +60 -0
  20. package/lib/src/services/recording/RecordingOrchestrator.js +4 -5
  21. package/lib/src/services/token/JwtKeyService.js +137 -0
  22. package/lib/src/services/token/StreamTicketService.js +61 -0
  23. package/lib/test/unit/ArtifactStore.spec.js +59 -0
  24. package/lib/test/unit/CapabilitiesRouter.spec.js +18 -0
  25. package/lib/test/unit/EventLogService.spec.js +67 -0
  26. package/lib/test/unit/JwtKeyService.spec.js +86 -0
  27. package/lib/test/unit/ProjectsRouter.spec.js +45 -0
  28. package/lib/test/unit/StreamTicketService.spec.js +95 -0
  29. package/lib/test/unit/TokenEndpoint.spec.js +68 -0
  30. package/lib/test/unit/authMiddleware.bearer.spec.js +102 -0
  31. package/lib/test/unit/authMiddleware.streamTicket.spec.js +217 -0
  32. package/lib/test/unit/recording-instrumentation.spec.js +8 -0
  33. package/lib/test/unit/recording-orchestrator.spec.js +14 -0
  34. package/lib/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +2 -1
  36. package/prisma/migrations/20260717063103_event_log/migration.sql +15 -0
  37. package/prisma/migrations/20260717070124_project_entity/migration.sql +10 -0
  38. package/prisma/schema.prisma +21 -0
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
@@ -92,6 +92,7 @@
92
92
  "http-proxy-middleware": "^3.0.0-beta.1",
93
93
  "https-proxy-agent": "7.0.2",
94
94
  "ip": "^1.1.8",
95
+ "jose": "^5.10.0",
95
96
  "jsonschema": "1.4.1",
96
97
  "lodash": "^4.17.21",
97
98
  "lokijs": "^1.5.12",
@@ -77,6 +77,8 @@ const auth_1 = require("./routers/auth");
77
77
  const processes_1 = require("./routers/processes");
78
78
  const profile_1 = require("./routers/profile");
79
79
  const users_1 = require("./routers/users");
80
+ const capabilities_1 = require("./routers/capabilities");
81
+ const projects_1 = require("./routers/projects");
80
82
  const authMiddleware_1 = require("../middleware/authMiddleware");
81
83
  const rateLimitMiddleware_1 = require("../middleware/rateLimitMiddleware");
82
84
  const csrfMiddleware_1 = require("../middleware/csrfMiddleware");
@@ -243,6 +245,10 @@ function createRouter(pluginArgs) {
243
245
  apiRouter.use('/teams', (0, teams_1.teamsRouter)());
244
246
  // Admin: running process snapshot (ops debugging)
245
247
  apiRouter.use('/processes', (0, processes_1.processesRouter)());
248
+ // Feature detection endpoint
249
+ apiRouter.use('/capabilities', (0, capabilities_1.capabilitiesRouter)());
250
+ // Project entity skeleton — container for later runs/flows/secrets (ARB guard #4)
251
+ apiRouter.use('/projects', (0, projects_1.projectsRouter)());
246
252
  // Exposes plugin CLI args (may include host, hub URL, etc.) — auth-gated.
247
253
  apiRouter.get('/cliArgs', (_req, res) => __awaiter(this, void 0, void 0, function* () {
248
254
  res.json(yield (0, pluginArgs_1.getCLIArgs)());
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.issueToken = issueToken;
12
13
  exports.authPublicRouter = authPublicRouter;
13
14
  exports.authAuthedRouter = authAuthedRouter;
14
15
  exports.authRouter = authRouter;
@@ -23,8 +24,24 @@ const EmailService_1 = require("../../services/EmailService");
23
24
  const loginRateLimiter_1 = require("../../middleware/loginRateLimiter");
24
25
  const config_1 = require("../../config");
25
26
  const prisma_1 = require("../../prisma");
27
+ const JwtKeyService_1 = require("../../services/token/JwtKeyService");
26
28
  const SESSION_COOKIE = 'xenon_dashboard_session';
27
29
  const isSecureFromReq = (req) => req.secure || req.headers['x-forwarded-proto'] === 'https';
30
+ const MCP_TTL_SEC = Number(process.env.XENON_MCP_TOKEN_TTL_SEC || 86400);
31
+ const REST_TTL_SEC = 3600;
32
+ const MINTABLE_AUDIENCES = ['xenon-rest', 'xenon-mcp'];
33
+ function issueToken(auth, body) {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ var _a, _b;
36
+ const audience = (_a = body.audience) !== null && _a !== void 0 ? _a : 'xenon-rest';
37
+ if (!MINTABLE_AUDIENCES.includes(audience)) {
38
+ throw new Error(`unsupported audience: ${audience}`);
39
+ }
40
+ const expiresIn = audience === 'xenon-mcp' ? MCP_TTL_SEC : REST_TTL_SEC;
41
+ const token = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).sign({ sub: auth.userId, role: auth.role, scopes: auth.scopes, teamId: (_b = auth.teamId) !== null && _b !== void 0 ? _b : null }, { audience, ttlSeconds: expiresIn });
42
+ return { token, expiresIn, audience };
43
+ });
44
+ }
28
45
  // Public — anonymous-callable. Mounted before authMiddleware.
29
46
  function authPublicRouter() {
30
47
  var _a, _b;
@@ -108,6 +125,9 @@ function authPublicRouter() {
108
125
  if (elapsed < 50)
109
126
  yield new Promise((res2) => setTimeout(res2, 50 - elapsed));
110
127
  }));
128
+ r.get('/jwks.json', (_req, res) => {
129
+ res.json(typedi_1.Container.get(JwtKeyService_1.JwtKeyService).jwks());
130
+ });
111
131
  r.get('/reset-password/check/:token', (req, res) => __awaiter(this, void 0, void 0, function* () {
112
132
  const resetSvc = typedi_1.Container.get(PasswordResetService_1.PasswordResetService);
113
133
  const row = yield resetSvc.verifyToken(req.params.token);
@@ -213,6 +233,16 @@ function authAuthedRouter() {
213
233
  teams,
214
234
  });
215
235
  }));
236
+ r.post('/token', (req, res) => __awaiter(this, void 0, void 0, function* () {
237
+ var _a;
238
+ try {
239
+ const out = yield issueToken(req.auth, (_a = req.body) !== null && _a !== void 0 ? _a : {});
240
+ res.json(out);
241
+ }
242
+ catch (e) {
243
+ res.status(400).json({ error: e.message });
244
+ }
245
+ }));
216
246
  r.post('/dashboard-session', (req, res) => __awaiter(this, void 0, void 0, function* () {
217
247
  const { apiKey } = req.body;
218
248
  if (!apiKey)
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCapabilities = buildCapabilities;
4
+ exports.capabilitiesRouter = capabilitiesRouter;
5
+ const express_1 = require("express");
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ const pkg = require('../../../package.json');
8
+ function buildCapabilities() {
9
+ return {
10
+ version: String(pkg.version),
11
+ features: {
12
+ bearerAuth: true,
13
+ tokenIssuance: true,
14
+ streamTickets: true,
15
+ leases: true, // pre-existing: /sdk/leases
16
+ eventLog: true, // Task 6
17
+ projects: true, // Task 8
18
+ },
19
+ };
20
+ }
21
+ function capabilitiesRouter() {
22
+ const r = (0, express_1.Router)();
23
+ r.get('/', (_req, res) => res.json(buildCapabilities()));
24
+ return r;
25
+ }
@@ -60,6 +60,7 @@ const os_1 = __importDefault(require("os"));
60
60
  const fs_extra_1 = __importDefault(require("fs-extra"));
61
61
  const OmniVisionService_1 = require("../../services/omni-vision/OmniVisionService");
62
62
  const InspectorService_1 = require("../../services/InspectorService");
63
+ const StreamTicketService_1 = require("../../services/token/StreamTicketService");
63
64
  const scopeGuard_1 = require("../../middleware/scopeGuard");
64
65
  const roleGuard_1 = require("../../middleware/roleGuard");
65
66
  const manualLock_1 = require("../../services/recording/manualLock");
@@ -607,6 +608,19 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
607
608
  });
608
609
  }
609
610
  }));
611
+ /**
612
+ * Mint a single-use, udid-bound stream ticket for the webview <img> MJPEG
613
+ * path (spec §2.3/§7.1) — a raw <img src> can carry neither auth headers nor
614
+ * the SameSite=strict dashboard cookie, so it authenticates via ?ticket=.
615
+ */
616
+ router.post('/:udid/stream/ticket', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
617
+ var _a, _b, _c;
618
+ const actorId = (_b = (_a = req.apiKey) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : (_c = req.auth) === null || _c === void 0 ? void 0 : _c.userId;
619
+ if (!actorId)
620
+ return res.status(401).json({ error: 'unauthenticated' });
621
+ const ticket = yield typedi_1.Container.get(StreamTicketService_1.StreamTicketService).mint(req.params.udid, actorId);
622
+ res.json({ ticket, expiresIn: 60 });
623
+ }));
610
624
  /**
611
625
  * Stop stream endpoint
612
626
  */
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.listProjects = listProjects;
13
+ exports.createProject = createProject;
14
+ exports.projectsRouter = projectsRouter;
15
+ const express_1 = require("express");
16
+ const prisma_1 = require("../../prisma");
17
+ const scopeGuard_1 = require("../../middleware/scopeGuard");
18
+ function listProjects(client, teamIds) {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ const where = teamIds ? { where: { OR: [{ teamId: { in: teamIds } }, { teamId: null }] } } : {};
21
+ return client.project.findMany(where);
22
+ });
23
+ }
24
+ function createProject(client, body) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ var _a;
27
+ if (!body.name || !body.name.trim())
28
+ throw new Error('name is required');
29
+ return client.project.create({
30
+ data: { name: body.name.trim(), teamId: ((_a = body.teamId) === null || _a === void 0 ? void 0 : _a.trim()) || null },
31
+ });
32
+ });
33
+ }
34
+ function projectsRouter() {
35
+ const r = (0, express_1.Router)();
36
+ r.get('/', (req, res) => __awaiter(this, void 0, void 0, function* () {
37
+ var _a;
38
+ try {
39
+ res.json(yield listProjects(prisma_1.prisma, (_a = req.auth) === null || _a === void 0 ? void 0 : _a.teamIds));
40
+ }
41
+ catch (e) {
42
+ res.status(500).json({ error: e.message });
43
+ }
44
+ }));
45
+ r.post('/', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
46
+ var _a;
47
+ try {
48
+ res.status(201).json(yield createProject(prisma_1.prisma, (_a = req.body) !== null && _a !== void 0 ? _a : {}));
49
+ }
50
+ catch (e) {
51
+ res.status(400).json({ error: e.message });
52
+ }
53
+ }));
54
+ return r;
55
+ }