@xenon-device-management/xenon 2.12.0 → 2.13.1

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 (150) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +223 -488
  3. package/lib/package.json +2 -2
  4. package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-Cvpkk4r9.js} +1 -1
  5. package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-Bhn5ZtVM.js} +1 -1
  6. package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-BA6aEjTC.js} +1 -1
  7. package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-ChhHnpFQ.js} +1 -1
  8. package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-CgzbuUvO.js} +1 -1
  9. package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-BHc_5qNc.js} +1 -1
  10. package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-D7OMGuRc.js} +1 -1
  11. package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-C173JJ0l.js} +1 -1
  12. package/lib/public/assets/{Modal-ByESeBRC.js → Modal-ChvVUI3B.js} +1 -1
  13. package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-Dhw4d_O6.js} +1 -1
  14. package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-z97ftGK-.js} +2 -2
  15. package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-DbXhBGZC.js} +1 -1
  16. package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-C908GgiI.js} +1 -1
  17. package/lib/public/assets/{Table-CIaR0QWV.js → Table-B1DB_ZN-.js} +1 -1
  18. package/lib/public/assets/{activity-Bengh9SD.js → activity-Css0xNvp.js} +1 -1
  19. package/lib/public/assets/ai-settings-BMQK3-yN.js +21 -0
  20. package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-CbJ42eDQ.js} +1 -1
  21. package/lib/public/assets/{apps-DIbcGVlU.js → apps-DW1pw2re.js} +2 -2
  22. package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-Dpog9L0F.js} +1 -1
  23. package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-CsBIlSIw.js} +1 -1
  24. package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-z1tRQE4R.js} +1 -1
  25. package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-mXiX5vNk.js} +2 -2
  26. package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-BcFn23bE.js} +1 -1
  27. package/lib/public/assets/{button-CqhPBGRj.js → button-Cq_zHETv.js} +1 -1
  28. package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CKxABX9q.js} +1 -1
  29. package/lib/public/assets/{check-DpbIM4E0.js → check-C7xu-hGU.js} +1 -1
  30. package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BiqfdCGA.js} +1 -1
  31. package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-CyBjjPxR.js} +1 -1
  32. package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-6gMRcx6z.js} +1 -1
  33. package/lib/public/assets/{clock-DMT60v1C.js → clock-U1rDTT1h.js} +1 -1
  34. package/lib/public/assets/{copy-BKNyOehd.js → copy-BIM0ZX0L.js} +1 -1
  35. package/lib/public/assets/device-explorer-DSpAA_t9.js +279 -0
  36. package/lib/public/assets/{download-BM6Xn22t.js → download-DDBRXnAZ.js} +1 -1
  37. package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-Dpg7yxM8.js} +1 -1
  38. package/lib/public/assets/{index-ClrpAMAT.js → index-BGBqohfQ.js} +43 -43
  39. package/lib/public/assets/{index-DauQh6ie.js → index-BQ2luT7L.js} +1 -1
  40. package/lib/public/assets/{input-CyKdLnEx.js → input-BvB1cjHE.js} +1 -1
  41. package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-m78VggnW.js} +1 -1
  42. package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-cY7o1QTL.js} +1 -1
  43. package/lib/public/assets/{lock-CVCe56TH.js → lock-loz8txQm.js} +1 -1
  44. package/lib/public/assets/{login-BJ8a7yVD.js → login-BYOvz4lx.js} +1 -1
  45. package/lib/public/assets/maintenance-settings-BxbA7SkB.js +6 -0
  46. package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-C4HPHb6s.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-BkdKtCzi.js} +1 -1
  48. package/lib/public/assets/{network-CBGUjJDJ.js → network-C5e0MIWo.js} +1 -1
  49. package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-tlu8ijGd.js} +1 -1
  50. package/lib/public/assets/{page-header-B92DKLiq.js → page-header-FXPlo6az.js} +1 -1
  51. package/lib/public/assets/{play-Ck0L-0_m.js → play-CWdk0_Sy.js} +1 -1
  52. package/lib/public/assets/{plus-Dq3tCy2N.js → plus-B2xCnntV.js} +1 -1
  53. package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-D080qzr4.js} +1 -1
  54. package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-B75435xR.js} +1 -1
  55. package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-D3u4Mbvn.js} +1 -1
  56. package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-hPRZMy7s.js} +1 -1
  57. package/lib/public/assets/{select-BBOIZTYm.js → select-12RThAXc.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-uQwvV7EQ.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-CThoxDFO.js} +2 -2
  60. package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-Bc02YjMm.js} +1 -1
  61. package/lib/public/assets/settings-C1VIluuT.js +6 -0
  62. package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-B02WEte8.js} +1 -1
  63. package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-Dj30VGZl.js} +1 -1
  64. package/lib/public/assets/{teams-uiiG4hZM.js → teams-uL4fjTDR.js} +1 -1
  65. package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-B54e-TfY.js} +1 -1
  66. package/lib/public/assets/{upload-BUX8TNFi.js → upload-B3k8WCSP.js} +1 -1
  67. package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-BFn90NH3.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DL2TmWZ0.js} +1 -1
  69. package/lib/public/assets/{users-BU4XBbMT.js → users-CqBuxMlX.js} +1 -1
  70. package/lib/public/assets/{users-BDh1xjad.js → users-DjQSucKb.js} +1 -1
  71. package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-T0rlt0GA.js} +1 -1
  72. package/lib/public/assets/webhook-settings-DAHgnJyZ.js +1 -0
  73. package/lib/public/assets/{zap-DTWwVMUg.js → zap-gBHqutZV.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/app/apiErrors.js +118 -0
  76. package/lib/src/app/index.js +6 -1
  77. package/lib/src/app/openapi/control.yaml +3129 -0
  78. package/lib/src/app/openapi/grid.yaml +2295 -0
  79. package/lib/src/app/openapi/identity.yaml +2168 -0
  80. package/lib/src/app/openapi/platform.yaml +2885 -0
  81. package/lib/src/app/openapi/sessions.yaml +3784 -0
  82. package/lib/src/app/routers/bug-report.js +4 -1
  83. package/lib/src/app/routers/config.js +6 -107
  84. package/lib/src/app/routers/control.js +117 -59
  85. package/lib/src/app/routers/dashboard.js +13 -7
  86. package/lib/src/app/routers/grid.js +54 -14
  87. package/lib/src/app/routers/profile.js +27 -13
  88. package/lib/src/app/routers/recordings.js +11 -5
  89. package/lib/src/app/routers/reservation.js +63 -15
  90. package/lib/src/app/routers/users.js +4 -0
  91. package/lib/src/app/routers/webhook.js +17 -8
  92. package/lib/src/app/swagger.js +259 -177
  93. package/lib/src/data-service/device-service.js +4 -1
  94. package/lib/src/data-service/deviceFieldOwners.js +1 -0
  95. package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
  96. package/lib/src/device-managers/ios/WDAClient.js +153 -41
  97. package/lib/src/generated/client/edge.js +4 -3
  98. package/lib/src/generated/client/index-browser.js +1 -0
  99. package/lib/src/generated/client/index.d.ts +38 -0
  100. package/lib/src/generated/client/index.js +4 -3
  101. package/lib/src/generated/client/package.json +1 -1
  102. package/lib/src/generated/client/schema.prisma +2 -0
  103. package/lib/src/generated/client/wasm.js +1 -0
  104. package/lib/src/middleware/csrfMiddleware.js +13 -5
  105. package/lib/src/middleware/rateLimitMiddleware.js +19 -5
  106. package/lib/src/middleware/roleGuard.js +20 -0
  107. package/lib/src/services/AIService.js +13 -3
  108. package/lib/src/services/NotificationService.js +28 -28
  109. package/lib/src/services/bug-report/BugReportService.js +8 -2
  110. package/lib/src/services/lease/LeaseService.js +71 -20
  111. package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
  112. package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
  113. package/lib/test/helpers/expressRoutes.js +41 -0
  114. package/lib/test/integration/team-visibility-control.spec.js +2 -2
  115. package/lib/test/unit/access-scopes.spec.js +227 -0
  116. package/lib/test/unit/api-error-handling.spec.js +179 -0
  117. package/lib/test/unit/bug-report/route.spec.js +29 -0
  118. package/lib/test/unit/bug-report/service.spec.js +30 -0
  119. package/lib/test/unit/control-honest-answers.spec.js +134 -0
  120. package/lib/test/unit/device-allocation-routes.spec.js +232 -0
  121. package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
  122. package/lib/test/unit/install-repository-app-team.spec.js +4 -1
  123. package/lib/test/unit/lease/LeaseService.spec.js +5 -4
  124. package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
  125. package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
  126. package/lib/test/unit/omni-vision-failures.spec.js +81 -0
  127. package/lib/test/unit/openapi-coverage.spec.js +114 -0
  128. package/lib/test/unit/profile-router.test.js +42 -0
  129. package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
  130. package/lib/test/unit/recording-orchestrator.spec.js +80 -0
  131. package/lib/test/unit/recordings-library-routes.spec.js +37 -0
  132. package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
  133. package/lib/test/unit/reset-link.test.js +2 -1
  134. package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
  135. package/lib/test/unit/users-router.test.js +14 -1
  136. package/lib/test/unit/wda-client-failures.spec.js +90 -0
  137. package/lib/test/unit/wda-client-session.spec.js +128 -0
  138. package/lib/test/unit/wda-clipboard-write.spec.js +135 -0
  139. package/lib/test/unit/webhook-delivery.spec.js +142 -0
  140. package/lib/tsconfig.tsbuildinfo +1 -1
  141. package/package.json +2 -2
  142. package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
  143. package/prisma/schema.prisma +2 -0
  144. package/scripts/dev/readme-screenshots.js +231 -0
  145. package/lib/public/assets/ai-settings-G9SymiPH.js +0 -21
  146. package/lib/public/assets/device-explorer-CoPR8DW3.js +0 -279
  147. package/lib/public/assets/maintenance-settings-DcRmTLS6.js +0 -6
  148. package/lib/public/assets/settings-B6cpMhsx.js +0 -6
  149. package/lib/public/assets/webhook-settings-BMzyUcx5.js +0 -1
  150. package/lib/src/app/swagger-docs.js +0 -1701
@@ -19,7 +19,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
19
19
  };
