@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.
Files changed (206) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/index-Dk5J6HB1.js +219 -0
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.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/index.js +2 -0
  78. package/lib/src/app/openapi/grid.yaml +102 -0
  79. package/lib/src/app/openapi/sessions.yaml +7 -5
  80. package/lib/src/app/routers/auth.js +2 -1
  81. package/lib/src/app/routers/users.js +2 -1
  82. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  83. package/lib/src/dashboard/commandLogFields.js +42 -0
  84. package/lib/src/dashboard/event-manager.js +23 -154
  85. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  86. package/lib/src/dashboard/services/failureCategories.js +264 -0
  87. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  88. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  89. package/lib/src/device-utils.js +25 -8
  90. package/lib/src/gateway/forwardToNode.js +4 -11
  91. package/lib/src/gateway/hubSessionToken.js +27 -4
  92. package/lib/src/gateway/nodeAsk.js +57 -0
  93. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  94. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  95. package/lib/src/helpers/outboundProxy.js +219 -0
  96. package/lib/src/index.js +21 -0
  97. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  98. package/lib/src/interceptors/xenonScripts.js +28 -7
  99. package/lib/src/logger.js +3 -1
  100. package/lib/src/plugin.js +12 -4
  101. package/lib/src/scripts/database-check.js +3 -3
  102. package/lib/src/scripts/initialize-database.js +29 -33
  103. package/lib/src/scripts/run-migrations.js +320 -25
  104. package/lib/src/services/ApiKeyService.js +16 -5
  105. package/lib/src/services/InterceptorService.js +6 -2
  106. package/lib/src/services/OrphanSweeper.js +6 -0
  107. package/lib/src/services/SelectorStateService.js +11 -7
  108. package/lib/src/services/SelectorVerificationJob.js +6 -2
  109. package/lib/src/services/SessionLifecycleService.js +52 -23
  110. package/lib/src/services/SocketClient.js +11 -0
  111. package/lib/src/services/SocketServer.js +361 -42
  112. package/lib/src/services/TeamService.js +5 -1
  113. package/lib/src/services/TracingService.js +171 -60
  114. package/lib/src/services/UserService.js +14 -3
  115. package/lib/src/services/UserSessionService.js +16 -4
  116. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  117. package/lib/src/services/identity/identityChanges.js +41 -0
  118. package/lib/src/services/interceptor/CertManager.js +50 -16
  119. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  120. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  121. package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
  122. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  123. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  124. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  125. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  126. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  127. package/lib/src/services/token/JwtKeyService.js +8 -2
  128. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  129. package/lib/src/sessions/LocalSession.js +29 -1
  130. package/lib/src/sessions/RemoteSession.js +23 -4
  131. package/lib/src/sessions/SessionManager.js +33 -9
  132. package/lib/test/helpers/driver-log.js +38 -0
  133. package/lib/test/helpers/fake-http-proxy.js +106 -0
  134. package/lib/test/helpers/fake-logcat.js +80 -0
  135. package/lib/test/helpers/hub-node-pair.js +227 -0
  136. package/lib/test/helpers/otlp-probe.js +107 -0
  137. package/lib/test/integration/androidDevices.spec.js +1 -1
  138. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  139. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  140. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  141. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  142. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  143. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  144. package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
  145. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  146. package/lib/test/unit/database-check.spec.js +1 -0
  147. package/lib/test/unit/device-utils.spec.js +13 -4
  148. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  149. package/lib/test/unit/failure-analysis.spec.js +22 -8
  150. package/lib/test/unit/failure-categories.spec.js +24 -22
  151. package/lib/test/unit/failure-categorization.spec.js +393 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  153. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  154. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  155. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  156. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  157. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  158. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  159. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  160. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  161. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  162. package/lib/test/unit/logcat-ws.spec.js +78 -37
  163. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  164. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  165. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  166. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  167. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  168. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  169. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  170. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  171. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  172. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  173. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  174. package/lib/test/unit/proxy-option.spec.js +358 -0
  175. package/lib/test/unit/run-migrations-database.spec.js +440 -0
  176. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  177. package/lib/test/unit/run-migrations.test.js +264 -0
  178. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  179. package/lib/test/unit/selector-state-service.spec.js +72 -23
  180. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  181. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  182. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  183. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  184. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  185. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  186. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  187. package/lib/test/unit/session-device-logs.spec.js +43 -96
  188. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  189. package/lib/test/unit/session-log-indexes.spec.js +16 -1
  190. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  191. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  192. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  193. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  194. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/package.json +2 -1
  197. package/schema.json +11 -7
  198. package/scripts/dev/readme-screenshots.js +276 -23
  199. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  200. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  201. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  202. package/scripts/export-openapi.js +32 -0
  203. package/scripts/lib/openapi-for-site.js +79 -0
  204. package/lib/public/assets/index-BOxe43p1.js +0 -219
  205. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  206. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -91,13 +91,14 @@ const keyOf = (r) => `${r.pid} ${r.tid} ${r.level} ${r.tag} ${r.message}`;
