@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
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "prisma-client-ef3fbf8faed5c4c712a2dce2686e5afc3ad0aa576aedb9c4cd9e5c99cfc0f03c",
2
+ "name": "prisma-client-ec1ae6fb328d1bc102086acd1f77e77477081d17b653d7f5092f083d0c8a4d34",
3
3
  "main": "index.js",
4
4
  "types": "index.d.ts",
5
5
  "browser": "index-browser.js",
@@ -242,6 +242,33 @@ model Device {
242
242
  @@index([claimSessionId])
243
243
  }
244
244
 
245
+ /// The settings people give one phone (an admin's team, tags and maintenance,
246
+ /// a member's reservation), kept apart from its Device row. That row is
247
+ /// deleted whenever the phone goes: unplugged, rebooted, offline in adb, a
248
+ /// restart, its node gone. This one stays, and a new row for the phone starts
249
+ /// from it, in the same write (src/data-service/deviceSettings.ts). Keyed as
250
+ /// Device is: a phone is a udid on the server that drives it, so another
251
+ /// server's phone with the same udid (an emulator's) never takes these. A hub
252
+ /// keeps the settings of its nodes' phones here too; a node's report never
253
+ /// writes them.
254
+ model DeviceSetting {
255
+ udid String
256
+ host String
257
+ teamId String?
258
+ team Team? @relation(fields: [teamId], references: [id], onDelete: SetNull)
259
+ /// JSON array, as Device.tags.
260
+ tags String?
261
+ userBlocked Boolean @default(false)
262
+ reservationReason String?
263
+ reservedBy String?
264
+ reservedByUserId String?
265
+ reservedUntil Float?
266
+ updatedAt DateTime @updatedAt
267
+
268
+ @@id([udid, host])
269
+ @@index([teamId])
270
+ }
271
+
245
272
  model PendingSession {
246
273
  id Int @id @default(autoincrement())
247
274
  capability_id String @unique
@@ -348,13 +375,14 @@ model ApiKey {
348
375
  }
349
376
 
350
377
  model Team {
351
- id String @id @default(uuid())
352
- name String @unique
353
- createdAt DateTime @default(now())
354
- devices Device[]
355
- apiKeys ApiKey[]
356
- members TeamMember[]
357
- apps App[]
378
+ id String @id @default(uuid())
379
+ name String @unique
380
+ createdAt DateTime @default(now())
381
+ devices Device[]
382
+ deviceSettings DeviceSetting[]
383
+ apiKeys ApiKey[]
384
+ members TeamMember[]
385
+ apps App[]
358
386
  }
359
387
 
360
388
  model SelectorState {
@@ -302,6 +302,19 @@ exports.Prisma.DeviceScalarFieldEnum = {
302
302
  nodeHold: 'nodeHold'
303
303
  };
304
304
 
305
+ exports.Prisma.DeviceSettingScalarFieldEnum = {
306
+ udid: 'udid',
307
+ host: 'host',
308
+ teamId: 'teamId',
309
+ tags: 'tags',
310
+ userBlocked: 'userBlocked',
311
+ reservationReason: 'reservationReason',
312
+ reservedBy: 'reservedBy',
313
+ reservedByUserId: 'reservedByUserId',
314
+ reservedUntil: 'reservedUntil',
315
+ updatedAt: 'updatedAt'
316
+ };
317
+
305
318
  exports.Prisma.PendingSessionScalarFieldEnum = {
306
319
  id: 'id',
307
320
  capability_id: 'capability_id',
@@ -536,6 +549,7 @@ exports.Prisma.ModelName = {
536
549
  SessionMetric: 'SessionMetric',
537
550
  App: 'App',
538
551
  Device: 'Device',
552
+ DeviceSetting: 'DeviceSetting',
539
553
  PendingSession: 'PendingSession',
540
554
  CLIArgs: 'CLIArgs',
541
555
  WebhookConfig: 'WebhookConfig',
@@ -65,6 +65,8 @@ const sessionContext_1 = require("../logging/sessionContext");
65
65
  const ProcessMetricsService_1 = require("../services/ProcessMetricsService");
66
66
  const AutowaitService_1 = require("../services/autowait/AutowaitService");
67
67
  const waitFor_1 = require("../services/autowait/waitFor");
68
+ const SelfHealingSwitch_1 = require("../services/settings/SelfHealingSwitch");
69
+ const xenonScripts_1 = require("./xenonScripts");
68
70
  let CommandInterceptor = class CommandInterceptor {
69
71
  constructor() {
70
72
  this.log = logger_1.default.scope('CommandInterceptor');
@@ -182,10 +184,14 @@ let CommandInterceptor = class CommandInterceptor {
182
184
  }
183
185
  if (aiCommand === 'assertVisualState') {
184
186
  this.log.info(`[Interceptor] Routing AI command: ${script} with payload: ${JSON.stringify(scriptArgs)}`);
185
- const instruction = typeof scriptArgs === 'string'
186
- ? scriptArgs
187
- : typeof scriptArgs === 'object' && (scriptArgs === null || scriptArgs === void 0 ? void 0 : scriptArgs.instruction)
188
- ? scriptArgs.instruction
187
+ // The condition as a plain string (`execute(script, 'The cart is
188
+ // empty')`, which arrives as ['The cart is empty']) or as
189
+ // { instruction }. A plain string used to arrive as ''.
190
+ const first = Array.isArray(scriptArgs) ? scriptArgs[0] : scriptArgs;
191
+ const instruction = typeof first === 'string'
192
+ ? first
193
+ : typeof first === 'object' && typeof (first === null || first === void 0 ? void 0 : first.instruction) === 'string'
194
+ ? first.instruction
189
195
  : '';
190
196
  return yield typedi_1.Container.get(AICommandService_1.AICommandService).assertVisualState(driver, instruction);
191
197
  }
@@ -216,13 +222,28 @@ let CommandInterceptor = class CommandInterceptor {
216
222
  }
217
223
  }
218
224
  }
225
+ // A session-details command (`xenon: setSessionName`, ...) is answered
226
+ // by the before-hook; its answer is what the test gets.
227
+ let answered = null;
219
228
  const shouldProceed = yield event_manager_1.DASHBORD_EVENT_MANAGER.beforeSessionCommand(sessionId, commandName, { body: { script: args[0], args: args[1] } }, {
220
- status: () => ({ json: (d) => d }),
229
+ status: () => ({
230
+ json: (d) => {
231
+ var _a;
232
+ answered = (_a = d === null || d === void 0 ? void 0 : d.value) !== null && _a !== void 0 ? _a : null;
233
+ return d;
234
+ },
235
+ }),
221
236
  setHeader: () => { },
222
237
  getHeader: () => { },
223
238
  });
224
239
  if (shouldProceed === false) {
225
- return null;
240
+ return answered;
241
+ }
242
+ // Every Xenon script this server has was answered above. Any other
243
+ // name used to be answered with null, as if it had worked.
244
+ if (commandName === 'execute' && (0, xenonScripts_1.xenonScriptName)(args[0]) !== null) {
245
+ const { errors } = yield Promise.resolve().then(() => __importStar(require('@appium/base-driver')));
246
+ throw new errors.UnknownCommandError((0, xenonScripts_1.unknownXenonScriptMessage)(args[0]));
226
247
  }
227
248
  }
228
249
  // --- OMNI-VISION: PROACTIVE SEARCH ---
@@ -288,7 +309,7 @@ let CommandInterceptor = class CommandInterceptor {
288
309
  catch (error) {
289
310
  if (this.isNoSuchElementError(error) &&
290
311
  ['findElement', 'findElements'].includes(commandName) &&
291
- pluginArgs.enableSelfHealing !== false) {
312
+ typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
292
313
  // §2.7 healing-tier capability gate: a session created with
293
314
  // xe:options.healingTiers (or the xenon:options alias) restricts
294
315
  // self-healing to those tier indices (1=Resilio, 2=Fuzzy XML, 3=OCR,
@@ -456,7 +477,7 @@ let CommandInterceptor = class CommandInterceptor {
456
477
  }, {}, JSON.stringify({ value: response, sessionId }));
457
478
  if (commandName === 'findElement' &&
458
479
  response &&
459
- pluginArgs.enableSelfHealing !== false) {
480
+ typedi_1.Container.get(SelfHealingSwitch_1.SelfHealingSwitch).isEnabled(pluginArgs)) {
460
481
  this.triggerLearning(driver, args, response, sessionId);
461
482
  }
462
483
  }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ /**
3
+ * Xenon's execute scripts: `driver.execute('xenon: <name>', ...)`, or `xe:`.
4
+ *
5
+ * Who answers them:
6
+ * - The five session-details commands (SESSION_DETAILS_COMMANDS in
7
+ * dashboard/commands.ts) are answered by the server that keeps the
8
+ * session's record: a hub, for a session on a node's phone, when its
9
+ * dashboard is on (its gateway's before-hook), otherwise the server that
10
+ * drives the phone.
11
+ * - Every other one is answered by the server that drives the phone
12
+ * (CommandInterceptor), because that is where the session's autowait
13
+ * settings, Omni-Vision and network capture live. A hub forwards them.
14
+ * - A name Xenon doesn't have fails with `unknown command`.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.XENON_SCRIPT_NAMES = void 0;
18
+ exports.xenonScriptName = xenonScriptName;
19
+ exports.unknownXenonScriptMessage = unknownXenonScriptMessage;
20
+ /** The names CommandInterceptor and the session-details commands answer. Keep in step with both. */
21
+ exports.XENON_SCRIPT_NAMES = [
22
+ 'setAutowaitProperties',
23
+ 'getAutowaitProperties',
24
+ 'smartTap',
25
+ 'omniClick',
26
+ 'visualTap',
27
+ 'uiInventory',
28
+ 'uiScanExport',
29
+ 'analyzeScreen',
30
+ 'omniScan',
31
+ 'assertVisualState',
32
+ 'addMock',
33
+ 'removeMock',
34
+ 'clearMocks',
35
+ 'getMocks',
36
+ 'getRequests',
37
+ 'exportHar',
38
+ 'setSessionName',
39
+ 'setSessionStatus',
40
+ 'debug',
41
+ 'addTag',
42
+ 'captureEvidence',
43
+ ];
44
+ /** The name in a `xenon: <name>` or `xe: <name>` script, or null for any other script. */
45
+ function xenonScriptName(script) {
46
+ if (typeof script !== 'string')
47
+ return null;
48
+ const match = /^\s*(?:xenon|xe)\s*:\s*(\S+)\s*$/.exec(script);
49
+ return match ? match[1] : null;
50
+ }
51
+ /** The message for a `xenon:` / `xe:` script Xenon doesn't have. */
52
+ function unknownXenonScriptMessage(script) {
53
+ return (`Unknown Xenon command "${script.trim()}". ` +
54
+ `Xenon's execute commands are: ${exports.XENON_SCRIPT_NAMES.join(', ')}.`);
55
+ }
@@ -30,7 +30,7 @@ exports.DefaultPluginArgs = {
30
30
  bindHostOrIp: 'auto',
31
31
  enableDashboard: false,
32
32
  bootedSimulators: false,
33
- healthCheckIntervalMs: 86400000,
33
+ healthCheckIntervalMs: 300000,
34
34
  healthCheckSchedule: undefined,
35
35
  removeDevicesFromDatabaseBeforeRunningThePlugin: false,
36
36
  databaseProvider: undefined,
@@ -50,7 +50,6 @@ exports.DefaultPluginArgs = {
50
50
  recordingCleanupMaxCount: 100,
51
51
  recordingFailedCleanupDays: 2,
52
52
  sessionHeartbeatIntervalMs: 30000,
53
- enableJsonLogging: false,
54
53
  autowait: {
55
54
  enabled: false,
56
55
  timeoutMs: 10000,
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.OWNERSHIP_CHECKED_READS = exports.UNGUARDED_CONTROL_MUTATIONS = void 0;
15
+ exports.LEASE_CHECKED_MUTATIONS = exports.OWNERSHIP_CHECKED_READS = exports.UNGUARDED_CONTROL_MUTATIONS = void 0;
16
16
  exports.deviceAccessGuard = deviceAccessGuard;
17
17
  const typedi_1 = require("typedi");
18
18
  const logger_1 = __importDefault(require("../logger"));
@@ -20,8 +20,10 @@ const manualLock_1 = require("../services/recording/manualLock");
20
20
  const controlDevice_1 = require("./controlDevice");
21
21
  const SessionOwnerResolver_1 = require("../services/device-access/SessionOwnerResolver");
22
22
  const deviceAccessPolicy_1 = require("../services/device-access/deviceAccessPolicy");
23
+ const leaseHold_1 = require("../services/device-access/leaseHold");
23
24
  const actor_1 = require("../services/device-access/actor");
24
25
  const deviceClaims_1 = require("../data-service/deviceClaims");
26
+ const activeLeases_1 = require("../services/lease/activeLeases");
25
27
  const STATE_CHANGING = new Set(['POST', 'PUT', 'DELETE', 'PATCH']);
26
28
  /**
27
29
  * Mutations under /control that this guard must NOT handle.
@@ -56,6 +58,14 @@ exports.UNGUARDED_CONTROL_MUTATIONS = [
56
58
  * "reads are cheap" assumption the mosaic depends on.
57
59
  */
58
60
  exports.OWNERSHIP_CHECKED_READS = ['clipboard', 'logs'];
61
+ /**
62
+ * Unguarded mutations that still take the lease rule: starting a preview
63
+ * takes a hold on the phone. stream/start's own conflict handling knows
64
+ * holds and sessions, not leases, and on a hub the call goes on to the
65
+ * node, which knows nothing of the hub's leases. So a stranger is refused a
66
+ * leased phone here, before either, and the lease holder passes on to it.
67
+ */
68
+ exports.LEASE_CHECKED_MUTATIONS = ['stream/start'];
59
69
  /**
60
70
  * Refuse /control mutations against a device somebody else holds.
61
71
  *
@@ -65,14 +75,16 @@ exports.OWNERSHIP_CHECKED_READS = ['clipboard', 'logs'];
65
75
  * handlers and was remembered in none of them.
66
76
  */
67
77
  function deviceAccessGuard(deps = {}) {
68
- var _a, _b, _c;
78
+ var _a, _b, _c, _d, _e;
69
79
  const findDevice = (_a = deps.findDevice) !== null && _a !== void 0 ? _a : controlDevice_1.findControlDeviceInStore;
70
80
  const resolveSessionOwner = (_b = deps.resolveSessionOwner) !== null && _b !== void 0 ? _b : ((sid) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).ownerOf(sid));
71
81
  const describeHolder = (_c = deps.describeHolder) !== null && _c !== void 0 ? _c : ((id) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).displayName(id));
82
+ const findActiveLease = (_d = deps.findActiveLease) !== null && _d !== void 0 ? _d : ((udid, host) => (0, activeLeases_1.activeLeaseOn)(udid, host));
83
+ const resolveLeaseHolder = (_e = deps.resolveLeaseHolder) !== null && _e !== void 0 ? _e : ((id) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).leaseHolderOf(id));
72
84
  const unavailable = (res) => res.status(503).json((0, deviceAccessPolicy_1.ownershipUnavailableBody)());
73
85
  return function (req, res, next) {
74
86
  return __awaiter(this, void 0, void 0, function* () {
75
- var _a, _b, _c;
87
+ var _a, _b, _c, _d, _e;
76
88
  // Router-level middleware has no req.params; controlAction reads the path.
77
89
  // It lowercases, as Express routes case-insensitively: `Stream/Start`
78
90
  // still reaches stream/start's own richer handling, and `Clipboard`
@@ -81,8 +93,9 @@ function deviceAccessGuard(deps = {}) {
81
93
  // Decide whether this request is in scope BEFORE touching the udid, so an
82
94
  // ordinary read (screenshot, stream, logs, page source) short-circuits here
83
95
  // exactly as it did when this guard was mutations-only.
96
+ const leaseOnly = req.method === 'POST' && exports.LEASE_CHECKED_MUTATIONS.includes(action);
84
97
  const inScope = STATE_CHANGING.has(req.method)
85
- ? !exports.UNGUARDED_CONTROL_MUTATIONS.includes(action)
98
+ ? !exports.UNGUARDED_CONTROL_MUTATIONS.includes(action) || leaseOnly
86
99
  : req.method === 'GET' && exports.OWNERSHIP_CHECKED_READS.includes(action);
87
100
  if (!action || !inScope)
88
101
  return next();
@@ -116,6 +129,26 @@ function deviceAccessGuard(deps = {}) {
116
129
  // resolved device to reason about ownership at all.
117
130
  if (!device)
118
131
  return next();
132
+ if (leaseOnly) {
133
+ if (actor.isAdmin)
134
+ return next();
135
+ let held;
136
+ try {
137
+ held = yield (0, leaseHold_1.leaseHoldFor)({ udid, host: device.host }, findActiveLease, resolveLeaseHolder);
138
+ }
139
+ catch (e) {
140
+ logger_1.default.error(`deviceAccessGuard: lease lookup failed for ${udid}: ${(_b = e === null || e === void 0 ? void 0 : e.message) !== null && _b !== void 0 ? _b : e}`);
141
+ return unavailable(res);
142
+ }
143
+ if (!held || (0, deviceAccessPolicy_1.isLeaseHolder)(held, actor.userId, actor.apiKeyId))
144
+ return next();
145
+ return deny(req, res, udid, actor.userId, {
146
+ allow: false,
147
+ code: 'device_held_by_another_user',
148
+ holderId: (_c = held.holderUserId) !== null && _c !== void 0 ? _c : '',
149
+ heldBy: 'lease',
150
+ });
151
+ }
119
152
  let sessionOwnerUserId = null;
120
153
  if (device.busy && device.session_id && !(0, manualLock_1.isManualLock)(device.session_id)) {
121
154
  try {
@@ -124,7 +157,19 @@ function deviceAccessGuard(deps = {}) {
124
157
  catch (e) {
125
158
  // Same reasoning as the device lookup above: not knowing who owns a
126
159
  // live session is exactly the case where we must not guess allow.
127
- logger_1.default.error(`deviceAccessGuard: session owner lookup failed for ${device.session_id}: ${(_b = e === null || e === void 0 ? void 0 : e.message) !== null && _b !== void 0 ? _b : e}`);
160
+ logger_1.default.error(`deviceAccessGuard: session owner lookup failed for ${device.session_id}: ${(_d = e === null || e === void 0 ? void 0 : e.message) !== null && _d !== void 0 ? _d : e}`);
161
+ return unavailable(res);
162
+ }
163
+ }
164
+ let lease = null;
165
+ if (!actor.isAdmin) {
166
+ try {
167
+ lease = yield (0, leaseHold_1.leaseHoldFor)({ udid, host: device.host }, findActiveLease, resolveLeaseHolder);
168
+ }
169
+ catch (e) {
170
+ // A leased phone is held whatever `busy` says: not knowing whether a
171
+ // lease holds it is not knowing who owns it.
172
+ logger_1.default.error(`deviceAccessGuard: lease lookup failed for ${udid}: ${(_e = e === null || e === void 0 ? void 0 : e.message) !== null && _e !== void 0 ? _e : e}`);
128
173
  return unavailable(res);
129
174
  }
130
175
  }
@@ -138,9 +183,16 @@ function deviceAccessGuard(deps = {}) {
138
183
  actorUserId: actor.userId,
139
184
  actorApiKeyId: actor.apiKeyId,
140
185
  isAdmin: actor.isAdmin,
186
+ lease,
141
187
  });
142
188
  if (decision.allow)
143
189
  return next();
190
+ return deny(req, res, udid, actor.userId, decision);
191
+ });
192
+ };
193
+ function deny(req, res, udid, actorUserId, decision) {
194
+ return __awaiter(this, void 0, void 0, function* () {
195
+ var _a;
144
196
  // describeHolder is cosmetic — it only resolves a display name for the
145
197
  // deny message. A failure here must still deny (never flip to allow) but
146
198
  // must not hang the request either; fall back to no holder name.
@@ -150,12 +202,14 @@ function deviceAccessGuard(deps = {}) {
150
202
  holderName = yield describeHolder(decision.holderId);
151
203
  }
152
204
  catch (e) {
153
- logger_1.default.warn(`deviceAccessGuard: holder name lookup failed for ${decision.holderId}: ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`);
205
+ logger_1.default.warn(`deviceAccessGuard: holder name lookup failed for ${decision.holderId}: ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
154
206
  }
155
207
  }
156
- logger_1.default.warn(`Device access denied: ${actor.userId} -> ${req.method} ${req.originalUrl} ` +
208
+ logger_1.default.warn(`Device access denied: ${actorUserId} -> ${req.method} ${req.originalUrl} ` +
157
209
  `on ${udid} (${decision.code}, holder=${decision.holderId || 'unknown'})`);
158
- return res.status(409).json((0, deviceAccessPolicy_1.denyBody)(decision.code, decision.holderId, holderName));
210
+ return res
211
+ .status(409)
212
+ .json((0, deviceAccessPolicy_1.denyBody)(decision.code, decision.holderId, holderName, decision.heldBy));
159
213
  });
160
- };
214
+ }
161
215
  }
package/lib/src/plugin.js CHANGED
@@ -67,6 +67,7 @@ const NodeDevices_1 = __importDefault(require("./device-managers/NodeDevices"));
67
67
  const appiumUmbrella_1 = require("./sessions/appiumUmbrella");
68
68
  const LiveSessionOwners_1 = require("./services/device-access/LiveSessionOwners");
69
69
  const SessionMetricsService_1 = require("./services/metrics/SessionMetricsService");
70
+ const PhoneNetworkRestore_1 = require("./services/network/PhoneNetworkRestore");
70
71
  const config_1 = require("./config");
71
72
  const SessionManager_1 = require("./sessions/SessionManager");
72
73
  const event_manager_1 = require("./dashboard/event-manager");
@@ -75,13 +76,22 @@ const session_service_1 = require("./dashboard/services/session-service");
75
76
  const DEVICE_MANAGER_LOCK_NAME = 'DeviceManager';
76
77
  class XenonPlugin extends base_plugin_1.default {
77
78
  constructor(pluginName, cliArgs) {
79
+ var _a;
78
80
  super(pluginName, cliArgs);
79
81
  this.xenonLog = logger_1.default.scope('Plugin');
80
82
  this.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs);
81
83
  this.aiCommandService = typedi_1.Container.get(AICommandService_1.AICommandService);
82
84
  this.xenonLog.debug(`📱 Plugin Args: ${JSON.stringify(cliArgs)}`);
83
85
  this.pluginArgs = Object.assign({}, IPluginArgs_1.DefaultPluginArgs, this.cliArgs);
84
- logger_1.XenonLogger.configure({ enableJsonLogging: this.pluginArgs.enableJsonLogging });
86
+ // The option as it was given, not the merged copy above: JSON logging is on
87
+ // when the option says so, or when it isn't given and XENON_JSON_LOGGING does.
88
+ // schema.json gives the option no default, so Appium leaves it out when
89
+ // nobody set it; a default here (the merge above used to add `false`) would
90
+ // overwrite the variable on every start. `configure` ignores undefined.
91
+ const jsonLogging = (_a = this.cliArgs) === null || _a === void 0 ? void 0 : _a.enableJsonLogging;
92
+ logger_1.XenonLogger.configure({
93
+ enableJsonLogging: typeof jsonLogging === 'boolean' ? jsonLogging : undefined,
94
+ });
85
95
  if ((0, networkAddresses_1.shouldAutoResolveBindHost)(this.pluginArgs.bindHostOrIp)) {
86
96
  this.pluginArgs.bindHostOrIp = (0, networkAddresses_1.resolveAdvertisedBindHost)(this.pluginArgs.bindHostOrIp);
87
97
  }
@@ -101,6 +111,10 @@ class XenonPlugin extends base_plugin_1.default {
101
111
  typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).forget(sessionId);
102
112
  if (sessionId)
103
113
  yield typedi_1.Container.get(SessionMetricsService_1.SessionMetricsService).stop(sessionId);
114
+ // Appium's new-command timeout ends a session here, not in deleteSession:
115
+ // the phone's network (profile, interceptor proxy) and the capture are
116
+ // put back before the phone is released.
117
+ yield typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore).restoreSession(sessionId, 'driver shut down');
104
118
  const deviceFilter = {
105
119
  session_id: sessionId ? sessionId : undefined,
106
120
  udid: driver.caps && driver.caps.udid ? driver.caps.udid : undefined,
package/lib/src/prisma.js CHANGED
@@ -33,7 +33,7 @@ exports.getPrismaClient = getPrismaClient;
33
33
  * can stub individual methods (Prisma's internal Proxy blocks property assignment).
34
34
  */
35
35
  const MODEL_DELEGATES = new Set([
36
- 'build', 'session', 'sessionLog', 'log', 'profiling', 'app', 'device',
36
+ 'build', 'session', 'sessionLog', 'log', 'profiling', 'app', 'device', 'deviceSetting',
37
37
  'pendingSession', 'cLIArgs', 'webhookConfig', 'webConfig', 'locatorEtalon',
38
38
  'lease', 'portLease', 'apiKey', 'selectorState', 'user', 'userSession',
39
39
  'passwordResetToken', 'team', 'teamMember', 'eventLog', 'project', 'recording', 'bookmark',
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.clientProviderFromSchema = clientProviderFromSchema;
40
+ exports.describeDatabaseUrl = describeDatabaseUrl;
41
+ exports.checkDatabase = checkDatabase;
42
+ exports.assertSupportedDatabase = assertSupportedDatabase;
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const logger_1 = __importDefault(require("../logger"));
46
+ const config_1 = require("../config");
47
+ const LABEL = { sqlite: 'SQLite', postgresql: 'PostgreSQL' };
48
+ function providerOfUrl(url) {
49
+ if (url.startsWith('file:'))
50
+ return 'sqlite';
51
+ if (/^postgres(ql)?:\/\//.test(url))
52
+ return 'postgresql';
53
+ return null;
54
+ }
55
+ /** The datasource provider in a Prisma schema's text, or null if it has none. */
56
+ function clientProviderFromSchema(schema) {
57
+ const match = /datasource\s+\w+\s*{[^}]*?provider\s*=\s*"([^"]+)"/.exec(schema);
58
+ return match ? match[1] : null;
59
+ }
60
+ /** A database URL fit for a log line: a SQLite file's path, or a server URL without credentials. */
61
+ function describeDatabaseUrl(url) {
62
+ if (url.startsWith('file:'))
63
+ return url.slice('file:'.length);
64
+ try {
65
+ const parsed = new URL(url);
66
+ return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
67
+ }
68
+ catch (_a) {
69
+ return url.split(':')[0] + ':';
70
+ }
71
+ }
72
+ /**
73
+ * Decide whether the server may start on `url`. Throws an Error whose message
74
+ * says what to change; returns a warning when `configured` disagrees with the
75
+ * client but the URL is usable.
76
+ */
77
+ function checkDatabase(opts) {
78
+ var _a;
79
+ const { configured, url, client } = opts;
80
+ const clientLabel = (_a = LABEL[client]) !== null && _a !== void 0 ? _a : client;
81
+ const urlProvider = providerOfUrl(url);
82
+ const where = describeDatabaseUrl(url);
83
+ if (urlProvider !== client) {
84
+ const kind = urlProvider
85
+ ? `a ${LABEL[urlProvider]} database`
86
+ : 'not a database this install knows';
87
+ const fix = client === 'sqlite'
88
+ ? 'Set DATABASE_URL (or databaseUrl) to a SQLite file, `file:/path/to/xenon.db`, or leave it ' +
89
+ 'unset to use the default under ~/.cache/xenon. The published plugin stores its data in SQLite only.'
90
+ : `Set DATABASE_URL (or databaseUrl) to a ${clientLabel} URL.`;
91
+ throw new Error(`[Database] Cannot start: the database URL (${where}) is ${kind}, but this install's ` +
92
+ `database client is built for ${clientLabel}. ${fix}`);
93
+ }
94
+ if (configured && configured !== client) {
95
+ return {
96
+ warning: `[Database] databaseProvider is "${configured}", but this install's database client is ` +
97
+ `built for ${clientLabel}, so the setting has no effect. Your data is in ${where}. ` +
98
+ `Set databaseProvider to "${client}", or remove it.`,
99
+ };
100
+ }
101
+ return {};
102
+ }
103
+ /** The provider the generated client next to this file was built for. */
104
+ function generatedClientProvider() {
105
+ var _a;
106
+ const schemaPath = path.join(__dirname, '..', 'generated', 'client', 'schema.prisma');
107
+ try {
108
+ return (_a = clientProviderFromSchema(fs.readFileSync(schemaPath, 'utf8'))) !== null && _a !== void 0 ? _a : 'sqlite';
109
+ }
110
+ catch (_b) {
111
+ // The published plugin's client is SQLite; a missing copy means a plain install.
112
+ return 'sqlite';
113
+ }
114
+ }
115
+ /** Startup gate: log the warning, or throw so the server doesn't start on the wrong database. */
116
+ function assertSupportedDatabase() {
117
+ const { warning } = checkDatabase({
118
+ configured: config_1.config.databaseProvider,
119
+ url: config_1.config.databaseUrl,
120
+ client: generatedClientProvider(),
121
+ });
122
+ if (warning)
123
+ logger_1.default.warn(warning);
124
+ }