@xenon-device-management/xenon 2.15.0 → 2.16.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.
Files changed (183) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
  37. package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
  39. package/lib/public/assets/index-B0gQkEg0.js +219 -0
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
  59. package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
  68. package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
  69. package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/openapi/sessions.yaml +7 -5
  78. package/lib/src/app/routers/auth.js +2 -1
  79. package/lib/src/app/routers/users.js +2 -1
  80. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  81. package/lib/src/dashboard/commandLogFields.js +42 -0
  82. package/lib/src/dashboard/event-manager.js +26 -154
  83. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  84. package/lib/src/dashboard/services/failureCategories.js +264 -0
  85. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  86. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  87. package/lib/src/device-utils.js +14 -6
  88. package/lib/src/gateway/forwardToNode.js +4 -11
  89. package/lib/src/gateway/hubSessionToken.js +27 -4
  90. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  91. package/lib/src/helpers/outboundProxy.js +219 -0
  92. package/lib/src/index.js +21 -0
  93. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  94. package/lib/src/interceptors/xenonScripts.js +28 -7
  95. package/lib/src/logger.js +3 -1
  96. package/lib/src/plugin.js +8 -3
  97. package/lib/src/scripts/database-check.js +3 -3
  98. package/lib/src/scripts/initialize-database.js +35 -33
  99. package/lib/src/scripts/run-migrations.js +1079 -27
  100. package/lib/src/services/ApiKeyService.js +16 -5
  101. package/lib/src/services/InterceptorService.js +6 -2
  102. package/lib/src/services/OrphanSweeper.js +6 -0
  103. package/lib/src/services/SelectorStateService.js +11 -7
  104. package/lib/src/services/SelectorVerificationJob.js +6 -2
  105. package/lib/src/services/SessionLifecycleService.js +32 -18
  106. package/lib/src/services/SocketClient.js +11 -0
  107. package/lib/src/services/SocketServer.js +361 -42
  108. package/lib/src/services/TeamService.js +5 -1
  109. package/lib/src/services/TracingService.js +171 -60
  110. package/lib/src/services/UserService.js +14 -3
  111. package/lib/src/services/UserSessionService.js +16 -4
  112. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  113. package/lib/src/services/identity/identityChanges.js +41 -0
  114. package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
  115. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  116. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  117. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  118. package/lib/src/services/token/JwtKeyService.js +8 -2
  119. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  120. package/lib/src/sessions/LocalSession.js +29 -1
  121. package/lib/src/sessions/RemoteSession.js +12 -2
  122. package/lib/src/sessions/SessionManager.js +5 -8
  123. package/lib/test/helpers/driver-log.js +38 -0
  124. package/lib/test/helpers/fake-http-proxy.js +106 -0
  125. package/lib/test/helpers/otlp-probe.js +107 -0
  126. package/lib/test/integration/androidDevices.spec.js +1 -1
  127. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  128. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  129. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  130. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  131. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  132. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  133. package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
  134. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  135. package/lib/test/unit/database-check.spec.js +1 -0
  136. package/lib/test/unit/device-utils.spec.js +13 -4
  137. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  138. package/lib/test/unit/failure-analysis.spec.js +22 -8
  139. package/lib/test/unit/failure-categories.spec.js +24 -22
  140. package/lib/test/unit/failure-categorization.spec.js +393 -0
  141. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  142. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  143. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  144. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  145. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  146. package/lib/test/unit/logcat-ws.spec.js +78 -37
  147. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  148. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  149. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  150. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  151. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  152. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  153. package/lib/test/unit/proxy-option.spec.js +358 -0
  154. package/lib/test/unit/run-migrations-database.spec.js +807 -0
  155. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  156. package/lib/test/unit/run-migrations.test.js +864 -0
  157. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  158. package/lib/test/unit/selector-state-service.spec.js +72 -23
  159. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  160. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  161. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  162. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  163. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  164. package/lib/test/unit/session-device-logs.spec.js +11 -2
  165. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  166. package/lib/test/unit/session-log-indexes.spec.js +1 -1
  167. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  168. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  169. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  170. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  171. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  172. package/lib/tsconfig.tsbuildinfo +1 -1
  173. package/package.json +2 -1
  174. package/schema.json +11 -7
  175. package/scripts/dev/readme-screenshots.js +276 -23
  176. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  177. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  178. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  179. package/scripts/export-openapi.js +32 -0
  180. package/scripts/lib/openapi-for-site.js +79 -0
  181. package/lib/public/assets/index-BOxe43p1.js +0 -219
  182. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  183. 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.initTraces(resource);
75
- this.initLogs(resource);
76
- this.initMetrics(resource);
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
- initTraces(resource) {
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
- const spanProcessors = exporters.map((e) => new sdk_trace_base_1.SimpleSpanProcessor(e));
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
- initLogs(resource) {
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
- const provider = new sdk_logs_1.LoggerProvider({ resource, processors });
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
- initMetrics(resource) {
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
- const provider = new sdk_metrics_1.MeterProvider({ resource, readers });
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.loggerProvider)
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.meterProvider)
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
- const span = this.activeSpans.get(id);
220
- if (span) {
221
- span.recordException(error);
222
- span.setStatus({
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
- var _a, _b, _c;
238
- (_a = this.sdk) === null || _a === void 0 ? void 0 : _a.shutdown();
239
- logger_1.XenonLogger.setOtlpReady(false);
240
- (_b = this.loggerProvider) === null || _b === void 0 ? void 0 : _b.shutdown();
241
- (_c = this.meterProvider) === null || _c === void 0 ? void 0 : _c.shutdown();
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
- return prisma_1.prisma.user.update({
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
- return prisma_1.prisma.user.update({ where: { id: userId }, data });
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
- return prisma_1.prisma.user.update({ where: { id: userId }, data: { role } });
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
- resolve(sessionId) {
42
- return __awaiter(this, void 0, void 0, function* () {
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 anything else, and [], ran every tier, the AI ones included.
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
+ }