@xenon-device-management/xenon 2.13.1 → 2.14.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 (223) hide show
  1. package/README.md +11 -5
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
  6. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
  7. package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
  10. package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
  11. package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
  14. package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
  15. package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
  16. package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
  17. package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
  18. package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
  19. package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
  20. package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
  21. package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
  22. package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
  24. package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
  25. package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
  26. package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
  27. package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
  28. package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
  29. package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
  30. package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
  31. package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
  32. package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
  33. package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
  34. package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
  35. package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
  36. package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
  37. package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
  38. package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
  39. package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
  40. package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
  41. package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
  42. package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
  43. package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
  44. package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
  45. package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
  47. package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
  49. package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
  50. package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
  51. package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
  52. package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
  53. package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
  54. package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
  55. package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
  56. package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
  57. package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-CThoxDFO.js → selector-health-page-B4rA1Xkv.js} +2 -2
  60. package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
  61. package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
  62. package/lib/public/assets/{stat-tile-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
  63. package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
  64. package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
  65. package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
  66. package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
  67. package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
  69. package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
  70. package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
  71. package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
  72. package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
  73. package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
  74. package/lib/public/index.html +2 -2
  75. package/lib/schema.json +19 -46
  76. package/lib/src/XenonCapabilityManager.js +22 -5
  77. package/lib/src/app/openapi/control.yaml +71 -20
  78. package/lib/src/app/openapi/grid.yaml +23 -5
  79. package/lib/src/app/openapi/platform.yaml +136 -28
  80. package/lib/src/app/routers/control.js +30 -5
  81. package/lib/src/app/routers/dashboard.js +29 -1
  82. package/lib/src/app/routers/grid.js +32 -10
  83. package/lib/src/app/routers/streamStartConflict.js +3 -0
  84. package/lib/src/app/routers/streamType.js +7 -2
  85. package/lib/src/app/routers/webhook.js +8 -4
  86. package/lib/src/config.js +50 -5
  87. package/lib/src/dashboard/commands.js +185 -141
  88. package/lib/src/dashboard/event-manager.js +39 -6
  89. package/lib/src/data-service/appleFamily.js +43 -0
  90. package/lib/src/data-service/db.js +11 -0
  91. package/lib/src/data-service/device-service.js +46 -1
  92. package/lib/src/data-service/device-store.js +66 -3
  93. package/lib/src/data-service/deviceClaims.js +37 -0
  94. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  95. package/lib/src/data-service/deviceSettings.js +79 -0
  96. package/lib/src/data-service/prisma-store.js +152 -9
  97. package/lib/src/data-service/web-config-service.js +43 -92
  98. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  99. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  100. package/lib/src/device-managers/shellCommands.js +96 -0
  101. package/lib/src/device-utils.js +143 -37
  102. package/lib/src/generated/client/edge.js +17 -3
  103. package/lib/src/generated/client/index-browser.js +14 -0
  104. package/lib/src/generated/client/index.d.ts +2054 -379
  105. package/lib/src/generated/client/index.js +17 -3
  106. package/lib/src/generated/client/package.json +1 -1
  107. package/lib/src/generated/client/schema.prisma +35 -7
  108. package/lib/src/generated/client/wasm.js +14 -0
  109. package/lib/src/interceptors/CommandInterceptor.js +29 -8
  110. package/lib/src/interceptors/xenonScripts.js +55 -0
  111. package/lib/src/interfaces/IPluginArgs.js +1 -2
  112. package/lib/src/middleware/deviceAccessGuard.js +63 -9
  113. package/lib/src/plugin.js +15 -1
  114. package/lib/src/prisma.js +1 -1
  115. package/lib/src/scripts/database-check.js +124 -0
  116. package/lib/src/services/AIService.js +99 -0
  117. package/lib/src/services/CleanupService.js +6 -1
  118. package/lib/src/services/InterceptorService.js +70 -13
  119. package/lib/src/services/NetworkConditioningService.js +85 -88
  120. package/lib/src/services/NotificationService.js +36 -29
  121. package/lib/src/services/OrphanSweeper.js +5 -0
  122. package/lib/src/services/PortAllocator.js +5 -0
  123. package/lib/src/services/ServerManager.js +97 -8
  124. package/lib/src/services/SessionLifecycleService.js +63 -53
  125. package/lib/src/services/ShutdownCoordinator.js +33 -0
  126. package/lib/src/services/TeamService.js +20 -3
  127. package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
  128. package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
  129. package/lib/src/services/device-access/leaseHold.js +26 -0
  130. package/lib/src/services/device-access/ticketActorAccess.js +6 -0
  131. package/lib/src/services/healing/HealingOrchestrator.js +32 -1
  132. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  133. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  134. package/lib/src/services/lease/activeLeases.js +37 -0
  135. package/lib/src/services/network/AndroidRadios.js +71 -0
  136. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  137. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  138. package/lib/src/services/network/adbForPhone.js +60 -0
  139. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  140. package/lib/src/services/network/xenonProxy.js +107 -0
  141. package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
  142. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  143. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  144. package/lib/src/services/omni-vision/screenScale.js +68 -0
  145. package/lib/src/services/recording/busy-precheck.js +24 -3
  146. package/lib/src/services/recording/concurrency-gate.js +12 -3
  147. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  148. package/lib/src/services/settings/labSettings.js +149 -0
  149. package/lib/src/services/webhookEvents.js +173 -0
  150. package/lib/test/helpers/fake-adb.js +58 -0
  151. package/lib/test/helpers/fake-device-table.js +63 -24
  152. package/lib/test/helpers/scratch-database.js +1 -0
  153. package/lib/test/unit/TeamService.test.js +3 -0
  154. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  155. package/lib/test/unit/apple-family.spec.js +36 -0
  156. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  157. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  158. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  159. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  160. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  161. package/lib/test/unit/database-check.spec.js +108 -0
  162. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  163. package/lib/test/unit/device-access-lease.spec.js +270 -0
  164. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  165. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  166. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  167. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  168. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  169. package/lib/test/unit/emulator-boot.spec.js +91 -0
  170. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
  171. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  172. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  173. package/lib/test/unit/json-logging-option.spec.js +74 -0
  174. package/lib/test/unit/lab-settings.spec.js +218 -0
  175. package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
  176. package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
  177. package/lib/test/unit/lease-held-phone.spec.js +196 -0
  178. package/lib/test/unit/max-sessions.spec.js +240 -0
  179. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  180. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  181. package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
  182. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  183. package/lib/test/unit/omni-vision.spec.js +1 -1
  184. package/lib/test/unit/partial-config-file.spec.js +114 -0
  185. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  186. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  187. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  188. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  189. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  190. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  191. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  192. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  193. package/lib/test/unit/plugin.spec.js +3 -3
  194. package/lib/test/unit/recording-options.spec.js +181 -0
  195. package/lib/test/unit/schema-required-args.spec.js +21 -16
  196. package/lib/test/unit/self-healing-boot.spec.js +75 -0
  197. package/lib/test/unit/self-healing-setting.spec.js +272 -0
  198. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  199. package/lib/test/unit/shell-commands.spec.js +133 -0
  200. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  201. package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
  202. package/lib/test/unit/stream-status-type.spec.js +4 -0
  203. package/lib/test/unit/vision-assertion.spec.js +118 -0
  204. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  205. package/lib/test/unit/web-config-service.spec.js +79 -0
  206. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  207. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  208. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  209. package/lib/tsconfig.tsbuildinfo +1 -1
  210. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  211. package/package.json +1 -1
  212. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  213. package/prisma/schema.prisma +35 -7
  214. package/schema.json +19 -46
  215. package/scripts/generate-types-from-schema.js +7 -2
  216. package/scripts/lib/always-present-plugin-args.js +42 -0
  217. package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
  218. package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
  219. package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
  220. package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
  221. package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
  222. package/lib/public/assets/settings-C1VIluuT.js +0 -6
  223. package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
@@ -18,109 +18,60 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.WebConfigService = void 0;
19
19
  const prisma_1 = require("../prisma");
20
20
  const typedi_1 = require("typedi");
21
+ /** How each saved setting is read back: they are all stored as text. */
22
+ const SETTINGS = {
23
+ enableSelfHealing: 'boolean',
24
+ healthCheckIntervalMs: 'number',
25
+ healthCheckSchedule: 'text',
26
+ buildCleanupDays: 'number',
27
+ buildCleanupMaxCount: 'number',
28
+ buildCleanupSchedule: 'text',
29
+ deleteBuildAssets: 'boolean',
30
+ };
31
+ /**
32
+ * The lab settings the dashboard saves, one `WebConfig` row each, keyed by the
33
+ * setting's name.
34
+ *
35
+ * A row's `id` is the table's primary key, and used to be written as 'global'
36
+ * for every setting, so the table held one: the first setting saved was kept,
37
+ * and saving a second failed with "Unique constraint failed on the fields
38
+ * (`id`)". A setting's row now has its own name as its id, as the metrics
39
+ * counters kept in the same table do. A row an earlier version saved under
40
+ * 'global' is still read, and still the one the next save of that setting
41
+ * updates.
42
+ */
21
43
  let WebConfigService = class WebConfigService {
22
- constructor() {
23
- this.CONFIG_ID = 'global';
24
- }
25
44
  getConfig() {
26
45
  return __awaiter(this, void 0, void 0, function* () {
27
- const configs = yield prisma_1.prisma.webConfig.findMany({
28
- where: { id: this.CONFIG_ID },
46
+ const rows = yield prisma_1.prisma.webConfig.findMany({
47
+ where: { name: { in: Object.keys(SETTINGS) } },
29
48
  });
30
49
  const result = {};
31
- for (const config of configs) {
32
- if (config.name === 'healthCheckIntervalMs') {
33
- result.healthCheckIntervalMs = parseInt(config.value);
34
- }
35
- else if (config.name === 'healthCheckSchedule') {
36
- result.healthCheckSchedule = config.value;
37
- }
38
- else if (config.name === 'buildCleanupDays') {
39
- result.buildCleanupDays = parseInt(config.value);
40
- }
41
- else if (config.name === 'buildCleanupMaxCount') {
42
- result.buildCleanupMaxCount = parseInt(config.value);
43
- }
44
- else if (config.name === 'buildCleanupSchedule') {
45
- result.buildCleanupSchedule = config.value;
46
- }
47
- else if (config.name === 'deleteBuildAssets') {
48
- result.deleteBuildAssets = config.value === 'true';
49
- }
50
+ for (const row of rows) {
51
+ const kind = SETTINGS[row.name];
52
+ if (kind === 'number')
53
+ result[row.name] = parseInt(row.value);
54
+ else if (kind === 'boolean')
55
+ result[row.name] = row.value === 'true';
56
+ else if (kind === 'text')
57
+ result[row.name] = row.value;
50
58
  }
51
59
  return result;
52
60
  });
53
61
  }
54
62
  setConfig(config) {
55
63
  return __awaiter(this, void 0, void 0, function* () {
56
- const promises = [];
57
- if (config.healthCheckIntervalMs !== undefined) {
58
- promises.push(prisma_1.prisma.webConfig.upsert({
59
- where: { name: 'healthCheckIntervalMs' },
60
- update: { value: config.healthCheckIntervalMs.toString() },
61
- create: {
62
- id: this.CONFIG_ID,
63
- name: 'healthCheckIntervalMs',
64
- value: config.healthCheckIntervalMs.toString(),
65
- },
66
- }));
67
- }
68
- if (config.healthCheckSchedule !== undefined) {
69
- promises.push(prisma_1.prisma.webConfig.upsert({
70
- where: { name: 'healthCheckSchedule' },
71
- update: { value: config.healthCheckSchedule },
72
- create: {
73
- id: this.CONFIG_ID,
74
- name: 'healthCheckSchedule',
75
- value: config.healthCheckSchedule,
76
- },
77
- }));
78
- }
79
- if (config.buildCleanupDays !== undefined) {
80
- promises.push(prisma_1.prisma.webConfig.upsert({
81
- where: { name: 'buildCleanupDays' },
82
- update: { value: config.buildCleanupDays.toString() },
83
- create: {
84
- id: this.CONFIG_ID,
85
- name: 'buildCleanupDays',
86
- value: config.buildCleanupDays.toString(),
87
- },
88
- }));
89
- }
90
- if (config.buildCleanupMaxCount !== undefined) {
91
- promises.push(prisma_1.prisma.webConfig.upsert({
92
- where: { name: 'buildCleanupMaxCount' },
93
- update: { value: config.buildCleanupMaxCount.toString() },
94
- create: {
95
- id: this.CONFIG_ID,
96
- name: 'buildCleanupMaxCount',
97
- value: config.buildCleanupMaxCount.toString(),
98
- },
99
- }));
100
- }
101
- if (config.buildCleanupSchedule !== undefined) {
102
- promises.push(prisma_1.prisma.webConfig.upsert({
103
- where: { name: 'buildCleanupSchedule' },
104
- update: { value: config.buildCleanupSchedule },
105
- create: {
106
- id: this.CONFIG_ID,
107
- name: 'buildCleanupSchedule',
108
- value: config.buildCleanupSchedule,
109
- },
110
- }));
111
- }
112
- if (config.deleteBuildAssets !== undefined) {
113
- promises.push(prisma_1.prisma.webConfig.upsert({
114
- where: { name: 'deleteBuildAssets' },
115
- update: { value: config.deleteBuildAssets.toString() },
116
- create: {
117
- id: this.CONFIG_ID,
118
- name: 'deleteBuildAssets',
119
- value: config.deleteBuildAssets.toString(),
120
- },
121
- }));
122
- }
123
- yield Promise.all(promises);
64
+ const writes = Object.keys(SETTINGS)
65
+ .filter((name) => config[name] !== undefined && config[name] !== null)
66
+ .map((name) => {
67
+ const value = String(config[name]);
68
+ return prisma_1.prisma.webConfig.upsert({
69
+ where: { name },
70
+ update: { value },
71
+ create: { id: name, name, value },
72
+ });
73
+ });
74
+ yield Promise.all(writes);
124
75
  });
125
76
  }
126
77
  };
@@ -77,6 +77,7 @@ const device_service_1 = require("../data-service/device-service");
77
77
  const device_store_1 = require("../data-service/device-store");
78
78
  const Devices_1 = __importDefault(require("./cloud/Devices"));
79
79
  const DeviceLockManager_1 = require("./android/DeviceLockManager");
80
+ const shellCommands_1 = require("./shellCommands");
80
81
  const androidIdentity_1 = require("./android/androidIdentity");
81
82
  const deviceIdentity_1 = require("./deviceIdentity");
82
83
  const localDeviceHosts_1 = require("./localDeviceHosts");
@@ -1151,43 +1152,20 @@ let AndroidDeviceManager = class AndroidDeviceManager {
1151
1152
  }
1152
1153
  executeShell(udid, command) {
1153
1154
  return __awaiter(this, void 0, void 0, function* () {
1154
- const ALLOWED_COMMANDS = [
1155
- 'ls',
1156
- 'ps',
1157
- 'top',
1158
- 'dumpsys battery',
1159
- 'dumpsys wifi',
1160
- 'dumpsys power',
1161
- 'whoami',
1162
- 'getprop',
1163
- 'pm list packages',
1164
- 'ip addr',
1165
- 'cat /proc/meminfo',
1166
- 'cat /proc/cpuinfo',
1167
- 'date',
1168
- 'uptime',
1169
- 'netstat',
1170
- ];
1171
- // Basic sanitation
1172
- const safeCommand = command.trim();
1173
- // Check if the command starts with any allowed prefix
1174
- const isAllowed = ALLOWED_COMMANDS.some((prefix) => safeCommand.startsWith(prefix));
1175
- if (!isAllowed) {
1176
- logger_1.default.warn(`Blocked potentially unsafe shell command on ${udid}: ${safeCommand}`);
1177
- throw new Error(`Command '${safeCommand}' is not allowed for security reasons.`);
1155
+ // An allowed command word for word, in words `sh` reads nothing into:
1156
+ // `adb shell` hands the joined line to the phone's shell (shellCommands.ts).
1157
+ const parsed = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.ANDROID_SHELL_COMMANDS);
1158
+ if ('refused' in parsed) {
1159
+ logger_1.default.warn(`Blocked shell command on ${udid}: ${command.trim()}`);
1160
+ throw new Error(parsed.refused);
1178
1161
  }
1179
- // Split command into args for adbExec
1180
- // This is a naive split, but safe enough for the allowed commands which don't use complex quoting
1181
- const args = safeCommand.split(/\s+/);
1182
- logger_1.default.info(`Executing shell command on ${udid}: ${safeCommand}`);
1162
+ logger_1.default.info(`Executing shell command on ${udid}: ${parsed.argv.join(' ')}`);
1183
1163
  const { adbInstance } = yield this.getAdb();
1184
1164
  if (!adbInstance)
1185
1165
  throw new Error('ADB is not available');
1186
1166
  // Use device lock to ensure thread safety
1187
1167
  return yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
1188
- // Direct raw shell execution might be better for piping, but adbExec is safer as it escapes args
1189
- // except we passed them as array, so standard child_process rules apply.
1190
- return yield adbInstance.adbExec(['-s', udid, 'shell', ...args], { timeout: 10000 });
1168
+ return yield adbInstance.adbExec(['-s', udid, 'shell', ...parsed.argv], { timeout: 10000 });
1191
1169
  }));
1192
1170
  });
1193
1171
  }
@@ -27,6 +27,7 @@ const typedi_1 = require("typedi");
27
27
  const ProcessRegistry_1 = require("../../services/ProcessRegistry");
28
28
  const IOSStreamService_1 = __importDefault(require("./IOSStreamService"));
29
29
  const IOSTunnels_1 = require("./IOSTunnels");
30
+ const shellCommands_1 = require("../shellCommands");
30
31
  const device_store_1 = require("../../data-service/device-store");
31
32
  const child_process_1 = require("child_process");
32
33
  const util_1 = require("util");
@@ -1005,57 +1006,57 @@ let WDAClient = WDAClient_1 = class WDAClient {
1005
1006
  }
1006
1007
  executeShell(udid, command) {
1007
1008
  return __awaiter(this, void 0, void 0, function* () {
1008
- // Separate allowlists for simulators vs real devices
1009
- const SIMCTL_COMMANDS = ['listapps', 'get_app_container', 'list', 'getenv'];
1010
- const GOIOS_COMMANDS = ['apps', 'info', 'syslog', 'list', 'deviceinfo', 'diagnostics'];
1011
- const GENERIC_COMMANDS = ['ls', 'ps', 'top', 'whoami', 'date', 'uptime', 'netstat', 'id'];
1012
- const safeCommand = command.trim();
1013
- const commandWord = safeCommand.split(/\s+/)[0]; // First word is the command
1014
- // Determine device type
1009
+ // Simulators: a few simctl commands, and generic commands spawned in the
1010
+ // simulator. Real iPhones: go-ios commands for this phone only. Every
1011
+ // command is matched word for word, in plain words (shellCommands.ts).
1015
1012
  const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
1016
- const isSimulator = device && !device.realDevice;
1017
- // Check allowlists based on device type
1018
- const isSimctlAllowed = SIMCTL_COMMANDS.includes(commandWord);
1019
- const isGoiosAllowed = GOIOS_COMMANDS.includes(commandWord);
1020
- const isGenericAllowed = GENERIC_COMMANDS.some((prefix) => safeCommand.startsWith(prefix));
1021
- if (!isSimctlAllowed && !isGoiosAllowed && !isGenericAllowed) {
1022
- this.log.warn(`Blocked shell command on ${udid}: ${safeCommand}`);
1023
- throw new Error(`Command '${safeCommand}' is not allowed for security reasons.`);
1024
- }
1025
- const args = safeCommand.split(/\s+/);
1026
- const s = typedi_1.Container.get(IOSStreamService_1.default);
1027
- // Route to the correct tool
1028
- if (isSimctlAllowed && isSimulator) {
1029
- // Simulator-specific: xcrun simctl <command> <udid> [args...]
1030
- const [cmd, ...rest] = args;
1031
- const { stdout } = yield execFilePromise('xcrun', ['simctl', cmd, udid, ...rest]);
1032
- return stdout;
1033
- }
1034
- if (isGoiosAllowed) {
1035
- // Real device: go-ios <command> --udid <udid> [args...]
1036
- if (yield s.isGoIOSAvailable()) {
1037
- const { stdout } = yield execFilePromise(s.goIOSPath, [...args, '--udid', udid], {
1038
- env: Object.assign(Object.assign({}, process.env), { ENABLE_GO_IOS_AGENT: 'yes' }),
1013
+ const isSimulator = !!device && !device.realDevice;
1014
+ const word = command.trim().split(/\s+/)[0];
1015
+ const refuse = (reason) => {
1016
+ this.log.warn(`Blocked shell command on ${udid}: ${command.trim()}`);
1017
+ return new Error(reason);
1018
+ };
1019
+ if (isSimulator) {
1020
+ const simctl = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_SIMCTL_COMMANDS);
1021
+ if ('argv' in simctl) {
1022
+ const [cmd, ...rest] = simctl.argv;
1023
+ const { stdout } = yield execFilePromise('xcrun', ['simctl', cmd, udid, ...rest], {
1024
+ timeout: 10000,
1039
1025
  });
1040
1026
  return stdout;
1041
1027
  }
1042
- throw new Error(`Command '${commandWord}' requires go-ios but it is not available.`);
1028
+ const spawned = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_SPAWN_COMMANDS);
1029
+ if ('refused' in spawned)
1030
+ throw refuse(spawned.refused);
1031
+ const { stdout } = yield execFilePromise('xcrun', ['simctl', 'spawn', udid, ...spawned.argv], {
1032
+ timeout: 10000,
1033
+ });
1034
+ return stdout;
1043
1035
  }
1044
- if (isSimctlAllowed && !isSimulator) {
1045
- // User tried a simctl command on a real device - give helpful error
1046
- throw new Error(`Command '${commandWord}' is only available for Simulators. For real devices, try: ${GOIOS_COMMANDS.join(', ')}`);
1036
+ const onDevice = (0, shellCommands_1.parseShellCommand)(command, shellCommands_1.IOS_DEVICE_COMMANDS);
1037
+ if ('refused' in onDevice) {
1038
+ const simulatorOnly = [...shellCommands_1.IOS_SIMCTL_COMMANDS, ...shellCommands_1.IOS_SPAWN_COMMANDS].some((c) => c.words[0] === word);
1039
+ if (simulatorOnly) {
1040
+ throw refuse(`'${word}' runs on simulators only. On a real iPhone, try: ${shellCommands_1.IOS_DEVICE_COMMANDS.map((c) => c.words.join(' ')).join(', ')}.`);
1041
+ }
1042
+ throw refuse(onDevice.refused);
1047
1043
  }
1048
- // Generic system command - try simctl first, fallback to go-ios
1049
- if (isSimulator) {
1050
- const { stdout } = yield execFilePromise('xcrun', ['simctl', 'spawn', udid, ...args]);
1051
- return stdout;
1044
+ // Xenon names the phone; a command may not name another one.
1045
+ if (onDevice.argv.some((arg) => arg.startsWith('--udid'))) {
1046
+ throw refuse(`Command '${command.trim()}' is not allowed: Xenon chooses the device.`);
1052
1047
  }
1053
- else if (yield s.isGoIOSAvailable()) {
1054
- // For real devices, generic commands aren't directly possible via go-ios
1055
- // Try go-ios launch/exec if available, otherwise inform user
1056
- throw new Error(`System command '${commandWord}' is not available on real devices. Use device-specific commands: ${GOIOS_COMMANDS.join(', ')}`);
1048
+ const s = typedi_1.Container.get(IOSStreamService_1.default);
1049
+ if (!(yield s.isGoIOSAvailable())) {
1050
+ throw new Error(`'${word}' needs go-ios, which is not installed on this server.`);
1057
1051
  }
1058
- throw new Error(`Could not execute command: no suitable runtime found for ${isSimulator ? 'simulator' : 'real device'}`);
1052
+ // On iOS 17+ go-ios reaches the phone only through its tunnel.
1053
+ const tunnels = typedi_1.Container.get(IOSTunnels_1.IOSTunnels);
1054
+ yield tunnels.borrow(udid);
1055
+ const { stdout } = yield execFilePromise(s.goIOSPath, [...onDevice.argv, '--udid', udid], {
1056
+ env: tunnels.envFor(udid),
1057
+ timeout: 10000,
1058
+ });
1059
+ return stdout;
1059
1060
  });
1060
1061
  }
1061
1062
  };
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /**
3
+ * What the device control Shell tab may run.
4
+ *
5
+ * Android runs a command through `adb shell`, which joins the words and hands
6
+ * them to the phone's `sh`. Through 2.13.2 the allow-list was a `startsWith`
7
+ * test on the whole line, so `ls; reboot`, `ls && pm uninstall …` and `lsof`
8
+ * all ran. Now a command must begin with an allowed command word for word, and
9
+ * every word of it may only use characters a shell gives no meaning to, so the
10
+ * phone's shell can't read anything into it. A command that changes or reads
11
+ * more than the entry is for (`dumpsys battery set …`, `cat` of another file)
12
+ * takes no arguments.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.IOS_DEVICE_COMMANDS = exports.IOS_SIMULATOR_COMMANDS = exports.IOS_SPAWN_COMMANDS = exports.IOS_SIMCTL_COMMANDS = exports.ANDROID_SHELL_COMMANDS = void 0;
16
+ exports.parseShellCommand = parseShellCommand;
17
+ /** Letters, digits and `._/:=@%+,-`: nothing `sh` expands, splits or redirects on. */
18
+ const PLAIN_WORD = /^[A-Za-z0-9._/:=@%+,-]+$/;
19
+ exports.ANDROID_SHELL_COMMANDS = [
20
+ { words: ['ls'], args: 'plain' },
21
+ { words: ['ps'], args: 'plain' },
22
+ { words: ['top'], args: 'plain' },
23
+ { words: ['dumpsys', 'battery'], args: 'none' },
24
+ { words: ['dumpsys', 'wifi'], args: 'none' },
25
+ { words: ['dumpsys', 'power'], args: 'none' },
26
+ { words: ['whoami'], args: 'none' },
27
+ { words: ['getprop'], args: 'plain' },
28
+ { words: ['pm', 'list', 'packages'], args: 'plain' },
29
+ { words: ['ip', 'addr'], args: 'plain' },
30
+ { words: ['cat', '/proc/meminfo'], args: 'none' },
31
+ { words: ['cat', '/proc/cpuinfo'], args: 'none' },
32
+ { words: ['date'], args: 'plain' },
33
+ { words: ['uptime'], args: 'none' },
34
+ { words: ['netstat'], args: 'plain' },
35
+ ];
36
+ /** `xcrun simctl <command> <udid> [args]` on a simulator. */
37
+ exports.IOS_SIMCTL_COMMANDS = [
38
+ { words: ['listapps'], args: 'none' },
39
+ { words: ['get_app_container'], args: 'plain' },
40
+ { words: ['getenv'], args: 'plain' },
41
+ ];
42
+ /**
43
+ * `xcrun simctl spawn <udid> <command>` on a simulator. A simulator's processes
44
+ * see this Mac's file system, so these take no arguments.
45
+ */
46
+ exports.IOS_SPAWN_COMMANDS = [
47
+ { words: ['ls'], args: 'none' },
48
+ { words: ['ps'], args: 'none' },
49
+ { words: ['whoami'], args: 'none' },
50
+ { words: ['date'], args: 'none' },
51
+ { words: ['uptime'], args: 'none' },
52
+ { words: ['id'], args: 'none' },
53
+ ];
54
+ exports.IOS_SIMULATOR_COMMANDS = [
55
+ ...exports.IOS_SIMCTL_COMMANDS,
56
+ ...exports.IOS_SPAWN_COMMANDS,
57
+ ];
58
+ /**
59
+ * go-ios commands on a real iPhone (`ios <command> [args] --udid <udid>`). None
60
+ * may name a device itself: Xenon adds this phone's `--udid`.
61
+ */
62
+ exports.IOS_DEVICE_COMMANDS = [
63
+ { words: ['apps'], args: 'plain' },
64
+ { words: ['info'], args: 'none' },
65
+ ];
66
+ /**
67
+ * The command's argv when it is allowed, or why it isn't. Words are split on
68
+ * spaces; quoting is not supported, since a quote is refused anyway.
69
+ */
70
+ function parseShellCommand(input, commands) {
71
+ const shown = input.trim();
72
+ // Words are separated by spaces only: a newline or tab means another line.
73
+ if (/[^\S ]/.test(shown)) {
74
+ return {
75
+ refused: `Command '${shown.split(/\s/)[0]}…' is not allowed: one line, words separated by spaces.`,
76
+ };
77
+ }
78
+ const argv = shown.split(/ +/).filter(Boolean);
79
+ if (argv.length === 0)
80
+ return { refused: 'Type a command.' };
81
+ const bad = argv.find((word) => !PLAIN_WORD.test(word));
82
+ if (bad !== undefined) {
83
+ return {
84
+ refused: `Command '${shown}' is not allowed: '${bad}' contains characters the shell would interpret.`,
85
+ };
86
+ }
87
+ const match = commands.find((command) => command.words.length <= argv.length && command.words.every((word, i) => argv[i] === word));
88
+ if (!match)
89
+ return { refused: `Command '${shown}' is not allowed for security reasons.` };
90
+ if (match.args === 'none' && argv.length > match.words.length) {
91
+ return {
92
+ refused: `Command '${shown}' is not allowed: '${match.words.join(' ')}' takes no arguments here.`,
93
+ };
94
+ }
95
+ return { argv };
96
+ }