@xenon-device-management/xenon 2.12.0 → 2.13.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 (140) hide show
  1. package/lib/package.json +2 -2
  2. package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
  6. package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
  9. package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
  10. package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
  15. package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
  16. package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
  17. package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
  18. package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
  19. package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
  21. package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
  24. package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
  25. package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
  26. package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
  27. package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
  28. package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
  29. package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
  30. package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
  31. package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
  32. package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
  33. package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
  34. package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
  35. package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
  36. package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
  37. package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
  38. package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
  39. package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
  40. package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
  41. package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
  42. package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
  43. package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
  44. package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
  45. package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
  46. package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
  47. package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
  48. package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
  49. package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
  50. package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
  51. package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
  52. package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
  53. package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
  54. package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
  55. package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
  56. package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
  57. package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
  58. package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
  59. package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
  60. package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
  61. package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
  62. package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
  63. package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
  64. package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
  65. package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
  66. package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
  67. package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
  68. package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
  69. package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
  70. package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
  71. package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
  72. package/lib/public/index.html +1 -1
  73. package/lib/src/app/apiErrors.js +118 -0
  74. package/lib/src/app/index.js +6 -1
  75. package/lib/src/app/openapi/control.yaml +3129 -0
  76. package/lib/src/app/openapi/grid.yaml +2295 -0
  77. package/lib/src/app/openapi/identity.yaml +2168 -0
  78. package/lib/src/app/openapi/platform.yaml +2885 -0
  79. package/lib/src/app/openapi/sessions.yaml +3784 -0
  80. package/lib/src/app/routers/bug-report.js +4 -1
  81. package/lib/src/app/routers/config.js +6 -107
  82. package/lib/src/app/routers/control.js +117 -59
  83. package/lib/src/app/routers/dashboard.js +13 -7
  84. package/lib/src/app/routers/grid.js +54 -14
  85. package/lib/src/app/routers/profile.js +27 -13
  86. package/lib/src/app/routers/recordings.js +11 -5
  87. package/lib/src/app/routers/reservation.js +63 -15
  88. package/lib/src/app/routers/users.js +4 -0
  89. package/lib/src/app/routers/webhook.js +17 -8
  90. package/lib/src/app/swagger.js +259 -177
  91. package/lib/src/data-service/device-service.js +4 -1
  92. package/lib/src/data-service/deviceFieldOwners.js +1 -0
  93. package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
  94. package/lib/src/device-managers/ios/WDAClient.js +32 -32
  95. package/lib/src/generated/client/edge.js +4 -3
  96. package/lib/src/generated/client/index-browser.js +1 -0
  97. package/lib/src/generated/client/index.d.ts +38 -0
  98. package/lib/src/generated/client/index.js +4 -3
  99. package/lib/src/generated/client/package.json +1 -1
  100. package/lib/src/generated/client/schema.prisma +2 -0
  101. package/lib/src/generated/client/wasm.js +1 -0
  102. package/lib/src/middleware/csrfMiddleware.js +13 -5
  103. package/lib/src/middleware/rateLimitMiddleware.js +19 -5
  104. package/lib/src/middleware/roleGuard.js +20 -0
  105. package/lib/src/services/AIService.js +13 -3
  106. package/lib/src/services/NotificationService.js +28 -28
  107. package/lib/src/services/bug-report/BugReportService.js +8 -2
  108. package/lib/src/services/lease/LeaseService.js +71 -20
  109. package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
  110. package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
  111. package/lib/test/helpers/expressRoutes.js +41 -0
  112. package/lib/test/integration/team-visibility-control.spec.js +2 -2
  113. package/lib/test/unit/access-scopes.spec.js +227 -0
  114. package/lib/test/unit/api-error-handling.spec.js +179 -0
  115. package/lib/test/unit/bug-report/route.spec.js +29 -0
  116. package/lib/test/unit/bug-report/service.spec.js +30 -0
  117. package/lib/test/unit/control-honest-answers.spec.js +134 -0
  118. package/lib/test/unit/device-allocation-routes.spec.js +232 -0
  119. package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
  120. package/lib/test/unit/install-repository-app-team.spec.js +4 -1
  121. package/lib/test/unit/lease/LeaseService.spec.js +5 -4
  122. package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
  123. package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
  124. package/lib/test/unit/omni-vision-failures.spec.js +81 -0
  125. package/lib/test/unit/openapi-coverage.spec.js +114 -0
  126. package/lib/test/unit/profile-router.test.js +42 -0
  127. package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
  128. package/lib/test/unit/recording-orchestrator.spec.js +80 -0
  129. package/lib/test/unit/recordings-library-routes.spec.js +37 -0
  130. package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
  131. package/lib/test/unit/reset-link.test.js +2 -1
  132. package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
  133. package/lib/test/unit/users-router.test.js +14 -1
  134. package/lib/test/unit/wda-client-failures.spec.js +90 -0
  135. package/lib/test/unit/webhook-delivery.spec.js +142 -0
  136. package/lib/tsconfig.tsbuildinfo +1 -1
  137. package/package.json +2 -2
  138. package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
  139. package/prisma/schema.prisma +2 -0
  140. package/lib/src/app/swagger-docs.js +0 -1701