91
91
  const isError = (r) => r.level === 'E' || r.level === 'F';
92
92
  class DeviceLogBook {
93
93
  constructor(opts) {
94
- var _a, _b;
94
+ var _a, _b, _c;
95
95
  this.kept = 0;
96
96
  this.leftOut = 0;
97
97
  this.limitNoted = false;
98
98
  this.clock = opts.clock;
99
- this.lineLimit = (_a = opts.lineLimit) !== null && _a !== void 0 ? _a : exports.DEVICE_LOG_LINE_LIMIT;
100
- this.errorLimit = (_b = opts.errorLimit) !== null && _b !== void 0 ? _b : exports.DEVICE_LOG_ERROR_LIMIT;
99
+ this.format = (_a = opts.format) !== null && _a !== void 0 ? _a : formatThreadtime;
100
+ this.lineLimit = (_b = opts.lineLimit) !== null && _b !== void 0 ? _b : exports.DEVICE_LOG_LINE_LIMIT;
101
+ this.errorLimit = (_c = opts.errorLimit) !== null && _c !== void 0 ? _c : exports.DEVICE_LOG_ERROR_LIMIT;
101
102
  // A record at `ts` was logged at ts + zoneShift + skew by this server's
102
103
  // clock (timeOf); it counts from WINDOW_SLACK_MS before `since`.
103
104
  const first = opts.since - exports.WINDOW_SLACK_MS - this.clock.zoneShiftMs - this.clock.skewMs;
@@ -126,7 +127,7 @@ class DeviceLogBook {
126
127
  const room = this.kept < this.lineLimit || (isError(rec) && this.kept < this.lineLimit + this.errorLimit);
127
128
  if (room) {
128
129
  this.kept += 1;
129
- out.push({ message: formatThreadtime(rec), timestamp: this.timeOf(rec) });
130
+ out.push({ message: this.format(rec), timestamp: this.timeOf(rec) });
130
131
  }
131
132
  else {
132
133
  this.leftOut += 1;
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IosAppLines = exports.iosLineText = void 0;
4
+ exports.iosLineLevel = iosLineLevel;
5
+ exports.recordFromDriverLog = recordFromDriverLog;
6
+ exports.bufferedDriverLog = bufferedDriverLog;
7
+ exports.isDriverLog = isDriverLog;
8
+ // A real iPhone's syslog line: "Oct 5 20:31:40 iPhone SpringBoard(FrontBoard)[57] <Notice>: …".
9
+ const SYSLOG_LEVEL = /<(Fault|Error|Warning|Notice|Default|Info|Debug)>:/;
10
+ // A simulator's compact line: "2026-10-05 20:31:40.123 E SpringBoard[1234:5678] …".
11
+ const COMPACT_LEVEL = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s+(Df|Db|E|F|I|A)\s/;
12
+ const SYSLOG_LETTERS = {
13
+ Fault: 'F',
14
+ Error: 'E',
15
+ Warning: 'W',
16
+ Notice: 'I',
17
+ Default: 'I',
18
+ Info: 'I',
19
+ Debug: 'D',
20
+ };
21
+ const COMPACT_LETTERS = {
22
+ F: 'F',
23
+ E: 'E',
24
+ Df: 'I',
25
+ I: 'I',
26
+ A: 'I',
27
+ Db: 'D',
28
+ };
29
+ /** The level an iOS log line carries, as a logcat letter; `I` when it carries none. */
30
+ function iosLineLevel(message) {
31
+ var _a, _b;
32
+ const syslog = (_a = SYSLOG_LEVEL.exec(message)) === null || _a === void 0 ? void 0 : _a[1];
33
+ if (syslog)
34
+ return SYSLOG_LETTERS[syslog];
35
+ const compact = (_b = COMPACT_LEVEL.exec(message)) === null || _b === void 0 ? void 0 : _b[1];
36
+ if (compact)
37
+ return COMPACT_LETTERS[compact];
38
+ return 'I';
39
+ }
40
+ // What `log stream` prints before its first line: not the device's.
41
+ const STREAM_BANNER = /^(Filtering the log data using |Timestamp\s+Ty\s+Process)/;
42
+ /**
43
+ * A driver entry as a record for the session's book, or null for what isn't
44
+ * a device line. A line without a level of its own (a message's continuation)
45
+ * is kept, as `I`.
46
+ */
47
+ function recordFromDriverLog(entry) {
48
+ if (!entry || typeof entry.message !== 'string')
49
+ return null;
50
+ const message = entry.message.replace(/\s+$/, '');
51
+ if (!message || STREAM_BANNER.test(message))
52
+ return null;
53
+ const ts = Number(entry.timestamp);
54
+ return {
55
+ ts: Number.isFinite(ts) ? ts : Date.now(),
56
+ pid: 0,
57
+ tid: 0,
58
+ level: iosLineLevel(message),
59
+ tag: '',
60
+ message,
61
+ };
62
+ }
63
+ /** An iOS record's row text: the line as the phone or simulator printed it. */
64
+ const iosLineText = (rec) => rec.message;
65
+ exports.iosLineText = iosLineText;
66
+ /**
67
+ * The lines the driver's log already holds, oldest first, without taking
68
+ * them: the ones from the create, before Xenon could listen. Reads the
69
+ * driver's internals (`logs`, an LRU cache keyed by arrival, and
70
+ * `_deserializeEntry`, as its own `getLogs()` does), so any other shape gives
71
+ * nothing rather than an error.
72
+ */
73
+ function bufferedDriverLog(driverLog) {
74
+ var _a;
75
+ const log = driverLog;
76
+ if (typeof ((_a = log === null || log === void 0 ? void 0 : log.logs) === null || _a === void 0 ? void 0 : _a.rvalues) !== 'function' || typeof log._deserializeEntry !== 'function') {
77
+ return [];
78
+ }
79
+ try {
80
+ const out = [];
81
+ for (const value of log.logs.rvalues())
82
+ out.push(log._deserializeEntry(value));
83
+ return out;
84
+ }
85
+ catch (_b) {
86
+ return [];
87
+ }
88
+ }
89
+ /** Whether `value` can be listened to as a driver log. */
90
+ function isDriverLog(value) {
91
+ const v = value;
92
+ return typeof (v === null || v === void 0 ? void 0 : v.on) === 'function' && typeof (v === null || v === void 0 ? void 0 : v.removeListener) === 'function';
93
+ }
94
+ // A line's process, the library that logged it and its pid: an iPhone's
95
+ // "… Rabindras-iPhone Edge(UIKitCore)[8788] <Notice>: …" or a simulator's
96
+ // "2026-10-05 20:31:40.123 Df Shop[4127:8812] (UIKitCore) …".
97
+ const SYSLOG_HEADER = /^\w{3}\s+\d+ \d\d:\d\d:\d\d \S+ ([^\s[(]+)(?:\(([^)]*)\))?\[(\d+)\]/;
98
+ const COMPACT_HEADER = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+\s+\S+\s+([^\s[]+)\[(\d+)(?::\d+)?\](?:\s+\(([^)]*)\))?/;
99
+ function headerOf(message) {
100
+ const syslog = SYSLOG_HEADER.exec(message);
101
+ if (syslog)
102
+ return { name: syslog[1], sender: syslog[2] || undefined, pid: syslog[3] };
103
+ const compact = COMPACT_HEADER.exec(message);
104
+ if (compact)
105
+ return { name: compact[1], sender: compact[3] || undefined, pid: compact[2] };
106
+ return null;
107
+ }
108
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
109
+ /**
110
+ * Which of an iPhone's or simulator's lines a session keeps: what the app
111
+ * under test's own code logs, every error and fault inside its process, what
112
+ * the phone's app manager says about it (its launch, its state, how it
113
+ * ended), the crash reports and memory kills that name it, and every fault.
114
+ *
115
+ * A phone logs hundreds of lines a second. Measured on an iPhone 14 Plus
116
+ * (iOS 26.5): about 53,000 in a one-minute session, which filled the
117
+ * 10,000-line limit during the create. Keeping the app's whole process wasn't
118
+ * enough either: Edge's process logged about 110 lines a second, of which
119
+ * its own code wrote 5 in 8,839. The rest was Apple's frameworks inside it
120
+ * (UIKit, Network, CFNetwork, WebKit), so their ordinary lines are left out
121
+ * and their errors kept. Only `runningboardd` and `SpringBoard` are kept for
122
+ * naming the app: about twenty other daemons each log every change of its
123
+ * state ("Received state update for 8723 (app<…"), 3,000 lines in 78 s.
124
+ *
125
+ * The app's process is learnt from the lines: the phone announces it by
126
+ * bundle id and pid (`app<com.example.shop(…)>:4127`, `pid: 4127 bundleID:
127
+ * com.example.shop`), and the lines of that pid give its executable's name,
128
+ * so a crash report or a memory kill that names it (`Shop[4127]`, `[Shop]`,
129
+ * `corpse[4127]`) is kept too. A line with no header of its own continues
130
+ * the one before.
131
+ */
132
+ // The processes whose lines about the app say how it lives and ends: its
133
+ // launch, assertions and exit with its reason (RunningBoard), its scenes and
134
+ // windows (SpringBoard).
135
+ const APP_MANAGERS = new Set(['runningboardd', 'SpringBoard']);
136
+ class IosAppLines {
137
+ constructor(bundleId) {
138
+ this.bundleId = bundleId;
139
+ this.pids = new Set();
140
+ this.names = new Set();
141
+ this.lastKept = false;
142
+ const id = escapeRegExp(bundleId);
143
+ this.pidAnnounced = [
144
+ new RegExp(`app<${id}[^>]*>:(\\d+)`, 'g'),
145
+ new RegExp(`pid:? (\\d+),? bundleID:? ${id}\\b`, 'g'),
146
+ ];
147
+ }
148
+ /** Learns the app's pid and executable from a line, without deciding on it. */
149
+ learn(message) {
150
+ if (message.includes(this.bundleId)) {
151
+ for (const re of this.pidAnnounced) {
152
+ for (const m of message.matchAll(re))
153
+ this.pids.add(m[1]);
154
+ }
155
+ }
156
+ const header = headerOf(message);
157
+ if (header && this.pids.has(header.pid))
158
+ this.names.add(header.name);
159
+ }
160
+ /** Whether the session keeps the line. */
161
+ keeps(message, level) {
162
+ this.learn(message);
163
+ const header = headerOf(message);
164
+ if (!header)
165
+ return this.lastKept;
166
+ const { name, sender, pid } = header;
167
+ const inApp = this.pids.has(pid) || this.names.has(name);
168
+ const keep = level === 'F' ||
169
+ (inApp && (level === 'E' || !sender || this.names.has(sender))) ||
170
+ (APP_MANAGERS.has(name) && message.includes(this.bundleId)) ||
171
+ (!inApp &&
172
+ ([...this.names].some((n) => message.includes(`${n}[`) || message.includes(`[${n}]`)) ||
173
+ [...this.pids].some((p) => message.includes(`[${p}]`))));
174
+ this.lastKept = keep;
175
+ return keep;
176
+ }
177
+ }
178
+ exports.IosAppLines = IosAppLines;
@@ -0,0 +1,91 @@
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 __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.NodeDeviceLogsSupport = exports.UNCLAIMED_MS = exports.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS = exports.NODE_DEVICE_LOG_PAGE = exports.NODE_DEVICE_LOGS_HEADER = exports.NODE_DEVICE_LOGS_ROUTE = void 0;
13
+ exports.readNodeDeviceLogsReply = readNodeDeviceLogsReply;
14
+ exports.nodeDeviceLogsSourceOf = nodeDeviceLogsSourceOf;
15
+ const typedi_1 = require("typedi");
16
+ const logger_1 = __importDefault(require("../../logger"));
17
+ const SessionType_1 = __importDefault(require("../../enums/SessionType"));
18
+ const nodeAsk_1 = require("../../gateway/nodeAsk");
19
+ /** Where a node serves a session's device log lines, under `/xenon/api`. */
20
+ exports.NODE_DEVICE_LOGS_ROUTE = '/node/sessions/:sessionId/device-logs';
21
+ /** On every answer of that route, so a hub can tell a node that has it. */
22
+ exports.NODE_DEVICE_LOGS_HEADER = 'x-xenon-node-device-logs';
23
+ /** Lines in one answer. */
24
+ exports.NODE_DEVICE_LOG_PAGE = 2000;
25
+ /** How long a node keeps an ended session's lines for its hub's last ask. */
26
+ exports.NODE_DEVICE_LOGS_KEEP_AFTER_END_MS = 10 * 60000;
27
+ /**
28
+ * A node stops recording a session no hub has asked about this long after
29
+ * it started: its hub doesn't collect (its dashboard is off, the default) or
30
+ * is older. A hub that collects asks as soon as the session is created.
31
+ */
32
+ exports.UNCLAIMED_MS = 2 * 60000;
33
+ const STATES = ['recording', 'ended', 'off'];
34
+ /**
35
+ * The line, field by field, or null when it isn't one: one malformed row
36
+ * would fail every later write of the session's lines (a failed batch goes
37
+ * back to the front of the buffer).
38
+ */
39
+ function lineOf(v) {
40
+ if (!v || typeof v !== 'object')
41
+ return null;
42
+ if (!Number.isSafeInteger(v.seq) || v.seq < 1)
43
+ return null;
44
+ if (typeof v.message !== 'string')
45
+ return null;
46
+ if (typeof v.timestamp !== 'number' || !Number.isFinite(v.timestamp))
47
+ return null;
48
+ return { seq: v.seq, message: v.message, timestamp: v.timestamp };
49
+ }
50
+ function deviceLogsAnswerOf(value) {
51
+ if (!value || !STATES.includes(value.state) || !Array.isArray(value.lines))
52
+ return null;
53
+ return {
54
+ state: value.state,
55
+ lines: value.lines.map(lineOf).filter((x) => !!x),
56
+ more: value.more === true,
57
+ };
58
+ }
59
+ function readNodeDeviceLogsReply(status, headers, data) {
60
+ return (0, nodeAsk_1.readNodeReply)(exports.NODE_DEVICE_LOGS_HEADER, status, headers, data, deviceLogsAnswerOf);
61
+ }
62
+ /**
63
+ * The session as a NodeDeviceLogsSource, when it runs on a node. A cloud
64
+ * provider's session (CloudSession) and this server's own (LocalSession)
65
+ * inherit the methods from RemoteSession, so the type decides.
66
+ */
67
+ function nodeDeviceLogsSourceOf(session) {
68
+ var _a;
69
+ const s = session;
70
+ return s &&
71
+ ((_a = s.getType) === null || _a === void 0 ? void 0 : _a.call(s)) === SessionType_1.default.REMOTE &&
72
+ typeof s.nodeDeviceLogs === 'function' &&
73
+ typeof s.nodeOrigin === 'function'
74
+ ? s
75
+ : undefined;
76
+ }
77
+ /** Hub side: which nodes lack the route (an older Xenon). Their sessions have no Device logs. */
78
+ let NodeDeviceLogsSupport = class NodeDeviceLogsSupport extends nodeAsk_1.OlderNodes {
79
+ constructor() {
80
+ super(...arguments);
81
+ this.logger = logger_1.default.scope('DeviceLogs');
82
+ }
83
+ warning(origin, status) {
84
+ return (`Node ${origin} has no session device log route (answered ${status}): an older Xenon. ` +
85
+ 'Its sessions have no device logs here. Upgrade the node.');
86
+ }
87
+ };
88
+ exports.NodeDeviceLogsSupport = NodeDeviceLogsSupport;
89
+ exports.NodeDeviceLogsSupport = NodeDeviceLogsSupport = __decorate([
90
+ (0, typedi_1.Service)()
91
+ ], NodeDeviceLogsSupport);
@@ -9,12 +9,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.NodeMetricsSupport = exports.NODE_METRICS_RECHECK_MS = exports.KEEP_AFTER_END_MS = exports.NODE_METRICS_HEADER = exports.NODE_METRICS_ROUTE = void 0;
12
+ exports.NodeMetricsSupport = exports.KEEP_AFTER_END_MS = exports.NODE_METRICS_HEADER = exports.NODE_METRICS_ROUTE = void 0;
13
13
  exports.readNodeMetricsReply = readNodeMetricsReply;
14
14
  exports.nodeMetricsSourceOf = nodeMetricsSourceOf;
15
15
  const typedi_1 = require("typedi");
16
16
  const logger_1 = __importDefault(require("../../logger"));
17
17
  const SessionType_1 = __importDefault(require("../../enums/SessionType"));
18
+ const nodeAsk_1 = require("../../gateway/nodeAsk");
18
19
  /** Where a node serves a session's figures, under `/xenon/api`. */
19
20
  exports.NODE_METRICS_ROUTE = '/node/sessions/:sessionId/metrics';
20
21
  /** On every answer of that route, so a hub can tell a node that has it. */
@@ -41,33 +42,20 @@ function sampleOf(v) {
41
42
  return null;
42
43
  return { at: v.at, deviceCpuPct, deviceMemMb, deviceMemTotalMb, appCpuPct, appMemMb, appId };
43
44
  }
44
- function readNodeMetricsReply(status, headers, data) {
45
+ function metricsAnswerOf(value) {
45
46
  var _a;
46
- if (!(headers === null || headers === void 0 ? void 0 : headers[exports.NODE_METRICS_HEADER])) {
47
- // An older node answers its login's 401, an unknown route's 404, or a
48
- // catch-all's 2xx. Anything else without the header (a proxy's 502 in
49
- // front of a node) is an outage: ask again, never give the node up.
50
- if (status === 401 || status === 404 || (status >= 200 && status < 300)) {
51
- return { kind: 'unsupported', status };
52
- }
53
- return { kind: 'unavailable', reason: `answered ${status}` };
54
- }
55
- if (status === 404)
56
- return { kind: 'refused' };
57
- const value = data === null || data === void 0 ? void 0 : data.value;
58
- if (status === 200 && value && STATES.includes(value.state) && Array.isArray(value.samples)) {
59
- return {
60
- kind: 'answer',
61
- answer: {
62
- platform: String((_a = value.platform) !== null && _a !== void 0 ? _a : ''),
63
- state: value.state,
64
- samples: value.samples
65
- .map(sampleOf)
66
- .filter((x) => x !== null),
67
- },
68
- };
69
- }
70
- return { kind: 'unavailable', reason: `answered ${status}` };
47
+ if (!value || !STATES.includes(value.state) || !Array.isArray(value.samples))
48
+ return null;
49
+ return {
50
+ platform: String((_a = value.platform) !== null && _a !== void 0 ? _a : ''),
51
+ state: value.state,
52
+ samples: value.samples
53
+ .map(sampleOf)
54
+ .filter((x) => x !== null),
55
+ };
56
+ }
57
+ function readNodeMetricsReply(status, headers, data) {
58
+ return (0, nodeAsk_1.readNodeReply)(exports.NODE_METRICS_HEADER, status, headers, data, metricsAnswerOf);
71
59
  }
72
60
  /**
73
61
  * The session as a NodeMetricsSource, when it runs on a node. A cloud
@@ -84,35 +72,18 @@ function nodeMetricsSourceOf(session) {
84
72
  ? s
85
73
  : undefined;
86
74
  }
87
- /** How long the hub leaves a node without the route alone before asking it again. */
88
- exports.NODE_METRICS_RECHECK_MS = 10 * 60000;
89
75
  /**
90
76
  * Hub side: which nodes lack the route (an older Xenon). Their sessions show
91
77
  * no figures; the hub says so once per node, and asks again after
92
- * NODE_METRICS_RECHECK_MS, so a node upgraded in place is picked up.
78
+ * OLDER_NODE_RECHECK_MS, so a node upgraded in place is picked up.
93
79
  */
94
- let NodeMetricsSupport = class NodeMetricsSupport {
80
+ let NodeMetricsSupport = class NodeMetricsSupport extends nodeAsk_1.OlderNodes {
95
81
  constructor() {
82
+ super(...arguments);
96
83
  this.logger = logger_1.default.scope('NodeMetrics');
97
- this.now = () => Date.now();
98
- this.unsupportedUntil = new Map();
99
- this.warned = new Set();
100
- }
101
- shouldAsk(origin) {
102
- const until = this.unsupportedUntil.get(origin);
103
- if (until === undefined)
104
- return true;
105
- if (until > this.now())
106
- return false;
107
- this.unsupportedUntil.delete(origin);
108
- return true;
109
84
  }
110
- unsupported(origin, status) {
111
- this.unsupportedUntil.set(origin, this.now() + exports.NODE_METRICS_RECHECK_MS);
112
- if (this.warned.has(origin))
113
- return;
114
- this.warned.add(origin);
115
- this.logger.warn(`Node ${origin} has no session metrics route (answered ${status}): an older Xenon. ` +
85
+ warning(origin, status) {
86
+ return (`Node ${origin} has no session metrics route (answered ${status}): an older Xenon. ` +
116
87
  'Its sessions show no CPU or memory. Upgrade the node.');
117
88
  }
118
89
  };
@@ -0,0 +1,121 @@
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 __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
12
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
13
+ return new (P || (P = Promise))(function (resolve, reject) {
14
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
15
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
16
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
17
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
18
+ });
19
+ };
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.SelectorVisibilityResolver = exports.SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS = exports.SELECTOR_VISIBILITY_TTL_MS = void 0;
25
+ const typedi_1 = require("typedi");
26
+ const logger_1 = __importDefault(require("../../logger"));
27
+ const sessionVisibility_1 = require("../device-access/sessionVisibility");
28
+ const access_1 = require("./access");
29
+ /**
30
+ * How long an answer is reused. A selector event is rare, but a verification
31
+ * run can send several at once, each to every dashboard connected.
32
+ */
33
+ exports.SELECTOR_VISIBILITY_TTL_MS = 5000;
34
+ /** A lookup that takes longer answers no for that event, and isn't cached. */
35
+ exports.SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS = 2000;
36
+ const MAX_ENTRIES = 10000;
37
+ /**
38
+ * Whether a dashboard caller may see a selector, for the `selector_*` live
39
+ * events: Selector Health's own rule (`access.ts`), a selector healed in at
40
+ * least one session the caller may see. Admins see every selector and are
41
+ * never asked about.
42
+ *
43
+ * Answers are cached for {@link SELECTOR_VISIBILITY_TTL_MS} per viewer (user
44
+ * and teams, so a member's tabs share one) and selector, and a viewer's
45
+ * sessions are read once for all their selectors. Concurrent questions share
46
+ * one lookup. A lookup that fails, or outlasts
47
+ * {@link SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS}, answers no and isn't cached.
48
+ */
49
+ let SelectorVisibilityResolver = class SelectorVisibilityResolver {
50
+ // An interface-typed parameter emits `Object`, which TypeDI leaves alone; a
51
+ // function-typed one would make Container.get throw.
52
+ constructor(deps = {}) {
53
+ this.deps = deps;
54
+ this.scopes = new Map();
55
+ this.answers = new Map();
56
+ }
57
+ /** Never rejects. */
58
+ canSee(key, viewer) {
59
+ var _a;
60
+ const who = JSON.stringify([(_a = viewer.userId) !== null && _a !== void 0 ? _a : null, Array.from(new Set(viewer.teamIds)).sort()]);
61
+ const answer = this.cached(this.answers, JSON.stringify([who, key.strategy, key.selector]), () => __awaiter(this, void 0, void 0, function* () {
62
+ const scope = yield this.cached(this.scopes, who, () => this.sessionScope(viewer));
63
+ return this.healedIn(key, scope);
64
+ }));
65
+ return answer.catch((err) => {
66
+ var _a;
67
+ logger_1.default.warn(`[SelectorVisibility] could not tell who may see ${key.strategy}=${key.selector}: ` +
68
+ `${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}; its event went to admins only`);
69
+ return false;
70
+ });
71
+ }
72
+ cached(map, key, load) {
73
+ const entry = map.get(key);
74
+ if (entry &&
75
+ (entry.settledAt === undefined || this.now() - entry.settledAt < exports.SELECTOR_VISIBILITY_TTL_MS)) {
76
+ return entry.value;
77
+ }
78
+ const fresh = { value: Promise.resolve(undefined) };
79
+ fresh.value = this.bounded(load).then((value) => {
80
+ fresh.settledAt = this.now();
81
+ return value;
82
+ }, (err) => {
83
+ if (map.get(key) === fresh)
84
+ map.delete(key);
85
+ throw err;
86
+ });
87
+ if (map.size >= MAX_ENTRIES && !map.has(key))
88
+ map.clear();
89
+ map.set(key, fresh);
90
+ return fresh.value;
91
+ }
92
+ bounded(load) {
93
+ var _a;
94
+ const timeoutMs = (_a = this.deps.lookupTimeoutMs) !== null && _a !== void 0 ? _a : exports.SELECTOR_VISIBILITY_LOOKUP_TIMEOUT_MS;
95
+ let timer;
96
+ const timedOut = new Promise((_, reject) => {
97
+ var _a;
98
+ timer = setTimeout(() => reject(new Error(`took over ${timeoutMs} ms`)), timeoutMs);
99
+ (_a = timer.unref) === null || _a === void 0 ? void 0 : _a.call(timer);
100
+ });
101
+ const lookup = new Promise((resolve) => resolve(load()));
102
+ return Promise.race([lookup, timedOut]).finally(() => clearTimeout(timer));
103
+ }
104
+ sessionScope(viewer) {
105
+ if (this.deps.sessionScope)
106
+ return this.deps.sessionScope(viewer);
107
+ return (0, sessionVisibility_1.visibleSessionWhere)(viewer);
108
+ }
109
+ healedIn(key, scope) {
110
+ var _a;
111
+ return ((_a = this.deps.healedIn) !== null && _a !== void 0 ? _a : access_1.canSeeSelector)(key, scope);
112
+ }
113
+ now() {
114
+ return this.deps.now ? this.deps.now() : Date.now();
115
+ }
116
+ };
117
+ exports.SelectorVisibilityResolver = SelectorVisibilityResolver;
118
+ exports.SelectorVisibilityResolver = SelectorVisibilityResolver = __decorate([
119
+ (0, typedi_1.Service)(),
120
+ __metadata("design:paramtypes", [Object])
121
+ ], SelectorVisibilityResolver);
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  return (mod && mod.__esModule) ? mod : { "default": mod };
52
52
  };
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
- exports.JwtKeyService = void 0;
54
+ exports.JwtKeyService = exports.JWT_CLOCK_TOLERANCE_SEC = void 0;
55
55
  const typedi_1 = require("typedi");
56
56
  const jose = __importStar(require("jose"));
57
57
  const crypto_1 = require("crypto");
@@ -59,6 +59,12 @@ const fs_1 = __importDefault(require("fs"));
59
59
  const path_1 = __importDefault(require("path"));
60
60
  const KEY_FILE = 'xenon-jwt-private.pem';
61
61
  const ISSUER = process.env.XENON_JWT_ISSUER || 'xenon-hub';
62
+ /**
63
+ * How long past `exp` verify() still takes a token (spec §7.1: ±60 s skew,
64
+ * mirrors appium-mcp-auth). A dashboard socket on a bearer token closes then,
65
+ * when REST stops taking it (SocketServer).
66
+ */
67
+ exports.JWT_CLOCK_TOLERANCE_SEC = 60;
62
68
  /**
63
69
  * RS256 signing key for hub-issued JWTs (REST/MCP tokens, stream tickets).
64
70
  * Key material lives on disk (0600), never in the database. `kid` is derived
@@ -119,7 +125,7 @@ let JwtKeyService = class JwtKeyService {
119
125
  const { payload } = yield jose.jwtVerify(token, this.publicKey, {
120
126
  issuer: ISSUER,
121
127
  audience: opts.audience,
122
- clockTolerance: 60, // spec §7.1: ±60 s skew, mirrors appium-mcp-auth
128
+ clockTolerance: exports.JWT_CLOCK_TOLERANCE_SEC,
123
129
  });
124
130
  return payload;
125
131
  });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_MCP_TOKEN_TTL_SEC = void 0;
7
+ exports.mcpTokenTtlSec = mcpTokenTtlSec;
8
+ const logger_1 = __importDefault(require("../../logger"));
9
+ exports.DEFAULT_MCP_TOKEN_TTL_SEC = 86400;
10
+ /**
11
+ * The lifetime of the xenon-mcp tokens and session tokens POST /auth/token
12
+ * mints, in seconds: XENON_MCP_TOKEN_TTL_SEC, a whole number above 0, else a
13
+ * day. Through 2.15 any other value (`abc`) gave every such token a NaN
14
+ * lifetime; it now warns and keeps the default.
15
+ */
16
+ function mcpTokenTtlSec(raw = process.env.XENON_MCP_TOKEN_TTL_SEC, warn = (message) => logger_1.default.warn(message)) {
17
+ var _a;
18
+ const text = (_a = raw === null || raw === void 0 ? void 0 : raw.trim()) !== null && _a !== void 0 ? _a : '';
19
+ if (text === '')
20
+ return exports.DEFAULT_MCP_TOKEN_TTL_SEC;
21
+ const seconds = /^\d+$/.test(text) ? Number(text) : NaN;
22
+ if (Number.isSafeInteger(seconds) && seconds > 0)
23
+ return seconds;
24
+ warn(`XENON_MCP_TOKEN_TTL_SEC is "${raw}", not a whole number of seconds above 0. ` +
25
+ `The tokens it sets the lifetime of last ${exports.DEFAULT_MCP_TOKEN_TTL_SEC} s, the default.`);
26
+ return exports.DEFAULT_MCP_TOKEN_TTL_SEC;
27
+ }
@@ -109,6 +109,26 @@ class LocalSession extends RemoteSession_1.RemoteSession {
109
109
  };
110
110
  });
111
111
  }
112
+ /**
113
+ * The device log the session's driver captures (XCUITest's
114
+ * `logs.syslog`), for the session's Device logs (SessionDeviceLogs).
115
+ * Undefined for a driver that keeps none, or a session that skips log
116
+ * capture.
117
+ */
118
+ deviceLog() {
119
+ var _a, _b, _c, _d, _e, _f;
120
+ const sessionDriver = ((_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.sessions) === null || _b === void 0 ? void 0 : _b[this.sessionId]) === null || _c === void 0 ? void 0 : _c.proxydriver) ||
121
+ ((_e = (_d = this.driver) === null || _d === void 0 ? void 0 : _d.sessions) === null || _e === void 0 ? void 0 : _e[this.sessionId]);
122
+ return (_f = sessionDriver === null || sessionDriver === void 0 ? void 0 : sessionDriver.logs) === null || _f === void 0 ? void 0 : _f.syslog;
123
+ }
124
+ /** The bundle id of the app under test, as the session's driver has it (XCUITest's `opts.bundleId`). */
125
+ appUnderTest() {
126
+ var _a, _b, _c, _d, _e, _f;
127
+ const sessionDriver = ((_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.sessions) === null || _b === void 0 ? void 0 : _b[this.sessionId]) === null || _c === void 0 ? void 0 : _c.proxydriver) ||
128
+ ((_e = (_d = this.driver) === null || _d === void 0 ? void 0 : _d.sessions) === null || _e === void 0 ? void 0 : _e[this.sessionId]);
129
+ const bundleId = (_f = sessionDriver === null || sessionDriver === void 0 ? void 0 : sessionDriver.opts) === null || _f === void 0 ? void 0 : _f.bundleId;
130
+ return typeof bundleId === 'string' && bundleId ? bundleId : undefined;
131
+ }
112
132
  /**
113
133
  * A local session's driver is in this process, so ask it directly instead of
114
134
  * looping back over HTTP. The loopback works, but it re-enters the Appium
@@ -246,7 +266,7 @@ class LocalSession extends RemoteSession_1.RemoteSession {
246
266
  // Override to use proper Appium URL for video commands
247
267
  startVideoRecording(options, driverOverride) {
248
268
  return __awaiter(this, void 0, void 0, function* () {
249
- var _a, _b, _c;
269
+ var _a, _b, _c, _d;
250
270
  logger_1.default.info(`[LocalSession] Starting video recording for session ${this.sessionId}`);
251
271
  // Principal Intelligence: For local sessions, try to call the driver directly.
252
272
  // We try driverOverride first (e.g. from onUnexpectedShutdown), then search the session map, then this.driver.
@@ -291,6 +311,14 @@ class LocalSession extends RemoteSession_1.RemoteSession {
291
311
  const result = yield typedi_1.Container.get(AndroidStreamService_1.default).startStream(device.udid);
292
312
  mjpegPort = result.mjpegPort;
293
313
  }
314
+ else if (device.platform === 'ios' && !device.realDevice) {
315
+ // A simulator's WebDriverAgent is the driver's, and serves the picture
316
+ // on this Mac at the session's mjpegServerPort. The stream service is
317
+ // an iPhone's (go-ios, iproxy over USB): on a simulator it forwarded
318
+ // nothing, and no video was written.
319
+ mjpegPort =
320
+ Number((_d = this.getCapabilities()) === null || _d === void 0 ? void 0 : _d.mjpegServerPort) || device.mjpegServerPort || undefined;
321
+ }
294
322
  else if (device.platform === 'ios') {
295
323
  const result = yield typedi_1.Container.get(IOSStreamService_1.default).startStream(device.udid);
296
324
  mjpegPort = result.mjpegPort;