20
20
  var WDAClient_1;
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.WDAClient = void 0;
22
+ exports.WDAClient = exports.UnsupportedKeyError = void 0;
23
23
  const axios_1 = __importDefault(require("axios"));
24
24
  const semver_1 = __importDefault(require("semver"));
25
25
  const logger_1 = __importDefault(require("../../logger"));
@@ -36,6 +36,19 @@ const os_1 = __importDefault(require("os"));
36
36
  const fs_extra_1 = __importDefault(require("fs-extra"));
37
37
  const execFilePromise = (0, util_1.promisify)(child_process_1.execFile);
38
38
  const execPromise = (0, util_1.promisify)(child_process_1.exec);
39
+ /** WDA's answer for a session that no longer exists: 404 "invalid session id". */
40
+ function isInvalidSession(err) {
41
+ var _a, _b, _c;
42
+ return ((_c = (_b = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.error) === 'invalid session id';
43
+ }
44
+ /** A key name the iPhone has no equivalent for. Device control answers 400. */
45
+ class UnsupportedKeyError extends Error {
46
+ constructor(key) {
47
+ super(`The key "${key}" isn't available on an iPhone.`);
48
+ this.name = 'UnsupportedKeyError';
49
+ }
50
+ }
51
+ exports.UnsupportedKeyError = UnsupportedKeyError;
39
52
  let WDAClient = WDAClient_1 = class WDAClient {
40
53
  constructor() {
41
54
  this.log = logger_1.default.scope('WDAClient');
@@ -71,7 +84,7 @@ let WDAClient = WDAClient_1 = class WDAClient {
71
84
  }
72
85
  performWDACommand(udid_1, method_1, endpoint_1, data_1) {
73
86
  return __awaiter(this, arguments, void 0, function* (udid, method, endpoint, data, retryCount = 0, options) {
74
- var _a, _b, _c;
87
+ var _a, _b, _c, _d;
75
88
  const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
76
89
  if (!device)
77
90
  throw new Error(`Device ${udid} not found`);
@@ -83,12 +96,12 @@ let WDAClient = WDAClient_1 = class WDAClient {
83
96
  throw new Error(`WDA port not available for ${udid}`);
84
97
  const cacheKey = `${udid}:${port}`;
85
98
  let cached = this.wdaConnectionCache.get(cacheKey);
86
- if (!(cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
87
- const sid = typedi_1.Container.get(IOSStreamService_1.default).getWDASessionId(udid);
88
- if (sid) {
89
- cached = { host: '127.0.0.1', pathPrefix: `/session/${sid}`, sessionId: sid };
90
- this.wdaConnectionCache.set(cacheKey, cached);
91
- }
99
+ // The preview's session wins: each preview creates its own, so after a
100
+ // restart on the same port the cached one is dead.
101
+ const previewSid = typedi_1.Container.get(IOSStreamService_1.default).getWDASessionId(udid);
102
+ if (previewSid && previewSid !== (cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
103
+ cached = { host: '127.0.0.1', pathPrefix: `/session/${previewSid}`, sessionId: previewSid };
104
+ this.wdaConnectionCache.set(cacheKey, cached);
92
105
  }
93
106
  const prefixes = (cached === null || cached === void 0 ? void 0 : cached.sessionId) ? [`/session/${cached.sessionId}`] : ['', '/session/any'];
94
107
  let lastError;
@@ -98,8 +111,10 @@ let WDAClient = WDAClient_1 = class WDAClient {
98
111
  const targetHost = '127.0.0.1';
99
112
  for (const prefix of prefixes) {
100
113
  try {
114
+ // WDA serves /wda/homescreen only without a session; inside one it
115
+ // answers 404 "unknown command".
101
116
  const isSessionless = !(options === null || options === void 0 ? void 0 : options.useSessionPath) &&
102
- (['/status', '/health', '/wda/healthcheck'].includes(endpoint));
117
+ ['/status', '/health', '/wda/healthcheck', '/wda/homescreen'].includes(endpoint);
103
118
  const url = `http://${targetHost}:${port}${isSessionless ? '' : prefix}${endpoint}`;
104
119
  // Principal Stability: Screenshots, activation, and script execution need longer timeouts.
105
120
  // Heartbeats and Status checks should be snappy to fail-over quickly.
@@ -139,17 +154,33 @@ let WDAClient = WDAClient_1 = class WDAClient {
139
154
  if (retryCount < 2) {
140
155
  // Phase 1: Dead Session Recovery (404)
141
156
  if (status === 404 && prefix.includes('/session/')) {
157
+ const deadSession = isInvalidSession(err);
142
158
  // Special Intelligence: Some WDA versions don't support /execute, /wda/homescreen, etc.
143
159
  // We should NOT clear the session if these specific commands fail with 404,
144
- // as they might just be unsupported features.
160
+ // as they might just be unsupported features. A dead session is
161
+ // not one of those: it was, for every button after a preview restart.
145
162
  const NON_FATAL_ENDPOINTS = ['execute', 'homescreen', 'pressButton'];
146
- if (NON_FATAL_ENDPOINTS.some((e) => endpoint.includes(e))) {
163
+ if (!deadSession && NON_FATAL_ENDPOINTS.some((e) => endpoint.includes(e))) {
147
164
  this.log.debug(`[WDA] Command ${endpoint} failed with 404 (Unsupported or Logical Error). Skipping session reset.`);
148
165
  throw err;
149
166
  }
150
- this.log.warn(`[WDA] Session ${cached === null || cached === void 0 ? void 0 : cached.sessionId} appears dead. Resetting and retrying...`);
151
- this.wdaConnectionCache.delete(cacheKey);
152
- typedi_1.Container.get(IOSStreamService_1.default).setWDASessionId(udid, '');
167
+ // WDA names its live session in every answer, a refusal included.
168
+ const liveSid = (_d = err.response.data) === null || _d === void 0 ? void 0 : _d.sessionId;
169
+ const stream = typedi_1.Container.get(IOSStreamService_1.default);
170
+ if (deadSession && typeof liveSid === 'string' && liveSid !== (cached === null || cached === void 0 ? void 0 : cached.sessionId)) {
171
+ this.log.warn(`[WDA] Session ${cached === null || cached === void 0 ? void 0 : cached.sessionId} is gone; WebDriverAgent's is ${liveSid}. Retrying in it...`);
172
+ this.wdaConnectionCache.set(cacheKey, {
173
+ host: targetHost,
174
+ pathPrefix: `/session/${liveSid}`,
175
+ sessionId: liveSid,
176
+ });
177
+ stream.setWDASessionId(udid, liveSid);
178
+ }
179
+ else {
180
+ this.log.warn(`[WDA] Session ${cached === null || cached === void 0 ? void 0 : cached.sessionId} appears dead. Resetting and retrying...`);
181
+ this.wdaConnectionCache.delete(cacheKey);
182
+ stream.setWDASessionId(udid, '');
183
+ }
153
184
  return this.performWDACommand(udid, method, endpoint, data, retryCount + 1, options);
154
185
  }
155
186
  // Phase 2: Transient State Recovery (500)
@@ -331,13 +362,12 @@ let WDAClient = WDAClient_1 = class WDAClient {
331
362
  yield this.sendWDACommand(udid, 'post', '/wda/pressButton', { name: buttonName });
332
363
  }
333
364
  catch (e) {
334
- if (buttonName === 'home') {
335
- // Fallback for extremely old WDA
336
- yield this.sendWDACommand(udid, 'post', '/wda/homescreen', {}).catch(() => { });
337
- }
338
- else {
339
- this.log.debug(`[WDA] Hardware button '${buttonName}' failed: ${e.message}`);
340
- }
365
+ // Fallback for extremely old WDA. A failure is the caller's to see:
366
+ // it used to be swallowed, and control answered 200 for a button
367
+ // that was never pressed.
368
+ if (buttonName !== 'home')
369
+ throw e;
370
+ yield this.sendWDACommand(udid, 'post', '/wda/homescreen', {});
341
371
  }
342
372
  return;
343
373
  }
@@ -356,17 +386,24 @@ let WDAClient = WDAClient_1 = class WDAClient {
356
386
  }
357
387
  catch (e) {
358
388
  this.log.debug(`[WDA] Special key '${n}' failed via /wda/keys: ${e.message}`);
359
- // Fallback to /wda/type if keys fails
360
- yield this.sendWDACommand(udid, 'post', '/wda/type', { text: keyboardKeys[n] }).catch(() => { });
389
+ // Fallback to /wda/type if keys fails; if that fails too, say so.
390
+ yield this.sendWDACommand(udid, 'post', '/wda/type', { text: keyboardKeys[n] });
361
391
  }
362
392
  return;
363
393
  }
364
- // Default: try pressButton but catch errors (as many buttons aren't physical on iPhone)
394
+ // Anything else may be a button WDA knows by name. If WDA answered and
395
+ // refused it, the iPhone has no such key: say so rather than answer as if
396
+ // it was pressed. If WDA couldn't be reached, that is the failure.
365
397
  try {
366
398
  yield this.sendWDACommand(udid, 'post', '/wda/pressButton', { name: n });
367
399
  }
368
400
  catch (e) {
369
401
  this.log.debug(`[WDA] Generic pressButton failed for '${n}': ${e.message}`);
402
+ // A 404 is a missing session or route, never a missing button: WDA
403
+ // refuses a button it doesn't know with a 500.
404
+ if ((e === null || e === void 0 ? void 0 : e.response) && e.response.status !== 404)
405
+ throw new UnsupportedKeyError(String(key));
406
+ throw e;
370
407
  }
371
408
  });
372
409
  }
@@ -577,37 +614,112 @@ let WDAClient = WDAClient_1 = class WDAClient {
577
614
  }
578
615
  return value;
579
616
  }
617
+ // These three used to catch WDA's error and log it at debug level, so
618
+ // device control answered 200 for a clipboard write, lock or unlock that
619
+ // never happened. The error now reaches the route, which answers 500.
620
+ //
621
+ // On an iPhone, iOS ignores a pasteboard write from an app in the
622
+ // background and WDA still answers success, so the write is made with WDA's
623
+ // own app in front and read back before it counts.
580
624
  setClipboard(udid, content) {
581
625
  return __awaiter(this, void 0, void 0, function* () {
582
- try {
583
- yield this.sendWDACommand(udid, 'post', '/wda/setPasteboard', {
584
- content: Buffer.from(content).toString('base64'),
626
+ const encoded = Buffer.from(content).toString('base64');
627
+ const write = () => this.sendWDACommand(udid, 'post', '/wda/setPasteboard', {
628
+ content: encoded,
629
+ contentType: 'plaintext',
630
+ });
631
+ if (!(yield this.isRealDevice(udid))) {
632
+ yield write();
633
+ return;
634
+ }
635
+ yield this.withWDAInFront(udid, () => __awaiter(this, void 0, void 0, function* () {
636
+ var _a;
637
+ yield write();
638
+ const res = yield this.sendWDACommand(udid, 'post', '/wda/getPasteboard', {
585
639
  contentType: 'plaintext',
586
640
  });
587
- }
588
- catch (e) {
589
- this.log.debug(`Failed to set clipboard for ${udid}: ${e.message}\n${e.stack}`);
590
- }
641
+ const readBack = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value;
642
+ if (readBack !== encoded && this.parsePasteboardResponse(res.data) !== content.trim()) {
643
+ throw new Error('The iPhone didn’t take the clipboard text.');
644
+ }
645
+ }));
591
646
  });
592
647
  }
593
- lock(udid) {
648
+ isRealDevice(udid) {
649
+ return __awaiter(this, void 0, void 0, function* () {
650
+ const device = yield device_store_1.DeviceStoreFactory.getStore()
651
+ .findDevice({ udid })
652
+ .catch(() => undefined);
653
+ return !!(device === null || device === void 0 ? void 0 : device.realDevice);
654
+ });
655
+ }
656
+ activeBundleId(udid) {
657
+ return __awaiter(this, void 0, void 0, function* () {
658
+ var _a, _b, _c;
659
+ const res = yield this.sendWDACommand(udid, 'get', '/wda/activeAppInfo');
660
+ return ((_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.bundleId) || ((_c = res.data) === null || _c === void 0 ? void 0 : _c.bundleId);
661
+ });
662
+ }
663
+ /**
664
+ * Runs `action` with WDA's own app in front, then puts back the app that
665
+ * was: the home screen, or the app by its bundle id.
666
+ *
667
+ * Bringing WDA's app forward waits for it to go idle, which it never does:
668
+ * about 21 s on an iPhone 14 Plus, against 0.1 s with the wait off. The wait
669
+ * is a WDA setting shared with any Appium session on the phone, so it is
670
+ * turned off only when its value could be read, and always put back.
671
+ */
672
+ withWDAInFront(udid, action) {
594
673
  return __awaiter(this, void 0, void 0, function* () {
674
+ const previous = yield this.activeBundleId(udid).catch(() => undefined);
675
+ const wdaBundleId = (yield typedi_1.Container.get(IOSStreamService_1.default).detectWDABundleId(udid)) ||
676
+ 'com.qasecret.WebDriverAgentRunner.xctrunner';
677
+ const idleWait = yield this.sendWDACommand(udid, 'get', '/appium/settings')
678
+ .then((res) => { var _a, _b; return (_b = (_a = res.data) === null || _a === void 0 ? void 0 : _a.value) === null || _b === void 0 ? void 0 : _b.waitForIdleTimeout; })
679
+ .catch(() => undefined);
680
+ const setIdleWait = (seconds) => this.sendWDACommand(udid, 'post', '/appium/settings', {
681
+ settings: { waitForIdleTimeout: seconds },
682
+ });
595
683
  try {
596
- yield this.sendWDACommand(udid, 'post', '/wda/lock', {});
684
+ if (typeof idleWait === 'number')
685
+ yield setIdleWait(0);
686
+ yield this.sendWDACommand(udid, 'post', '/wda/apps/activate', { bundleId: wdaBundleId });
687
+ // Activation answers before the app is in front, and a write made
688
+ // before then is lost.
689
+ const deadline = Date.now() + 3000;
690
+ while (Date.now() < deadline) {
691
+ if ((yield this.activeBundleId(udid).catch(() => undefined)) === wdaBundleId)
692
+ break;
693
+ yield new Promise((r) => setTimeout(r, 100));
694
+ }
695
+ yield action();
597
696
  }
598
- catch (e) {
599
- this.log.debug(`Failed to lock device ${udid}: ${e.message}\n${e.stack}`);
697
+ finally {
698
+ try {
699
+ if (previous === 'com.apple.springboard') {
700
+ yield this.sendWDACommand(udid, 'post', '/wda/homescreen', {});
701
+ }
702
+ else if (previous && previous !== wdaBundleId) {
703
+ yield this.sendWDACommand(udid, 'post', '/wda/apps/activate', { bundleId: previous });
704
+ }
705
+ }
706
+ catch (e) {
707
+ this.log.warn(`[Clipboard] Couldn't put back ${previous} on ${udid}: ${e.message}`);
708
+ }
709
+ if (typeof idleWait === 'number') {
710
+ yield setIdleWait(idleWait).catch((e) => this.log.warn(`[Clipboard] Couldn't restore WDA's idle wait on ${udid}: ${e.message}`));
711
+ }
600
712
  }
601
713
  });
602
714
  }
715
+ lock(udid) {
716
+ return __awaiter(this, void 0, void 0, function* () {
717
+ yield this.sendWDACommand(udid, 'post', '/wda/lock', {});
718
+ });
719
+ }
603
720
  unlock(udid) {
604
721
  return __awaiter(this, void 0, void 0, function* () {
605
- try {
606
- yield this.sendWDACommand(udid, 'post', '/wda/unlock', {});
607
- }
608
- catch (e) {
609
- this.log.debug(`Failed to unlock device ${udid}: ${e.message}\n${e.stack}`);
610
- }
722
+ yield this.sendWDACommand(udid, 'post', '/wda/unlock', {});
611
723
  });
612
724
  }
613
725
  /**