@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.
- package/README.md +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
- package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-CThoxDFO.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +19 -46
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +30 -5
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamStartConflict.js +3 -0
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +46 -1
- package/lib/src/data-service/device-store.js +66 -3
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +152 -9
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +143 -37
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/middleware/deviceAccessGuard.js +63 -9
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +97 -8
- package/lib/src/services/SessionLifecycleService.js +63 -53
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
- package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
- package/lib/src/services/device-access/leaseHold.js +26 -0
- package/lib/src/services/device-access/ticketActorAccess.js +6 -0
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/lease/activeLeases.js +37 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/busy-precheck.js +24 -3
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-access-lease.spec.js +270 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
- package/lib/test/unit/lease-held-phone.spec.js +196 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +114 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/schema-required-args.spec.js +21 -16
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -46
- package/scripts/generate-types-from-schema.js +7 -2
- package/scripts/lib/always-present-plugin-args.js +42 -0
- package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
- package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
- package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
- package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
- package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
- package/lib/public/assets/settings-C1VIluuT.js +0 -6
- package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
|
@@ -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
|
|
352
|
-
name
|
|
353
|
-
createdAt
|
|
354
|
-
devices
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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: () => ({
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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}: ${(
|
|
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}: ${(
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|