@xenon-device-management/xenon 1.7.2 → 1.7.4

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 (103) hide show
  1. package/lib/package.json +2 -1
  2. package/lib/public/assets/{ApiKeyGate-BousCIQZ.js → ApiKeyGate-DX4HKDBA.js} +1 -1
  3. package/lib/public/assets/{BugReportButton-Gw4lnGnV.js → BugReportButton-N-NBBb3l.js} +1 -1
  4. package/lib/public/assets/{DeviceMosaicView-BX2hfhBf.js → DeviceMosaicView-BEDnJqk5.js} +1 -1
  5. package/lib/public/assets/{EmptyState-CPOfuV3O.js → EmptyState-X7DYPoPj.js} +1 -1
  6. package/lib/public/assets/{FieldGroup-C5UkCHbe.js → FieldGroup-a9gzUh4W.js} +1 -1
  7. package/lib/public/assets/{Modal-MMGhL8eW.js → Modal-Cuva-zxU.js} +1 -1
  8. package/lib/public/assets/{SegmentedControl-CZfAicws.js → SegmentedControl-Bdc4A0UR.js} +1 -1
  9. package/lib/public/assets/{SettingCard-Dp7XIdcl.js → SettingCard-DVZUpQJR.js} +1 -1
  10. package/lib/public/assets/{Table-DsN_IX6J.js → Table-Casq-_Hh.js} +1 -1
  11. package/lib/public/assets/{activity-DCBAvxuo.js → activity-KiLkzk_U.js} +1 -1
  12. package/lib/public/assets/{ai-settings-yNS_bU1m.js → ai-settings-0dEPftlc.js} +1 -1
  13. package/lib/public/assets/{api-keys-CQ9g1g1b.js → api-keys-Cg1cbSUt.js} +1 -1
  14. package/lib/public/assets/{apps-Bqh1eav9.js → apps-CyetRRfI.js} +1 -1
  15. package/lib/public/assets/{arrow-left-GX0vzYtn.js → arrow-left-C1v7ukDM.js} +1 -1
  16. package/lib/public/assets/{arrow-up-right-DePHHOby.js → arrow-up-right-B-YmrLSY.js} +1 -1
  17. package/lib/public/assets/{auth-shell-BBGlC6i3.js → auth-shell-BXwxuIPK.js} +1 -1
  18. package/lib/public/assets/{builds-page-Czf7RsmD.js → builds-page-wquO6Xb2.js} +1 -1
  19. package/lib/public/assets/{button-ybNY3GYx.js → button-xZrEx8cE.js} +1 -1
  20. package/lib/public/assets/{check-BZMx8AhR.js → check-CDOZKQih.js} +1 -1
  21. package/lib/public/assets/{chevron-right-WL_bcB02.js → chevron-right-CnpP0W_l.js} +1 -1
  22. package/lib/public/assets/{circle-check-CI2xUlDo.js → circle-check-DDqpikKW.js} +1 -1
  23. package/lib/public/assets/{circle-x-De3thbcr.js → circle-x-DcBe5J67.js} +1 -1
  24. package/lib/public/assets/{clock-BFVLE7wW.js → clock-CfNmWqai.js} +1 -1
  25. package/lib/public/assets/{copy-ve3lNhva.js → copy-DkXT5Ggc.js} +1 -1
  26. package/lib/public/assets/{device-explorer-vzZxT4-Y.js → device-explorer-B3PK2XW9.js} +1 -1
  27. package/lib/public/assets/{download-BDZj44ke.js → download-B0tbRUtY.js} +1 -1
  28. package/lib/public/assets/{forgot-password-BSYQX2oR.js → forgot-password-DvHMwK0-.js} +1 -1
  29. package/lib/public/assets/{funnel-CptlpFZ9.js → funnel-DfsfiM0o.js} +1 -1
  30. package/lib/public/assets/{index-PtnKslHU.js → index-wbxPcwMM.js} +2 -2
  31. package/lib/public/assets/{layers-DRpwoKhI.js → layers-D-4LYQG3.js} +1 -1
  32. package/lib/public/assets/{lock-CrQhC34I.js → lock-Cf_J2NEl.js} +1 -1
  33. package/lib/public/assets/{login-DaJOt8Qz.js → login-DCIOMQb8.js} +1 -1
  34. package/lib/public/assets/{maintenance-settings-D629C2IC.js → maintenance-settings-CDDl4Q8B.js} +1 -1
  35. package/lib/public/assets/{mouse-pointer-2-Dpcc76zN.js → mouse-pointer-2-DoCqhosb.js} +1 -1
  36. package/lib/public/assets/{overview-CGEKYX62.js → overview-DNeH4FfG.js} +1 -1
  37. package/lib/public/assets/{page-header-CSvuGbSO.js → page-header-DZpQDeCw.js} +1 -1
  38. package/lib/public/assets/{plus-vJJF9g79.js → plus-CTjPrkU5.js} +1 -1
  39. package/lib/public/assets/{profile-page-CF7ZQNQz.js → profile-page-DKiZOb_Y.js} +1 -1
  40. package/lib/public/assets/{reset-password-BgpjbCac.js → reset-password-6rl6YA7S.js} +1 -1
  41. package/lib/public/assets/{runbook-page-Dxuzftyv.js → runbook-page-DTTw_cb3.js} +1 -1
  42. package/lib/public/assets/{selector-detail-page-B_puCd9g.js → selector-detail-page-BVPMp3ZK.js} +1 -1
  43. package/lib/public/assets/{selector-health-BjMHf106.js → selector-health-BVJ8fGyw.js} +1 -1
  44. package/lib/public/assets/{selector-health-page-Du_bGD2p.js → selector-health-page-CZ_AAjtV.js} +1 -1
  45. package/lib/public/assets/{session-detail-page-lQTe5_B1.js → session-detail-page-BApz2wZr.js} +1 -1
  46. package/lib/public/assets/{settings-DhxWGUzy.js → settings-CuYjTdtb.js} +1 -1
  47. package/lib/public/assets/{shield-JGzamJo5.js → shield-DVYhrtS6.js} +1 -1
  48. package/lib/public/assets/{shield-alert-DzE36jcb.js → shield-alert-BhxlmXys.js} +1 -1
  49. package/lib/public/assets/{status-pill-outline-50aN0wRU.js → status-pill-outline-DjKuXDLG.js} +1 -1
  50. package/lib/public/assets/{tablet-DHY88mxw.js → tablet-CxgLRRaU.js} +1 -1
  51. package/lib/public/assets/{teams-EOAIPbZC.js → teams-EuX6i7H2.js} +1 -1
  52. package/lib/public/assets/{trash-2-BrTSfFh5.js → trash-2-8zA1h40t.js} +1 -1
  53. package/lib/public/assets/useSocket-qyqK9PWr.js +1 -0
  54. package/lib/public/assets/{users-Dd44jLAB.js → users-CPafGHcL.js} +1 -1
  55. package/lib/public/assets/{webhook-settings-DBYmqim_.js → webhook-settings-y8EAM79b.js} +1 -1
  56. package/lib/public/assets/{zap-JUbOoK3o.js → zap-C-b2-XlO.js} +1 -1
  57. package/lib/public/index.html +1 -1
  58. package/lib/src/app/index.js +6 -0
  59. package/lib/src/app/routers/auth.js +30 -0
  60. package/lib/src/app/routers/capabilities.js +25 -0
  61. package/lib/src/app/routers/control.js +14 -0
  62. package/lib/src/app/routers/projects.js +55 -0
  63. package/lib/src/device-managers/IOSDeviceManager.js +24 -3
  64. package/lib/src/device-managers/ios/IOSStreamService.js +39 -1
  65. package/lib/src/device-managers/ios/iosBattery.js +24 -0
  66. package/lib/src/device-managers/ios/iosStreamDiagnostics.js +61 -0
  67. package/lib/src/generated/client/edge.js +22 -4
  68. package/lib/src/generated/client/index-browser.js +19 -1
  69. package/lib/src/generated/client/index.d.ts +2376 -158
  70. package/lib/src/generated/client/index.js +22 -4
  71. package/lib/src/generated/client/package.json +1 -1
  72. package/lib/src/generated/client/schema.prisma +21 -0
  73. package/lib/src/generated/client/wasm.js +19 -1
  74. package/lib/src/middleware/authMiddleware.js +64 -3
  75. package/lib/src/prisma.js +1 -1
  76. package/lib/src/services/EventLogService.js +81 -0
  77. package/lib/src/services/ServerManager.js +36 -0
  78. package/lib/src/services/SocketServer.js +2 -0
  79. package/lib/src/services/VideoPipelineService.js +51 -43
  80. package/lib/src/services/artifacts/ArtifactStore.js +60 -0
  81. package/lib/src/services/recording/RecordingOrchestrator.js +4 -5
  82. package/lib/src/services/token/JwtKeyService.js +137 -0
  83. package/lib/src/services/token/StreamTicketService.js +61 -0
  84. package/lib/test/unit/ArtifactStore.spec.js +59 -0
  85. package/lib/test/unit/CapabilitiesRouter.spec.js +18 -0
  86. package/lib/test/unit/EventLogService.spec.js +67 -0
  87. package/lib/test/unit/JwtKeyService.spec.js +86 -0
  88. package/lib/test/unit/ProjectsRouter.spec.js +45 -0
  89. package/lib/test/unit/StreamTicketService.spec.js +95 -0
  90. package/lib/test/unit/TokenEndpoint.spec.js +68 -0
  91. package/lib/test/unit/authMiddleware.bearer.spec.js +102 -0
  92. package/lib/test/unit/authMiddleware.streamTicket.spec.js +217 -0
  93. package/lib/test/unit/iosBattery.spec.js +29 -0
  94. package/lib/test/unit/iosStreamDiagnostics.spec.js +61 -0
  95. package/lib/test/unit/recording-instrumentation.spec.js +8 -0
  96. package/lib/test/unit/recording-orchestrator.spec.js +14 -0
  97. package/lib/test/unit/video-pipeline-args.spec.js +46 -0
  98. package/lib/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +2 -1
  100. package/prisma/migrations/20260717063103_event_log/migration.sql +15 -0
  101. package/prisma/migrations/20260717070124_project_entity/migration.sql +10 -0
  102. package/prisma/schema.prisma +21 -0
  103. package/lib/public/assets/useSocket-BRto9a5m.js +0 -1
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ 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;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
42
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
43
+ return new (P || (P = Promise))(function (resolve, reject) {
44
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
45
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
46
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
47
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
48
+ });
49
+ };
50
+ var __importDefault = (this && this.__importDefault) || function (mod) {
51
+ return (mod && mod.__esModule) ? mod : { "default": mod };
52
+ };
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.JwtKeyService = void 0;
55
+ const typedi_1 = require("typedi");
56
+ const jose = __importStar(require("jose"));
57
+ const crypto_1 = require("crypto");
58
+ const fs_1 = __importDefault(require("fs"));
59
+ const path_1 = __importDefault(require("path"));
60
+ const KEY_FILE = 'xenon-jwt-private.pem';
61
+ const ISSUER = process.env.XENON_JWT_ISSUER || 'xenon-hub';
62
+ /**
63
+ * RS256 signing key for hub-issued JWTs (REST/MCP tokens, stream tickets).
64
+ * Key material lives on disk (0600), never in the database. `kid` is derived
65
+ * from the public key so it is stable across restarts; rotation = drop a new
66
+ * PEM in place, old tokens fail verification (acceptable: TTLs are short).
67
+ */
68
+ let JwtKeyService = class JwtKeyService {
69
+ constructor() {
70
+ // Boot degrades gracefully if key material is unavailable (bad perms, full
71
+ // disk, corrupt PEM): the server still comes up, and only the token routes
72
+ // fail. sign()/verify() throw a clear error; jwks() serves an empty set.
73
+ this.initialized = false;
74
+ }
75
+ init(keyDir) {
76
+ return __awaiter(this, void 0, void 0, function* () {
77
+ fs_1.default.mkdirSync(keyDir, { recursive: true });
78
+ const keyPath = path_1.default.join(keyDir, KEY_FILE);
79
+ let pkcs8;
80
+ if (fs_1.default.existsSync(keyPath)) {
81
+ pkcs8 = fs_1.default.readFileSync(keyPath, 'utf8');
82
+ }
83
+ else {
84
+ const { privateKey } = yield jose.generateKeyPair('RS256', { extractable: true });
85
+ pkcs8 = yield jose.exportPKCS8(privateKey);
86
+ fs_1.default.writeFileSync(keyPath, pkcs8, { mode: 0o600 });
87
+ }
88
+ this.privateKey = yield jose.importPKCS8(pkcs8, 'RS256');
89
+ // Public JWK derived from the private key; strip private fields.
90
+ const fullJwk = yield jose.exportJWK(this.privateKey);
91
+ this.publicJwk = { kty: fullJwk.kty, n: fullJwk.n, e: fullJwk.e };
92
+ this.publicKey = yield jose.importJWK(Object.assign(Object.assign({}, this.publicJwk), { alg: 'RS256' }), 'RS256');
93
+ this.kid = (0, crypto_1.createHash)('sha256')
94
+ .update(`${fullJwk.n}.${fullJwk.e}`)
95
+ .digest('base64url')
96
+ .slice(0, 16);
97
+ this.initialized = true;
98
+ });
99
+ }
100
+ sign(claims, opts) {
101
+ return __awaiter(this, void 0, void 0, function* () {
102
+ if (!this.initialized)
103
+ throw new Error('JWT key service not initialized');
104
+ const jwt = new jose.SignJWT(claims)
105
+ .setProtectedHeader({ alg: 'RS256', kid: this.kid })
106
+ .setIssuer(ISSUER)
107
+ .setAudience(opts.audience)
108
+ .setIssuedAt()
109
+ .setExpirationTime(Math.floor(Date.now() / 1000) + opts.ttlSeconds);
110
+ if (opts.jti)
111
+ jwt.setJti(opts.jti);
112
+ return jwt.sign(this.privateKey);
113
+ });
114
+ }
115
+ verify(token, opts) {
116
+ return __awaiter(this, void 0, void 0, function* () {
117
+ if (!this.initialized)
118
+ throw new Error('JWT key service not initialized');
119
+ const { payload } = yield jose.jwtVerify(token, this.publicKey, {
120
+ issuer: ISSUER,
121
+ audience: opts.audience,
122
+ clockTolerance: 60, // spec §7.1: ±60 s skew, mirrors appium-mcp-auth
123
+ });
124
+ return payload;
125
+ });
126
+ }
127
+ jwks() {
128
+ // Empty JWKS when uninitialized — valid JSON, honest about having no keys.
129
+ if (!this.initialized)
130
+ return { keys: [] };
131
+ return { keys: [Object.assign(Object.assign({}, this.publicJwk), { kid: this.kid, use: 'sig', alg: 'RS256' })] };
132
+ }
133
+ };
134
+ exports.JwtKeyService = JwtKeyService;
135
+ exports.JwtKeyService = JwtKeyService = __decorate([
136
+ (0, typedi_1.Service)()
137
+ ], JwtKeyService);
@@ -0,0 +1,61 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.StreamTicketService = void 0;
19
+ const typedi_1 = require("typedi");
20
+ const crypto_1 = require("crypto");
21
+ const JwtKeyService_1 = require("./JwtKeyService");
22
+ const TICKET_TTL_SEC = 60;
23
+ /** Single-use, udid-bound, 60 s tokens for the webview <img> MJPEG path. */
24
+ let StreamTicketService = class StreamTicketService {
25
+ constructor() {
26
+ // jti -> expiry epoch-ms. Pruned on each redeem; bounded by 60 s TTL.
27
+ this.used = new Map();
28
+ }
29
+ mint(udid, actorId) {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ return typedi_1.Container.get(JwtKeyService_1.JwtKeyService).sign({ udid, actorId }, { audience: 'xenon-stream', ttlSeconds: TICKET_TTL_SEC, jti: (0, crypto_1.randomUUID)() });
32
+ });
33
+ }
34
+ redeem(ticket, udid) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ const payload = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(ticket, { audience: 'xenon-stream' });
37
+ if (payload.udid !== udid)
38
+ throw new Error('ticket udid mismatch');
39
+ const jti = String(payload.jti);
40
+ const now = Date.now();
41
+ for (const [k, exp] of this.used)
42
+ if (exp < now)
43
+ this.used.delete(k);
44
+ if (this.used.has(jti))
45
+ throw new Error('ticket already used');
46
+ // Evict the anti-replay entry only once the JWT itself is genuinely
47
+ // unverifiable. JwtKeyService.verify() honors clockTolerance:60, so the
48
+ // token stays acceptable until exp + 60s. Keying eviction off redeem-time
49
+ // + TTL (mint..mint+60) would drop the jti at mint+60 while verify() still
50
+ // accepts a replay up to mint+120 — a ~60s single-use bypass. Key off the
51
+ // token's own exp claim plus the same tolerance so memory outlives it.
52
+ const evictAt = (typeof payload.exp === 'number' ? payload.exp * 1000 : now + TICKET_TTL_SEC * 1000) + 60000; // token exp + clockTolerance (mirrors JwtKeyService.verify)
53
+ this.used.set(jti, evictAt);
54
+ return { actorId: String(payload.actorId) };
55
+ });
56
+ }
57
+ };
58
+ exports.StreamTicketService = StreamTicketService;
59
+ exports.StreamTicketService = StreamTicketService = __decorate([
60
+ (0, typedi_1.Service)()
61
+ ], StreamTicketService);
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const chai_1 = require("chai");
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const os_1 = __importDefault(require("os"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const ArtifactStore_1 = require("../../src/services/artifacts/ArtifactStore");
20
+ describe('FsArtifactStore', () => {
21
+ let root;
22
+ beforeEach(() => {
23
+ root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-art-'));
24
+ });
25
+ afterEach(() => fs_1.default.rmSync(root, { recursive: true, force: true }));
26
+ it('resolve() is byte-identical to legacy path concatenation', () => {
27
+ const store = new ArtifactStore_1.FsArtifactStore(root);
28
+ const legacy = path_1.default.join(root, '_groups', 'g1', 'composite.mp4');
29
+ (0, chai_1.expect)(store.resolve('_groups', 'g1', 'composite.mp4')).to.equal(legacy);
30
+ });
31
+ it('resolve() stays byte-identical (relative, NOT absolutized) under a relative root', () => {
32
+ // config.recordingsAssetsPath is overridable via XENON_RECORDINGS_ASSETS_PATH
33
+ // with no absolute-path validation. Legacy code was path.join, which keeps a
34
+ // relative root relative; path.resolve would silently absolutize it and flip
35
+ // every persisted filePath relative→absolute. Byte-identical means path.join.
36
+ const store = new ArtifactStore_1.FsArtifactStore('rel/root');
37
+ const legacy = path_1.default.join('rel/root', '_groups', 'g1', 'composite.mp4');
38
+ (0, chai_1.expect)(store.resolve('_groups', 'g1', 'composite.mp4')).to.equal(legacy);
39
+ });
40
+ it('resolve() with no segments returns the root and does not throw', () => {
41
+ const store = new ArtifactStore_1.FsArtifactStore(root);
42
+ (0, chai_1.expect)(store.resolve()).to.equal(root);
43
+ });
44
+ it('refuses path traversal outside the root', () => {
45
+ const store = new ArtifactStore_1.FsArtifactStore(root);
46
+ (0, chai_1.expect)(() => store.resolve('..', 'etc', 'passwd')).to.throw(/escapes store root/);
47
+ });
48
+ it('ensureDir creates nested directories and returns the absolute path', () => __awaiter(void 0, void 0, void 0, function* () {
49
+ const store = new ArtifactStore_1.FsArtifactStore(root);
50
+ const dir = yield store.ensureDir('_groups', 'g2');
51
+ (0, chai_1.expect)(fs_1.default.statSync(dir).isDirectory()).to.equal(true);
52
+ }));
53
+ it('exists() reflects the filesystem', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const store = new ArtifactStore_1.FsArtifactStore(root);
55
+ (0, chai_1.expect)(yield store.exists('nope.mp4')).to.equal(false);
56
+ fs_1.default.writeFileSync(path_1.default.join(root, 'yes.mp4'), 'x');
57
+ (0, chai_1.expect)(yield store.exists('yes.mp4')).to.equal(true);
58
+ }));
59
+ });
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const capabilities_1 = require("../../src/app/routers/capabilities");
5
+ describe('capabilities payload', () => {
6
+ it('reports version and the hub feature set', () => {
7
+ const caps = (0, capabilities_1.buildCapabilities)();
8
+ (0, chai_1.expect)(caps.version).to.be.a('string').and.not.equal('');
9
+ (0, chai_1.expect)(caps.features).to.deep.equal({
10
+ bearerAuth: true,
11
+ tokenIssuance: true,
12
+ streamTickets: true,
13
+ leases: true,
14
+ eventLog: true,
15
+ projects: true,
16
+ });
17
+ });
18
+ });
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const chai_1 = require("chai");
16
+ const sinon_1 = __importDefault(require("sinon"));
17
+ const EventLogService_1 = require("../../src/services/EventLogService");
18
+ describe('EventLogService', () => {
19
+ function fakePrisma() {
20
+ return { eventLog: { create: sinon_1.default.stub().resolves({}), deleteMany: sinon_1.default.stub().resolves({ count: 3 }) } };
21
+ }
22
+ it('appendSafe writes type + JSON payload asynchronously', () => __awaiter(void 0, void 0, void 0, function* () {
23
+ const db = fakePrisma();
24
+ const svc = new EventLogService_1.EventLogService({ client: db });
25
+ svc.appendSafe({ type: 'device_blocked', payload: { udid: 'U1' }, teamId: 't1' });
26
+ yield new Promise((r) => setImmediate(r)); // let the fire-and-forget tick run
27
+ (0, chai_1.expect)(db.eventLog.create.calledOnce).to.equal(true);
28
+ const arg = db.eventLog.create.firstCall.args[0].data;
29
+ (0, chai_1.expect)(arg.type).to.equal('device_blocked');
30
+ (0, chai_1.expect)(JSON.parse(arg.payload)).to.deep.equal({ udid: 'U1' });
31
+ }));
32
+ it('appendSafe never throws even when the DB write fails', () => __awaiter(void 0, void 0, void 0, function* () {
33
+ const db = fakePrisma();
34
+ db.eventLog.create.rejects(new Error('disk full'));
35
+ const svc = new EventLogService_1.EventLogService({ client: db });
36
+ (0, chai_1.expect)(() => svc.appendSafe({ type: 'x', payload: {} })).to.not.throw();
37
+ yield new Promise((r) => setImmediate(r));
38
+ }));
39
+ it('appendSafe degrades a circular payload to a placeholder instead of crashing', () => __awaiter(void 0, void 0, void 0, function* () {
40
+ const db = fakePrisma();
41
+ const svc = new EventLogService_1.EventLogService({ client: db });
42
+ const c = {};
43
+ c.self = c;
44
+ (0, chai_1.expect)(() => svc.appendSafe({ type: 'device_blocked', payload: c })).to.not.throw();
45
+ yield new Promise((r) => setImmediate(r));
46
+ (0, chai_1.expect)(db.eventLog.create.calledOnce).to.equal(true);
47
+ const arg = db.eventLog.create.firstCall.args[0].data;
48
+ (0, chai_1.expect)(JSON.parse(arg.payload)).to.deep.equal({ _unserializable: true, type: 'device_blocked' });
49
+ }));
50
+ it('is a no-op when XENON_EVENT_LOG=off', () => __awaiter(void 0, void 0, void 0, function* () {
51
+ process.env.XENON_EVENT_LOG = 'off';
52
+ const db = fakePrisma();
53
+ const svc = new EventLogService_1.EventLogService({ client: db });
54
+ svc.appendSafe({ type: 'x', payload: {} });
55
+ yield new Promise((r) => setImmediate(r));
56
+ (0, chai_1.expect)(db.eventLog.create.called).to.equal(false);
57
+ delete process.env.XENON_EVENT_LOG;
58
+ }));
59
+ it('prune deletes rows older than retention', () => __awaiter(void 0, void 0, void 0, function* () {
60
+ const db = fakePrisma();
61
+ const svc = new EventLogService_1.EventLogService({ client: db });
62
+ const n = yield svc.prune(30);
63
+ (0, chai_1.expect)(n).to.equal(3);
64
+ const where = db.eventLog.deleteMany.firstCall.args[0].where;
65
+ (0, chai_1.expect)(where.occurredAt.lt).to.be.a('date');
66
+ }));
67
+ });
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const chai_1 = require("chai");
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const os_1 = __importDefault(require("os"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
20
+ describe('JwtKeyService', () => {
21
+ let dir;
22
+ let svc;
23
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
24
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-jwt-'));
25
+ svc = new JwtKeyService_1.JwtKeyService();
26
+ yield svc.init(dir);
27
+ }));
28
+ afterEach(() => fs_1.default.rmSync(dir, { recursive: true, force: true }));
29
+ it('generates a private key file with 0600 on first init', () => {
30
+ const keyPath = path_1.default.join(dir, 'xenon-jwt-private.pem');
31
+ (0, chai_1.expect)(fs_1.default.existsSync(keyPath)).to.equal(true);
32
+ const mode = fs_1.default.statSync(keyPath).mode & 0o777;
33
+ (0, chai_1.expect)(mode).to.equal(0o600);
34
+ });
35
+ it('reloads the same key on second init (stable kid)', () => __awaiter(void 0, void 0, void 0, function* () {
36
+ const kid1 = svc.jwks().keys[0].kid;
37
+ const svc2 = new JwtKeyService_1.JwtKeyService();
38
+ yield svc2.init(dir);
39
+ (0, chai_1.expect)(svc2.jwks().keys[0].kid).to.equal(kid1);
40
+ }));
41
+ it('signs and verifies a token round-trip with audience check', () => __awaiter(void 0, void 0, void 0, function* () {
42
+ const token = yield svc.sign({ sub: 'user-1', scopes: 'devices,read' }, { audience: 'xenon-rest', ttlSeconds: 60 });
43
+ const payload = yield svc.verify(token, { audience: 'xenon-rest' });
44
+ (0, chai_1.expect)(payload.sub).to.equal('user-1');
45
+ (0, chai_1.expect)(payload.scopes).to.equal('devices,read');
46
+ }));
47
+ it('rejects wrong audience', () => __awaiter(void 0, void 0, void 0, function* () {
48
+ const token = yield svc.sign({ sub: 'u' }, { audience: 'xenon-mcp', ttlSeconds: 60 });
49
+ try {
50
+ yield svc.verify(token, { audience: 'xenon-rest' });
51
+ chai_1.expect.fail('should have thrown');
52
+ }
53
+ catch (e) {
54
+ (0, chai_1.expect)(String(e.message)).to.match(/aud/i);
55
+ }
56
+ }));
57
+ it('rejects tokens expired beyond the 60s clock tolerance', () => __awaiter(void 0, void 0, void 0, function* () {
58
+ var _a;
59
+ const token = yield svc.sign({ sub: 'u' }, { audience: 'xenon-rest', ttlSeconds: -120 });
60
+ try {
61
+ yield svc.verify(token, { audience: 'xenon-rest' });
62
+ chai_1.expect.fail('should have thrown');
63
+ }
64
+ catch (e) {
65
+ (0, chai_1.expect)(String((_a = e.code) !== null && _a !== void 0 ? _a : e.message)).to.match(/expired|ERR_JWT_EXPIRED/i);
66
+ }
67
+ }));
68
+ it('degrades gracefully when uninitialized: sign() rejects, jwks() is empty', () => __awaiter(void 0, void 0, void 0, function* () {
69
+ const fresh = new JwtKeyService_1.JwtKeyService(); // init() never called
70
+ try {
71
+ yield fresh.sign({ sub: 'u' }, { audience: 'xenon-rest', ttlSeconds: 60 });
72
+ chai_1.expect.fail('should have thrown');
73
+ }
74
+ catch (e) {
75
+ (0, chai_1.expect)(String(e.message)).to.match(/not initialized/);
76
+ }
77
+ (0, chai_1.expect)(fresh.jwks()).to.deep.equal({ keys: [] });
78
+ }));
79
+ it('exposes a JWKS with kid, use=sig, alg=RS256 and no private material', () => {
80
+ const jwk = svc.jwks().keys[0];
81
+ (0, chai_1.expect)(jwk.kid).to.be.a('string');
82
+ (0, chai_1.expect)(jwk.use).to.equal('sig');
83
+ (0, chai_1.expect)(jwk.alg).to.equal('RS256');
84
+ (0, chai_1.expect)(jwk.d).to.equal(undefined); // private exponent must never appear
85
+ });
86
+ });
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ const chai_1 = require("chai");
16
+ const sinon_1 = __importDefault(require("sinon"));
17
+ const projects_1 = require("../../src/app/routers/projects");
18
+ describe('projects handlers', () => {
19
+ function db() {
20
+ return { project: { findMany: sinon_1.default.stub().resolves([{ id: 'p1', name: 'App A', teamId: 't1' }]), create: sinon_1.default.stub().resolves({ id: 'p2', name: 'New', teamId: null }) } };
21
+ }
22
+ it('lists projects scoped to the caller teamIds when narrowed', () => __awaiter(void 0, void 0, void 0, function* () {
23
+ const client = db();
24
+ const out = yield (0, projects_1.listProjects)(client, ['t1']);
25
+ (0, chai_1.expect)(out).to.have.length(1);
26
+ (0, chai_1.expect)(client.project.findMany.firstCall.args[0].where).to.deep.equal({ OR: [{ teamId: { in: ['t1'] } }, { teamId: null }] });
27
+ }));
28
+ it('lists all projects for unscoped (admin) callers', () => __awaiter(void 0, void 0, void 0, function* () {
29
+ const client = db();
30
+ yield (0, projects_1.listProjects)(client, undefined);
31
+ (0, chai_1.expect)(client.project.findMany.firstCall.args[0]).to.deep.equal({});
32
+ }));
33
+ it('creates a project with a required name', () => __awaiter(void 0, void 0, void 0, function* () {
34
+ const client = db();
35
+ const p = yield (0, projects_1.createProject)(client, { name: 'New' });
36
+ (0, chai_1.expect)(p.id).to.equal('p2');
37
+ try {
38
+ yield (0, projects_1.createProject)(client, { name: '' });
39
+ chai_1.expect.fail('should throw');
40
+ }
41
+ catch (e) {
42
+ (0, chai_1.expect)(e.message).to.match(/name/);
43
+ }
44
+ }));
45
+ });
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const sinon_1 = __importDefault(require("sinon"));
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const typedi_1 = require("typedi");
22
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
23
+ const StreamTicketService_1 = require("../../src/services/token/StreamTicketService");
24
+ describe('StreamTicketService', () => {
25
+ let dir;
26
+ let svc;
27
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
28
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-ticket-'));
29
+ const keys = new JwtKeyService_1.JwtKeyService();
30
+ yield keys.init(dir);
31
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
32
+ svc = new StreamTicketService_1.StreamTicketService();
33
+ }));
34
+ afterEach(() => {
35
+ typedi_1.Container.reset();
36
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
37
+ });
38
+ it('mints and redeems a ticket once for the bound udid', () => __awaiter(void 0, void 0, void 0, function* () {
39
+ const t = yield svc.mint('UDID-1', 'actor-1');
40
+ const out = yield svc.redeem(t, 'UDID-1');
41
+ (0, chai_1.expect)(out.actorId).to.equal('actor-1');
42
+ }));
43
+ it('rejects redemption for a different udid', () => __awaiter(void 0, void 0, void 0, function* () {
44
+ const t = yield svc.mint('UDID-1', 'actor-1');
45
+ try {
46
+ yield svc.redeem(t, 'UDID-2');
47
+ chai_1.expect.fail('should throw');
48
+ }
49
+ catch (e) {
50
+ (0, chai_1.expect)(e.message).to.match(/udid/);
51
+ }
52
+ }));
53
+ it('rejects second redemption (single-use)', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const t = yield svc.mint('UDID-1', 'actor-1');
55
+ yield svc.redeem(t, 'UDID-1');
56
+ try {
57
+ yield svc.redeem(t, 'UDID-1');
58
+ chai_1.expect.fail('should throw');
59
+ }
60
+ catch (e) {
61
+ (0, chai_1.expect)(e.message).to.match(/used/);
62
+ }
63
+ }));
64
+ // Regression for the replay window: JwtKeyService.verify honors
65
+ // clockTolerance:60, so a 60s ticket stays *verifiable* until mint+120s.
66
+ // If the anti-replay entry evicts at redeem-time+TTL (mint+60s) instead of
67
+ // token-exp+tolerance (mint+120s), a replay at ~mint+90s slips through:
68
+ // verify() still accepts it, the prune loop has already dropped the jti, and
69
+ // `used.has(jti)` is false → REPLAY ACCEPTED. This test advances the clock
70
+ // ~90s (past TTL, still within exp+tolerance) and asserts single-use holds.
71
+ // It fails under redeem-time+TTL eviction and passes under exp+tolerance.
72
+ it('rejects a replay after TTL but within verify clock tolerance (advanced clock)', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ const clock = sinon_1.default.useFakeTimers({ now: 0, toFake: ['Date'] });
74
+ let err;
75
+ try {
76
+ const t = yield svc.mint('UDID-1', 'actor-1');
77
+ yield svc.redeem(t, 'UDID-1'); // legitimate first use at ~t=0
78
+ clock.tick(90000); // t≈90s: past 60s TTL, still < mint+120s verify window
79
+ try {
80
+ yield svc.redeem(t, 'UDID-1');
81
+ }
82
+ catch (e) {
83
+ err = e;
84
+ }
85
+ }
86
+ finally {
87
+ clock.restore();
88
+ }
89
+ // Assert OUTSIDE the try so a wrongly-accepted replay (err === undefined)
90
+ // fails loudly instead of being swallowed. Note: the failure message here
91
+ // must NOT contain the word matched below, or a vacuous pass sneaks back in.
92
+ (0, chai_1.expect)(err, 'replay was accepted — single-use bypassed').to.be.an('error');
93
+ (0, chai_1.expect)(String(err && err.message)).to.match(/already used/);
94
+ }));
95
+ });
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ require("reflect-metadata");
16
+ const chai_1 = require("chai");
17
+ const typedi_1 = require("typedi");
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
22
+ const auth_1 = require("../../src/app/routers/auth");
23
+ describe('POST /auth/token handler (issueToken)', () => {
24
+ let dir;
25
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
26
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-tok-'));
27
+ const svc = new JwtKeyService_1.JwtKeyService();
28
+ yield svc.init(dir);
29
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, svc);
30
+ }));
31
+ afterEach(() => {
32
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
33
+ typedi_1.Container.reset();
34
+ });
35
+ const auth = {
36
+ kind: 'api-key',
37
+ userId: 'u1',
38
+ role: 'MEMBER',
39
+ scopes: 'devices,read',
40
+ teamId: 't1',
41
+ rateLimit: 300,
42
+ };
43
+ it('mints a xenon-rest token by default with 3600s TTL', () => __awaiter(void 0, void 0, void 0, function* () {
44
+ const out = yield (0, auth_1.issueToken)(auth, {});
45
+ (0, chai_1.expect)(out.audience).to.equal('xenon-rest');
46
+ (0, chai_1.expect)(out.expiresIn).to.equal(3600);
47
+ const payload = yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(out.token, {
48
+ audience: 'xenon-rest',
49
+ });
50
+ (0, chai_1.expect)(payload.sub).to.equal('u1');
51
+ (0, chai_1.expect)(payload.scopes).to.equal('devices,read');
52
+ (0, chai_1.expect)(payload.teamId).to.equal('t1');
53
+ }));
54
+ it('mints a xenon-mcp token with 86400s TTL', () => __awaiter(void 0, void 0, void 0, function* () {
55
+ const out = yield (0, auth_1.issueToken)(auth, { audience: 'xenon-mcp' });
56
+ (0, chai_1.expect)(out.expiresIn).to.equal(86400);
57
+ yield typedi_1.Container.get(JwtKeyService_1.JwtKeyService).verify(out.token, { audience: 'xenon-mcp' });
58
+ }));
59
+ it('rejects unknown audiences', () => __awaiter(void 0, void 0, void 0, function* () {
60
+ try {
61
+ yield (0, auth_1.issueToken)(auth, { audience: 'xenon-stream' }); // tickets are not minted here
62
+ chai_1.expect.fail('should have thrown');
63
+ }
64
+ catch (e) {
65
+ (0, chai_1.expect)(e.message).to.match(/audience/);
66
+ }
67
+ }));
68
+ });