@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,2168 @@
1
+ paths:
2
+ # ---------------------------------------------------------------------------
3
+ # Authentication: public routes (no credential)
4
+ # ---------------------------------------------------------------------------
5
+ /api/auth/options:
6
+ get:
7
+ operationId: getSignInOptions
8
+ summary: Get what the sign-in page may offer
9
+ description: |
10
+ Public. Tells the sign-in page how a user who forgot their password can get back in, before
11
+ anyone is signed in.
12
+
13
+ `passwordReset` is `email` when the server has SMTP configured (the forgot-password form
14
+ really emails a link), otherwise `admin` (the user must ask an administrator, who issues a
15
+ link with `POST /api/users/{id}/reset-link`). Nothing else about the mail setup is revealed.
16
+ tags:
17
+ - Authentication
18
+ security: []
19
+ responses:
20
+ '200':
21
+ description: The sign-in options.
22
+ content:
23
+ application/json:
24
+ schema:
25
+ $ref: '#/components/schemas/IdentitySignInOptions'
26
+ example:
27
+ passwordReset: admin
28
+
29
+ /api/auth/login:
30
+ post:
31
+ operationId: signIn
32
+ summary: Sign in with email and password
33
+ description: |
34
+ Public. Checks the email (case-insensitive) and password of an `ACTIVE` user and, on success,
35
+ sets the `xenon_dashboard_session` cookie (HttpOnly, `SameSite=Strict`, `Secure` when the
36
+ request came over HTTPS or with `X-Forwarded-Proto: https`). The cookie lives for the
37
+ server's user-session lifetime (24 hours by default, `XENON_USER_SESSION_TTL_MS`) and is
38
+ renewed on every authenticated request. Updates the user's last sign-in time.
39
+
40
+ A wrong password, an unknown email and an inactive user all answer the same `401`.
41
+
42
+ **Rate limit.** Each client IP (the first `X-Forwarded-For` address, else the socket address)
43
+ gets 5 attempts per 5 minutes by default, counting successful ones; a successful sign-in resets
44
+ the count. Past that the answer is `429` with `Retry-After`.
45
+
46
+ **Same-origin rule.** Like every state-changing request without an `x-xenon-access-key`
47
+ header, this needs an `Origin` or `Referer` from the same host (or one listed in
48
+ `XENON_ALLOWED_ORIGINS`), even though it carries no cookie yet. Without one it answers `403`.
49
+ tags:
50
+ - Authentication
51
+ security: []
52
+ requestBody:
53
+ required: true
54
+ content:
55
+ application/json:
56
+ schema:
57
+ $ref: '#/components/schemas/IdentityLoginRequest'
58
+ example:
59
+ email: priya.shah@example.com
60
+ password: correct-horse-battery
61
+ responses:
62
+ '204':
63
+ description: Signed in. The session cookie is in `Set-Cookie`.
64
+ headers:
65
+ Set-Cookie:
66
+ description: The dashboard session cookie.
67
+ schema:
68
+ type: string
69
+ example: xenon_dashboard_session=5b0f8c1e-6f43-4a51-9a5e-2c4d8e1b7a90; Max-Age=86400; Path=/; HttpOnly; SameSite=Strict
70
+ '400':
71
+ description: '`email` or `password` is missing.'
72
+ content:
73
+ application/json:
74
+ schema:
75
+ $ref: '#/components/schemas/Error'
76
+ example:
77
+ error: email and password required
78
+ '401':
79
+ description: The email, password or account status is wrong.
80
+ content:
81
+ application/json:
82
+ schema:
83
+ $ref: '#/components/schemas/Error'
84
+ example:
85
+ error: invalid credentials
86
+ '403':
87
+ $ref: '#/components/responses/IdentityCsrfRefused'
88
+ '429':
89
+ $ref: '#/components/responses/IdentityTooManyAttempts'
90
+
91
+ /api/auth/logout:
92
+ post:
93
+ operationId: signOut
94
+ summary: Sign out of the dashboard
95
+ description: |
96
+ Public. Revokes the dashboard session named by the `xenon_dashboard_session` cookie, if the
97
+ request carries one, and clears the cookie. Always answers `204`, signed in or not.
98
+
99
+ Needs a same-host `Origin` or `Referer`, like every state-changing request without an
100
+ `x-xenon-access-key` header.
101
+ tags:
102
+ - Authentication
103
+ security: []
104
+ responses:
105
+ '204':
106
+ description: Signed out; the cookie is cleared.
107
+ '403':
108
+ $ref: '#/components/responses/IdentityCsrfRefused'
109
+
110
+ /api/auth/forgot-password:
111
+ post:
112
+ operationId: requestPasswordReset
113
+ summary: Email a password-reset link
114
+ description: |
115
+ Public. Answers `204` at once, whatever the email, so it can't be used to find out which
116
+ addresses have accounts. Then, in the background, if the email belongs to an `ACTIVE` user and
117
+ the server can deliver mail (SMTP configured, or the opt-in log fallback
118
+ `XENON_PASSWORD_RESET_LOG_FALLBACK=true`), it creates a single-use reset token (valid 1 hour by
119
+ default) and sends a link to `/xenon/reset-password#<token>`. Without either, no token is created;
120
+ `GET /api/auth/options` says `admin` in that case.
121
+
122
+ **Rate limit.** 3 requests per 15 minutes per client IP by default, then `429` with
123
+ `Retry-After`.
124
+
125
+ Needs a same-host `Origin` or `Referer`, like every state-changing request without an
126
+ `x-xenon-access-key` header.
127
+ tags:
128
+ - Authentication
129
+ security: []
130
+ requestBody:
131
+ required: true
132
+ content:
133
+ application/json:
134
+ schema:
135
+ type: object
136
+ properties:
137
+ email:
138
+ type: string
139
+ format: email
140
+ example: priya.shah@example.com
141
+ example:
142
+ email: priya.shah@example.com
143
+ responses:
144
+ '204':
145
+ description: Accepted. Sent whether or not a link was emailed.
146
+ '403':
147
+ $ref: '#/components/responses/IdentityCsrfRefused'
148
+ '429':
149
+ $ref: '#/components/responses/IdentityTooManyAttempts'
150
+
151
+ /api/auth/reset-password/check:
152
+ post:
153
+ operationId: checkPasswordResetToken
154
+ summary: Check a password-reset token
155
+ description: |
156
+ Public. Says whether a reset token from a reset link is still usable (exists, not used, not
157
+ expired, not revoked), so the reset page can show the form or an error. Doesn't use the token
158
+ up. The token goes in the body rather than the URL so it never reaches the request log.
159
+
160
+ Needs a same-host `Origin` or `Referer`, like every state-changing request without an
161
+ `x-xenon-access-key` header.
162
+ tags:
163
+ - Authentication
164
+ security: []
165
+ requestBody:
166
+ required: true
167
+ content:
168
+ application/json:
169
+ schema:
170
+ type: object
171
+ properties:
172
+ token:
173
+ type: string
174
+ description: The token from the reset link.
175
+ example: 9f1c2e7a4b5d6c8e0f1a2b3c4d5e6f708192a3b4c5d6e7f8
176
+ responses:
177
+ '200':
178
+ description: The token is valid.
179
+ content:
180
+ application/json:
181
+ schema:
182
+ type: object
183
+ properties:
184
+ ok:
185
+ type: boolean
186
+ example: true
187
+ example:
188
+ ok: true
189
+ '403':
190
+ $ref: '#/components/responses/IdentityCsrfRefused'
191
+ '404':
192
+ $ref: '#/components/responses/IdentityResetTokenInvalid'
193
+
194
+ /api/auth/reset-password:
195
+ post:
196
+ operationId: resetPassword
197
+ summary: Set a new password with a reset token
198
+ description: |
199
+ Public. Sets the user's password from a reset link's token. On success the token is used up,
200
+ every other outstanding reset link for that user is revoked, and every dashboard session of
201
+ that user is signed out. API tokens are not affected.
202
+
203
+ Needs a same-host `Origin` or `Referer`, like every state-changing request without an
204
+ `x-xenon-access-key` header.
205
+ tags:
206
+ - Authentication
207
+ security: []
208
+ requestBody:
209
+ required: true
210
+ content:
211
+ application/json:
212
+ schema:
213
+ type: object
214
+ required:
215
+ - token
216
+ - newPassword
217
+ properties:
218
+ token:
219
+ type: string
220
+ example: 9f1c2e7a4b5d6c8e0f1a2b3c4d5e6f708192a3b4c5d6e7f8
221
+ newPassword:
222
+ type: string
223
+ minLength: 8
224
+ example: n3w-Passw0rd-2026
225
+ responses:
226
+ '204':
227
+ description: The password is changed.
228
+ '400':
229
+ description: A field is missing, or the password is shorter than 8 characters.
230
+ content:
231
+ application/json:
232
+ schema:
233
+ $ref: '#/components/schemas/Error'
234
+ examples:
235
+ missing:
236
+ value:
237
+ error: token and newPassword required
238
+ short:
239
+ value:
240
+ error: password must be at least 8 characters
241
+ '403':
242
+ $ref: '#/components/responses/IdentityCsrfRefused'
243
+ '404':
244
+ $ref: '#/components/responses/IdentityResetTokenInvalid'
245
+
246
+ /api/auth/jwks.json:
247
+ get:
248
+ operationId: getJwks
249
+ summary: Get the server's public signing keys
250
+ description: |
251
+ Public. The JSON Web Key Set that verifies the RS256 tokens this server signs: bearer tokens
252
+ from `POST /api/auth/token`, session tokens, and the hub tokens a hub sends its nodes. Nodes
253
+ fetch it from their hub. Answers `{ "keys": [] }` if the signing key hasn't been set up.
254
+ tags:
255
+ - Authentication
256
+ security: []
257
+ responses:
258
+ '200':
259
+ description: The key set.
260
+ content:
261
+ application/json:
262
+ schema:
263
+ $ref: '#/components/schemas/IdentityJwks'
264
+ example:
265
+ keys:
266
+ - kty: RSA
267
+ n: 0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw
268
+ e: AQAB
269
+ kid: 3f2a9c4e7d1b
270
+ use: sig
271
+ alg: RS256
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Authentication: signed-in routes
275
+ # ---------------------------------------------------------------------------
276
+ /api/auth/me:
277
+ get:
278
+ operationId: getCurrentIdentity
279
+ summary: Get who you are signed in as
280
+ description: |
281
+ Who the credential belongs to: the user, their role, the scopes this credential carries, the
282
+ team the credential is bound to (`teamId`, API keys and bearer tokens only) and the user's
283
+ teams. `kind` says how the request authenticated: `user-session` (the dashboard cookie),
284
+ `api-key` (the key pair, or a raw API key in the cookie), or `bearer`.
285
+
286
+ `teams` lists the caller's teams by name; it is empty for admins, who see every team's
287
+ devices. With authentication disabled on the server the answer is a fixed synthetic
288
+ `SUPER_ADMIN` identity with `authDisabled: true` and no access key.
289
+ tags:
290
+ - Authentication
291
+ responses:
292
+ '200':
293
+ description: The caller's identity.
294
+ content:
295
+ application/json:
296
+ schema:
297
+ $ref: '#/components/schemas/IdentityMe'
298
+ example:
299
+ userId: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
300
+ email: priya.shah@example.com
301
+ name: Priya Shah
302
+ role: MEMBER
303
+ accessKey: xen_4Hq8ZkP2mW7tR9vB
304
+ scopes: devices,sessions,read
305
+ teamId: null
306
+ kind: user-session
307
+ teams:
308
+ - id: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
309
+ name: Mobile Platform
310
+ authDisabled: false
311
+ '401':
312
+ $ref: '#/components/responses/Unauthorized'
313
+ '429':
314
+ $ref: '#/components/responses/RateLimited'
315
+
316
+ /api/auth/change-password:
317
+ post:
318
+ operationId: changeMyPassword
319
+ summary: Change your own password
320
+ description: |
321
+ Changes the caller's password after checking the current one. Any signed-in user may call it.
322
+
323
+ Side effects: every outstanding password-reset link of the user is revoked, and, when the
324
+ request was made with the dashboard cookie, every other dashboard session of the user is
325
+ signed out (the current one stays). Made with an API key or bearer token, other dashboard
326
+ sessions stay signed in. API tokens are never affected.
327
+
328
+ A wrong current password answers `400`, not `401`. With authentication disabled there is no
329
+ real user and the answer is `400` (`user not found`). Cookie callers need a same-host
330
+ `Origin` or `Referer`.
331
+ tags:
332
+ - Authentication
333
+ requestBody:
334
+ required: true
335
+ content:
336
+ application/json:
337
+ schema:
338
+ type: object
339
+ required:
340
+ - oldPassword
341
+ - newPassword
342
+ properties:
343
+ oldPassword:
344
+ type: string
345
+ example: correct-horse-battery
346
+ newPassword:
347
+ type: string
348
+ minLength: 8
349
+ example: n3w-Passw0rd-2026
350
+ responses:
351
+ '204':
352
+ description: The password is changed.
353
+ '400':
354
+ description: A field is missing, the new password is too short, or the current password is wrong.
355
+ content:
356
+ application/json:
357
+ schema:
358
+ $ref: '#/components/schemas/Error'
359
+ examples:
360
+ missing:
361
+ value:
362
+ error: oldPassword and newPassword required
363
+ short:
364
+ value:
365
+ error: password must be at least 8 characters
366
+ wrongPassword:
367
+ value:
368
+ error: incorrect current password
369
+ '401':
370
+ $ref: '#/components/responses/Unauthorized'
371
+ '403':
372
+ $ref: '#/components/responses/Forbidden'
373
+ '429':
374
+ $ref: '#/components/responses/RateLimited'
375
+
376
+ /api/auth/token:
377
+ post:
378
+ operationId: mintBearerToken
379
+ summary: Mint a short-lived signed token
380
+ description: |
381
+ Mints an RS256 JWT for the caller, signed by this server and verifiable with
382
+ `/api/auth/jwks.json`. Any signed-in user may call it.
383
+
384
+ - `audience: xenon-rest` (the default): a bearer token for this REST API, valid 1 hour. It
385
+ carries the caller's role, the scopes of the credential used to mint it and its bound team.
386
+ - `audience: xenon-mcp`: a token for an MCP gateway, valid 24 hours by default
387
+ (`XENON_MCP_TOKEN_TTL_SEC`). It carries granular `scopes` (by default `appium:use` and
388
+ `xenon:devices:read`, as far as the caller's scopes allow) and, as a REST bearer token, only
389
+ the flat scopes those need (`appium:use` gives `sessions`, `xenon:devices:lock` gives
390
+ `devices`). The answer also has a `sessionToken` (audience `xenon-session`, same lifetime)
391
+ to put in an Appium session's `xe:options.sessionToken`.
392
+
393
+ Granular scopes a credential may grant: `read` gives `xenon:devices:read` and
394
+ `xenon:analytics:read`; `sessions` gives `appium:use` and `xenon:recordings`; `devices` gives
395
+ `xenon:devices:read`, `xenon:devices:lock` and `xenon:recordings`; `admin` gives all of them.
396
+
397
+ Cookie callers need a same-host `Origin` or `Referer`. So, as the server is written, does a
398
+ caller using only `Authorization: Bearer`: the same-origin check exempts only requests with an
399
+ `x-xenon-access-key` header.
400
+ tags:
401
+ - Authentication
402
+ requestBody:
403
+ required: false
404
+ content:
405
+ application/json:
406
+ schema:
407
+ $ref: '#/components/schemas/IdentityTokenRequest'
408
+ examples:
409
+ rest:
410
+ summary: A REST bearer token
411
+ value:
412
+ audience: xenon-rest
413
+ mcp:
414
+ summary: An MCP token with chosen scopes
415
+ value:
416
+ audience: xenon-mcp
417
+ scopes:
418
+ - appium:use
419
+ - xenon:devices:read
420
+ - xenon:devices:lock
421
+ responses:
422
+ '200':
423
+ description: The token.
424
+ content:
425
+ application/json:
426
+ schema:
427
+ $ref: '#/components/schemas/IdentityTokenResponse'
428
+ examples:
429
+ rest:
430
+ value:
431
+ token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjNmMmE5YzRlN2QxYiJ9.eyJzdWIiOiI3YzFlMmQ0YSIsInJvbGUiOiJNRU1CRVIiLCJzY29wZXMiOiJkZXZpY2VzLHNlc3Npb25zLHJlYWQiLCJhdWQiOiJ4ZW5vbi1yZXN0In0.c2lnbmF0dXJl
432
+ expiresIn: 3600
433
+ audience: xenon-rest
434
+ mcp:
435
+ value:
436
+ token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjNmMmE5YzRlN2QxYiJ9.eyJzdWIiOiI3YzFlMmQ0YSIsInNjb3BlIjoiYXBwaXVtOnVzZSB4ZW5vbjpkZXZpY2VzOnJlYWQiLCJhdWQiOiJ4ZW5vbi1tY3AifQ.c2lnbmF0dXJl
437
+ expiresIn: 86400
438
+ audience: xenon-mcp
439
+ scopes:
440
+ - appium:use
441
+ - xenon:devices:read
442
+ sessionToken: eyJhbGciOiJSUzI1NiIsImtpZCI6IjNmMmE5YzRlN2QxYiJ9.eyJzdWIiOiI3YzFlMmQ0YSIsImF1ZCI6Inhlbm9uLXNlc3Npb24ifQ.c2lnbmF0dXJl
443
+ '400':
444
+ description: An unsupported audience, `scopes` that isn't an array, or an unknown granular scope.
445
+ content:
446
+ application/json:
447
+ schema:
448
+ $ref: '#/components/schemas/Error'
449
+ examples:
450
+ audience:
451
+ value:
452
+ error: bad_request
453
+ details: 'unsupported audience: xenon-node'
454
+ unknownScope:
455
+ value:
456
+ error: unknown_scope
457
+ details: 'unknown scope(s): xenon:admin'
458
+ '401':
459
+ $ref: '#/components/responses/Unauthorized'
460
+ '403':
461
+ description: |
462
+ An MCP scope the caller's credential can't grant (`scope_exceeds_key`), or the same-origin
463
+ check failed.
464
+ content:
465
+ application/json:
466
+ schema:
467
+ $ref: '#/components/schemas/Error'
468
+ examples:
469
+ exceeds:
470
+ value:
471
+ error: scope_exceeds_key
472
+ details: "requested scope(s) exceed this key's grant: xenon:devices:lock"
473
+ csrf:
474
+ value:
475
+ error: 'CSRF: Origin or Referer header required'
476
+ '429':
477
+ $ref: '#/components/responses/RateLimited'
478
+
479
+ /api/auth/dashboard-session:
480
+ post:
481
+ operationId: exchangeApiKeyForDashboardSession
482
+ summary: Turn a super-admin API token into a dashboard cookie
483
+ description: |
484
+ Legacy. Puts a raw API token in the `xenon_dashboard_session` cookie (HttpOnly,
485
+ `SameSite=Strict`, same lifetime as a sign-in session), so a browser can use the dashboard as
486
+ that token. The request itself must already be authenticated, and the token in the body must
487
+ be a live API token whose owner is a `SUPER_ADMIN`. The cookie then authenticates with that
488
+ token's own scopes and team. Prefer `POST /api/auth/login`.
489
+
490
+ Cookie callers need a same-host `Origin` or `Referer`.
491
+ tags:
492
+ - Authentication
493
+ requestBody:
494
+ required: true
495
+ content:
496
+ application/json:
497
+ schema:
498
+ type: object
499
+ required:
500
+ - apiKey
501
+ properties:
502
+ apiKey:
503
+ type: string
504
+ description: A raw API token (the value shown once when it was created).
505
+ example: 3b9f1d7c2a8e4f6b0c5d9e1a7f3b2c8d4e6a0b9c1d7f5e3a2b8c4d6e0f1a9b7c
506
+ responses:
507
+ '200':
508
+ description: The cookie is set.
509
+ content:
510
+ application/json:
511
+ schema:
512
+ type: object
513
+ properties:
514
+ ok:
515
+ type: boolean
516
+ example: true
517
+ scopes:
518
+ type: string
519
+ description: The token's scopes, comma-separated.
520
+ example: admin
521
+ example:
522
+ ok: true
523
+ scopes: admin
524
+ '400':
525
+ description: '`apiKey` is missing.'
526
+ content:
527
+ application/json:
528
+ schema:
529
+ $ref: '#/components/schemas/Error'
530
+ example:
531
+ error: apiKey required
532
+ '401':
533
+ description: No valid credential on the request, or the token in the body is unknown, revoked or expired.
534
+ content:
535
+ application/json:
536
+ schema:
537
+ $ref: '#/components/schemas/Error'
538
+ example:
539
+ error: invalid key
540
+ '403':
541
+ description: The token's owner isn't a `SUPER_ADMIN`, or the same-origin check failed.
542
+ content:
543
+ application/json:
544
+ schema:
545
+ $ref: '#/components/schemas/Error'
546
+ example:
547
+ error: super-admin scope required for dashboard-session exchange
548
+ '429':
549
+ $ref: '#/components/responses/RateLimited'
550
+
551
+ # ---------------------------------------------------------------------------
552
+ # Profile
553
+ # ---------------------------------------------------------------------------
554
+ /api/profile/access-key:
555
+ get:
556
+ operationId: getMyAccessKey
557
+ summary: Get your access key
558
+ description: |
559
+ The caller's access key (`xen_…`), sent as `x-xenon-access-key` together with one of their API
560
+ tokens. Any signed-in user may call it. With authentication disabled it answers for the first
561
+ `SUPER_ADMIN` account.
562
+ tags:
563
+ - Profile
564
+ responses:
565
+ '200':
566
+ description: The access key.
567
+ content:
568
+ application/json:
569
+ schema:
570
+ $ref: '#/components/schemas/IdentityAccessKey'
571
+ example:
572
+ accessKey: xen_4Hq8ZkP2mW7tR9vB
573
+ '401':
574
+ $ref: '#/components/responses/Unauthorized'
575
+ '404':
576
+ $ref: '#/components/responses/IdentityUserNotFound'
577
+ '429':
578
+ $ref: '#/components/responses/RateLimited'
579
+
580
+ /api/profile/access-key/rotate:
581
+ post:
582
+ operationId: rotateMyAccessKey
583
+ summary: Rotate your access key
584
+ description: |
585
+ Gives the caller a new access key. The old one stops working at once, for every one of their
586
+ API tokens: scripts and CI using the key pair must be updated. The tokens themselves are kept.
587
+ Any signed-in user may call it; with authentication disabled it rotates the first
588
+ `SUPER_ADMIN`'s key. Cookie callers need a same-host `Origin` or `Referer`.
589
+ tags:
590
+ - Profile
591
+ responses:
592
+ '200':
593
+ description: The new access key.
594
+ content:
595
+ application/json:
596
+ schema:
597
+ $ref: '#/components/schemas/IdentityAccessKey'
598
+ example:
599
+ accessKey: xen_9Lc3TfW6nQ1yK5dJ
600
+ '401':
601
+ $ref: '#/components/responses/Unauthorized'
602
+ '403':
603
+ $ref: '#/components/responses/Forbidden'
604
+ '404':
605
+ $ref: '#/components/responses/IdentityUserNotFound'
606
+ '429':
607
+ $ref: '#/components/responses/RateLimited'
608
+
609
+ /api/profile/tokens:
610
+ get:
611
+ operationId: listMyApiTokens
612
+ summary: List your API tokens
613
+ description: |
614
+ The caller's API tokens that haven't been revoked, newest first, including expired ones. The
615
+ token values themselves are never returned after creation. Any signed-in user may call it;
616
+ with authentication disabled it lists the first `SUPER_ADMIN`'s tokens.
617
+ tags:
618
+ - Profile
619
+ responses:
620
+ '200':
621
+ description: The tokens.
622
+ content:
623
+ application/json:
624
+ schema:
625
+ type: array
626
+ items:
627
+ $ref: '#/components/schemas/IdentityProfileToken'
628
+ example:
629
+ - id: 0e4b8a2c-6d1f-4c3e-9a7b-5f2d1c8e4a60
630
+ name: GitHub Actions
631
+ scopes:
632
+ - sessions
633
+ - read
634
+ createdAt: '2026-09-14T08:22:31.000Z'
635
+ lastUsedAt: '2026-10-03T17:45:02.000Z'
636
+ expiresAt: '2027-01-01T00:00:00.000Z'
637
+ '401':
638
+ $ref: '#/components/responses/Unauthorized'
639
+ '429':
640
+ $ref: '#/components/responses/RateLimited'
641
+ post:
642
+ operationId: createMyApiToken
643
+ summary: Create an API token for yourself
644
+ description: |
645
+ Creates an API token owned by the caller and returns its value **once**. Use it as
646
+ `x-xenon-token` with your access key.
647
+
648
+ `scopes` (some of `read`, `sessions`, `devices`, `admin`) may go beyond neither what the
649
+ caller's role allows nor the scopes of the credential making the request. The role allows:
650
+ `MEMBER`, `sessions` and `read`; `ADMIN`, `devices`, `sessions` and `read`; `SUPER_ADMIN`,
651
+ anything, `admin` included, or any narrower set. Without `scopes`, the token gets all of the
652
+ role's allowance, so a credential with fewer scopes must name them. Through 2.12 only the
653
+ role was checked, so an admin's `read`-only key could mint a wider token. The token has the
654
+ default rate limit (300 a minute) and no team binding.
655
+
656
+ Any signed-in user may call it. Cookie callers need a same-host `Origin` or `Referer`.
657
+ tags:
658
+ - Profile
659
+ requestBody:
660
+ required: true
661
+ content:
662
+ application/json:
663
+ schema:
664
+ $ref: '#/components/schemas/IdentityProfileTokenRequest'
665
+ example:
666
+ name: GitHub Actions
667
+ scopes:
668
+ - sessions
669
+ - read
670
+ expiresAt: '2027-01-01T00:00:00.000Z'
671
+ responses:
672
+ '201':
673
+ description: The token, shown once.
674
+ content:
675
+ application/json:
676
+ schema:
677
+ $ref: '#/components/schemas/IdentityCreatedToken'
678
+ example:
679
+ id: 0e4b8a2c-6d1f-4c3e-9a7b-5f2d1c8e4a60
680
+ token: 3b9f1d7c2a8e4f6b0c5d9e1a7f3b2c8d4e6a0b9c1d7f5e3a2b8c4d6e0f1a9b7c
681
+ expiresAt: '2027-01-01T00:00:00.000Z'
682
+ '400':
683
+ description: >-
684
+ `name` is missing, `expiresAt` is invalid or past, a scope is unknown, or a scope is
685
+ beyond your role or the credential you are using.
686
+ content:
687
+ application/json:
688
+ schema:
689
+ $ref: '#/components/schemas/Error'
690
+ examples:
691
+ name:
692
+ value:
693
+ error: name required
694
+ expiry:
695
+ value:
696
+ error: expiresAt must be in the future
697
+ unknown:
698
+ value:
699
+ error: scopes must be some of read, sessions, devices, admin
700
+ widen:
701
+ value:
702
+ error: cannot widen scopes beyond your role or the credential you are using
703
+ '401':
704
+ $ref: '#/components/responses/Unauthorized'
705
+ '403':
706
+ $ref: '#/components/responses/Forbidden'
707
+ '404':
708
+ $ref: '#/components/responses/IdentityUserNotFound'
709
+ '429':
710
+ $ref: '#/components/responses/RateLimited'
711
+
712
+ /api/profile/tokens/{id}:
713
+ delete:
714
+ operationId: revokeMyApiToken
715
+ summary: Revoke one of your API tokens
716
+ description: |
717
+ Revokes one of the caller's own API tokens; requests made with it are refused from then on.
718
+ A token of another user answers `404`, as an unknown one does. Revoking an already revoked
719
+ token of your own answers `204` again. Any signed-in user may call it. Cookie callers need a
720
+ same-host `Origin` or `Referer`.
721
+ tags:
722
+ - Profile
723
+ parameters:
724
+ - in: path
725
+ name: id
726
+ required: true
727
+ description: The token's id.
728
+ schema:
729
+ type: string
730
+ format: uuid
731
+ example: 0e4b8a2c-6d1f-4c3e-9a7b-5f2d1c8e4a60
732
+ responses:
733
+ '204':
734
+ description: Revoked.
735
+ '401':
736
+ $ref: '#/components/responses/Unauthorized'
737
+ '403':
738
+ $ref: '#/components/responses/Forbidden'
739
+ '404':
740
+ description: No token of yours has this id.
741
+ content:
742
+ application/json:
743
+ schema:
744
+ $ref: '#/components/schemas/Error'
745
+ example:
746
+ error: token not found
747
+ '429':
748
+ $ref: '#/components/responses/RateLimited'
749
+
750
+ # ---------------------------------------------------------------------------
751
+ # Users
752
+ # ---------------------------------------------------------------------------
753
+ /api/users:
754
+ get:
755
+ operationId: listUsers
756
+ summary: List user accounts
757
+ description: |
758
+ Needs role `ADMIN` or above. A `SUPER_ADMIN` sees every account; an `ADMIN` sees only
759
+ `MEMBER` accounts. Oldest first.
760
+
761
+ Every `/api/users` request also needs the `admin` scope, as `/api/apikeys` and `/api/teams`
762
+ do: an admin's `read`-only key or token can't manage accounts. Through 2.12 it could.
763
+ tags:
764
+ - Users
765
+ responses:
766
+ '200':
767
+ description: The users.
768
+ content:
769
+ application/json:
770
+ schema:
771
+ type: array
772
+ items:
773
+ $ref: '#/components/schemas/IdentityUser'
774
+ example:
775
+ - id: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
776
+ email: priya.shah@example.com
777
+ name: Priya Shah
778
+ role: MEMBER
779
+ status: ACTIVE
780
+ createdAt: '2026-03-02T10:15:00.000Z'
781
+ lastLoginAt: '2026-10-04T07:58:12.000Z'
782
+ '401':
783
+ $ref: '#/components/responses/Unauthorized'
784
+ '403':
785
+ $ref: '#/components/responses/Forbidden'
786
+ '429':
787
+ $ref: '#/components/responses/RateLimited'
788
+ post:
789
+ operationId: createUser
790
+ summary: Create a user account
791
+ description: |
792
+ Needs role `ADMIN` or above. An `ADMIN` may create only `MEMBER` accounts; a `SUPER_ADMIN` any
793
+ role. The email is stored lower-case. The account is `ACTIVE` and gets its own access key.
794
+
795
+ Without `password` the server generates a 12-character temporary password and returns it once
796
+ as `temporaryPassword`; with one, it isn't echoed back. Cookie callers need a same-host
797
+ `Origin` or `Referer`.
798
+ tags:
799
+ - Users
800
+ requestBody:
801
+ required: true
802
+ content:
803
+ application/json:
804
+ schema:
805
+ $ref: '#/components/schemas/IdentityCreateUserRequest'
806
+ example:
807
+ email: diego.alvarez@example.com
808
+ name: Diego Alvarez
809
+ role: MEMBER
810
+ responses:
811
+ '201':
812
+ description: The new account.
813
+ content:
814
+ application/json:
815
+ schema:
816
+ $ref: '#/components/schemas/IdentityCreatedUser'
817
+ example:
818
+ id: 4f8d2a1b-3c6e-4b7a-8d9f-0e1c2b3a4d5e
819
+ email: diego.alvarez@example.com
820
+ name: Diego Alvarez
821
+ role: MEMBER
822
+ status: ACTIVE
823
+ accessKey: xen_7Rb2YpM5sV8kN3qH
824
+ temporaryPassword: Zq4pT8wL1xNe
825
+ '400':
826
+ description: A field is missing, the role is unknown, or the password is shorter than 8 characters.
827
+ content:
828
+ application/json:
829
+ schema:
830
+ $ref: '#/components/schemas/Error'
831
+ examples:
832
+ missing:
833
+ value:
834
+ error: email, name, role required
835
+ role:
836
+ value:
837
+ error: role must be SUPER_ADMIN | ADMIN | MEMBER
838
+ short:
839
+ value:
840
+ error: password must be at least 8 characters
841
+ '401':
842
+ $ref: '#/components/responses/Unauthorized'
843
+ '403':
844
+ description: Your role is below `ADMIN`, or you may not create an account with that role.
845
+ content:
846
+ application/json:
847
+ schema:
848
+ $ref: '#/components/schemas/Error'
849
+ example:
850
+ error: cannot create user with role ADMIN
851
+ '409':
852
+ description: An account with this email already exists.
853
+ content:
854
+ application/json:
855
+ schema:
856
+ $ref: '#/components/schemas/Error'
857
+ example:
858
+ error: conflict
859
+ message: email is already in use
860
+ '429':
861
+ $ref: '#/components/responses/RateLimited'
862
+
863
+ /api/users/{id}:
864
+ patch:
865
+ operationId: updateUser
866
+ summary: Change a user's name, role or status
867
+ description: |
868
+ Needs role `ADMIN` or above. An `ADMIN` may change only `MEMBER` accounts and may not promote
869
+ anyone above `MEMBER`; a `SUPER_ADMIN` may change anyone. Only the fields sent are changed; the
870
+ email can't be changed.
871
+
872
+ - You can't change your own role.
873
+ - The last active `SUPER_ADMIN` can't be demoted or deactivated.
874
+ - Setting `status` to `INACTIVE` signs the user out of every dashboard session. Their API
875
+ tokens and bearer tokens stop working too, since an inactive user's credentials are refused.
876
+
877
+ Cookie callers need a same-host `Origin` or `Referer`.
878
+ tags:
879
+ - Users
880
+ parameters:
881
+ - $ref: '#/components/parameters/IdentityUserId'
882
+ requestBody:
883
+ required: true
884
+ content:
885
+ application/json:
886
+ schema:
887
+ $ref: '#/components/schemas/IdentityUpdateUserRequest'
888
+ example:
889
+ role: ADMIN
890
+ responses:
891
+ '200':
892
+ description: The account as it now is.
893
+ content:
894
+ application/json:
895
+ schema:
896
+ $ref: '#/components/schemas/IdentityUpdatedUser'
897
+ example:
898
+ id: 4f8d2a1b-3c6e-4b7a-8d9f-0e1c2b3a4d5e
899
+ email: diego.alvarez@example.com
900
+ name: Diego Alvarez
901
+ role: ADMIN
902
+ status: ACTIVE
903
+ '400':
904
+ description: Your own role, an unknown role, or the last active super-admin.
905
+ content:
906
+ application/json:
907
+ schema:
908
+ $ref: '#/components/schemas/Error'
909
+ examples:
910
+ self:
911
+ value:
912
+ error: cannot role-change yourself; use a different super-admin to manage your own account
913
+ lastSuperAdmin:
914
+ value:
915
+ error: cannot remove or demote the last active super-admin; promote another user first
916
+ '401':
917
+ $ref: '#/components/responses/Unauthorized'
918
+ '403':
919
+ description: Your role is below `ADMIN`, or too low for this account or the new role.
920
+ content:
921
+ application/json:
922
+ schema:
923
+ $ref: '#/components/schemas/Error'
924
+ example:
925
+ error: insufficient permissions for this target
926
+ '404':
927
+ $ref: '#/components/responses/IdentityUserNotFound'
928
+ '429':
929
+ $ref: '#/components/responses/RateLimited'
930
+ delete:
931
+ operationId: deleteUser
932
+ summary: Delete a user account
933
+ description: |
934
+ Needs role `ADMIN` or above. An `ADMIN` may delete only `MEMBER` accounts. Deletes the account,
935
+ its dashboard sessions, its API tokens and its team memberships. You can't delete yourself, and
936
+ the last active `SUPER_ADMIN` can't be deleted. Cookie callers need a same-host `Origin` or
937
+ `Referer`.
938
+ tags:
939
+ - Users
940
+ parameters:
941
+ - $ref: '#/components/parameters/IdentityUserId'
942
+ responses:
943
+ '204':
944
+ description: Deleted.
945
+ '400':
946
+ description: Yourself, or the last active super-admin.
947
+ content:
948
+ application/json:
949
+ schema:
950
+ $ref: '#/components/schemas/Error'
951
+ example:
952
+ error: cannot delete yourself; use a different super-admin to manage your own account
953
+ '401':
954
+ $ref: '#/components/responses/Unauthorized'
955
+ '403':
956
+ description: Your role is below `ADMIN`, or too low for this account.
957
+ content:
958
+ application/json:
959
+ schema:
960
+ $ref: '#/components/schemas/Error'
961
+ example:
962
+ error: insufficient permissions for this target
963
+ '404':
964
+ $ref: '#/components/responses/IdentityUserNotFound'
965
+ '429':
966
+ $ref: '#/components/responses/RateLimited'
967
+
968
+ /api/users/{id}/reset-link:
969
+ post:
970
+ operationId: issueUserResetLink
971
+ summary: Issue a password-reset link for a user
972
+ description: |
973
+ Needs role `ADMIN` or above. An `ADMIN` may issue links only for `MEMBER` accounts. Creates a
974
+ single-use reset link for an `ACTIVE` user, valid 1 hour by default.
975
+
976
+ With SMTP configured the link is emailed to the user and the answer is `emailed: true`.
977
+ Without it the link is returned once, in this answer only (`Cache-Control: no-store`), for the
978
+ admin to pass on; it is never written to the server log. Your own password goes through
979
+ `POST /api/auth/change-password` instead. Cookie callers need a same-host `Origin` or
980
+ `Referer`.
981
+ tags:
982
+ - Users
983
+ parameters:
984
+ - $ref: '#/components/parameters/IdentityUserId'
985
+ responses:
986
+ '200':
987
+ description: The link was emailed, or here it is.
988
+ content:
989
+ application/json:
990
+ schema:
991
+ $ref: '#/components/schemas/IdentityResetLink'
992
+ examples:
993
+ emailed:
994
+ value:
995
+ emailed: true
996
+ expiresAt: '2026-10-04T10:30:00.000Z'
997
+ returned:
998
+ value:
999
+ emailed: false
1000
+ link: https://xenon.example.com/xenon/reset-password#9f1c2e7a4b5d6c8e0f1a2b3c4d5e6f708192a3b4c5d6e7f8
1001
+ expiresAt: '2026-10-04T10:30:00.000Z'
1002
+ '400':
1003
+ description: The account is your own.
1004
+ content:
1005
+ application/json:
1006
+ schema:
1007
+ $ref: '#/components/schemas/Error'
1008
+ example:
1009
+ error: cannot issue a reset link for yourself; use a different super-admin to manage your own account
1010
+ '401':
1011
+ $ref: '#/components/responses/Unauthorized'
1012
+ '403':
1013
+ description: Your role is below `ADMIN`, or too low for this account.
1014
+ content:
1015
+ application/json:
1016
+ schema:
1017
+ $ref: '#/components/schemas/Error'
1018
+ example:
1019
+ error: insufficient permissions for this target
1020
+ '404':
1021
+ $ref: '#/components/responses/IdentityUserNotFound'
1022
+ '409':
1023
+ description: The account isn't active.
1024
+ content:
1025
+ application/json:
1026
+ schema:
1027
+ $ref: '#/components/schemas/Error'
1028
+ example:
1029
+ error: user is not active
1030
+ '429':
1031
+ $ref: '#/components/responses/RateLimited'
1032
+
1033
+ # ---------------------------------------------------------------------------
1034
+ # Teams
1035
+ # ---------------------------------------------------------------------------
1036
+ /api/teams:
1037
+ get:
1038
+ operationId: listTeams
1039
+ summary: List the teams
1040
+ description: |
1041
+ Every team, oldest first, with how many devices and members each has. Needs role `ADMIN` or
1042
+ above and the `admin` scope.
1043
+ tags:
1044
+ - Teams
1045
+ responses:
1046
+ '200':
1047
+ description: The teams.
1048
+ content:
1049
+ application/json:
1050
+ schema:
1051
+ type: array
1052
+ items:
1053
+ $ref: '#/components/schemas/IdentityTeam'
1054
+ example:
1055
+ - id: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1056
+ name: Mobile Platform
1057
+ createdAt: '2026-02-11T09:00:00.000Z'
1058
+ deviceCount: 6
1059
+ memberCount: 4
1060
+ '401':
1061
+ $ref: '#/components/responses/Unauthorized'
1062
+ '403':
1063
+ $ref: '#/components/responses/Forbidden'
1064
+ '429':
1065
+ $ref: '#/components/responses/RateLimited'
1066
+ post:
1067
+ operationId: createTeam
1068
+ summary: Create a team
1069
+ description: |
1070
+ Creates a team. The name is trimmed and must be unique. Needs role `ADMIN` or above and the
1071
+ `admin` scope. Cookie callers need a same-host `Origin` or `Referer`.
1072
+ tags:
1073
+ - Teams
1074
+ requestBody:
1075
+ required: true
1076
+ content:
1077
+ application/json:
1078
+ schema:
1079
+ type: object
1080
+ required:
1081
+ - name
1082
+ properties:
1083
+ name:
1084
+ type: string
1085
+ example: Mobile Platform
1086
+ responses:
1087
+ '200':
1088
+ description: The new team.
1089
+ content:
1090
+ application/json:
1091
+ schema:
1092
+ $ref: '#/components/schemas/IdentityTeamRecord'
1093
+ example:
1094
+ id: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1095
+ name: Mobile Platform
1096
+ createdAt: '2026-10-04T09:12:44.000Z'
1097
+ '400':
1098
+ description: '`name` is missing or blank.'
1099
+ content:
1100
+ application/json:
1101
+ schema:
1102
+ $ref: '#/components/schemas/Error'
1103
+ example:
1104
+ error: name required
1105
+ '401':
1106
+ $ref: '#/components/responses/Unauthorized'
1107
+ '403':
1108
+ $ref: '#/components/responses/Forbidden'
1109
+ '409':
1110
+ description: A team with this name exists.
1111
+ content:
1112
+ application/json:
1113
+ schema:
1114
+ $ref: '#/components/schemas/Error'
1115
+ example:
1116
+ error: team name already exists
1117
+ '429':
1118
+ $ref: '#/components/responses/RateLimited'
1119
+
1120
+ /api/teams/{id}:
1121
+ delete:
1122
+ operationId: deleteTeam
1123
+ summary: Delete an empty team
1124
+ description: |
1125
+ Deletes a team that owns no devices, has no members and owns no apps; otherwise it refuses with
1126
+ `400` and says what is left, so nothing is quietly moved to the shared pool. Any API key still
1127
+ bound to the team is unbound. An unknown id also answers `400`. Needs role `ADMIN` or above and
1128
+ the `admin` scope. Cookie callers need a same-host `Origin` or `Referer`.
1129
+ tags:
1130
+ - Teams
1131
+ parameters:
1132
+ - $ref: '#/components/parameters/IdentityTeamId'
1133
+ responses:
1134
+ '200':
1135
+ description: Deleted.
1136
+ content:
1137
+ application/json:
1138
+ schema:
1139
+ $ref: '#/components/schemas/IdentityOk'
1140
+ example:
1141
+ ok: true
1142
+ '400':
1143
+ description: The team still owns devices, members or apps, or doesn't exist.
1144
+ content:
1145
+ application/json:
1146
+ schema:
1147
+ $ref: '#/components/schemas/Error'
1148
+ example:
1149
+ error: Team still has 2 device(s), 3 member(s) and 0 app(s). Reassign them before deleting.
1150
+ '401':
1151
+ $ref: '#/components/responses/Unauthorized'
1152
+ '403':
1153
+ $ref: '#/components/responses/Forbidden'
1154
+ '429':
1155
+ $ref: '#/components/responses/RateLimited'
1156
+
1157
+ /api/teams/{id}/members:
1158
+ get:
1159
+ operationId: listTeamMembers
1160
+ summary: List a team's members
1161
+ description: |
1162
+ The users in a team, in the order they were added. An unknown team answers an empty list.
1163
+ Needs role `ADMIN` or above and the `admin` scope.
1164
+ tags:
1165
+ - Teams
1166
+ parameters:
1167
+ - $ref: '#/components/parameters/IdentityTeamId'
1168
+ responses:
1169
+ '200':
1170
+ description: The members.
1171
+ content:
1172
+ application/json:
1173
+ schema:
1174
+ type: array
1175
+ items:
1176
+ $ref: '#/components/schemas/IdentityTeamMember'
1177
+ example:
1178
+ - userId: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
1179
+ email: priya.shah@example.com
1180
+ name: Priya Shah
1181
+ role: MEMBER
1182
+ addedAt: '2026-03-02T10:20:00.000Z'
1183
+ '401':
1184
+ $ref: '#/components/responses/Unauthorized'
1185
+ '403':
1186
+ $ref: '#/components/responses/Forbidden'
1187
+ '429':
1188
+ $ref: '#/components/responses/RateLimited'
1189
+ post:
1190
+ operationId: addTeamMember
1191
+ summary: Add a user to a team
1192
+ description: |
1193
+ Adds a user to a team. The user then sees the team's devices, apps and sessions from their next
1194
+ request (live dashboard events from their next reconnect). Needs role `ADMIN` or above and the
1195
+ `admin` scope. An unknown team or user, or a user already in the team, answers `400` with the
1196
+ database's message. Cookie callers need a same-host `Origin` or `Referer`.
1197
+ tags:
1198
+ - Teams
1199
+ parameters:
1200
+ - $ref: '#/components/parameters/IdentityTeamId'
1201
+ requestBody:
1202
+ required: true
1203
+ content:
1204
+ application/json:
1205
+ schema:
1206
+ type: object
1207
+ required:
1208
+ - userId
1209
+ properties:
1210
+ userId:
1211
+ type: string
1212
+ format: uuid
1213
+ example: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
1214
+ responses:
1215
+ '200':
1216
+ description: Added.
1217
+ content:
1218
+ application/json:
1219
+ schema:
1220
+ $ref: '#/components/schemas/IdentityOk'
1221
+ example:
1222
+ ok: true
1223
+ '400':
1224
+ description: '`userId` is missing, or the user or team is unknown, or the user is already a member.'
1225
+ content:
1226
+ application/json:
1227
+ schema:
1228
+ $ref: '#/components/schemas/Error'
1229
+ example:
1230
+ error: userId required
1231
+ '401':
1232
+ $ref: '#/components/responses/Unauthorized'
1233
+ '403':
1234
+ $ref: '#/components/responses/Forbidden'
1235
+ '429':
1236
+ $ref: '#/components/responses/RateLimited'
1237
+
1238
+ /api/teams/{id}/members/{userId}:
1239
+ delete:
1240
+ operationId: removeTeamMember
1241
+ summary: Remove a user from a team
1242
+ description: |
1243
+ Takes a user out of a team. The user's account and API tokens are kept. A user who isn't in the
1244
+ team answers `400`. Needs role `ADMIN` or above and the `admin` scope. Cookie callers need a
1245
+ same-host `Origin` or `Referer`.
1246
+ tags:
1247
+ - Teams
1248
+ parameters:
1249
+ - $ref: '#/components/parameters/IdentityTeamId'
1250
+ - in: path
1251
+ name: userId
1252
+ required: true
1253
+ description: The user's id.
1254
+ schema:
1255
+ type: string
1256
+ format: uuid
1257
+ example: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
1258
+ responses:
1259
+ '200':
1260
+ description: Removed.
1261
+ content:
1262
+ application/json:
1263
+ schema:
1264
+ $ref: '#/components/schemas/IdentityOk'
1265
+ example:
1266
+ ok: true
1267
+ '400':
1268
+ description: The user isn't a member of this team.
1269
+ content:
1270
+ application/json:
1271
+ schema:
1272
+ $ref: '#/components/schemas/Error'
1273
+ example:
1274
+ error: An operation failed because it depends on one or more records that were required but not found.
1275
+ '401':
1276
+ $ref: '#/components/responses/Unauthorized'
1277
+ '403':
1278
+ $ref: '#/components/responses/Forbidden'
1279
+ '429':
1280
+ $ref: '#/components/responses/RateLimited'
1281
+
1282
+ # ---------------------------------------------------------------------------
1283
+ # API keys (lab-wide)
1284
+ # ---------------------------------------------------------------------------
1285
+ /api/apikeys:
1286
+ get:
1287
+ operationId: listLabApiKeys
1288
+ summary: List the lab's API keys
1289
+ description: |
1290
+ Every API token in the lab that hasn't been revoked, whoever owns it, newest first, expired ones
1291
+ included. The token values are never returned. Needs role `ADMIN` or above and the `admin`
1292
+ scope.
1293
+ tags:
1294
+ - API Keys
1295
+ responses:
1296
+ '200':
1297
+ description: The keys.
1298
+ content:
1299
+ application/json:
1300
+ schema:
1301
+ type: array
1302
+ items:
1303
+ $ref: '#/components/schemas/IdentityApiKey'
1304
+ example:
1305
+ - id: 9a3c5e7f-1b2d-4f6a-8c0e-2d4f6a8c0e1b
1306
+ name: Nightly regression
1307
+ scopes: sessions,devices
1308
+ rateLimit: 600
1309
+ createdAt: '2026-08-20T12:00:00.000Z'
1310
+ lastUsedAt: '2026-10-04T02:13:55.000Z'
1311
+ expiresAt: null
1312
+ teamId: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1313
+ role: member
1314
+ '401':
1315
+ $ref: '#/components/responses/Unauthorized'
1316
+ '403':
1317
+ $ref: '#/components/responses/Forbidden'
1318
+ '429':
1319
+ $ref: '#/components/responses/RateLimited'
1320
+ post:
1321
+ operationId: createLabApiKey
1322
+ summary: Create an API key
1323
+ description: |
1324
+ Creates an API token owned by the caller and returns its value **once** as `key`; it is used as
1325
+ `x-xenon-token` with the caller's access key. Unlike `POST /api/profile/tokens`, it can bind the
1326
+ key to a team (the key then sees that team's devices only, within its owner's teams), set its
1327
+ rate limit and grant any scope, `admin` included. Needs role `ADMIN` or above and the `admin`
1328
+ scope. Cookie callers need a same-host `Origin` or `Referer`.
1329
+ tags:
1330
+ - API Keys
1331
+ requestBody:
1332
+ required: true
1333
+ content:
1334
+ application/json:
1335
+ schema:
1336
+ $ref: '#/components/schemas/IdentityCreateApiKeyRequest'
1337
+ example:
1338
+ name: Nightly regression
1339
+ scopes:
1340
+ - sessions
1341
+ - devices
1342
+ rateLimit: 600
1343
+ teamId: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1344
+ expiresAt: '2027-04-01T00:00:00.000Z'
1345
+ responses:
1346
+ '200':
1347
+ description: The key, shown once.
1348
+ content:
1349
+ application/json:
1350
+ schema:
1351
+ type: object
1352
+ properties:
1353
+ id:
1354
+ type: string
1355
+ format: uuid
1356
+ key:
1357
+ type: string
1358
+ description: The token value. Shown only now.
1359
+ expiresAt:
1360
+ type: string
1361
+ format: date-time
1362
+ nullable: true
1363
+ example:
1364
+ id: 9a3c5e7f-1b2d-4f6a-8c0e-2d4f6a8c0e1b
1365
+ key: 5d2e8f1a9c4b7e3d6a0f2c8b1e5d9a4c7f3b6e0d2a8c5f1e9b4d7a3c6f0e2b8d
1366
+ expiresAt: '2027-04-01T00:00:00.000Z'
1367
+ '400':
1368
+ description: '`name` or `scopes` is missing, or `expiresAt` is invalid or past.'
1369
+ content:
1370
+ application/json:
1371
+ schema:
1372
+ $ref: '#/components/schemas/Error'
1373
+ examples:
1374
+ missing:
1375
+ value:
1376
+ error: name and scopes required
1377
+ expiry:
1378
+ value:
1379
+ error: expiresAt must be an ISO-8601 datetime
1380
+ '401':
1381
+ $ref: '#/components/responses/Unauthorized'
1382
+ '403':
1383
+ $ref: '#/components/responses/Forbidden'
1384
+ '429':
1385
+ $ref: '#/components/responses/RateLimited'
1386
+
1387
+ /api/apikeys/{id}:
1388
+ delete:
1389
+ operationId: revokeLabApiKey
1390
+ summary: Revoke an API key
1391
+ description: |
1392
+ Revokes any API token in the lab, whoever owns it. Requests made with it are refused from then
1393
+ on; the row is kept, marked revoked. Needs role `ADMIN` or above and the `admin` scope. Cookie
1394
+ callers need a same-host `Origin` or `Referer`.
1395
+
1396
+ An unknown id answers `404`.
1397
+ tags:
1398
+ - API Keys
1399
+ parameters:
1400
+ - in: path
1401
+ name: id
1402
+ required: true
1403
+ description: The key's id.
1404
+ schema:
1405
+ type: string
1406
+ format: uuid
1407
+ example: 9a3c5e7f-1b2d-4f6a-8c0e-2d4f6a8c0e1b
1408
+ responses:
1409
+ '200':
1410
+ description: Revoked.
1411
+ content:
1412
+ application/json:
1413
+ schema:
1414
+ $ref: '#/components/schemas/IdentityOk'
1415
+ example:
1416
+ ok: true
1417
+ '401':
1418
+ $ref: '#/components/responses/Unauthorized'
1419
+ '403':
1420
+ $ref: '#/components/responses/Forbidden'
1421
+ '404':
1422
+ $ref: '#/components/responses/NotFound'
1423
+ '429':
1424
+ $ref: '#/components/responses/RateLimited'
1425
+
1426
+ # ---------------------------------------------------------------------------
1427
+ # Projects
1428
+ # ---------------------------------------------------------------------------
1429
+ /api/projects:
1430
+ get:
1431
+ operationId: listProjects
1432
+ summary: List projects
1433
+ description: |
1434
+ The projects the caller may see: a member sees their teams' projects and the shared ones
1435
+ (no team); an admin sees all. Any signed-in user may call it.
1436
+ tags:
1437
+ - Projects
1438
+ responses:
1439
+ '200':
1440
+ description: The projects.
1441
+ content:
1442
+ application/json:
1443
+ schema:
1444
+ type: array
1445
+ items:
1446
+ $ref: '#/components/schemas/IdentityProject'
1447
+ example:
1448
+ - id: clx9k2m4p0000qz8h3v1d7f2a
1449
+ name: Checkout app
1450
+ teamId: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1451
+ createdAt: '2026-09-01T08:00:00.000Z'
1452
+ '401':
1453
+ $ref: '#/components/responses/Unauthorized'
1454
+ '429':
1455
+ $ref: '#/components/responses/RateLimited'
1456
+ '500':
1457
+ $ref: '#/components/responses/InternalError'
1458
+ post:
1459
+ operationId: createProject
1460
+ summary: Create a project
1461
+ description: |
1462
+ Creates a project, optionally owned by a team (`teamId`, null or blank for shared). The name is
1463
+ trimmed. Needs the `admin` scope; no role is checked beyond that. Cookie callers need a
1464
+ same-host `Origin` or `Referer`.
1465
+ tags:
1466
+ - Projects
1467
+ requestBody:
1468
+ required: true
1469
+ content:
1470
+ application/json:
1471
+ schema:
1472
+ type: object
1473
+ required:
1474
+ - name
1475
+ properties:
1476
+ name:
1477
+ type: string
1478
+ example: Checkout app
1479
+ teamId:
1480
+ type: string
1481
+ description: The owning team. Not checked against existing teams.
1482
+ example: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1483
+ responses:
1484
+ '201':
1485
+ description: The new project.
1486
+ content:
1487
+ application/json:
1488
+ schema:
1489
+ $ref: '#/components/schemas/IdentityProject'
1490
+ example:
1491
+ id: clx9k2m4p0000qz8h3v1d7f2a
1492
+ name: Checkout app
1493
+ teamId: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1494
+ createdAt: '2026-10-04T09:30:00.000Z'
1495
+ '400':
1496
+ description: '`name` is missing or blank.'
1497
+ content:
1498
+ application/json:
1499
+ schema:
1500
+ $ref: '#/components/schemas/Error'
1501
+ example:
1502
+ error: name is required
1503
+ '401':
1504
+ $ref: '#/components/responses/Unauthorized'
1505
+ '403':
1506
+ $ref: '#/components/responses/Forbidden'
1507
+ '429':
1508
+ $ref: '#/components/responses/RateLimited'
1509
+
1510
+ # ---------------------------------------------------------------------------
1511
+ # Audit
1512
+ # ---------------------------------------------------------------------------
1513
+ /api/audit/events:
1514
+ post:
1515
+ operationId: ingestAuditEvents
1516
+ summary: Send in a batch of MCP audit events
1517
+ description: |
1518
+ For an MCP gateway: records a batch of tool-call audit events in the server's event log
1519
+ (type `mcp_audit`), tagged with the credential's team. Needs the `admin` scope (any role). The
1520
+ whole batch is checked first: one bad event refuses all of it. At most 1000 events per batch.
1521
+ Answers `202` with the count; writes happen in the background and a failed write is dropped.
1522
+ Cookie callers need a same-host `Origin` or `Referer`.
1523
+ tags:
1524
+ - Audit
1525
+ requestBody:
1526
+ required: true
1527
+ content:
1528
+ application/json:
1529
+ schema:
1530
+ type: object
1531
+ required:
1532
+ - events
1533
+ properties:
1534
+ events:
1535
+ type: array
1536
+ maxItems: 1000
1537
+ items:
1538
+ $ref: '#/components/schemas/IdentityAuditEvent'
1539
+ example:
1540
+ events:
1541
+ - subject: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
1542
+ tool: xenon_list_devices
1543
+ decision: allow
1544
+ latencyMs: 42
1545
+ correlationId: 0f6c2a1e-5b8d-4e3f-9a7c-1d2e3f4a5b6c
1546
+ - subject: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
1547
+ tool: appium_create_session
1548
+ decision: deny
1549
+ latencyMs: 3
1550
+ sessionId: 8d1f4b2e-7a3c-4e9d-b6f0-2c5a8e1d3b7f
1551
+ responses:
1552
+ '202':
1553
+ description: Accepted.
1554
+ content:
1555
+ application/json:
1556
+ schema:
1557
+ type: object
1558
+ properties:
1559
+ ingested:
1560
+ type: integer
1561
+ example: 2
1562
+ example:
1563
+ ingested: 2
1564
+ '400':
1565
+ description: '`events` is not an array, is longer than 1000, or an event is invalid.'
1566
+ content:
1567
+ application/json:
1568
+ schema:
1569
+ $ref: '#/components/schemas/Error'
1570
+ example:
1571
+ error: bad_request
1572
+ details: 'event[1]: latencyMs is required (number)'
1573
+ '401':
1574
+ $ref: '#/components/responses/Unauthorized'
1575
+ '403':
1576
+ $ref: '#/components/responses/Forbidden'
1577
+ '429':
1578
+ $ref: '#/components/responses/RateLimited'
1579
+
1580
+ # ---------------------------------------------------------------------------
1581
+ # Capabilities
1582
+ # ---------------------------------------------------------------------------
1583
+ /api/capabilities:
1584
+ get:
1585
+ operationId: getServerCapabilities
1586
+ summary: Get the server's version and features
1587
+ description: |
1588
+ Feature detection for SDKs and tools: the server's version and which features it has. Most are
1589
+ always `true` in this version; `sessionTokenGate` (`XENON_REQUIRE_SESSION_TOKEN`) and
1590
+ `commandAuth` (`XENON_REQUIRE_COMMAND_AUTH`) report the live settings. Any signed-in user may
1591
+ call it.
1592
+ tags:
1593
+ - Health & Ops
1594
+ responses:
1595
+ '200':
1596
+ description: The version and features.
1597
+ content:
1598
+ application/json:
1599
+ schema:
1600
+ $ref: '#/components/schemas/IdentityCapabilities'
1601
+ example:
1602
+ version: 2.12.0
1603
+ features:
1604
+ bearerAuth: true
1605
+ tokenIssuance: true
1606
+ streamTickets: true
1607
+ leases: true
1608
+ eventLog: true
1609
+ projects: true
1610
+ mcpScopedTokens: true
1611
+ sessionTokenGate: false
1612
+ commandAuth: true
1613
+ '401':
1614
+ $ref: '#/components/responses/Unauthorized'
1615
+ '429':
1616
+ $ref: '#/components/responses/RateLimited'
1617
+
1618
+ components:
1619
+ parameters:
1620
+ IdentityUserId:
1621
+ in: path
1622
+ name: id
1623
+ required: true
1624
+ description: The user's id.
1625
+ schema:
1626
+ type: string
1627
+ format: uuid
1628
+ example: 4f8d2a1b-3c6e-4b7a-8d9f-0e1c2b3a4d5e
1629
+ IdentityTeamId:
1630
+ in: path
1631
+ name: id
1632
+ required: true
1633
+ description: The team's id.
1634
+ schema:
1635
+ type: string
1636
+ format: uuid
1637
+ example: 2b6f0c3e-8d1a-4f5b-9c7e-1a2b3c4d5e6f
1638
+
1639
+ responses:
1640
+ IdentityCsrfRefused:
1641
+ description: |
1642
+ No same-host `Origin` or `Referer` on a state-changing request without an
1643
+ `x-xenon-access-key` header or an `Authorization: Bearer` token.
1644
+ content:
1645
+ application/json:
1646
+ schema:
1647
+ $ref: '#/components/schemas/Error'
1648
+ example:
1649
+ error: 'CSRF: Origin or Referer header required'
1650
+ IdentityTooManyAttempts:
1651
+ description: Too many attempts from this IP address; try again after `Retry-After` seconds.
1652
+ headers:
1653
+ Retry-After:
1654
+ $ref: '#/components/headers/Retry-After'
1655
+ content:
1656
+ application/json:
1657
+ schema:
1658
+ $ref: '#/components/schemas/Error'
1659
+ example:
1660
+ error: too many login attempts
1661
+ IdentityResetTokenInvalid:
1662
+ description: The token is unknown, used, revoked or expired.
1663
+ content:
1664
+ application/json:
1665
+ schema:
1666
+ $ref: '#/components/schemas/Error'
1667
+ example:
1668
+ error: invalid or expired token
1669
+ IdentityUserNotFound:
1670
+ description: No such user.
1671
+ content:
1672
+ application/json:
1673
+ schema:
1674
+ $ref: '#/components/schemas/Error'
1675
+ example:
1676
+ error: user not found
1677
+
1678
+ schemas:
1679
+ IdentityRole:
1680
+ type: string
1681
+ enum:
1682
+ - SUPER_ADMIN
1683
+ - ADMIN
1684
+ - MEMBER
1685
+ example: MEMBER
1686
+ IdentityScope:
1687
+ type: string
1688
+ enum:
1689
+ - read
1690
+ - sessions
1691
+ - devices
1692
+ - admin
1693
+ example: sessions
1694
+ IdentityOk:
1695
+ type: object
1696
+ properties:
1697
+ ok:
1698
+ type: boolean
1699
+ example: true
1700
+ IdentitySignInOptions:
1701
+ type: object
1702
+ properties:
1703
+ passwordReset:
1704
+ type: string
1705
+ enum:
1706
+ - email
1707
+ - admin
1708
+ description: '`email`: the forgot-password form emails a link. `admin`: ask an administrator.'
1709
+ IdentityLoginRequest:
1710
+ type: object
1711
+ required:
1712
+ - email
1713
+ - password
1714
+ properties:
1715
+ email:
1716
+ type: string
1717
+ format: email
1718
+ description: Compared case-insensitively.
1719
+ password:
1720
+ type: string
1721
+ format: password
1722
+ IdentityJwks:
1723
+ type: object
1724
+ properties:
1725
+ keys:
1726
+ type: array
1727
+ items:
1728
+ type: object
1729
+ properties:
1730
+ kty:
1731
+ type: string
1732
+ example: RSA
1733
+ n:
1734
+ type: string
1735
+ e:
1736
+ type: string
1737
+ example: AQAB
1738
+ kid:
1739
+ type: string
1740
+ use:
1741
+ type: string
1742
+ example: sig
1743
+ alg:
1744
+ type: string
1745
+ example: RS256
1746
+ additionalProperties: true
1747
+ IdentityMe:
1748
+ type: object
1749
+ properties:
1750
+ userId:
1751
+ type: string
1752
+ example: 7c1e2d4a-9b3f-4e8a-a1c2-5d6e7f809a1b
1753
+ email:
1754
+ type: string
1755
+ format: email
1756
+ name:
1757
+ type: string
1758
+ role:
1759
+ $ref: '#/components/schemas/IdentityRole'
1760
+ accessKey:
1761
+ type: string
1762
+ nullable: true
1763
+ description: The user's access key; null with authentication disabled.
1764
+ scopes:
1765
+ type: string
1766
+ description: The scopes this credential carries, comma-separated.
1767
+ example: devices,sessions,read
1768
+ teamId:
1769
+ type: string
1770
+ nullable: true
1771
+ description: The team this API key or bearer token is bound to; null for a dashboard session or an unbound key.
1772
+ kind:
1773
+ type: string
1774
+ enum:
1775
+ - user-session
1776
+ - api-key
1777
+ - bearer
1778
+ description: How this request authenticated.
1779
+ teams:
1780
+ type: array
1781
+ description: The caller's teams. Empty for admins.
1782
+ items:
1783
+ type: object
1784
+ properties:
1785
+ id:
1786
+ type: string
1787
+ name:
1788
+ type: string
1789
+ authDisabled:
1790
+ type: boolean
1791
+ description: True when the server has authentication switched off.
1792
+ IdentityTokenRequest:
1793
+ type: object
1794
+ properties:
1795
+ audience:
1796
+ type: string
1797
+ enum:
1798
+ - xenon-rest
1799
+ - xenon-mcp
1800
+ default: xenon-rest
1801
+ scopes:
1802
+ type: array
1803
+ description: '`xenon-mcp` only: the granular scopes to grant. Defaults to `appium:use` and `xenon:devices:read`, as far as allowed.'
1804
+ items:
1805
+ type: string
1806
+ enum:
1807
+ - appium:use
1808
+ - xenon:devices:read
1809
+ - xenon:devices:lock
1810
+ - xenon:analytics:read
1811
+ - xenon:recordings
1812
+ IdentityTokenResponse:
1813
+ type: object
1814
+ required:
1815
+ - token
1816
+ - expiresIn
1817
+ - audience
1818
+ properties:
1819
+ token:
1820
+ type: string
1821
+ description: The RS256 JWT.
1822
+ expiresIn:
1823
+ type: integer
1824
+ description: Lifetime in seconds.
1825
+ example: 3600
1826
+ audience:
1827
+ type: string
1828
+ enum:
1829
+ - xenon-rest
1830
+ - xenon-mcp
1831
+ scopes:
1832
+ type: array
1833
+ description: '`xenon-mcp` only: the granular scopes granted.'
1834
+ items:
1835
+ type: string
1836
+ sessionToken:
1837
+ type: string
1838
+ description: '`xenon-mcp` only: a session token for `xe:options.sessionToken`, with the same lifetime.'
1839
+ IdentityAccessKey:
1840
+ type: object
1841
+ properties:
1842
+ accessKey:
1843
+ type: string
1844
+ example: xen_4Hq8ZkP2mW7tR9vB
1845
+ IdentityProfileToken:
1846
+ type: object
1847
+ properties:
1848
+ id:
1849
+ type: string
1850
+ format: uuid
1851
+ name:
1852
+ type: string
1853
+ scopes:
1854
+ type: array
1855
+ items:
1856
+ $ref: '#/components/schemas/IdentityScope'
1857
+ createdAt:
1858
+ type: string
1859
+ format: date-time
1860
+ lastUsedAt:
1861
+ type: string
1862
+ format: date-time
1863
+ nullable: true
1864
+ expiresAt:
1865
+ type: string
1866
+ format: date-time
1867
+ nullable: true
1868
+ IdentityProfileTokenRequest:
1869
+ type: object
1870
+ required:
1871
+ - name
1872
+ properties:
1873
+ name:
1874
+ type: string
1875
+ scopes:
1876
+ type: array
1877
+ description: Defaults to everything your role allows.
1878
+ items:
1879
+ $ref: '#/components/schemas/IdentityScope'
1880
+ expiresAt:
1881
+ type: string
1882
+ format: date-time
1883
+ nullable: true
1884
+ description: When the token stops working; must be in the future. Null or absent for never.
1885
+ IdentityCreatedToken:
1886
+ type: object
1887
+ properties:
1888
+ id:
1889
+ type: string
1890
+ format: uuid
1891
+ token:
1892
+ type: string
1893
+ description: The token value. Shown only now.
1894
+ expiresAt:
1895
+ type: string
1896
+ format: date-time
1897
+ nullable: true
1898
+ IdentityUser:
1899
+ type: object
1900
+ properties:
1901
+ id:
1902
+ type: string
1903
+ format: uuid
1904
+ email:
1905
+ type: string
1906
+ format: email
1907
+ name:
1908
+ type: string
1909
+ role:
1910
+ $ref: '#/components/schemas/IdentityRole'
1911
+ status:
1912
+ type: string
1913
+ example: ACTIVE
1914
+ description: '`ACTIVE` or `INACTIVE`.'
1915
+ createdAt:
1916
+ type: string
1917
+ format: date-time
1918
+ lastLoginAt:
1919
+ type: string
1920
+ format: date-time
1921
+ nullable: true
1922
+ IdentityCreateUserRequest:
1923
+ type: object
1924
+ required:
1925
+ - email
1926
+ - name
1927
+ - role
1928
+ properties:
1929
+ email:
1930
+ type: string
1931
+ format: email
1932
+ name:
1933
+ type: string
1934
+ role:
1935
+ $ref: '#/components/schemas/IdentityRole'
1936
+ password:
1937
+ type: string
1938
+ minLength: 8
1939
+ description: The initial password. Omit it to have one generated and returned.
1940
+ IdentityCreatedUser:
1941
+ type: object
1942
+ properties:
1943
+ id:
1944
+ type: string
1945
+ format: uuid
1946
+ email:
1947
+ type: string
1948
+ format: email
1949
+ name:
1950
+ type: string
1951
+ role:
1952
+ $ref: '#/components/schemas/IdentityRole'
1953
+ status:
1954
+ type: string
1955
+ example: ACTIVE
1956
+ accessKey:
1957
+ type: string
1958
+ temporaryPassword:
1959
+ type: string
1960
+ description: Present only when the server generated the password.
1961
+ IdentityUpdateUserRequest:
1962
+ type: object
1963
+ properties:
1964
+ name:
1965
+ type: string
1966
+ role:
1967
+ $ref: '#/components/schemas/IdentityRole'
1968
+ status:
1969
+ type: string
1970
+ enum:
1971
+ - ACTIVE
1972
+ - INACTIVE
1973
+ IdentityUpdatedUser:
1974
+ type: object
1975
+ properties:
1976
+ id:
1977
+ type: string
1978
+ format: uuid
1979
+ email:
1980
+ type: string
1981
+ format: email
1982
+ name:
1983
+ type: string
1984
+ role:
1985
+ $ref: '#/components/schemas/IdentityRole'
1986
+ status:
1987
+ type: string
1988
+ example: ACTIVE
1989
+ IdentityResetLink:
1990
+ type: object
1991
+ required:
1992
+ - emailed
1993
+ - expiresAt
1994
+ properties:
1995
+ emailed:
1996
+ type: boolean
1997
+ description: True when the link was emailed to the user.
1998
+ link:
1999
+ type: string
2000
+ format: uri
2001
+ description: Present only when `emailed` is false. A credential until used or expired.
2002
+ expiresAt:
2003
+ type: string
2004
+ format: date-time
2005
+ IdentityTeam:
2006
+ type: object
2007
+ properties:
2008
+ id:
2009
+ type: string
2010
+ format: uuid
2011
+ name:
2012
+ type: string
2013
+ createdAt:
2014
+ type: string
2015
+ format: date-time
2016
+ deviceCount:
2017
+ type: integer
2018
+ memberCount:
2019
+ type: integer
2020
+ IdentityTeamRecord:
2021
+ type: object
2022
+ properties:
2023
+ id:
2024
+ type: string
2025
+ format: uuid
2026
+ name:
2027
+ type: string
2028
+ createdAt:
2029
+ type: string
2030
+ format: date-time
2031
+ IdentityTeamMember:
2032
+ type: object
2033
+ properties:
2034
+ userId:
2035
+ type: string
2036
+ format: uuid
2037
+ email:
2038
+ type: string
2039
+ format: email
2040
+ name:
2041
+ type: string
2042
+ role:
2043
+ $ref: '#/components/schemas/IdentityRole'
2044
+ addedAt:
2045
+ type: string
2046
+ format: date-time
2047
+ IdentityApiKey:
2048
+ type: object
2049
+ properties:
2050
+ id:
2051
+ type: string
2052
+ format: uuid
2053
+ name:
2054
+ type: string
2055
+ scopes:
2056
+ type: string
2057
+ description: Comma-separated scopes.
2058
+ example: sessions,devices
2059
+ rateLimit:
2060
+ type: integer
2061
+ description: Requests per minute in each budget.
2062
+ example: 300
2063
+ createdAt:
2064
+ type: string
2065
+ format: date-time
2066
+ lastUsedAt:
2067
+ type: string
2068
+ format: date-time
2069
+ nullable: true
2070
+ expiresAt:
2071
+ type: string
2072
+ format: date-time
2073
+ nullable: true
2074
+ teamId:
2075
+ type: string
2076
+ nullable: true
2077
+ description: The team the key is bound to.
2078
+ role:
2079
+ type: string
2080
+ description: A legacy column (default `member`); the key's power comes from its scopes and its owner's role.
2081
+ example: member
2082
+ IdentityCreateApiKeyRequest:
2083
+ type: object
2084
+ required:
2085
+ - name
2086
+ - scopes
2087
+ properties:
2088
+ name:
2089
+ type: string
2090
+ scopes:
2091
+ type: array
2092
+ minItems: 1
2093
+ items:
2094
+ $ref: '#/components/schemas/IdentityScope'
2095
+ rateLimit:
2096
+ type: integer
2097
+ default: 300
2098
+ description: Requests per minute in each budget.
2099
+ teamId:
2100
+ type: string
2101
+ nullable: true
2102
+ description: Bind the key to this team.
2103
+ expiresAt:
2104
+ type: string
2105
+ format: date-time
2106
+ nullable: true
2107
+ description: When the key stops working; must be in the future. Null or absent for never.
2108
+ IdentityProject:
2109
+ type: object
2110
+ properties:
2111
+ id:
2112
+ type: string
2113
+ name:
2114
+ type: string
2115
+ teamId:
2116
+ type: string
2117
+ nullable: true
2118
+ createdAt:
2119
+ type: string
2120
+ format: date-time
2121
+ IdentityAuditEvent:
2122
+ type: object
2123
+ required:
2124
+ - subject
2125
+ - tool
2126
+ - decision
2127
+ - latencyMs
2128
+ properties:
2129
+ subject:
2130
+ type: string
2131
+ description: Who called the tool.
2132
+ tool:
2133
+ type: string
2134
+ decision:
2135
+ type: string
2136
+ example: allow
2137
+ latencyMs:
2138
+ type: number
2139
+ correlationId:
2140
+ type: string
2141
+ sessionId:
2142
+ type: string
2143
+ IdentityCapabilities:
2144
+ type: object
2145
+ properties:
2146
+ version:
2147
+ type: string
2148
+ features:
2149
+ type: object
2150
+ properties:
2151
+ bearerAuth:
2152
+ type: boolean
2153
+ tokenIssuance:
2154
+ type: boolean
2155
+ streamTickets:
2156
+ type: boolean
2157
+ leases:
2158
+ type: boolean
2159
+ eventLog:
2160
+ type: boolean
2161
+ projects:
2162
+ type: boolean
2163
+ mcpScopedTokens:
2164
+ type: boolean
2165
+ sessionTokenGate:
2166
+ type: boolean
2167
+ commandAuth:
2168
+ type: boolean