@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.
- package/lib/package.json +2 -1
- package/lib/public/assets/{ApiKeyGate-BousCIQZ.js → ApiKeyGate-DX4HKDBA.js} +1 -1
- package/lib/public/assets/{BugReportButton-Gw4lnGnV.js → BugReportButton-N-NBBb3l.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-BX2hfhBf.js → DeviceMosaicView-BEDnJqk5.js} +1 -1
- package/lib/public/assets/{EmptyState-CPOfuV3O.js → EmptyState-X7DYPoPj.js} +1 -1
- package/lib/public/assets/{FieldGroup-C5UkCHbe.js → FieldGroup-a9gzUh4W.js} +1 -1
- package/lib/public/assets/{Modal-MMGhL8eW.js → Modal-Cuva-zxU.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CZfAicws.js → SegmentedControl-Bdc4A0UR.js} +1 -1
- package/lib/public/assets/{SettingCard-Dp7XIdcl.js → SettingCard-DVZUpQJR.js} +1 -1
- package/lib/public/assets/{Table-DsN_IX6J.js → Table-Casq-_Hh.js} +1 -1
- package/lib/public/assets/{activity-DCBAvxuo.js → activity-KiLkzk_U.js} +1 -1
- package/lib/public/assets/{ai-settings-yNS_bU1m.js → ai-settings-0dEPftlc.js} +1 -1
- package/lib/public/assets/{api-keys-CQ9g1g1b.js → api-keys-Cg1cbSUt.js} +1 -1
- package/lib/public/assets/{apps-Bqh1eav9.js → apps-CyetRRfI.js} +1 -1
- package/lib/public/assets/{arrow-left-GX0vzYtn.js → arrow-left-C1v7ukDM.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DePHHOby.js → arrow-up-right-B-YmrLSY.js} +1 -1
- package/lib/public/assets/{auth-shell-BBGlC6i3.js → auth-shell-BXwxuIPK.js} +1 -1
- package/lib/public/assets/{builds-page-Czf7RsmD.js → builds-page-wquO6Xb2.js} +1 -1
- package/lib/public/assets/{button-ybNY3GYx.js → button-xZrEx8cE.js} +1 -1
- package/lib/public/assets/{check-BZMx8AhR.js → check-CDOZKQih.js} +1 -1
- package/lib/public/assets/{chevron-right-WL_bcB02.js → chevron-right-CnpP0W_l.js} +1 -1
- package/lib/public/assets/{circle-check-CI2xUlDo.js → circle-check-DDqpikKW.js} +1 -1
- package/lib/public/assets/{circle-x-De3thbcr.js → circle-x-DcBe5J67.js} +1 -1
- package/lib/public/assets/{clock-BFVLE7wW.js → clock-CfNmWqai.js} +1 -1
- package/lib/public/assets/{copy-ve3lNhva.js → copy-DkXT5Ggc.js} +1 -1
- package/lib/public/assets/{device-explorer-vzZxT4-Y.js → device-explorer-B3PK2XW9.js} +1 -1
- package/lib/public/assets/{download-BDZj44ke.js → download-B0tbRUtY.js} +1 -1
- package/lib/public/assets/{forgot-password-BSYQX2oR.js → forgot-password-DvHMwK0-.js} +1 -1
- package/lib/public/assets/{funnel-CptlpFZ9.js → funnel-DfsfiM0o.js} +1 -1
- package/lib/public/assets/{index-PtnKslHU.js → index-wbxPcwMM.js} +2 -2
- package/lib/public/assets/{layers-DRpwoKhI.js → layers-D-4LYQG3.js} +1 -1
- package/lib/public/assets/{lock-CrQhC34I.js → lock-Cf_J2NEl.js} +1 -1
- package/lib/public/assets/{login-DaJOt8Qz.js → login-DCIOMQb8.js} +1 -1
- package/lib/public/assets/{maintenance-settings-D629C2IC.js → maintenance-settings-CDDl4Q8B.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-Dpcc76zN.js → mouse-pointer-2-DoCqhosb.js} +1 -1
- package/lib/public/assets/{overview-CGEKYX62.js → overview-DNeH4FfG.js} +1 -1
- package/lib/public/assets/{page-header-CSvuGbSO.js → page-header-DZpQDeCw.js} +1 -1
- package/lib/public/assets/{plus-vJJF9g79.js → plus-CTjPrkU5.js} +1 -1
- package/lib/public/assets/{profile-page-CF7ZQNQz.js → profile-page-DKiZOb_Y.js} +1 -1
- package/lib/public/assets/{reset-password-BgpjbCac.js → reset-password-6rl6YA7S.js} +1 -1
- package/lib/public/assets/{runbook-page-Dxuzftyv.js → runbook-page-DTTw_cb3.js} +1 -1
- package/lib/public/assets/{selector-detail-page-B_puCd9g.js → selector-detail-page-BVPMp3ZK.js} +1 -1
- package/lib/public/assets/{selector-health-BjMHf106.js → selector-health-BVJ8fGyw.js} +1 -1
- package/lib/public/assets/{selector-health-page-Du_bGD2p.js → selector-health-page-CZ_AAjtV.js} +1 -1
- package/lib/public/assets/{session-detail-page-lQTe5_B1.js → session-detail-page-BApz2wZr.js} +1 -1
- package/lib/public/assets/{settings-DhxWGUzy.js → settings-CuYjTdtb.js} +1 -1
- package/lib/public/assets/{shield-JGzamJo5.js → shield-DVYhrtS6.js} +1 -1
- package/lib/public/assets/{shield-alert-DzE36jcb.js → shield-alert-BhxlmXys.js} +1 -1
- package/lib/public/assets/{status-pill-outline-50aN0wRU.js → status-pill-outline-DjKuXDLG.js} +1 -1
- package/lib/public/assets/{tablet-DHY88mxw.js → tablet-CxgLRRaU.js} +1 -1
- package/lib/public/assets/{teams-EOAIPbZC.js → teams-EuX6i7H2.js} +1 -1
- package/lib/public/assets/{trash-2-BrTSfFh5.js → trash-2-8zA1h40t.js} +1 -1
- package/lib/public/assets/useSocket-qyqK9PWr.js +1 -0
- package/lib/public/assets/{users-Dd44jLAB.js → users-CPafGHcL.js} +1 -1
- package/lib/public/assets/{webhook-settings-DBYmqim_.js → webhook-settings-y8EAM79b.js} +1 -1
- package/lib/public/assets/{zap-JUbOoK3o.js → zap-C-b2-XlO.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/index.js +6 -0
- package/lib/src/app/routers/auth.js +30 -0
- package/lib/src/app/routers/capabilities.js +25 -0
- package/lib/src/app/routers/control.js +14 -0
- package/lib/src/app/routers/projects.js +55 -0
- package/lib/src/device-managers/IOSDeviceManager.js +24 -3
- package/lib/src/device-managers/ios/IOSStreamService.js +39 -1
- package/lib/src/device-managers/ios/iosBattery.js +24 -0
- package/lib/src/device-managers/ios/iosStreamDiagnostics.js +61 -0
- package/lib/src/generated/client/edge.js +22 -4
- package/lib/src/generated/client/index-browser.js +19 -1
- package/lib/src/generated/client/index.d.ts +2376 -158
- package/lib/src/generated/client/index.js +22 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +21 -0
- package/lib/src/generated/client/wasm.js +19 -1
- package/lib/src/middleware/authMiddleware.js +64 -3
- package/lib/src/prisma.js +1 -1
- package/lib/src/services/EventLogService.js +81 -0
- package/lib/src/services/ServerManager.js +36 -0
- package/lib/src/services/SocketServer.js +2 -0
- package/lib/src/services/VideoPipelineService.js +51 -43
- package/lib/src/services/artifacts/ArtifactStore.js +60 -0
- package/lib/src/services/recording/RecordingOrchestrator.js +4 -5
- package/lib/src/services/token/JwtKeyService.js +137 -0
- package/lib/src/services/token/StreamTicketService.js +61 -0
- package/lib/test/unit/ArtifactStore.spec.js +59 -0
- package/lib/test/unit/CapabilitiesRouter.spec.js +18 -0
- package/lib/test/unit/EventLogService.spec.js +67 -0
- package/lib/test/unit/JwtKeyService.spec.js +86 -0
- package/lib/test/unit/ProjectsRouter.spec.js +45 -0
- package/lib/test/unit/StreamTicketService.spec.js +95 -0
- package/lib/test/unit/TokenEndpoint.spec.js +68 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +102 -0
- package/lib/test/unit/authMiddleware.streamTicket.spec.js +217 -0
- package/lib/test/unit/iosBattery.spec.js +29 -0
- package/lib/test/unit/iosStreamDiagnostics.spec.js +61 -0
- package/lib/test/unit/recording-instrumentation.spec.js +8 -0
- package/lib/test/unit/recording-orchestrator.spec.js +14 -0
- package/lib/test/unit/video-pipeline-args.spec.js +46 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260717063103_event_log/migration.sql +15 -0
- package/prisma/migrations/20260717070124_project_entity/migration.sql +10 -0
- package/prisma/schema.prisma +21 -0
- 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
|
+
});
|