@@ -0,0 +1,2295 @@
1
+ paths:
2
+ /api/devices:
3
+ get:
4
+ operationId: listGridDevices
5
+ summary: List the devices in the lab
6
+ description: |
7
+ Every device this server knows: its own phones and, on a hub, the
8
+ phones its nodes report. Each row carries `teamName`, the name of
9
+ its team (null for the shared pool).
10
+
11
+ Needs role `MEMBER` or above; no scope is checked. A member sees the
12
+ shared pool and their teams' devices only. An admin, or any caller
13
+ on a server with auth disabled, sees every device.
14
+
15
+ With `sessionId`, the answer is the one visible device whose
16
+ `session_id` is that value, as an object, or an empty `200` body
17
+ when there is none. When the Appium dashboard plugin is installed,
18
+ each row in the list also gets `dashboard_link` and
19
+ `total_session_count`.
20
+
21
+ `GET /api/device` is the same endpoint.
22
+ tags:
23
+ - Devices
24
+ parameters:
25
+ - $ref: '#/components/parameters/GridSessionIdQuery'
26
+ responses:
27
+ '200':
28
+ $ref: '#/components/responses/GridDeviceListOk'
29
+ '401':
30
+ $ref: '#/components/responses/Unauthorized'
31
+ '403':
32
+ $ref: '#/components/responses/Forbidden'
33
+ '429':
34
+ $ref: '#/components/responses/RateLimited'
35
+ /api/device:
36
+ get:
37
+ operationId: listGridDevicesAlias
38
+ summary: List the devices in the lab (alias)
39
+ description: |
40
+ The same as `GET /api/devices`: every device the caller may see,
41
+ with `teamName`, or with `sessionId` the one device running that
42
+ session. Needs role `MEMBER` or above. A member sees the shared pool
43
+ and their teams' devices only.
44
+ tags:
45
+ - Devices
46
+ parameters:
47
+ - $ref: '#/components/parameters/GridSessionIdQuery'
48
+ responses:
49
+ '200':
50
+ $ref: '#/components/responses/GridDeviceListOk'
51
+ '401':
52
+ $ref: '#/components/responses/Unauthorized'
53
+ '403':
54
+ $ref: '#/components/responses/Forbidden'
55
+ '429':
56
+ $ref: '#/components/responses/RateLimited'
57
+ /api/device/{platform}:
58
+ get:
59
+ operationId: listGridDevicesByPlatform
60
+ summary: List the devices of one platform
61
+ description: |
62
+ The devices of one platform, filtered like `GET /api/devices`: a
63
+ member sees the shared pool and their teams' devices only. Each row
64
+ carries `teamName`. Needs role `MEMBER` or above.
65
+
66
+ `platform` is matched case-insensitively. Any other value than
67
+ `ios` or `android` answers `200` with an empty list, not an error.
68
+ `booted` keeps only devices whose `state` is `Booted` (simulators):
69
+ any value turns it on, `booted=false` included.
70
+ tags:
71
+ - Devices
72
+ parameters:
73
+ - in: path
74
+ name: platform
75
+ required: true
76
+ description: The platform, `ios` or `android` (any letter case).
77
+ schema:
78
+ type: string
79
+ example: android
80
+ - in: query
81
+ name: deviceType
82
+ required: false
83
+ description: Keep only devices of this type.
84
+ schema:
85
+ type: string
86
+ enum:
87
+ - real
88
+ - simulator
89
+ - emulator
90
+ - in: query
91
+ name: booted
92
+ required: false
93
+ description: Present (with any value) to keep only devices whose `state` is `Booted`.
94
+ schema:
95
+ type: string
96
+ example: 'true'
97
+ responses:
98
+ '200':
99
+ description: The matching devices; empty for an unknown platform.
100
+ content:
101
+ application/json:
102
+ schema:
103
+ type: array
104
+ items:
105
+ $ref: '#/components/schemas/GridDevice'
106
+ example:
107
+ - udid: emulator-5554
108
+ name: Pixel 7 API 34
109
+ platform: android
110
+ host: http://192.168.1.100:4723
111
+ nodeId: 6b1e4c2a-8f3d-4a9e-b7c5-2d0f1e8a9b3c
112
+ busy: false
113
+ session_id: null
114
+ state: device
115
+ sdk: '14'
116
+ deviceType: emulator
117
+ realDevice: false
118
+ offline: false
119
+ userBlocked: false
120
+ teamId: null
121
+ teamName: null
122
+ tags:
123
+ - lab-row-3
124
+ reservedBy: null
125
+ reservedByUserId: null
126
+ reservedUntil: null
127
+ healthStatus: Healthy
128
+ batteryLevel: 100
129
+ screenWidth: '1080'
130
+ screenHeight: '2400'
131
+ '401':
132
+ $ref: '#/components/responses/Unauthorized'
133
+ '403':
134
+ $ref: '#/components/responses/Forbidden'
135
+ '429':
136
+ $ref: '#/components/responses/RateLimited'
137
+ /api/block:
138
+ post:
139
+ operationId: blockGridDevice
140
+ summary: Block a device for maintenance
141
+ description: |
142
+ Sets `userBlocked` on a device, so no new session or lease is
143
+ allocated to it. A session already running on it is not affected,
144
+ and neither is `busy`.
145
+
146
+ Send both `udid` and `host`, exactly as `GET /api/devices` shows
147
+ them: the device is the row of that udid on that host. Without
148
+ either, `400`; with no such row, `404`. Through 2.12 the body was a
149
+ device filter, so an empty body blocked the first device in the
150
+ list, and a udid alone either row of a udid two nodes report
151
+ (emulators).
152
+
153
+ Needs role `ADMIN` and the `devices` scope.
154
+ tags:
155
+ - Devices
156
+ requestBody:
157
+ required: true
158
+ content:
159
+ application/json:
160
+ schema:
161
+ $ref: '#/components/schemas/GridDeviceSelector'
162
+ example:
163
+ udid: emulator-5554
164
+ host: http://192.168.1.100:4723
165
+ responses:
166
+ '200':
167
+ description: The device is blocked.
168
+ content:
169
+ application/json:
170
+ schema:
171
+ $ref: '#/components/schemas/Success'
172
+ example:
173
+ success: true
174
+ '400':
175
+ $ref: '#/components/responses/GridDeviceSelectorMissing'
176
+ '401':
177
+ $ref: '#/components/responses/Unauthorized'
178
+ '403':
179
+ $ref: '#/components/responses/Forbidden'
180
+ '404':
181
+ $ref: '#/components/responses/GridDeviceNotFound'
182
+ '429':
183
+ $ref: '#/components/responses/RateLimited'
184
+ '500':
185
+ description: The device could not be updated.
186
+ content:
187
+ application/json:
188
+ schema:
189
+ $ref: '#/components/schemas/Error'
190
+ example:
191
+ success: false
192
+ error: Failed to block device
193
+ /api/unblock:
194
+ post:
195
+ operationId: unblockGridDevice
196
+ summary: Unblock a device
197
+ description: |
198
+ Clears `userBlocked`, so the device can be allocated again. Send
199
+ both `udid` and `host`, as for `POST /api/block`: without either,
200
+ `400`; with no such row, `404`.
201
+
202
+ Needs role `ADMIN` and the `devices` scope.
203
+ tags:
204
+ - Devices
205
+ requestBody:
206
+ required: true
207
+ content:
208
+ application/json:
209
+ schema:
210
+ $ref: '#/components/schemas/GridDeviceSelector'
211
+ example:
212
+ udid: 00008110-00084CE80E51401E
213
+ host: http://192.168.1.101:4723
214
+ responses:
215
+ '200':
216
+ description: The device is unblocked.
217
+ content:
218
+ application/json:
219
+ schema:
220
+ $ref: '#/components/schemas/Success'
221
+ example:
222
+ success: true
223
+ '400':
224
+ $ref: '#/components/responses/GridDeviceSelectorMissing'
225
+ '401':
226
+ $ref: '#/components/responses/Unauthorized'
227
+ '403':
228
+ $ref: '#/components/responses/Forbidden'
229
+ '404':
230
+ $ref: '#/components/responses/GridDeviceNotFound'
231
+ '429':
232
+ $ref: '#/components/responses/RateLimited'
233
+ '500':
234
+ description: The device could not be updated.
235
+ content:
236
+ application/json:
237
+ schema:
238
+ $ref: '#/components/schemas/Error'
239
+ example:
240
+ success: false
241
+ error: Failed to unblock device
242
+ /api/device/tags:
243
+ post:
244
+ operationId: setGridDeviceTags
245
+ summary: Replace a device's tags
246
+ description: |
247
+ Replaces the device's tag list with `tags` (it doesn't append). Tags
248
+ are free-form labels, such as `flaky` or `lab-row-3`, that a lease
249
+ can filter on. The row is chosen by exact `udid` and `host`; a pair
250
+ that names no device changes nothing and still answers `200`.
251
+
252
+ Needs role `ADMIN` and the `devices` scope.
253
+ tags:
254
+ - Devices
255
+ requestBody:
256
+ required: true
257
+ content:
258
+ application/json:
259
+ schema:
260
+ type: object
261
+ required:
262
+ - udid
263
+ - host
264
+ - tags
265
+ properties:
266
+ udid:
267
+ type: string
268
+ example: emulator-5554
269
+ host:
270
+ type: string
271
+ description: The device's host, exactly as `GET /api/devices` shows it.
272
+ example: http://192.168.1.100:4723
273
+ tags:
274
+ type: array
275
+ items:
276
+ type: string
277
+ example:
278
+ - flaky
279
+ - lab-row-3
280
+ responses:
281
+ '200':
282
+ description: The tags were written.
283
+ content:
284
+ application/json:
285
+ schema:
286
+ $ref: '#/components/schemas/Success'
287
+ example:
288
+ success: true
289
+ '400':
290
+ description: '`udid` or `host` is missing, or `tags` is not an array.'
291
+ content:
292
+ application/json:
293
+ schema:
294
+ $ref: '#/components/schemas/Error'
295
+ example:
296
+ error: Missing udid, host, or tags array
297
+ '401':
298
+ $ref: '#/components/responses/Unauthorized'
299
+ '403':
300
+ $ref: '#/components/responses/Forbidden'
301
+ '429':
302
+ $ref: '#/components/responses/RateLimited'
303
+ /api/device/{udid}/team:
304
+ put:
305
+ operationId: assignGridDeviceTeam
306
+ summary: Assign a device to a team
307
+ description: |
308
+ Gives the device to a team, or with `teamId: null` returns it to the
309
+ shared pool, which every member sees. A missing `teamId` counts as
310
+ null. Every row with this udid changes (on a hub, each node that
311
+ reports it), and allocation and live dashboard events follow the new
312
+ team at once.
313
+
314
+ Needs role `ADMIN` and the `admin` scope.
315
+ tags:
316
+ - Devices
317
+ parameters:
318
+ - in: path
319
+ name: udid
320
+ required: true
321
+ description: The device's udid.
322
+ schema:
323
+ type: string
324
+ example: 00008110-00084CE80E51401E
325
+ requestBody:
326
+ required: true
327
+ content:
328
+ application/json:
329
+ schema:
330
+ type: object
331
+ properties:
332
+ teamId:
333
+ type: string
334
+ nullable: true
335
+ description: The team's id, or null for the shared pool.
336
+ example: clx9k2v0a0000qz8r4h7m3n2p
337
+ responses:
338
+ '200':
339
+ description: The device's team changed. `updated` is how many rows changed.
340
+ content:
341
+ application/json:
342
+ schema:
343
+ type: object
344
+ properties:
345
+ ok:
346
+ type: boolean
347
+ updated:
348
+ type: integer
349
+ example:
350
+ ok: true
351
+ updated: 1
352
+ '401':
353
+ $ref: '#/components/responses/Unauthorized'
354
+ '403':
355
+ $ref: '#/components/responses/Forbidden'
356
+ '404':
357
+ description: The team or the device doesn't exist.
358
+ content:
359
+ application/json:
360
+ schema:
361
+ $ref: '#/components/schemas/Error'
362
+ examples:
363
+ team:
364
+ value:
365
+ error: team not found
366
+ device:
367
+ value:
368
+ error: device not found
369
+ '429':
370
+ $ref: '#/components/responses/RateLimited'
371
+ /api/reservation:
372
+ get:
373
+ operationId: listGridReservations
374
+ summary: List the current reservations
375
+ description: |
376
+ The devices reserved right now (an expired reservation isn't
377
+ listed), with the time left. A member sees only reservations on the
378
+ shared pool and their teams' devices; an admin sees all.
379
+
380
+ Needs role `MEMBER` or above. Reservations are deprecated in favour
381
+ of leases (`POST /api/sdk/leases`): every answer carries
382
+ `Deprecation`, `Sunset` and `Link` headers.
383
+ tags:
384
+ - Reservations
385
+ responses:
386
+ '200':
387
+ description: The current reservations.
388
+ headers:
389
+ Deprecation:
390
+ $ref: '#/components/headers/GridDeprecation'
391
+ Sunset:
392
+ $ref: '#/components/headers/GridSunset'
393
+ Link:
394
+ $ref: '#/components/headers/GridLink'
395
+ content:
396
+ application/json:
397
+ schema:
398
+ type: object
399
+ properties:
400
+ success:
401
+ type: boolean
402
+ reservations:
403
+ type: array
404
+ items:
405
+ $ref: '#/components/schemas/GridReservation'
406
+ example:
407
+ success: true
408
+ reservations:
409
+ - udid: emulator-5554
410
+ host: http://192.168.1.100:4723
411
+ name: Pixel 7 API 34
412
+ platform: android
413
+ reservedBy: priya
414
+ reservedUntil: 1791118800000
415
+ reservationReason: Manual checkout regression
416
+ remainingMs: 5400000
417
+ '401':
418
+ $ref: '#/components/responses/Unauthorized'
419
+ '403':
420
+ $ref: '#/components/responses/Forbidden'
421
+ '429':
422
+ $ref: '#/components/responses/RateLimited'
423
+ '500':
424
+ $ref: '#/components/responses/GridReservationFailed'
425
+ post:
426
+ operationId: createGridReservation
427
+ summary: Reserve a device
428
+ description: |
429
+ Marks a device as reserved by `reservedBy` until now plus
430
+ `duration`. `reservedBy` is the name shown on the device; the
431
+ reservation also records the signed-in user who took it
432
+ (`reservedByUserId`), who alone, or an admin, may release or extend
433
+ it. A device already reserved by someone else, or running an Appium
434
+ session, is refused with `409`. The same user under the same
435
+ `reservedBy` may reserve it again, which restarts the clock.
436
+
437
+ The device is the row of `udid` on `host`, so send both exactly as
438
+ `GET /api/devices` shows them. No such row, or a device outside the
439
+ caller's teams, answers `404`, as an unknown one.
440
+
441
+ Needs role `MEMBER` or above and the `devices` scope. Deprecated in
442
+ favour of leases: every answer carries `Deprecation`, `Sunset` and
443
+ `Link` headers.
444
+ tags:
445
+ - Reservations
446
+ requestBody:
447
+ required: true
448
+ content:
449
+ application/json:
450
+ schema:
451
+ type: object
452
+ required:
453
+ - udid
454
+ - host
455
+ - reservedBy
456
+ - duration
457
+ properties:
458
+ udid:
459
+ type: string
460
+ example: emulator-5554
461
+ host:
462
+ type: string
463
+ example: http://192.168.1.100:4723
464
+ reservedBy:
465
+ type: string
466
+ description: Who the reservation is for, as free text.
467
+ example: priya
468
+ duration:
469
+ $ref: '#/components/schemas/GridReservationDuration'
470
+ reason:
471
+ type: string
472
+ example: Manual checkout regression
473
+ example:
474
+ udid: emulator-5554
475
+ host: http://192.168.1.100:4723
476
+ reservedBy: priya
477
+ duration: 2h
478
+ reason: Manual checkout regression
479
+ responses:
480
+ '200':
481
+ description: The device is reserved.
482
+ headers:
483
+ Deprecation:
484
+ $ref: '#/components/headers/GridDeprecation'
485
+ Sunset:
486
+ $ref: '#/components/headers/GridSunset'
487
+ Link:
488
+ $ref: '#/components/headers/GridLink'
489
+ content:
490
+ application/json:
491
+ schema:
492
+ type: object
493
+ properties:
494
+ success:
495
+ type: boolean
496
+ message:
497
+ type: string
498
+ reservation:
499
+ type: object
500
+ properties:
501
+ udid:
502
+ type: string
503
+ host:
504
+ type: string
505
+ reservedBy:
506
+ type: string
507
+ reservedUntil:
508
+ type: integer
509
+ description: Epoch milliseconds.
510
+ reservationReason:
511
+ type: string
512
+ expiresAt:
513
+ type: string
514
+ format: date-time
515
+ example:
516
+ success: true
517
+ message: Device emulator-5554 reserved for priya
518
+ reservation:
519
+ udid: emulator-5554
520
+ host: http://192.168.1.100:4723
521
+ reservedBy: priya
522
+ reservedUntil: 1791118800000
523
+ reservationReason: Manual checkout regression
524
+ expiresAt: '2026-10-04T15:00:00.000Z'
525
+ '400':
526
+ description: A field is missing, or `duration` is invalid or outside 1 minute to 24 hours.
527
+ content:
528
+ application/json:
529
+ schema:
530
+ $ref: '#/components/schemas/Error'
531
+ examples:
532
+ missing:
533
+ value:
534
+ success: false
535
+ error: 'Missing required fields: udid, host, reservedBy, duration'
536
+ duration:
537
+ value:
538
+ success: false
539
+ error: duration must be between 60000ms (1 minute) and 86400000ms (24 hours)
540
+ '401':
541
+ $ref: '#/components/responses/Unauthorized'
542
+ '403':
543
+ $ref: '#/components/responses/Forbidden'
544
+ '404':
545
+ $ref: '#/components/responses/GridDeviceNotFound'
546
+ '409':
547
+ description: Reserved by someone else, or running an Appium session.
548
+ content:
549
+ application/json:
550
+ schema:
551
+ $ref: '#/components/schemas/Error'
552
+ examples:
553
+ reserved:
554
+ value:
555
+ success: false
556
+ error: Device is already reserved by marco until 2026-10-04T15:00:00.000Z
557
+ busy:
558
+ value:
559
+ success: false
560
+ error: Device is currently busy with an Appium session. Cannot reserve.
561
+ '429':
562
+ $ref: '#/components/responses/RateLimited'
563
+ '500':
564
+ $ref: '#/components/responses/GridReservationFailed'
565
+ /api/reservation/{udid}/{host}:
566
+ delete:
567
+ operationId: releaseGridReservation
568
+ summary: Release a reservation
569
+ description: |
570
+ Ends the device's reservation at once. Only the user who took it,
571
+ or an admin, may: anyone else gets `403 not_reservation_holder`. A
572
+ reservation taken before 2.13 records no user, and anyone who may
573
+ reserve may still release it. A device outside the caller's teams
574
+ answers `404`.
575
+
576
+ `host` is URL-encoded (`http%3A%2F%2F192.168.1.100%3A4723`). The
577
+ device is the row of `udid` on that `host`; no such row answers
578
+ `404`.
579
+
580
+ Needs role `MEMBER` or above and the `devices` scope. Deprecated in
581
+ favour of leases.
582
+ tags:
583
+ - Reservations
584
+ parameters:
585
+ - $ref: '#/components/parameters/GridReservationUdid'
586
+ - $ref: '#/components/parameters/GridReservationHost'
587
+ responses:
588
+ '200':
589
+ description: The reservation is released.
590
+ headers:
591
+ Deprecation:
592
+ $ref: '#/components/headers/GridDeprecation'
593
+ Sunset:
594
+ $ref: '#/components/headers/GridSunset'
595
+ Link:
596
+ $ref: '#/components/headers/GridLink'
597
+ content:
598
+ application/json:
599
+ schema:
600
+ type: object
601
+ properties:
602
+ success:
603
+ type: boolean
604
+ message:
605
+ type: string
606
+ example:
607
+ success: true
608
+ message: Reservation released for device emulator-5554
609
+ '400':
610
+ $ref: '#/components/responses/GridNotReserved'
611
+ '401':
612
+ $ref: '#/components/responses/Unauthorized'
613
+ '403':
614
+ description: >-
615
+ The caller isn't the user who took the reservation, nor an admin; or lacks the
616
+ role or scope.
617
+ content:
618
+ application/json:
619
+ schema:
620
+ $ref: '#/components/schemas/Error'
621
+ examples:
622
+ notHolder:
623
+ summary: Someone else's reservation
624
+ value:
625
+ success: false
626
+ error: not_reservation_holder
627
+ message: >-
628
+ Only the person who reserved this device, or an admin, can change the
629
+ reservation.
630
+ scope:
631
+ summary: Role or scope missing
632
+ value:
633
+ error: insufficient scope
634
+ '404':
635
+ $ref: '#/components/responses/GridDeviceNotFound'
636
+ '429':
637
+ $ref: '#/components/responses/RateLimited'
638
+ '500':
639
+ $ref: '#/components/responses/GridReservationFailed'
640
+ /api/reservation/{udid}/{host}/extend:
641
+ post:
642
+ operationId: extendGridReservation
643
+ summary: Extend a reservation
644
+ description: |
645
+ Adds `duration` (at least 1 minute) to the current reservation's
646
+ end, keeping who holds it, its `reservedBy` and its reason. As with
647
+ a new reservation, it may end at most 24 hours from now. Only the
648
+ user who took it, or an admin, may extend it, as for releasing. A
649
+ device outside the caller's teams answers `404`.
650
+
651
+ `host` is URL-encoded, as for releasing. Needs role `MEMBER` or
652
+ above and the `devices` scope. Deprecated in favour of leases.
653
+ tags:
654
+ - Reservations
655
+ parameters:
656
+ - $ref: '#/components/parameters/GridReservationUdid'
657
+ - $ref: '#/components/parameters/GridReservationHost'
658
+ requestBody:
659
+ required: true
660
+ content:
661
+ application/json:
662
+ schema:
663
+ type: object
664
+ required:
665
+ - duration
666
+ properties:
667
+ duration:
668
+ $ref: '#/components/schemas/GridReservationDuration'
669
+ example:
670
+ duration: 1h
671
+ responses:
672
+ '200':
673
+ description: The reservation is extended.
674
+ headers:
675
+ Deprecation:
676
+ $ref: '#/components/headers/GridDeprecation'
677
+ Sunset:
678
+ $ref: '#/components/headers/GridSunset'
679
+ Link:
680
+ $ref: '#/components/headers/GridLink'
681
+ content:
682
+ application/json:
683
+ schema:
684
+ type: object
685
+ properties:
686
+ success:
687
+ type: boolean
688
+ message:
689
+ type: string
690
+ newExpiresAt:
691
+ type: string
692
+ format: date-time
693
+ example:
694
+ success: true
695
+ message: Reservation extended for device emulator-5554
696
+ newExpiresAt: '2026-10-04T16:00:00.000Z'
697
+ '400':
698
+ description: >-
699
+ The device isn't reserved, or `duration` is invalid, under a minute, or would end the
700
+ reservation more than 24 hours from now.
701
+ content:
702
+ application/json:
703
+ schema:
704
+ $ref: '#/components/schemas/Error'
705
+ examples:
706
+ notReserved:
707
+ value:
708
+ success: false
709
+ error: Device is not reserved
710
+ duration:
711
+ value:
712
+ success: false
713
+ error: 'Invalid duration. Use: 1h, 2h, 4h, 8h or a number in milliseconds'
714
+ tooShort:
715
+ value:
716
+ success: false
717
+ error: duration must be at least 60000ms (1 minute)
718
+ tooLong:
719
+ value:
720
+ success: false
721
+ error: A reservation can end at most 24 hours from now
722
+ '401':
723
+ $ref: '#/components/responses/Unauthorized'
724
+ '403':
725
+ description: >-
726
+ The caller isn't the user who took the reservation, nor an admin; or lacks the
727
+ role or scope.
728
+ content:
729
+ application/json:
730
+ schema:
731
+ $ref: '#/components/schemas/Error'
732
+ examples:
733
+ notHolder:
734
+ summary: Someone else's reservation
735
+ value:
736
+ success: false
737
+ error: not_reservation_holder
738
+ message: >-
739
+ Only the person who reserved this device, or an admin, can change the
740
+ reservation.
741
+ scope:
742
+ summary: Role or scope missing
743
+ value:
744
+ error: insufficient scope
745
+ '404':
746
+ $ref: '#/components/responses/GridDeviceNotFound'
747
+ '429':
748
+ $ref: '#/components/responses/RateLimited'
749
+ '500':
750
+ $ref: '#/components/responses/GridReservationFailed'
751
+ /api/sdk/leases:
752
+ post:
753
+ operationId: createGridLease
754
+ summary: Lease a device
755
+ description: |
756
+ Claims a free device that matches `filters` for `durationMs`, for a
757
+ programmatic client such as an SDK or MCP tool. The device is marked
758
+ busy, ports for the Appium driver are reserved on the server it is
759
+ attached to, and the answer carries `appiumCapabilities`: pass them
760
+ to `POST /session` unchanged. They include `xe:options.leaseId` and
761
+ `xe:options.leaseToken`, which prove the session holds the lease.
762
+
763
+ `leaseToken` is shown only in this answer and is never stored in
764
+ clear. Keep it: heartbeat, extend and release need it in the
765
+ `x-xenon-lease-token` header.
766
+
767
+ The lease ends after three missed heartbeats (every
768
+ `heartbeatSeconds`) or at `expiresAt`, whichever comes first; a
769
+ sweep runs every 30 s. Ending it frees the device unless a session
770
+ still holds it.
771
+
772
+ Matching: among the shared pool and the caller's teams (an admin is
773
+ unscoped), the devices that match every filter. `filters.platform`
774
+ is lowercase. `udid`, `deviceType`, `tags` (all must be present),
775
+ `platformVersion` (or `sdk`, the same thing), `minSDK`, `maxSDK`
776
+ and `deviceName` (the device's name, ignoring case) narrow it.
777
+ `404` means no device visible to you matches. Of those that match,
778
+ the lease takes one that is free, online, not already leased, not
779
+ blocked, not reserved, and not failing its health check, as a new
780
+ session would; `409` means some match but none of them is. Through
781
+ 2.12 `sdk` and `deviceName` were ignored, blocked, reserved and
782
+ unhealthy devices could be leased, and `404` or `409` depended on
783
+ the platform alone.
784
+
785
+ Needs role `MEMBER` or above and the `devices` scope.
786
+ tags:
787
+ - Leases
788
+ requestBody:
789
+ required: true
790
+ content:
791
+ application/json:
792
+ schema:
793
+ $ref: '#/components/schemas/GridLeaseCreateRequest'
794
+ example:
795
+ filters:
796
+ platform: android
797
+ tags:
798
+ - lab-row-3
799
+ durationMs: 1800000
800
+ heartbeatSeconds: 30
801
+ reason: nightly checkout suite
802
+ buildId: build-2026-10-04-nightly
803
+ responses:
804
+ '201':
805
+ description: The lease. Keep `leaseToken`; it is not shown again.
806
+ content:
807
+ application/json:
808
+ schema:
809
+ $ref: '#/components/schemas/GridLease'
810
+ example:
811
+ leaseId: clxa1b2c30000s8k2f9d1e7aa
812
+ leaseToken: 7f3c9a1e5b2d48f0a6c4e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1
813
+ device:
814
+ udid: emulator-5554
815
+ host: http://192.168.1.100:4723
816
+ platform: android
817
+ sdk: '14'
818
+ name: Pixel 7 API 34
819
+ screen:
820
+ width: '1080'
821
+ height: '2400'
822
+ realDevice: false
823
+ expiresAt: 1791113400000
824
+ heartbeatSeconds: 30
825
+ allocatedPorts:
826
+ systemPort: 52311
827
+ chromedriverPort: 52312
828
+ mjpegServerPort: 52313
829
+ appiumCapabilities:
830
+ platformName: Android
831
+ appium:automationName: UiAutomator2
832
+ appium:udid: emulator-5554
833
+ appium:newCommandTimeout: 120
834
+ appium:deviceName: Pixel 7 API 34
835
+ appium:platformVersion: '14'
836
+ appium:systemPort: 52311
837
+ appium:chromedriverPort: 52312
838
+ appium:mjpegServerPort: 52313
839
+ xe:options:
840
+ leaseId: clxa1b2c30000s8k2f9d1e7aa
841
+ buildId: build-2026-10-04-nightly
842
+ leaseToken: 7f3c9a1e5b2d48f0a6c4e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1
843
+ '400':
844
+ description: '`filters.platform` is missing.'
845
+ content:
846
+ application/json:
847
+ schema:
848
+ $ref: '#/components/schemas/Error'
849
+ example:
850
+ error: bad_request
851
+ details: filters.platform required
852
+ '401':
853
+ $ref: '#/components/responses/Unauthorized'
854
+ '403':
855
+ $ref: '#/components/responses/Forbidden'
856
+ '404':
857
+ description: No device the caller can see matches the filters.
858
+ content:
859
+ application/json:
860
+ schema:
861
+ $ref: '#/components/schemas/Error'
862
+ example:
863
+ error: no_matching_device
864
+ message: No device matches the filters
865
+ '409':
866
+ description: >-
867
+ Devices match, but none is free: busy, leased, blocked, reserved or unhealthy. Retry
868
+ after `retryAfterMs`.
869
+ content:
870
+ application/json:
871
+ schema:
872
+ $ref: '#/components/schemas/Error'
873
+ example:
874
+ error: all_matching_busy
875
+ retryAfterMs: 2000
876
+ '429':
877
+ $ref: '#/components/responses/RateLimited'
878
+ '500':
879
+ description: The lease could not be recorded.
880
+ content:
881
+ application/json:
882
+ schema:
883
+ $ref: '#/components/schemas/Error'
884
+ example:
885
+ error: internal
886
+ message: Unique constraint failed on the fields
887
+ '503':
888
+ description: |
889
+ A device was found but its ports could not be reserved: its server
890
+ didn't answer, or this server has no credentials for it
891
+ (`XENON_HUB_ACCESS_KEY` and `XENON_HUB_TOKEN`, needed whenever auth
892
+ is enabled). The device is freed again.
893
+ content:
894
+ application/json:
895
+ schema:
896
+ $ref: '#/components/schemas/Error'
897
+ example:
898
+ error: device_unhealthy
899
+ details: 'port allocation failed: connect ECONNREFUSED 192.168.1.100:4723'
900
+ /api/sdk/leases/{id}/heartbeat:
901
+ post:
902
+ operationId: heartbeatGridLease
903
+ summary: Keep a lease alive
904
+ description: |
905
+ Records a heartbeat, so the lease isn't ended for missed heartbeats.
906
+ It does not move `expiresAt`; use extend for that. Needs the lease
907
+ token in `x-xenon-lease-token`, role `MEMBER` or above and the
908
+ `devices` scope.
909
+
910
+ An unknown id answers like a wrong token (`403 token_mismatch`), so
911
+ lease ids can't be probed. A lease that has ended or passed
912
+ `expiresAt` answers `410` to its token's holder.
913
+ tags:
914
+ - Leases
915
+ parameters:
916
+ - $ref: '#/components/parameters/GridLeaseId'
917
+ - $ref: '#/components/parameters/GridLeaseToken'
918
+ responses:
919
+ '200':
920
+ description: The heartbeat was recorded.
921
+ content:
922
+ application/json:
923
+ schema:
924
+ type: object
925
+ properties:
926
+ heartbeatedAt:
927
+ type: integer
928
+ description: Epoch milliseconds.
929
+ expiresAt:
930
+ type: integer
931
+ description: Epoch milliseconds.
932
+ example:
933
+ heartbeatedAt: 1791111630000
934
+ expiresAt: 1791113400000
935
+ '401':
936
+ $ref: '#/components/responses/Unauthorized'
937
+ '403':
938
+ $ref: '#/components/responses/GridLeaseForbidden'
939
+ '410':
940
+ description: The lease has ended (released, expired or swept).
941
+ content:
942
+ application/json:
943
+ schema:
944
+ $ref: '#/components/schemas/Error'
945
+ example:
946
+ error: gone
947
+ message: lease clxa1b2c30000s8k2f9d1e7aa is expired
948
+ '429':
949
+ $ref: '#/components/responses/RateLimited'
950
+ '500':
951
+ $ref: '#/components/responses/GridLeaseInternal'
952
+ /api/sdk/leases/{id}/extend:
953
+ post:
954
+ operationId: extendGridLease
955
+ summary: Extend a lease
956
+ description: |
957
+ Sets the lease to end `durationMs` from now (not from its current
958
+ end), and counts as a heartbeat. A lease never runs past 24 hours
959
+ after it was created: a later end is cut to that. A smaller
960
+ `durationMs` than the time left shortens the lease.
961
+
962
+ Needs the lease token in `x-xenon-lease-token`, role `MEMBER` or
963
+ above and the `devices` scope. An unknown id answers `403
964
+ token_mismatch`; an ended or expired lease answers `410`.
965
+ tags:
966
+ - Leases
967
+ parameters:
968
+ - $ref: '#/components/parameters/GridLeaseId'
969
+ - $ref: '#/components/parameters/GridLeaseToken'
970
+ requestBody:
971
+ required: true
972
+ content:
973
+ application/json:
974
+ schema:
975
+ type: object
976
+ required:
977
+ - durationMs
978
+ properties:
979
+ durationMs:
980
+ type: integer
981
+ description: How long from now the lease should last, in milliseconds.
982
+ example: 1800000
983
+ responses:
984
+ '200':
985
+ description: The lease's new end.
986
+ content:
987
+ application/json:
988
+ schema:
989
+ type: object
990
+ properties:
991
+ expiresAt:
992
+ type: integer
993
+ description: Epoch milliseconds.
994
+ example:
995
+ expiresAt: 1791115200000
996
+ '400':
997
+ description: '`durationMs` is missing or not a number.'
998
+ content:
999
+ application/json:
1000
+ schema:
1001
+ $ref: '#/components/schemas/Error'
1002
+ example:
1003
+ error: bad_request
1004
+ details: durationMs required
1005
+ '401':
1006
+ $ref: '#/components/responses/Unauthorized'
1007
+ '403':
1008
+ $ref: '#/components/responses/GridLeaseForbidden'
1009
+ '410':
1010
+ description: The lease has ended (released, expired or swept).
1011
+ content:
1012
+ application/json:
1013
+ schema:
1014
+ $ref: '#/components/schemas/Error'
1015
+ example:
1016
+ error: gone
1017
+ '429':
1018
+ $ref: '#/components/responses/RateLimited'
1019
+ '500':
1020
+ $ref: '#/components/responses/GridLeaseInternal'
1021
+ /api/sdk/leases/{id}:
1022
+ delete:
1023
+ operationId: releaseGridLease
1024
+ summary: Release a lease
1025
+ description: |
1026
+ Ends the lease and gives back its ports. The device is freed unless
1027
+ an Appium session still holds it; that session keeps it until it
1028
+ ends. A lease past `expiresAt` that the sweep hasn't ended yet can
1029
+ still be released.
1030
+
1031
+ Needs the lease token in `x-xenon-lease-token`, role `MEMBER` or
1032
+ above and the `devices` scope. An unknown id answers `403
1033
+ token_mismatch`; a lease that has already ended answers `404`.
1034
+ tags:
1035
+ - Leases
1036
+ parameters:
1037
+ - $ref: '#/components/parameters/GridLeaseId'
1038
+ - $ref: '#/components/parameters/GridLeaseToken'
1039
+ responses:
1040
+ '204':
1041
+ description: The lease is released. No body.
1042
+ '401':
1043
+ $ref: '#/components/responses/Unauthorized'
1044
+ '403':
1045
+ $ref: '#/components/responses/GridLeaseForbidden'
1046
+ '404':
1047
+ description: The lease has already ended.
1048
+ content:
1049
+ application/json:
1050
+ schema:
1051
+ $ref: '#/components/schemas/Error'
1052
+ example:
1053
+ error: not_found
1054
+ '429':
1055
+ $ref: '#/components/responses/RateLimited'
1056
+ '500':
1057
+ $ref: '#/components/responses/GridLeaseInternal'
1058
+ /api/sdk/version:
1059
+ get:
1060
+ operationId: getGridSdkVersion
1061
+ summary: Get the version and features for SDKs
1062
+ description: |
1063
+ The plugin's version and the SDK features this server supports, so
1064
+ a client can check compatibility before leasing. Needs role
1065
+ `MEMBER` or above.
1066
+ tags:
1067
+ - Leases
1068
+ responses:
1069
+ '200':
1070
+ description: The version and features.
1071
+ content:
1072
+ application/json:
1073
+ schema:
1074
+ type: object
1075
+ properties:
1076
+ pluginVersion:
1077
+ type: string
1078
+ supports:
1079
+ type: array
1080
+ items:
1081
+ type: string
1082
+ enum:
1083
+ - leases
1084
+ - ports
1085
+ - heartbeat
1086
+ example:
1087
+ pluginVersion: 2.12.0
1088
+ supports:
1089
+ - leases
1090
+ - ports
1091
+ - heartbeat
1092
+ '401':
1093
+ $ref: '#/components/responses/Unauthorized'
1094
+ '403':
1095
+ $ref: '#/components/responses/Forbidden'
1096
+ '429':
1097
+ $ref: '#/components/responses/RateLimited'
1098
+ /api/ports/allocate:
1099
+ post:
1100
+ operationId: allocateGridPorts
1101
+ summary: Reserve driver ports on this server
1102
+ description: |
1103
+ Picks a free local port on this server for each purpose and records
1104
+ it as leased to `udid` for `durationMs` plus 5 minutes. A hub calls
1105
+ it on the server a device is attached to (itself included) when it
1106
+ creates a lease, with its node credentials. The device isn't
1107
+ checked to exist.
1108
+
1109
+ Needs role `ADMIN` and the `devices` scope.
1110
+ tags:
1111
+ - Leases
1112
+ requestBody:
1113
+ required: true
1114
+ content:
1115
+ application/json:
1116
+ schema:
1117
+ type: object
1118
+ required:
1119
+ - udid
1120
+ - host
1121
+ - purposes
1122
+ properties:
1123
+ udid:
1124
+ type: string
1125
+ example: emulator-5554
1126
+ host:
1127
+ type: string
1128
+ example: http://192.168.1.100:4723
1129
+ purposes:
1130
+ type: array
1131
+ minItems: 1
1132
+ items:
1133
+ $ref: '#/components/schemas/GridPortPurpose'
1134
+ durationMs:
1135
+ type: integer
1136
+ description: How long to hold the ports. Default 1800000 (30 minutes); a value of 0 or less uses the default.
1137
+ default: 1800000
1138
+ leaseId:
1139
+ type: string
1140
+ description: The lease the ports are for, if known.
1141
+ example:
1142
+ udid: emulator-5554
1143
+ host: http://192.168.1.100:4723
1144
+ purposes:
1145
+ - systemPort
1146
+ - chromedriverPort
1147
+ - mjpegServerPort
1148
+ durationMs: 1800000
1149
+ responses:
1150
+ '200':
1151
+ description: The port reserved for each purpose.
1152
+ content:
1153
+ application/json:
1154
+ schema:
1155
+ type: object
1156
+ properties:
1157
+ ports:
1158
+ type: object
1159
+ additionalProperties:
1160
+ type: integer
1161
+ example:
1162
+ ports:
1163
+ systemPort: 52311
1164
+ chromedriverPort: 52312
1165
+ mjpegServerPort: 52313
1166
+ '400':
1167
+ description: '`udid`, `host` or `purposes` is missing, or a purpose is unknown.'
1168
+ content:
1169
+ application/json:
1170
+ schema:
1171
+ $ref: '#/components/schemas/Error'
1172
+ example:
1173
+ error: bad_request
1174
+ details: udid, host, purposes (non-empty, valid) required
1175
+ '401':
1176
+ $ref: '#/components/responses/Unauthorized'
1177
+ '403':
1178
+ $ref: '#/components/responses/Forbidden'
1179
+ '429':
1180
+ $ref: '#/components/responses/RateLimited'
1181
+ '500':
1182
+ description: A port could not be reserved; none of this request's ports are kept.
1183
+ content:
1184
+ application/json:
1185
+ schema:
1186
+ $ref: '#/components/schemas/Error'
1187
+ example:
1188
+ error: allocate_failed
1189
+ details: Unique constraint failed on the fields (`port`)
1190
+ /api/queue:
1191
+ get:
1192
+ operationId: listGridQueue
1193
+ summary: List the session requests waiting for a device
1194
+ description: |
1195
+ The session requests waiting for a free device, as the
1196
+ capabilities each client sent (`alwaysMatch` merged over the first
1197
+ `firstMatch`), with `capability_id` and `createdAt`.
1198
+
1199
+ An admin sees all of them. A member sees a request that names
1200
+ (`appium:udid`) a device they can see, or that they made, or that
1201
+ came from one of their teams; the rest are only counted, in
1202
+ `otherCount` of `GET /api/queue/summary`. A request queued by a
1203
+ server that didn't record who made it is only counted for a
1204
+ member.
1205
+
1206
+ Credentials are removed before a request is queued; any
1207
+ secret-named key a row still holds reads `***REDACTED***`, for
1208
+ every caller. Needs role `MEMBER` or above.
1209
+ tags:
1210
+ - Queue
1211
+ responses:
1212
+ '200':
1213
+ description: The waiting requests the caller may see, oldest first as stored.
1214
+ content:
1215
+ application/json:
1216
+ schema:
1217
+ type: array
1218
+ items:
1219
+ $ref: '#/components/schemas/GridQueuedRequest'
1220
+ example:
1221
+ - platformName: Android
1222
+ appium:automationName: UiAutomator2
1223
+ appium:udid: emulator-5554
1224
+ xe:options:
1225
+ buildId: build-2026-10-04-nightly
1226
+ capability_id: 3f2b8c1e-9a4d-4e7b-b2c6-1d8e5f7a9c0b
1227
+ createdAt: 1791111600000
1228
+ '401':
1229
+ $ref: '#/components/responses/Unauthorized'
1230
+ '403':
1231
+ $ref: '#/components/responses/Forbidden'
1232
+ '429':
1233
+ $ref: '#/components/responses/RateLimited'
1234
+ /api/queue/length:
1235
+ get:
1236
+ operationId: getGridQueueLength
1237
+ summary: Count the session requests waiting
1238
+ description: |
1239
+ How many session requests are waiting in the whole queue, as a bare
1240
+ number. Every caller gets the same number, including requests they
1241
+ can't see in `GET /api/queue`; it equals `total` in the summary.
1242
+ Needs role `MEMBER` or above.
1243
+ tags:
1244
+ - Queue
1245
+ responses:
1246
+ '200':
1247
+ description: The number of waiting requests.
1248
+ content:
1249
+ application/json:
1250
+ schema:
1251
+ type: integer
1252
+ example: 3
1253
+ '401':
1254
+ $ref: '#/components/responses/Unauthorized'
1255
+ '403':
1256
+ $ref: '#/components/responses/Forbidden'
1257
+ '429':
1258
+ $ref: '#/components/responses/RateLimited'
1259
+ /api/queue/summary:
1260
+ get:
1261
+ operationId: getGridQueueSummary
1262
+ summary: Summarise the session queue
1263
+ description: |
1264
+ Counts only. `total` and `byPlatform` cover the whole queue, as
1265
+ `GET /api/queue/length` does; `otherCount` is how many of them the
1266
+ caller doesn't get in detail from `GET /api/queue` (0 for an admin).
1267
+ `avgDurationMs` is the average of the platform's last 20 finished
1268
+ sessions, or 300000 (5 minutes) without history. Platforms are
1269
+ lowercase; a request naming none is `any`. Needs role `MEMBER` or
1270
+ above.
1271
+ tags:
1272
+ - Queue
1273
+ responses:
1274
+ '200':
1275
+ description: The queue's counts.
1276
+ content:
1277
+ application/json:
1278
+ schema:
1279
+ type: object
1280
+ properties:
1281
+ total:
1282
+ type: integer
1283
+ otherCount:
1284
+ type: integer
1285
+ byPlatform:
1286
+ type: object
1287
+ additionalProperties:
1288
+ type: object
1289
+ properties:
1290
+ count:
1291
+ type: integer
1292
+ avgDurationMs:
1293
+ type: integer
1294
+ example:
1295
+ total: 3
1296
+ otherCount: 1
1297
+ byPlatform:
1298
+ android:
1299
+ count: 2
1300
+ avgDurationMs: 412000
1301
+ ios:
1302
+ count: 1
1303
+ avgDurationMs: 300000
1304
+ '401':
1305
+ $ref: '#/components/responses/Unauthorized'
1306
+ '403':
1307
+ $ref: '#/components/responses/Forbidden'
1308
+ '429':
1309
+ $ref: '#/components/responses/RateLimited'
1310
+ /api/queue/status/{capability_id}:
1311
+ get:
1312
+ operationId: getGridQueuedRequestStatus
1313
+ summary: Get a waiting request's position and wait
1314
+ description: |
1315
+ Where one waiting session request stands among the requests for
1316
+ its platform, and a rough wait: its position divided by the number
1317
+ of devices of that platform, times the average session duration
1318
+ (0 when it is first and a device is free).
1319
+
1320
+ `404` when the request has already been given a device, was
1321
+ dropped, or is one the caller can't see in `GET /api/queue`: the
1322
+ same answer as for an unknown id. Needs role `MEMBER` or above.
1323
+ tags:
1324
+ - Queue
1325
+ parameters:
1326
+ - in: path
1327
+ name: capability_id
1328
+ required: true
1329
+ description: The request's `capability_id`, as `GET /api/queue` lists it.
1330
+ schema:
1331
+ type: string
1332
+ example: 3f2b8c1e-9a4d-4e7b-b2c6-1d8e5f7a9c0b
1333
+ responses:
1334
+ '200':
1335
+ description: The request's place in the queue.
1336
+ content:
1337
+ application/json:
1338
+ schema:
1339
+ type: object
1340
+ properties:
1341
+ position:
1342
+ type: integer
1343
+ description: 1 is next.
1344
+ totalInQueue:
1345
+ type: integer
1346
+ description: Waiting requests for this platform (or any platform).
1347
+ etaInMs:
1348
+ type: integer
1349
+ platform:
1350
+ type: string
1351
+ matchedDevicesCount:
1352
+ type: integer
1353
+ availableDevicesCount:
1354
+ type: integer
1355
+ example:
1356
+ position: 2
1357
+ totalInQueue: 2
1358
+ etaInMs: 206000
1359
+ platform: android
1360
+ matchedDevicesCount: 4
1361
+ availableDevicesCount: 0
1362
+ '401':
1363
+ $ref: '#/components/responses/Unauthorized'
1364
+ '403':
1365
+ $ref: '#/components/responses/Forbidden'
1366
+ '404':
1367
+ description: No such waiting request, or one the caller can't see.
1368
+ content:
1369
+ application/json:
1370
+ schema:
1371
+ $ref: '#/components/schemas/Error'
1372
+ example:
1373
+ error: Pending session not found
1374
+ '429':
1375
+ $ref: '#/components/responses/RateLimited'
1376
+ /api/register:
1377
+ post:
1378
+ operationId: reportGridNodeDevices
1379
+ summary: Report a node's devices to its hub
1380
+ description: |
1381
+ How a node keeps its hub's device list current. Nodes call it
1382
+ themselves, with the access key and token they were provisioned
1383
+ with (`XENON_HUB_ACCESS_KEY`, `XENON_HUB_TOKEN`); it isn't meant for
1384
+ other clients. Needs role `ADMIN` and the `devices` scope.
1385
+
1386
+ `type` selects the operation:
1387
+
1388
+ | `type` | Body | Effect |
1389
+ |---|---|---|
1390
+ | `add` | the node's devices | Adds or refreshes each one |
1391
+ | `remove` | devices, each with `udid` and `nodeId` or `host` | Forgets those devices |
1392
+ | `unregister` | `[]` | Forgets every device of the node named by `nodeId` or `host` in the query |
1393
+
1394
+ A report (`add`) updates only what the node observes: discovery
1395
+ fields such as name, version and ports, and health. Whether the
1396
+ device is busy on the node goes to `nodeBusy`, and a preview hold
1397
+ there to `nodeHold`. The hub's own settings (team, tags,
1398
+ reservation, block, its claim for a session) are never taken from
1399
+ a report, and a report of "free" never frees a device the hub has
1400
+ claimed.
1401
+
1402
+ A removal takes only the node's own rows: by `nodeId` when given,
1403
+ otherwise by exactly `host`. It never takes one of the hub's own
1404
+ devices, and an entry with no udid, or with neither `nodeId` nor
1405
+ `host`, takes nothing. Any other `type`, or none, changes nothing
1406
+ and still answers `200`.
1407
+ tags:
1408
+ - Hub-Node
1409
+ security:
1410
+ - AccessKeyAuth: []
1411
+ TokenAuth: []
1412
+ parameters:
1413
+ - in: query
1414
+ name: type
1415
+ required: true
1416
+ description: The operation.
1417
+ schema:
1418
+ type: string
1419
+ enum:
1420
+ - add
1421
+ - remove
1422
+ - unregister
1423
+ - in: query
1424
+ name: host
1425
+ required: false
1426
+ description: For `unregister`, the node's host, used when `nodeId` isn't sent.
1427
+ schema:
1428
+ type: string
1429
+ example: http://192.168.1.100:4723
1430
+ - in: query
1431
+ name: nodeId
1432
+ required: false
1433
+ description: For `unregister`, the node's id; every device the node reported goes, whatever its host.
1434
+ schema:
1435
+ type: string
1436
+ example: 6b1e4c2a-8f3d-4a9e-b7c5-2d0f1e8a9b3c
1437
+ requestBody:
1438
+ required: true
1439
+ content:
1440
+ application/json:
1441
+ schema:
1442
+ type: array
1443
+ items:
1444
+ $ref: '#/components/schemas/GridNodeDeviceReport'
1445
+ examples:
1446
+ add:
1447
+ summary: type=add
1448
+ value:
1449
+ - udid: emulator-5554
1450
+ host: http://192.168.1.100:4723
1451
+ nodeId: 6b1e4c2a-8f3d-4a9e-b7c5-2d0f1e8a9b3c
1452
+ name: Pixel 7 API 34
1453
+ platform: android
1454
+ sdk: '14'
1455
+ state: device
1456
+ deviceType: emulator
1457
+ realDevice: false
1458
+ busy: false
1459
+ offline: false
1460
+ healthStatus: Healthy
1461
+ remove:
1462
+ summary: type=remove
1463
+ value:
1464
+ - udid: emulator-5554
1465
+ host: http://192.168.1.100:4723
1466
+ nodeId: 6b1e4c2a-8f3d-4a9e-b7c5-2d0f1e8a9b3c
1467
+ unregister:
1468
+ summary: type=unregister
1469
+ value: []
1470
+ responses:
1471
+ '200':
1472
+ description: The report was applied (or ignored, for an unknown `type`).
1473
+ content:
1474
+ application/json:
1475
+ schema:
1476
+ $ref: '#/components/schemas/Success'
1477
+ example:
1478
+ success: true
1479
+ '401':
1480
+ $ref: '#/components/responses/Unauthorized'
1481
+ '403':
1482
+ $ref: '#/components/responses/Forbidden'
1483
+ '429':
1484
+ $ref: '#/components/responses/RateLimited'
1485
+ /api/node:
1486
+ get:
1487
+ operationId: listGridNodeHosts
1488
+ summary: List the hosts that have devices
1489
+ description: |
1490
+ The distinct `host` of every device the caller can see: this
1491
+ server and, on a hub, each node. A host whose devices all belong to
1492
+ teams the caller isn't in isn't listed. Needs role `MEMBER` or
1493
+ above.
1494
+ tags:
1495
+ - Hub-Node
1496
+ responses:
1497
+ '200':
1498
+ description: The hosts.
1499
+ content:
1500
+ application/json:
1501
+ schema:
1502
+ type: array
1503
+ items:
1504
+ type: string
1505
+ example:
1506
+ - http://192.168.1.10:4723
1507
+ - http://192.168.1.100:4723
1508
+ '401':
1509
+ $ref: '#/components/responses/Unauthorized'
1510
+ '403':
1511
+ $ref: '#/components/responses/Forbidden'
1512
+ '429':
1513
+ $ref: '#/components/responses/RateLimited'
1514
+ /api/node/status:
1515
+ get:
1516
+ operationId: getGridLocalNodeStatus
1517
+ summary: List the devices attached to this server
1518
+ description: |
1519
+ Runs device discovery on this server now (Android and iOS, real
1520
+ devices and emulators or simulators) and lists what it finds, with
1521
+ each device's `state` as discovery reports it. Devices of other
1522
+ servers aren't included. Needs role `ADMIN` and the `admin` scope.
1523
+ tags:
1524
+ - Hub-Node
1525
+ responses:
1526
+ '200':
1527
+ $ref: '#/components/responses/GridNodeStatusOk'
1528
+ '401':
1529
+ $ref: '#/components/responses/Unauthorized'
1530
+ '403':
1531
+ $ref: '#/components/responses/Forbidden'
1532
+ '429':
1533
+ $ref: '#/components/responses/RateLimited'
1534
+ /api/node/{host}/status:
1535
+ get:
1536
+ operationId: getGridNodeStatus
1537
+ summary: List the devices attached to one server
1538
+ description: |
1539
+ When `host` is this server's bind address (`bindHostOrIp`), the
1540
+ same as `GET /api/node/status`. Otherwise the hub looks for a
1541
+ device whose host contains `host` and asks that server's
1542
+ `/xenon/api/node/status`, returning its answer. That request
1543
+ carries no credentials, so it works only against a node with auth
1544
+ disabled; a node that refuses it, or can't be reached, answers
1545
+ `502`.
1546
+
1547
+ Needs role `ADMIN` and the `admin` scope.
1548
+ tags:
1549
+ - Hub-Node
1550
+ parameters:
1551
+ - in: path
1552
+ name: host
1553
+ required: true
1554
+ description: This server's bind address, or part of a node's host (such as its IP).
1555
+ schema:
1556
+ type: string
1557
+ example: 192.168.1.100
1558
+ responses:
1559
+ '200':
1560
+ $ref: '#/components/responses/GridNodeStatusOk'
1561
+ '401':
1562
+ $ref: '#/components/responses/Unauthorized'
1563
+ '403':
1564
+ $ref: '#/components/responses/Forbidden'
1565
+ '404':
1566
+ description: No device is listed under that host, so there is nowhere to ask. Plain text.
1567
+ content:
1568
+ text/html:
1569
+ schema:
1570
+ type: string
1571
+ example: Host 192.168.1.200 does not have any devices listed in database. I don't know how to forward request to that host
1572
+ '429':
1573
+ $ref: '#/components/responses/RateLimited'
1574
+ '502':
1575
+ description: The node refused the request or couldn't be reached.
1576
+ content:
1577
+ application/json:
1578
+ schema:
1579
+ $ref: '#/components/schemas/Error'
1580
+ example:
1581
+ error: node_unreachable
1582
+ message: "Could not get http://192.168.1.200:4723's status: connect ECONNREFUSED 192.168.1.200:4723"
1583
+ /api/node/sessions/{sessionId}:
1584
+ get:
1585
+ operationId: getGridNodeSessionStatus
1586
+ summary: Say whether a session exists on this node
1587
+ description: |
1588
+ Node only: a hub asks it about each session it routes to the node,
1589
+ about every 30 s, instead of sending a WebDriver command, which
1590
+ would keep an abandoned session alive. It runs no command. Only a
1591
+ server started with `hub` set has this route.
1592
+
1593
+ It is answered before the login, with what a command to that
1594
+ session would need. With per-command auth off
1595
+ (`XENON_REQUIRE_COMMAND_AUTH`) or auth disabled, no credential.
1596
+ With it on, the hub's session token for that session in
1597
+ `x-xenon-hub-token`; any other caller gets WebDriver's
1598
+ unknown-session answer, `404`, and a hub key set that can't be
1599
+ fetched is `503`. It isn't rate-limited.
1600
+
1601
+ Every answer from a node carries `x-xenon-node-sessions: 1`, so a
1602
+ hub can tell a node that has the route from an older one.
1603
+ tags:
1604
+ - Hub-Node
1605
+ security:
1606
+ - HubToken: []
1607
+ - {}
1608
+ parameters:
1609
+ - $ref: '#/components/parameters/GridNodeSessionId'
1610
+ responses:
1611
+ '200':
1612
+ description: Whether this node's Appium has the session.
1613
+ headers:
1614
+ x-xenon-node-sessions:
1615
+ description: Always `1` on a node.
1616
+ schema:
1617
+ type: string
1618
+ example: '1'
1619
+ content:
1620
+ application/json:
1621
+ schema:
1622
+ type: object
1623
+ properties:
1624
+ value:
1625
+ type: object
1626
+ properties:
1627
+ sessionId:
1628
+ type: string
1629
+ exists:
1630
+ type: boolean
1631
+ example:
1632
+ value:
1633
+ sessionId: 9d4c2f1a-7b3e-4e8a-a6d5-0c1b2e3f4a5b
1634
+ exists: true
1635
+ '401':
1636
+ $ref: '#/components/responses/GridNotANodeUnauthorized'
1637
+ '404':
1638
+ $ref: '#/components/responses/GridNodeUnknownSession'
1639
+ '503':
1640
+ $ref: '#/components/responses/GridNodeTokenUnavailable'
1641
+ /api/node/sessions/{sessionId}/metrics:
1642
+ get:
1643
+ operationId: getGridNodeSessionMetrics
1644
+ summary: Collect a session's CPU and memory from this node
1645
+ description: |
1646
+ Node only: a hub collects the CPU and memory samples a node has
1647
+ taken for a session on one of its phones, every 10 s and once more
1648
+ when the session ends. The node keeps them in memory: the newest
1649
+ 900 per session, an ended session's for 10 minutes. Only a server
1650
+ started with `hub` set has this route.
1651
+
1652
+ `after` asks for the samples newer than that time, and the node
1653
+ drops the older ones, so a later call never returns them again.
1654
+ `state` is `sampling`, `stopped` (the sampler gave up), `ended`, or
1655
+ `off` (the node has nothing for this session).
1656
+
1657
+ It is answered before the login, with the same rule as
1658
+ `GET /api/node/sessions/{sessionId}`: no credential with
1659
+ per-command auth off or auth disabled, otherwise the hub's session
1660
+ token for that session. Every answer from a node carries
1661
+ `x-xenon-node-metrics: 1`. It isn't rate-limited.
1662
+ tags:
1663
+ - Hub-Node
1664
+ security:
1665
+ - HubToken: []
1666
+ - {}
1667
+ parameters:
1668
+ - $ref: '#/components/parameters/GridNodeSessionId'
1669
+ - in: query
1670
+ name: after
1671
+ required: false
1672
+ description: Epoch milliseconds; only newer samples are returned, and older ones are dropped. Anything that isn't a plain number is ignored.
1673
+ schema:
1674
+ type: number
1675
+ example: 1791111600000
1676
+ responses:
1677
+ '200':
1678
+ description: The session's samples.
1679
+ headers:
1680
+ x-xenon-node-metrics:
1681
+ description: Always `1` on a node.
1682
+ schema:
1683
+ type: string
1684
+ example: '1'
1685
+ content:
1686
+ application/json:
1687
+ schema:
1688
+ type: object
1689
+ properties:
1690
+ value:
1691
+ $ref: '#/components/schemas/GridNodeMetrics'
1692
+ example:
1693
+ value:
1694
+ platform: android
1695
+ state: sampling
1696
+ samples:
1697
+ - at: 1791111602000
1698
+ deviceCpuPct: 23.5
1699
+ deviceMemMb: 4120
1700
+ deviceMemTotalMb: 7680
1701
+ appCpuPct: 11.2
1702
+ appMemMb: 286
1703
+ appId: com.example.shop
1704
+ - at: 1791111604000
1705
+ deviceCpuPct: 19.1
1706
+ deviceMemMb: 4128
1707
+ deviceMemTotalMb: 7680
1708
+ appCpuPct: 8.7
1709
+ appMemMb: 289
1710
+ appId: com.example.shop
1711
+ '401':
1712
+ $ref: '#/components/responses/GridNotANodeUnauthorized'
1713
+ '404':
1714
+ $ref: '#/components/responses/GridNodeUnknownSession'
1715
+ '503':
1716
+ $ref: '#/components/responses/GridNodeTokenUnavailable'
1717
+ /api/status:
1718
+ get:
1719
+ operationId: getGridServerStatus
1720
+ summary: Check that the server answers
1721
+ description: |
1722
+ A signed-in liveness check. `version` comes from the npm
1723
+ environment, so a server not started through an npm script reports
1724
+ `unknown (not running from npm package)`; `GET /api/ping` reports
1725
+ the plugin's version. Needs role `MEMBER` or above.
1726
+ tags:
1727
+ - Health & Ops
1728
+ responses:
1729
+ '200':
1730
+ description: The server is up.
1731
+ content:
1732
+ application/json:
1733
+ schema:
1734
+ type: object
1735
+ properties:
1736
+ status:
1737
+ type: string
1738
+ enum:
1739
+ - ok
1740
+ version:
1741
+ type: string
1742
+ example:
1743
+ status: ok
1744
+ version: unknown (not running from npm package)
1745
+ '401':
1746
+ $ref: '#/components/responses/Unauthorized'
1747
+ '403':
1748
+ $ref: '#/components/responses/Forbidden'
1749
+ '429':
1750
+ $ref: '#/components/responses/RateLimited'
1751
+ /api/webdriver:
1752
+ get:
1753
+ operationId: getGridWebDriverBasePath
1754
+ summary: Get this server's WebDriver base path
1755
+ description: |
1756
+ Where this server's Appium WebDriver API lives, relative to its
1757
+ origin: `''` for Appium's default, or a path such as `/wd/hub`. A
1758
+ hub asks each node before it sends the node a session or a command
1759
+ (and caches the answer for a minute), since hub and node needn't
1760
+ share a base path. Public: no credential and no rate limit.
1761
+ tags:
1762
+ - Health & Ops
1763
+ security: []
1764
+ responses:
1765
+ '200':
1766
+ description: The base path.
1767
+ content:
1768
+ application/json:
1769
+ schema:
1770
+ type: object
1771
+ properties:
1772
+ basePath:
1773
+ type: string
1774
+ description: Empty, or a path starting with `/` and without a trailing `/`.
1775
+ example:
1776
+ basePath: /wd/hub
1777
+ components:
1778
+ parameters:
1779
+ GridSessionIdQuery:
1780
+ in: query
1781
+ name: sessionId
1782
+ required: false
1783
+ description: Return only the device running this Appium session (an object), or an empty body when none does.
1784
+ schema:
1785
+ type: string
1786
+ example: 9d4c2f1a-7b3e-4e8a-a6d5-0c1b2e3f4a5b
1787
+ GridReservationUdid:
1788
+ in: path
1789
+ name: udid
1790
+ required: true
1791
+ description: The device's udid.
1792
+ schema:
1793
+ type: string
1794
+ example: emulator-5554
1795
+ GridReservationHost:
1796
+ in: path
1797
+ name: host
1798
+ required: true
1799
+ description: The device's host, URL-encoded.
1800
+ schema:
1801
+ type: string
1802
+ example: http%3A%2F%2F192.168.1.100%3A4723
1803
+ GridLeaseId:
1804
+ in: path
1805
+ name: id
1806
+ required: true
1807
+ description: The lease's `leaseId`.
1808
+ schema:
1809
+ type: string
1810
+ example: clxa1b2c30000s8k2f9d1e7aa
1811
+ GridLeaseToken:
1812
+ in: header
1813
+ name: x-xenon-lease-token
1814
+ required: true
1815
+ description: The `leaseToken` from the lease's creation.
1816
+ schema:
1817
+ type: string
1818
+ example: 7f3c9a1e5b2d48f0a6c4e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1
1819
+ GridNodeSessionId:
1820
+ in: path
1821
+ name: sessionId
1822
+ required: true
1823
+ description: The Appium session's id on the node.
1824
+ schema:
1825
+ type: string
1826
+ example: 9d4c2f1a-7b3e-4e8a-a6d5-0c1b2e3f4a5b
1827
+ headers:
1828
+ GridDeprecation:
1829
+ description: Always `true`; reservations are deprecated in favour of leases.
1830
+ schema:
1831
+ type: string
1832
+ example: 'true'
1833
+ GridSunset:
1834
+ description: When reservations will be removed.
1835
+ schema:
1836
+ type: string
1837
+ example: '2027-01-01T00:00:00Z'
1838
+ GridLink:
1839
+ description: Where the lease API that replaces reservations is described (`rel="alternate"`).
1840
+ schema:
1841
+ type: string
1842
+ responses:
1843
+ GridDeviceListOk:
1844
+ description: |
1845
+ The visible devices; with `sessionId`, the one device running that
1846
+ session, or an empty body when none does.
1847
+ content:
1848
+ application/json:
1849
+ schema:
1850
+ oneOf:
1851
+ - type: array
1852
+ items:
1853
+ $ref: '#/components/schemas/GridDevice'
1854
+ - $ref: '#/components/schemas/GridDevice'
1855
+ example:
1856
+ - udid: 00008110-00084CE80E51401E
1857
+ name: iPhone 14 Pro
1858
+ platform: ios
1859
+ host: http://192.168.1.10:4723
1860
+ busy: true
1861
+ session_id: 9d4c2f1a-7b3e-4e8a-a6d5-0c1b2e3f4a5b
1862
+ state: device
1863
+ sdk: '17.4'
1864
+ deviceType: real
1865
+ realDevice: true
1866
+ offline: false
1867
+ userBlocked: false
1868
+ teamId: clx9k2v0a0000qz8r4h7m3n2p
1869
+ teamName: Checkout
1870
+ healthStatus: Healthy
1871
+ batteryLevel: 87
1872
+ - udid: emulator-5554
1873
+ name: Pixel 7 API 34
1874
+ platform: android
1875
+ host: http://192.168.1.100:4723
1876
+ nodeId: 6b1e4c2a-8f3d-4a9e-b7c5-2d0f1e8a9b3c
1877
+ busy: false
1878
+ session_id: null
1879
+ state: device
1880
+ sdk: '14'
1881
+ deviceType: emulator
1882
+ realDevice: false
1883
+ offline: false
1884
+ userBlocked: false
1885
+ teamId: null
1886
+ teamName: null
1887
+ tags:
1888
+ - lab-row-3
1889
+ reservedBy: null
1890
+ reservedUntil: null
1891
+ healthStatus: Healthy
1892
+ batteryLevel: 100
1893
+ screenWidth: '1080'
1894
+ screenHeight: '2400'
1895
+ GridDeviceNotFound:
1896
+ description: No such device, or one outside your teams (the two answer the same).
1897
+ content:
1898
+ application/json:
1899
+ schema:
1900
+ $ref: '#/components/schemas/Error'
1901
+ example:
1902
+ success: false
1903
+ error: Device not found
1904
+ GridDeviceSelectorMissing:
1905
+ description: '`udid` or `host` is missing, or not a string.'
1906
+ content:
1907
+ application/json:
1908
+ schema:
1909
+ $ref: '#/components/schemas/Error'
1910
+ example:
1911
+ success: false
1912
+ error: bad_request
1913
+ message: udid and host are required
1914
+ GridNotReserved:
1915
+ description: The device isn't reserved.
1916
+ content:
1917
+ application/json:
1918
+ schema:
1919
+ $ref: '#/components/schemas/Error'
1920
+ example:
1921
+ success: false
1922
+ error: Device is not reserved
1923
+ GridReservationFailed:
1924
+ description: The reservation store failed.
1925
+ content:
1926
+ application/json:
1927
+ schema:
1928
+ $ref: '#/components/schemas/Error'
1929
+ example:
1930
+ success: false
1931
+ error: Failed to reserve device
1932
+ GridLeaseForbidden:
1933
+ description: |
1934
+ No lease token (`missing_lease_token`), a token that doesn't match or
1935
+ an unknown lease id (`token_mismatch`), the role or scope is missing,
1936
+ or the same-origin check failed.
1937
+ content:
1938
+ application/json:
1939
+ schema:
1940
+ $ref: '#/components/schemas/Error'
1941
+ examples:
1942
+ missing:
1943
+ value:
1944
+ error: missing_lease_token
1945
+ mismatch:
1946
+ value:
1947
+ error: token_mismatch
1948
+ scope:
1949
+ value:
1950
+ error: insufficient scope
1951
+ GridLeaseInternal:
1952
+ description: An unexpected error.
1953
+ content:
1954
+ application/json:
1955
+ schema:
1956
+ $ref: '#/components/schemas/Error'
1957
+ example:
1958
+ error: internal
1959
+ message: Can't reach database server
1960
+ GridNodeStatusOk:
1961
+ description: The attached devices.
1962
+ content:
1963
+ application/json:
1964
+ schema:
1965
+ type: array
1966
+ items:
1967
+ type: object
1968
+ properties:
1969
+ udid:
1970
+ type: string
1971
+ host:
1972
+ type: string
1973
+ state:
1974
+ type: string
1975
+ platform:
1976
+ type: string
1977
+ enum:
1978
+ - ios
1979
+ - android
1980
+ example:
1981
+ - udid: emulator-5554
1982
+ host: http://192.168.1.100:4723
1983
+ state: device
1984
+ platform: android
1985
+ - udid: 00008110-00084CE80E51401E
1986
+ host: http://192.168.1.100:4723
1987
+ state: device
1988
+ platform: ios
1989
+ GridNotANodeUnauthorized:
1990
+ description: |
1991
+ Never sent by a node. A server that isn't a node has no such route,
1992
+ so its login answers first: `401` without credentials (and `404`
1993
+ with them).
1994
+ content:
1995
+ application/json:
1996
+ schema:
1997
+ $ref: '#/components/schemas/Error'
1998
+ example:
1999
+ error: unauthenticated
2000
+ GridNodeUnknownSession:
2001
+ description: |
2002
+ Per-command auth is on and the request has no valid hub session
2003
+ token for this session. WebDriver's unknown-session answer, the same
2004
+ whether or not the session exists.
2005
+ content:
2006
+ application/json:
2007
+ schema:
2008
+ $ref: '#/components/schemas/GridWebDriverError'
2009
+ example:
2010
+ value:
2011
+ error: invalid session id
2012
+ message: A session is either terminated or not started
2013
+ stacktrace: ''
2014
+ GridNodeTokenUnavailable:
2015
+ description: The hub's token could not be checked (its key set couldn't be fetched). Try again.
2016
+ content:
2017
+ application/json:
2018
+ schema:
2019
+ $ref: '#/components/schemas/GridWebDriverError'
2020
+ example:
2021
+ value:
2022
+ error: unknown error
2023
+ message: Xenon could not verify access to this session. Try again.
2024
+ stacktrace: ''
2025
+ schemas:
2026
+ GridDevice:
2027
+ description: A device as the device lists return it, with its team's name.
2028
+ allOf:
2029
+ - $ref: '#/components/schemas/Device'
2030
+ - type: object
2031
+ properties:
2032
+ teamName:
2033
+ type: string
2034
+ nullable: true
2035
+ description: The team's name; null for the shared pool.
2036
+ tags:
2037
+ type: array
2038
+ items:
2039
+ type: string
2040
+ nodeBusy:
2041
+ type: boolean
2042
+ nullable: true
2043
+ description: On a hub, whether the node last reported the device busy.
2044
+ nodeHold:
2045
+ type: string
2046
+ nullable: true
2047
+ description: On a hub, the preview hold the node last reported (`manual_<userId>_<udid>`).
2048
+ dashboard_link:
2049
+ type: string
2050
+ description: Only with the Appium dashboard plugin installed.
2051
+ total_session_count:
2052
+ type: integer
2053
+ description: Only with the Appium dashboard plugin installed.
2054
+ GridDeviceSelector:
2055
+ type: object
2056
+ description: Picks a device by both its `udid` and its `host`, exactly as `GET /api/devices` shows them.
2057
+ required:
2058
+ - udid
2059
+ - host
2060
+ properties:
2061
+ udid:
2062
+ type: string
2063
+ host:
2064
+ type: string
2065
+ GridReservationDuration:
2066
+ description: '`1h`, `2h`, `4h` or `8h`, or a number of milliseconds. A reservation lasts from 1 minute to 24 hours, and an extension may not end it more than 24 hours from now.'
2067
+ oneOf:
2068
+ - type: string
2069
+ enum:
2070
+ - 1h
2071
+ - 2h
2072
+ - 4h
2073
+ - 8h
2074
+ - type: integer
2075
+ example: 5400000
2076
+ GridReservation:
2077
+ type: object
2078
+ properties:
2079
+ udid:
2080
+ type: string
2081
+ host:
2082
+ type: string
2083
+ name:
2084
+ type: string
2085
+ platform:
2086
+ type: string
2087
+ enum:
2088
+ - ios
2089
+ - android
2090
+ reservedBy:
2091
+ type: string
2092
+ reservedUntil:
2093
+ type: integer
2094
+ description: Epoch milliseconds.
2095
+ reservationReason:
2096
+ type: string
2097
+ nullable: true
2098
+ remainingMs:
2099
+ type: integer
2100
+ GridLeaseCreateRequest:
2101
+ type: object
2102
+ required:
2103
+ - filters
2104
+ properties:
2105
+ filters:
2106
+ type: object
2107
+ required:
2108
+ - platform
2109
+ properties:
2110
+ platform:
2111
+ type: string
2112
+ enum:
2113
+ - android
2114
+ - ios
2115
+ udid:
2116
+ type: string
2117
+ description: Lease this device only.
2118
+ deviceType:
2119
+ type: string
2120
+ enum:
2121
+ - real
2122
+ - simulator
2123
+ - emulator
2124
+ tags:
2125
+ type: array
2126
+ description: The device must carry every one of these tags.
2127
+ items:
2128
+ type: string
2129
+ platformVersion:
2130
+ type: string
2131
+ description: The OS version must equal this (compared as a version).
2132
+ minSDK:
2133
+ type: string
2134
+ example: '13'
2135
+ maxSDK:
2136
+ type: string
2137
+ example: '15'
2138
+ sdk:
2139
+ type: string
2140
+ description: The OS version, as `platformVersion`; `platformVersion` wins if both are sent.
2141
+ deviceName:
2142
+ type: string
2143
+ description: The device's name, ignoring case.
2144
+ durationMs:
2145
+ type: integer
2146
+ description: How long the lease lasts. Kept between 60000 (1 minute) and 86400000 (24 hours).
2147
+ default: 1800000
2148
+ heartbeatSeconds:
2149
+ type: integer
2150
+ description: How often you will send a heartbeat. Kept between 10 and 300.
2151
+ default: 30
2152
+ reason:
2153
+ type: string
2154
+ buildId:
2155
+ type: string
2156
+ description: Carried into `appiumCapabilities` as `xe:options.buildId`.
2157
+ GridLease:
2158
+ type: object
2159
+ properties:
2160
+ leaseId:
2161
+ type: string
2162
+ leaseToken:
2163
+ type: string
2164
+ description: The lease's secret. Shown only here.
2165
+ device:
2166
+ type: object
2167
+ properties:
2168
+ udid:
2169
+ type: string
2170
+ host:
2171
+ type: string
2172
+ platform:
2173
+ type: string
2174
+ sdk:
2175
+ type: string
2176
+ name:
2177
+ type: string
2178
+ screen:
2179
+ type: object
2180
+ properties:
2181
+ width:
2182
+ type: string
2183
+ nullable: true
2184
+ height:
2185
+ type: string
2186
+ nullable: true
2187
+ realDevice:
2188
+ type: boolean
2189
+ expiresAt:
2190
+ type: integer
2191
+ description: Epoch milliseconds.
2192
+ heartbeatSeconds:
2193
+ type: integer
2194
+ allocatedPorts:
2195
+ type: object
2196
+ description: '`systemPort`, `chromedriverPort` and `mjpegServerPort` for Android; `wdaLocalPort` and `mjpegServerPort` for iOS.'
2197
+ additionalProperties:
2198
+ type: integer
2199
+ appiumCapabilities:
2200
+ type: object
2201
+ description: Capabilities for `POST /session`, including `xe:options.leaseId` and `xe:options.leaseToken`.
2202
+ additionalProperties: true
2203
+ GridPortPurpose:
2204
+ type: string
2205
+ enum:
2206
+ - systemPort
2207
+ - wdaLocalPort
2208
+ - chromedriverPort
2209
+ - mjpegServerPort
2210
+ GridQueuedRequest:
2211
+ type: object
2212
+ description: The capabilities a waiting session request sent, merged, plus its queue fields.
2213
+ properties:
2214
+ capability_id:
2215
+ type: string
2216
+ createdAt:
2217
+ type: integer
2218
+ description: Epoch milliseconds.
2219
+ additionalProperties: true
2220
+ GridNodeDeviceReport:
2221
+ type: object
2222
+ description: |
2223
+ One device in a node's report. For `add`, the node's full view of the
2224
+ device (the fields of `Device`); for `remove`, `udid` with `nodeId`
2225
+ or `host`.
2226
+ required:
2227
+ - udid
2228
+ properties:
2229
+ udid:
2230
+ type: string
2231
+ host:
2232
+ type: string
2233
+ nodeId:
2234
+ type: string
2235
+ busy:
2236
+ type: boolean
2237
+ description: Busy on the node; stored as `nodeBusy`.
2238
+ session_id:
2239
+ type: string
2240
+ nullable: true
2241
+ description: The node's session or hold; a preview hold is stored as `nodeHold`.
2242
+ additionalProperties: true
2243
+ GridNodeMetrics:
2244
+ type: object
2245
+ properties:
2246
+ platform:
2247
+ type: string
2248
+ description: Lowercase; empty when `state` is `off`.
2249
+ state:
2250
+ type: string
2251
+ enum:
2252
+ - sampling
2253
+ - stopped
2254
+ - ended
2255
+ - 'off'
2256
+ samples:
2257
+ type: array
2258
+ items:
2259
+ type: object
2260
+ properties:
2261
+ at:
2262
+ type: integer
2263
+ description: Epoch milliseconds.
2264
+ deviceCpuPct:
2265
+ type: number
2266
+ nullable: true
2267
+ deviceMemMb:
2268
+ type: number
2269
+ nullable: true
2270
+ deviceMemTotalMb:
2271
+ type: number
2272
+ nullable: true
2273
+ appCpuPct:
2274
+ type: number
2275
+ nullable: true
2276
+ appMemMb:
2277
+ type: number
2278
+ nullable: true
2279
+ appId:
2280
+ type: string
2281
+ nullable: true
2282
+ description: The app the app figures are for (Android), else null.
2283
+ GridWebDriverError:
2284
+ type: object
2285
+ description: A WebDriver-style error, as Appium answers one.
2286
+ properties:
2287
+ value:
2288
+ type: object
2289
+ properties:
2290
+ error:
2291
+ type: string
2292
+ message:
2293
+ type: string
2294
+ stacktrace:
2295
+ type: string