@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.
- package/lib/package.json +2 -2
- package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
- package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
- package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
- package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
- package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
- package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
- package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
- package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
- package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
- package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
- package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
- package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
- package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
- package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
- package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
- package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
- package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
- package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
- package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
- package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
- package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
- package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
- package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
- package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
- package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
- package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
- package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
- package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
- package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
- package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
- package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
- package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
- package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
- package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
- package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
- package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
- package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
- package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
- package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
- package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
- package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
- package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
- package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
- package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
- package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
- package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
- package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
- package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
- package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
- package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
- package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
- package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
- package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
- package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
- package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
- package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
- package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
- package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
- package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
- package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
- package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
- package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
- package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/apiErrors.js +118 -0
- package/lib/src/app/index.js +6 -1
- package/lib/src/app/openapi/control.yaml +3129 -0
- package/lib/src/app/openapi/grid.yaml +2295 -0
- package/lib/src/app/openapi/identity.yaml +2168 -0
- package/lib/src/app/openapi/platform.yaml +2885 -0
- package/lib/src/app/openapi/sessions.yaml +3784 -0
- package/lib/src/app/routers/bug-report.js +4 -1
- package/lib/src/app/routers/config.js +6 -107
- package/lib/src/app/routers/control.js +117 -59
- package/lib/src/app/routers/dashboard.js +13 -7
- package/lib/src/app/routers/grid.js +54 -14
- package/lib/src/app/routers/profile.js +27 -13
- package/lib/src/app/routers/recordings.js +11 -5
- package/lib/src/app/routers/reservation.js +63 -15
- package/lib/src/app/routers/users.js +4 -0
- package/lib/src/app/routers/webhook.js +17 -8
- package/lib/src/app/swagger.js +259 -177
- package/lib/src/data-service/device-service.js +4 -1
- package/lib/src/data-service/deviceFieldOwners.js +1 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
- package/lib/src/device-managers/ios/WDAClient.js +32 -32
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +38 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +2 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/middleware/csrfMiddleware.js +13 -5
- package/lib/src/middleware/rateLimitMiddleware.js +19 -5
- package/lib/src/middleware/roleGuard.js +20 -0
- package/lib/src/services/AIService.js +13 -3
- package/lib/src/services/NotificationService.js +28 -28
- package/lib/src/services/bug-report/BugReportService.js +8 -2
- package/lib/src/services/lease/LeaseService.js +71 -20
- package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
- package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
- package/lib/test/helpers/expressRoutes.js +41 -0
- package/lib/test/integration/team-visibility-control.spec.js +2 -2
- package/lib/test/unit/access-scopes.spec.js +227 -0
- package/lib/test/unit/api-error-handling.spec.js +179 -0
- package/lib/test/unit/bug-report/route.spec.js +29 -0
- package/lib/test/unit/bug-report/service.spec.js +30 -0
- package/lib/test/unit/control-honest-answers.spec.js +134 -0
- package/lib/test/unit/device-allocation-routes.spec.js +232 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
- package/lib/test/unit/install-repository-app-team.spec.js +4 -1
- package/lib/test/unit/lease/LeaseService.spec.js +5 -4
- package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
- package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
- package/lib/test/unit/omni-vision-failures.spec.js +81 -0
- package/lib/test/unit/openapi-coverage.spec.js +114 -0
- package/lib/test/unit/profile-router.test.js +42 -0
- package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
- package/lib/test/unit/recording-orchestrator.spec.js +80 -0
- package/lib/test/unit/recordings-library-routes.spec.js +37 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
- package/lib/test/unit/reset-link.test.js +2 -1
- package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
- package/lib/test/unit/users-router.test.js +14 -1
- package/lib/test/unit/wda-client-failures.spec.js +90 -0
- package/lib/test/unit/webhook-delivery.spec.js +142 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
- package/prisma/schema.prisma +2 -0
- 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
|