@xenon-device-management/xenon 2.15.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -38,6 +38,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
return result;
|
|
39
39
|
};
|
|
40
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
|
+
};
|
|
41
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
51
|
exports.TracingService = void 0;
|
|
43
52
|
const api_1 = require("@opentelemetry/api");
|
|
@@ -53,15 +62,32 @@ const resources_1 = require("@opentelemetry/resources");
|
|
|
53
62
|
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
54
63
|
const typedi_1 = require("typedi");
|
|
55
64
|
const logger_1 = __importStar(require("../logger"));
|
|
65
|
+
/** Why a session's span ended, on the span. */
|
|
66
|
+
const SESSION_STOP_REASON = 'xenon.session.stop_reason';
|
|
67
|
+
function failSpan(span, error) {
|
|
68
|
+
var _a;
|
|
69
|
+
const exception = error instanceof Error ? error : { message: String((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error) };
|
|
70
|
+
span.recordException(exception);
|
|
71
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: exception.message });
|
|
72
|
+
}
|
|
56
73
|
let TracingService = class TracingService {
|
|
57
74
|
constructor() {
|
|
58
75
|
this.sdk = null;
|
|
76
|
+
// Built here only while tracing is off; with it on, the NodeSDK owns them.
|
|
59
77
|
this.loggerProvider = null;
|
|
60
78
|
this.meterProvider = null;
|
|
79
|
+
// Whether Xenon's own log export and metric export are configured.
|
|
80
|
+
this.logsOn = false;
|
|
81
|
+
this.metricsOn = false;
|
|
61
82
|
this.tracer = api_1.trace.getTracer('xenon-core');
|
|
62
83
|
this.activeSpans = new Map();
|
|
84
|
+
// Which of activeSpans are sessions' (keyed by session id) rather than
|
|
85
|
+
// commands' (`<sessionId>:<command>`).
|
|
86
|
+
this.sessionSpanIds = new Set();
|
|
87
|
+
this.spanProcessorsInUse = [];
|
|
63
88
|
}
|
|
64
89
|
initialize(opts = { isHub: true }) {
|
|
90
|
+
var _a;
|
|
65
91
|
if (process.env.OTEL_SDK_DISABLED === 'true') {
|
|
66
92
|
logger_1.default.info('[TracingService] OTEL_SDK_DISABLED=true — OTel disabled.');
|
|
67
93
|
return;
|
|
@@ -71,14 +97,56 @@ let TracingService = class TracingService {
|
|
|
71
97
|
[semantic_conventions_1.ATTR_SERVICE_NAME]: 'xenon',
|
|
72
98
|
service: role,
|
|
73
99
|
});
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
100
|
+
const spanProcessors = this.spanProcessors();
|
|
101
|
+
const logRecordProcessors = this.logRecordProcessors();
|
|
102
|
+
const metricReaders = this.metricReaders();
|
|
103
|
+
if (spanProcessors.length > 0) {
|
|
104
|
+
// A NodeSDK that isn't handed log processors and metric readers builds
|
|
105
|
+
// its own from the standard OTEL_* variables (OTLP exporters at
|
|
106
|
+
// OTEL_EXPORTER_OTLP_ENDPOINT + /v1/logs and /v1/metrics, which is
|
|
107
|
+
// Xenon's trace URL) and registers them globally before Xenon could, so
|
|
108
|
+
// Xenon's settings for logs and metrics did nothing while tracing was
|
|
109
|
+
// on. It gets Xenon's, or none. With none for logs it still registers
|
|
110
|
+
// a logger provider, one that exports nothing.
|
|
111
|
+
this.spanProcessorsInUse = spanProcessors;
|
|
112
|
+
this.sdk = new sdk_node_1.NodeSDK({
|
|
113
|
+
resource,
|
|
114
|
+
spanProcessors,
|
|
115
|
+
logRecordProcessors,
|
|
116
|
+
metricReaders,
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
this.sdk.start();
|
|
120
|
+
logger_1.default.info('[TracingService] Trace SDK started.');
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logger_1.default.error(`[TracingService] Failed to start trace SDK: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
if (logRecordProcessors.length > 0) {
|
|
128
|
+
this.loggerProvider = new sdk_logs_1.LoggerProvider({ resource, processors: logRecordProcessors });
|
|
129
|
+
api_logs_1.logs.setGlobalLoggerProvider(this.loggerProvider);
|
|
130
|
+
}
|
|
131
|
+
if (metricReaders.length > 0) {
|
|
132
|
+
this.meterProvider = new sdk_metrics_1.MeterProvider({ resource, readers: metricReaders });
|
|
133
|
+
api_1.metrics.setGlobalMeterProvider(this.meterProvider);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (logRecordProcessors.length > 0) {
|
|
137
|
+
this.logsOn = true;
|
|
138
|
+
logger_1.XenonLogger.setOtlpReady(true);
|
|
139
|
+
logger_1.default.info(`[TracingService] Log OTLP endpoint: ${process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}. Log SDK started.`);
|
|
140
|
+
}
|
|
141
|
+
if (metricReaders.length > 0) {
|
|
142
|
+
this.metricsOn = true;
|
|
143
|
+
logger_1.default.info(`[TracingService] Metric OTLP endpoint: ${(_a = process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) !== null && _a !== void 0 ? _a : '(console-only)'}. Metrics SDK started.`);
|
|
144
|
+
}
|
|
77
145
|
}
|
|
78
|
-
|
|
146
|
+
spanProcessors() {
|
|
79
147
|
if (process.env.OTEL_TRACES_ENABLED === 'false') {
|
|
80
148
|
logger_1.default.info('[TracingService] OTEL_TRACES_ENABLED=false — trace export disabled.');
|
|
81
|
-
return;
|
|
149
|
+
return [];
|
|
82
150
|
}
|
|
83
151
|
const exporters = [];
|
|
84
152
|
if (process.env.XENON_OTEL_DEBUG === 'true') {
|
|
@@ -93,52 +161,37 @@ let TracingService = class TracingService {
|
|
|
93
161
|
}
|
|
94
162
|
if (exporters.length === 0) {
|
|
95
163
|
logger_1.default.info('[TracingService] No trace exporters configured. Spans recorded in-memory only.');
|
|
96
|
-
return;
|
|
164
|
+
return [];
|
|
97
165
|
}
|
|
98
166
|
// Pre-existing pre-PR-86 bug: only exporters[0] was wired, so combining
|
|
99
167
|
// XENON_OTEL_DEBUG=true with an OTLP endpoint silently dropped one of the
|
|
100
168
|
// two destinations. Each exporter now gets its own SimpleSpanProcessor.
|
|
101
|
-
|
|
102
|
-
this.sdk = new sdk_node_1.NodeSDK({
|
|
103
|
-
resource,
|
|
104
|
-
spanProcessors,
|
|
105
|
-
});
|
|
106
|
-
try {
|
|
107
|
-
this.sdk.start();
|
|
108
|
-
logger_1.default.info('[TracingService] Trace SDK started.');
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
logger_1.default.error(`[TracingService] Failed to start trace SDK: ${err.message}`);
|
|
112
|
-
}
|
|
169
|
+
return exporters.map((e) => new sdk_trace_base_1.SimpleSpanProcessor(e));
|
|
113
170
|
}
|
|
114
|
-
|
|
171
|
+
logRecordProcessors() {
|
|
115
172
|
if (process.env.OTEL_LOGS_ENABLED === 'false') {
|
|
116
173
|
logger_1.default.info('[TracingService] OTEL_LOGS_ENABLED=false — log export disabled.');
|
|
117
|
-
return;
|
|
174
|
+
return [];
|
|
118
175
|
}
|
|
119
176
|
const endpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
|
|
120
177
|
if (!endpoint)
|
|
121
|
-
return;
|
|
178
|
+
return [];
|
|
122
179
|
const processors = [];
|
|
123
180
|
if (process.env.XENON_OTEL_DEBUG === 'true') {
|
|
124
181
|
processors.push(new sdk_logs_1.SimpleLogRecordProcessor(new sdk_logs_1.ConsoleLogRecordExporter()));
|
|
125
182
|
}
|
|
126
183
|
processors.push(new sdk_logs_1.BatchLogRecordProcessor(new exporter_logs_otlp_http_1.OTLPLogExporter({ url: endpoint })));
|
|
127
|
-
|
|
128
|
-
api_logs_1.logs.setGlobalLoggerProvider(provider);
|
|
129
|
-
this.loggerProvider = provider;
|
|
130
|
-
logger_1.XenonLogger.setOtlpReady(true);
|
|
131
|
-
logger_1.default.info(`[TracingService] Log OTLP endpoint: ${endpoint}. Log SDK started.`);
|
|
184
|
+
return processors;
|
|
132
185
|
}
|
|
133
|
-
|
|
186
|
+
metricReaders() {
|
|
134
187
|
if (process.env.OTEL_METRICS_ENABLED === 'false') {
|
|
135
188
|
logger_1.default.info('[TracingService] OTEL_METRICS_ENABLED=false — metrics export disabled.');
|
|
136
|
-
return;
|
|
189
|
+
return [];
|
|
137
190
|
}
|
|
138
191
|
const endpoint = process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT;
|
|
139
192
|
const debug = process.env.XENON_OTEL_DEBUG === 'true';
|
|
140
193
|
if (!endpoint && !debug)
|
|
141
|
-
return;
|
|
194
|
+
return [];
|
|
142
195
|
const readers = [];
|
|
143
196
|
if (debug) {
|
|
144
197
|
// 10s interval keeps console output readable during dev sessions.
|
|
@@ -154,18 +207,15 @@ let TracingService = class TracingService {
|
|
|
154
207
|
exportIntervalMillis: 60000,
|
|
155
208
|
}));
|
|
156
209
|
}
|
|
157
|
-
|
|
158
|
-
api_1.metrics.setGlobalMeterProvider(provider);
|
|
159
|
-
this.meterProvider = provider;
|
|
160
|
-
logger_1.default.info(`[TracingService] Metric OTLP endpoint: ${endpoint !== null && endpoint !== void 0 ? endpoint : '(console-only)'}. Metrics SDK started.`);
|
|
210
|
+
return readers;
|
|
161
211
|
}
|
|
162
212
|
getLogger(name = 'xenon-core') {
|
|
163
|
-
if (!this.
|
|
213
|
+
if (!this.logsOn)
|
|
164
214
|
return undefined;
|
|
165
215
|
return api_logs_1.logs.getLogger(name);
|
|
166
216
|
}
|
|
167
217
|
getMeter(name = 'xenon-core') {
|
|
168
|
-
if (!this.
|
|
218
|
+
if (!this.metricsOn)
|
|
169
219
|
return undefined;
|
|
170
220
|
return api_1.metrics.getMeter(name);
|
|
171
221
|
}
|
|
@@ -177,8 +227,18 @@ let TracingService = class TracingService {
|
|
|
177
227
|
attributes: Object.assign({ 'xenon.session_id': sessionId }, attributes),
|
|
178
228
|
});
|
|
179
229
|
this.activeSpans.set(sessionId, span);
|
|
230
|
+
this.sessionSpanIds.add(sessionId);
|
|
180
231
|
return span;
|
|
181
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Ends a session's span. Every way a session ends calls this (the client's
|
|
235
|
+
* delete, Appium's new-command timeout, the idle release, the heartbeat
|
|
236
|
+
* sweeps, a shutdown's drain), so whichever comes second finds nothing.
|
|
237
|
+
* `failed` for a session that ended failed; `reason` says why it ended.
|
|
238
|
+
*/
|
|
239
|
+
endSessionSpan(sessionId, ending = {}) {
|
|
240
|
+
this.endSpan(sessionId, ending.failed ? 'ERROR' : 'OK', ending.reason ? { [SESSION_STOP_REASON]: ending.reason } : {});
|
|
241
|
+
}
|
|
182
242
|
startCommandSpan(sessionId, commandName, attributes = {}) {
|
|
183
243
|
if (!this.sdk)
|
|
184
244
|
return undefined;
|
|
@@ -202,28 +262,34 @@ let TracingService = class TracingService {
|
|
|
202
262
|
if (!this.sdk)
|
|
203
263
|
return;
|
|
204
264
|
const span = this.activeSpans.get(id);
|
|
205
|
-
if (span)
|
|
206
|
-
if (Object.keys(attributes).length > 0) {
|
|
207
|
-
span.setAttributes(attributes);
|
|
208
|
-
}
|
|
209
|
-
span.setStatus({
|
|
210
|
-
code: status === 'OK' ? api_1.SpanStatusCode.OK : api_1.SpanStatusCode.ERROR,
|
|
211
|
-
});
|
|
212
|
-
span.end();
|
|
213
|
-
this.activeSpans.delete(id);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
recordError(id, error) {
|
|
217
|
-
if (!this.sdk)
|
|
265
|
+
if (!span)
|
|
218
266
|
return;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
span.
|
|
223
|
-
code: api_1.SpanStatusCode.ERROR,
|
|
224
|
-
message: typeof error === 'string' ? error : error.message,
|
|
225
|
-
});
|
|
267
|
+
this.activeSpans.delete(id);
|
|
268
|
+
this.sessionSpanIds.delete(id);
|
|
269
|
+
if (Object.keys(attributes).length > 0) {
|
|
270
|
+
span.setAttributes(attributes);
|
|
226
271
|
}
|
|
272
|
+
span.setStatus({
|
|
273
|
+
code: status === 'OK' ? api_1.SpanStatusCode.OK : api_1.SpanStatusCode.ERROR,
|
|
274
|
+
});
|
|
275
|
+
span.end();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Ends the span a command started: ERROR with the exception when it
|
|
279
|
+
* `failed`, else OK. By the span itself, not by its id: two commands of the
|
|
280
|
+
* same name on one session at once (the Inspector reading the page source
|
|
281
|
+
* while a test does) share `<sessionId>:<command>`, and the second's span
|
|
282
|
+
* replaced the first's under it, so one command's error went on the other's
|
|
283
|
+
* span and its own never ended.
|
|
284
|
+
*/
|
|
285
|
+
endCommandSpan(id, span, failed) {
|
|
286
|
+
if (failed)
|
|
287
|
+
failSpan(span, failed.error);
|
|
288
|
+
else
|
|
289
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
290
|
+
span.end();
|
|
291
|
+
if (this.activeSpans.get(id) === span)
|
|
292
|
+
this.activeSpans.delete(id);
|
|
227
293
|
}
|
|
228
294
|
getTraceId(sessionId) {
|
|
229
295
|
var _a;
|
|
@@ -233,12 +299,57 @@ let TracingService = class TracingService {
|
|
|
233
299
|
var _a;
|
|
234
300
|
return (_a = this.activeSpans.get(id)) === null || _a === void 0 ? void 0 : _a.spanContext().spanId;
|
|
235
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* shutdown(), waiting no longer than `budgetMs`: an export to a collector
|
|
304
|
+
* that doesn't answer is held for the exporter's own timeout (10 s), and a
|
|
305
|
+
* process that is exiting can't wait that long.
|
|
306
|
+
*/
|
|
307
|
+
shutdownWithin(budgetMs) {
|
|
308
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
309
|
+
let timer;
|
|
310
|
+
try {
|
|
311
|
+
yield Promise.race([
|
|
312
|
+
this.shutdown(),
|
|
313
|
+
new Promise((resolve) => {
|
|
314
|
+
timer = setTimeout(resolve, budgetMs);
|
|
315
|
+
}),
|
|
316
|
+
]);
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
if (timer)
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Stops every exporter, once what each still holds is sent. A session span
|
|
326
|
+
* still open belongs to a session this process didn't end: one running on a
|
|
327
|
+
* node or at a cloud provider, which a hub's shutdown leaves running, or one
|
|
328
|
+
* whose ending was missed. It ends here, saying so, or it is never exported.
|
|
329
|
+
*/
|
|
236
330
|
shutdown() {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
331
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
332
|
+
for (const sessionId of [...this.sessionSpanIds]) {
|
|
333
|
+
const span = this.activeSpans.get(sessionId);
|
|
334
|
+
this.activeSpans.delete(sessionId);
|
|
335
|
+
span === null || span === void 0 ? void 0 : span.setAttribute(SESSION_STOP_REASON, 'Xenon shut down while the session was running');
|
|
336
|
+
span === null || span === void 0 ? void 0 : span.end();
|
|
337
|
+
}
|
|
338
|
+
this.sessionSpanIds.clear();
|
|
339
|
+
// A span processor's shutdown doesn't wait for the exports it started,
|
|
340
|
+
// and with the NodeSDK's resource detection every export waits on it.
|
|
341
|
+
yield Promise.allSettled(this.spanProcessorsInUse.map((p) => p.forceFlush()));
|
|
342
|
+
this.spanProcessorsInUse = [];
|
|
343
|
+
logger_1.XenonLogger.setOtlpReady(false);
|
|
344
|
+
const stopping = [this.sdk, this.loggerProvider, this.meterProvider].map((owner) => owner === null || owner === void 0 ? void 0 : owner.shutdown());
|
|
345
|
+
this.sdk = null;
|
|
346
|
+
this.loggerProvider = null;
|
|
347
|
+
this.meterProvider = null;
|
|
348
|
+
this.logsOn = false;
|
|
349
|
+
this.metricsOn = false;
|
|
350
|
+
this.activeSpans.clear();
|
|
351
|
+
yield Promise.allSettled(stopping);
|
|
352
|
+
});
|
|
242
353
|
}
|
|
243
354
|
};
|
|
244
355
|
exports.TracingService = TracingService;
|
|
@@ -24,6 +24,7 @@ const bcrypt_1 = __importDefault(require("bcrypt"));
|
|
|
24
24
|
const crypto_1 = __importDefault(require("crypto"));
|
|
25
25
|
const logger_1 = __importDefault(require("../logger"));
|
|
26
26
|
const prisma_1 = require("../prisma");
|
|
27
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
27
28
|
const ACCESS_KEY_PREFIX = 'xen_';
|
|
28
29
|
const ACCESS_KEY_LEN = 12;
|
|
29
30
|
const PASSWORD_MIN = 8;
|
|
@@ -92,14 +93,18 @@ let UserService = class UserService {
|
|
|
92
93
|
return prisma_1.prisma.user.findUnique({ where: { id } });
|
|
93
94
|
});
|
|
94
95
|
}
|
|
96
|
+
// The old access key's (accessKey, token) pairs stop working, a node's
|
|
97
|
+
// socket's included.
|
|
95
98
|
rotateAccessKey(userId) {
|
|
96
99
|
return __awaiter(this, void 0, void 0, function* () {
|
|
97
100
|
const accessKey = this.generateAccessKey();
|
|
98
|
-
|
|
101
|
+
const updated = yield prisma_1.prisma.user.update({
|
|
99
102
|
where: { id: userId },
|
|
100
103
|
data: { accessKey },
|
|
101
104
|
select: { id: true, accessKey: true },
|
|
102
105
|
});
|
|
106
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
107
|
+
return updated;
|
|
103
108
|
});
|
|
104
109
|
}
|
|
105
110
|
changePassword(userId, oldPassword, newPassword) {
|
|
@@ -163,18 +168,23 @@ let UserService = class UserService {
|
|
|
163
168
|
data.role = patch.role;
|
|
164
169
|
if (patch.status !== undefined)
|
|
165
170
|
data.status = patch.status;
|
|
166
|
-
|
|
171
|
+
const updated = yield prisma_1.prisma.user.update({ where: { id: userId }, data });
|
|
172
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
173
|
+
return updated;
|
|
167
174
|
});
|
|
168
175
|
}
|
|
169
176
|
setRole(userId, role) {
|
|
170
177
|
return __awaiter(this, void 0, void 0, function* () {
|
|
171
|
-
|
|
178
|
+
const updated = yield prisma_1.prisma.user.update({ where: { id: userId }, data: { role } });
|
|
179
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
180
|
+
return updated;
|
|
172
181
|
});
|
|
173
182
|
}
|
|
174
183
|
deactivateUser(userId) {
|
|
175
184
|
return __awaiter(this, void 0, void 0, function* () {
|
|
176
185
|
yield prisma_1.prisma.user.update({ where: { id: userId }, data: { status: 'INACTIVE' } });
|
|
177
186
|
yield prisma_1.prisma.userSession.deleteMany({ where: { userId } });
|
|
187
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
178
188
|
});
|
|
179
189
|
}
|
|
180
190
|
deleteUser(userId) {
|
|
@@ -184,6 +194,7 @@ let UserService = class UserService {
|
|
|
184
194
|
// relying on cascade semantics for security-critical cleanup.
|
|
185
195
|
yield prisma_1.prisma.userSession.deleteMany({ where: { userId } });
|
|
186
196
|
yield prisma_1.prisma.user.delete({ where: { id: userId } });
|
|
197
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
187
198
|
});
|
|
188
199
|
}
|
|
189
200
|
};
|
|
@@ -22,6 +22,7 @@ exports.UserSessionService = void 0;
|
|
|
22
22
|
const typedi_1 = require("typedi");
|
|
23
23
|
const prisma_1 = require("../prisma");
|
|
24
24
|
const logger_1 = __importDefault(require("../logger"));
|
|
25
|
+
const identityChanges_1 = require("./identity/identityChanges");
|
|
25
26
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
26
27
|
let UserSessionService = class UserSessionService {
|
|
27
28
|
constructor() {
|
|
@@ -38,13 +39,20 @@ let UserSessionService = class UserSessionService {
|
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The sign-in, if it exists and hasn't expired. A use of it renews it
|
|
44
|
+
* (sliding TTL); `renew: false` only looks, for a check that isn't a use: a
|
|
45
|
+
* socket's re-check must not keep a sign-in alive for as long as a tab is open.
|
|
46
|
+
*/
|
|
47
|
+
resolve(sessionId_1) {
|
|
48
|
+
return __awaiter(this, arguments, void 0, function* (sessionId, { renew = true } = {}) {
|
|
43
49
|
const row = yield prisma_1.prisma.userSession.findUnique({ where: { id: sessionId } });
|
|
44
50
|
if (!row)
|
|
45
51
|
return null;
|
|
46
52
|
if (row.expiresAt.getTime() < Date.now())
|
|
47
53
|
return null;
|
|
54
|
+
if (!renew)
|
|
55
|
+
return row;
|
|
48
56
|
const newExpiresAt = new Date(Date.now() + this.ttlMs());
|
|
49
57
|
// Fire-and-forget sliding-TTL update; failures must not block auth, but
|
|
50
58
|
// log them at debug level so chronic write issues are diagnosable.
|
|
@@ -62,13 +70,15 @@ let UserSessionService = class UserSessionService {
|
|
|
62
70
|
// which is fine. Other errors get logged for diagnosis but don't bubble.
|
|
63
71
|
revoke(sessionId) {
|
|
64
72
|
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
-
yield prisma_1.prisma.userSession.delete({ where: { id: sessionId } }).catch((e) => {
|
|
73
|
+
const row = yield prisma_1.prisma.userSession.delete({ where: { id: sessionId } }).catch((e) => {
|
|
66
74
|
var _a;
|
|
67
75
|
const code = e === null || e === void 0 ? void 0 : e.code;
|
|
68
76
|
if (code === 'P2025')
|
|
69
|
-
return;
|
|
77
|
+
return null;
|
|
70
78
|
this.log.warn(`revoke: unexpected delete error for session ${sessionId}: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
79
|
+
return null;
|
|
71
80
|
});
|
|
81
|
+
yield (0, identityChanges_1.identityChanged)(row === null || row === void 0 ? void 0 : row.userId);
|
|
72
82
|
});
|
|
73
83
|
}
|
|
74
84
|
revokeAllForUserExcept(userId, keepSessionId) {
|
|
@@ -78,6 +88,7 @@ let UserSessionService = class UserSessionService {
|
|
|
78
88
|
});
|
|
79
89
|
if (r.count > 0) {
|
|
80
90
|
this.log.info(`revoked ${r.count} sessions for user ${userId} (kept ${keepSessionId})`);
|
|
91
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
81
92
|
}
|
|
82
93
|
return r.count;
|
|
83
94
|
});
|
|
@@ -87,6 +98,7 @@ let UserSessionService = class UserSessionService {
|
|
|
87
98
|
const r = yield prisma_1.prisma.userSession.deleteMany({ where: { userId } });
|
|
88
99
|
if (r.count > 0) {
|
|
89
100
|
this.log.info(`revoked all ${r.count} sessions for user ${userId}`);
|
|
101
|
+
yield (0, identityChanges_1.identityChanged)(userId);
|
|
90
102
|
}
|
|
91
103
|
return r.count;
|
|
92
104
|
});
|
|
@@ -65,7 +65,9 @@ const isHealingTierList = (raw) => Array.isArray(raw) && raw.every((t) => Number
|
|
|
65
65
|
// - not set (or null): every tier (undefined);
|
|
66
66
|
// - a list of tier numbers 1 to 5: exactly those, and [] none;
|
|
67
67
|
// - anything else ("1,2", ["1","2"], [1, 6], ...): LOCAL_HEALING_TIERS.
|
|
68
|
-
// Through 2.14
|
|
68
|
+
// Through 2.14 [], or a value that wasn't a list or had no numbers in it, ran
|
|
69
|
+
// every tier, the AI ones included; a list that held some numbers ran those
|
|
70
|
+
// and skipped the rest ([1, 6] ran tier 1, [6] none).
|
|
69
71
|
function coerceHealingTiersCap(raw) {
|
|
70
72
|
if (raw === undefined || raw === null)
|
|
71
73
|
return undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
exports.onIdentityChanged = onIdentityChanged;
|
|
16
|
+
exports.identityChanged = identityChanged;
|
|
17
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
/** Returns the function that stops listening. */
|
|
20
|
+
function onIdentityChanged(listener) {
|
|
21
|
+
listeners.add(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
listeners.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Never rejects: a listener's failure is logged and the write it follows stands. */
|
|
27
|
+
function identityChanged(userId) {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
if (!userId)
|
|
30
|
+
return;
|
|
31
|
+
yield Promise.all(Array.from(listeners, (listener) => __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
var _a;
|
|
33
|
+
try {
|
|
34
|
+
yield listener(userId);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
logger_1.default.warn(`[Identity] A change to user ${userId} was not applied to a live connection: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
38
|
+
}
|
|
39
|
+
})));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -41,13 +41,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
41
41
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
42
|
});
|
|
43
43
|
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
44
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
48
|
exports.CertManager = void 0;
|
|
49
|
+
exports.caSerialNumber = caSerialNumber;
|
|
50
|
+
exports.subjectHashOld = subjectHashOld;
|
|
46
51
|
const fs = __importStar(require("fs"));
|
|
47
52
|
const path = __importStar(require("path"));
|
|
48
|
-
const child_process_1 = require("child_process");
|
|
49
53
|
const crypto_1 = require("crypto");
|
|
50
54
|
const node_forge_1 = require("node-forge");
|
|
55
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
56
|
+
const logger = logger_1.default.scope('CertManager');
|
|
51
57
|
class CertManager {
|
|
52
58
|
constructor(dir) {
|
|
53
59
|
this.dir = dir;
|
|
@@ -71,6 +77,12 @@ class CertManager {
|
|
|
71
77
|
if (!fs.existsSync(this.certPath) || !fs.existsSync(this.keyPath)) {
|
|
72
78
|
this.generateCa();
|
|
73
79
|
}
|
|
80
|
+
else if (!loadsInOpenSsl(this.certPath)) {
|
|
81
|
+
// Through 2.15 the serial had illegal leading zeros, so OpenSSL, Go and
|
|
82
|
+
// a phone's BoringSSL refused the CA. It never worked; make a new one.
|
|
83
|
+
logger.warn(`Replacing the interceptor CA at ${this.certPath}: it can't be loaded`);
|
|
84
|
+
this.generateCa();
|
|
85
|
+
}
|
|
74
86
|
const subjectHash = this.computeAndroidHash(this.certPath);
|
|
75
87
|
return { certPath: this.certPath, keyPath: this.keyPath, subjectHash };
|
|
76
88
|
});
|
|
@@ -83,7 +95,7 @@ class CertManager {
|
|
|
83
95
|
const keys = node_forge_1.pki.rsa.generateKeyPair(2048);
|
|
84
96
|
const cert = node_forge_1.pki.createCertificate();
|
|
85
97
|
cert.publicKey = keys.publicKey;
|
|
86
|
-
cert.serialNumber =
|
|
98
|
+
cert.serialNumber = caSerialNumber();
|
|
87
99
|
const now = new Date();
|
|
88
100
|
cert.validity.notBefore = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
89
101
|
cert.validity.notAfter = new Date(now.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
|
|
@@ -112,20 +124,42 @@ class CertManager {
|
|
|
112
124
|
fs.writeFileSync(this.publicKeyPath, node_forge_1.pki.publicKeyToPem(keys.publicKey), 'utf8');
|
|
113
125
|
}
|
|
114
126
|
computeAndroidHash(certPath) {
|
|
115
|
-
|
|
116
|
-
const out = (0, child_process_1.execSync)(`openssl x509 -inform PEM -subject_hash_old -noout -in "${certPath}"`, {
|
|
117
|
-
encoding: 'utf8',
|
|
118
|
-
timeout: 5000,
|
|
119
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
120
|
-
}).trim();
|
|
121
|
-
if (/^[0-9a-f]{8}$/.test(out))
|
|
122
|
-
return out;
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
/* openssl unavailable — fall through */
|
|
126
|
-
}
|
|
127
|
-
const pem = fs.readFileSync(certPath, 'utf8');
|
|
128
|
-
return (0, crypto_1.createHash)('sha1').update(pem).digest('hex').slice(0, 8);
|
|
127
|
+
return subjectHashOld(fs.readFileSync(certPath, 'utf8'));
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
exports.CertManager = CertManager;
|
|
131
|
+
/**
|
|
132
|
+
* 16 random bytes, positive and minimally encoded as DER wants: the first
|
|
133
|
+
* byte is 0x01..0x7f (no leading zero, no sign bit), as node-forge writes the
|
|
134
|
+
* hex as the INTEGER's bytes unchanged.
|
|
135
|
+
*/
|
|
136
|
+
function caSerialNumber() {
|
|
137
|
+
const bytes = (0, crypto_1.randomBytes)(16);
|
|
138
|
+
bytes[0] = bytes[0] & 0x7f || 0x01;
|
|
139
|
+
return bytes.toString('hex');
|
|
140
|
+
}
|
|
141
|
+
function loadsInOpenSsl(certPath) {
|
|
142
|
+
try {
|
|
143
|
+
new crypto_1.X509Certificate(fs.readFileSync(certPath));
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch (_a) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* OpenSSL's `x509 -subject_hash_old`, the name Android files a system CA
|
|
152
|
+
* under: MD5 of the subject's DER as the certificate holds it, the first four
|
|
153
|
+
* bytes read little-endian.
|
|
154
|
+
*/
|
|
155
|
+
function subjectHashOld(pem) {
|
|
156
|
+
const der = node_forge_1.pki.pemToDer(pem).getBytes();
|
|
157
|
+
const tbs = node_forge_1.asn1.fromDer(der).value[0];
|
|
158
|
+
const fields = tbs.value;
|
|
159
|
+
const hasVersion = fields[0].tagClass === node_forge_1.asn1.Class.CONTEXT_SPECIFIC;
|
|
160
|
+
const subject = fields[hasVersion ? 5 : 4];
|
|
161
|
+
const digest = (0, crypto_1.createHash)('md5')
|
|
162
|
+
.update(Buffer.from(node_forge_1.asn1.toDer(subject).getBytes(), 'binary'))
|
|
163
|
+
.digest();
|
|
164
|
+
return digest.readUInt32LE(0).toString(16).padStart(8, '0');
|
|
165
|
+
}
|