@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
@@ -1,1701 +0,0 @@
1
- "use strict";
2
- /**
3
- * @swagger
4
- * /api/devices:
5
- * get:
6
- * summary: Get all connected devices
7
- * description: Retrieve a list of all connected devices with their current status
8
- * tags: [Devices]
9
- * responses:
10
- * 200:
11
- * description: List of devices
12
- * content:
13
- * application/json:
14
- * schema:
15
- * type: array
16
- * items:
17
- * $ref: '#/components/schemas/Device'
18
- * 401: { $ref: '#/components/responses/Unauthorized' }
19
- * 429: { $ref: '#/components/responses/RateLimited' }
20
- */
21
- Object.defineProperty(exports, "__esModule", { value: true });
22
- /**
23
- * @swagger
24
- * /api/config:
25
- * get:
26
- * summary: Get current configuration
27
- * description: Retrieve the current runtime configuration of the Xenon plugin
28
- * tags: [Configuration]
29
- * responses:
30
- * 200:
31
- * description: Current configuration object
32
- * content:
33
- * application/json:
34
- * schema:
35
- * type: object
36
- * 401: { $ref: '#/components/responses/Unauthorized' }
37
- * 429: { $ref: '#/components/responses/RateLimited' }
38
- * put:
39
- * summary: Update configuration
40
- * description: Update plugin configuration properties. Changes are persisted. Some changes may require a server restart.
41
- * tags: [Configuration]
42
- * requestBody:
43
- * required: true
44
- * content:
45
- * application/json:
46
- * schema:
47
- * type: object
48
- * description: Partial configuration object
49
- * example:
50
- * deviceAvailabilityTimeoutMs: 60000
51
- * maxSessions: 5
52
- * responses:
53
- * 200:
54
- * description: Configuration updated
55
- * content:
56
- * application/json:
57
- * schema:
58
- * type: object
59
- * properties:
60
- * success:
61
- * type: boolean
62
- * restartRequired:
63
- * type: boolean
64
- * description: True if the updated properties require a server restart
65
- * message:
66
- * type: string
67
- * 401: { $ref: '#/components/responses/Unauthorized' }
68
- * 403: { $ref: '#/components/responses/Forbidden' }
69
- * 429: { $ref: '#/components/responses/RateLimited' }
70
- */
71
- /**
72
- * @swagger
73
- * /api/sessions/active:
74
- * get:
75
- * summary: Get active sessions
76
- * description: Get statistics and list of currently active sessions in memory
77
- * tags: [Sessions]
78
- * responses:
79
- * 200:
80
- * description: Active session information
81
- * content:
82
- * application/json:
83
- * schema:
84
- * type: object
85
- * properties:
86
- * stats:
87
- * type: object
88
- * properties:
89
- * total:
90
- * type: number
91
- * description: Total number of active sessions
92
- * byType:
93
- * type: object
94
- * properties:
95
- * local:
96
- * type: number
97
- * remote:
98
- * type: number
99
- * cloud:
100
- * type: number
101
- * sessions:
102
- * type: array
103
- * items:
104
- * type: object
105
- * properties:
106
- * id:
107
- * type: string
108
- * type:
109
- * type: string
110
- * enum: [local, remote, cloud]
111
- * deviceUdid:
112
- * type: string
113
- * deviceName:
114
- * type: string
115
- * platform:
116
- * type: string
117
- * 401: { $ref: '#/components/responses/Unauthorized' }
118
- * 429: { $ref: '#/components/responses/RateLimited' }
119
- */
120
- /**
121
- * @swagger
122
- * /api/logs/requests:
123
- * get:
124
- * summary: Get HTTP request logs
125
- * description: Retrieve recent HTTP request logs for debugging hub-node communication
126
- * tags: [Observability]
127
- * parameters:
128
- * - in: query
129
- * name: limit
130
- * schema:
131
- * type: integer
132
- * default: 50
133
- * description: Maximum number of logs to return
134
- * - in: query
135
- * name: method
136
- * schema:
137
- * type: string
138
- * enum: [GET, POST, PUT, DELETE]
139
- * description: Filter by HTTP method
140
- * - in: query
141
- * name: url
142
- * schema:
143
- * type: string
144
- * description: Filter by URL pattern (substring match)
145
- * - in: query
146
- * name: hasError
147
- * schema:
148
- * type: boolean
149
- * description: Filter by error status (true = only errors, false = only success)
150
- * responses:
151
- * 200:
152
- * description: Request logs and statistics
153
- * content:
154
- * application/json:
155
- * schema:
156
- * type: object
157
- * properties:
158
- * stats:
159
- * type: object
160
- * properties:
161
- * totalLogged:
162
- * type: number
163
- * description: Total requests in buffer
164
- * errorCount:
165
- * type: number
166
- * description: Number of failed requests
167
- * avgDurationMs:
168
- * type: number
169
- * description: Average request duration in milliseconds
170
- * byMethod:
171
- * type: object
172
- * additionalProperties:
173
- * type: number
174
- * byStatusCode:
175
- * type: object
176
- * additionalProperties:
177
- * type: number
178
- * logs:
179
- * type: array
180
- * items:
181
- * type: object
182
- * properties:
183
- * timestamp:
184
- * type: string
185
- * format: date-time
186
- * direction:
187
- * type: string
188
- * enum: [outgoing, incoming]
189
- * method:
190
- * type: string
191
- * url:
192
- * type: string
193
- * statusCode:
194
- * type: number
195
- * durationMs:
196
- * type: number
197
- * error:
198
- * type: string
199
- * nullable: true
200
- * requestBody:
201
- * type: string
202
- * description: Sanitized request body (sensitive data redacted)
203
- * responseBody:
204
- * type: string
205
- * description: Truncated response body
206
- * 401: { $ref: '#/components/responses/Unauthorized' }
207
- * 429: { $ref: '#/components/responses/RateLimited' }
208
- */
209
- /**
210
- * @swagger
211
- * tags:
212
- * - name: Observability
213
- * description: Debugging and monitoring endpoints
214
- */
215
- /**
216
- * @swagger
217
- * /api/healing/selector/state:
218
- * post:
219
- * summary: Apply a lifecycle action to a selector
220
- * description: |
221
- * Mutates the SelectorState row for a (strategy, selector) tuple and
222
- * records the change, with the person, in SelectorEvent. Actions are
223
- * bounded — `mark_fixed`, `mute`, `unmute`, `cancel_verification`.
224
- * Members and admins may act on a selector healed in a session they
225
- * can see; any other answers 404, as an unknown one. Needs the
226
- * `sessions` scope (`admin` implies it). `original_strategy` may be
227
- * empty, for heals recorded with no strategy. Returns 409 with
228
- * `currentStatus` if the action is incompatible with the row's
229
- * current state (e.g. mark_fixed on a muted selector).
230
- * tags: [Selector Health]
231
- * security:
232
- * - apiKey: []
233
- * requestBody:
234
- * required: true
235
- * content:
236
- * application/json:
237
- * schema:
238
- * type: object
239
- * required: [original_strategy, original_selector, action]
240
- * properties:
241
- * original_strategy:
242
- * type: string
243
- * example: accessibility id
244
- * original_selector:
245
- * type: string
246
- * example: login-btn
247
- * action:
248
- * type: string
249
- * enum: [mark_fixed, mute, unmute, cancel_verification]
250
- * reason: { type: string, maxLength: 500, description: 'Why the selector is muted; mute only' }
251
- * responses:
252
- * 200:
253
- * description: Updated SelectorState row
254
- * 400:
255
- * description: Missing fields, unknown action, or a reason over 500 characters
256
- * 403:
257
- * description: The caller lacks the `sessions` scope
258
- * 404:
259
- * description: '`{ error: "not_found", message: "Selector not found" }`'
260
- * 409:
261
- * description: Action conflicts with current state (returns currentStatus)
262
- * 401: { $ref: '#/components/responses/Unauthorized' }
263
- * 429: { $ref: '#/components/responses/RateLimited' }
264
- */
265
- /**
266
- * @swagger
267
- * /api/healing/state/muted:
268
- * get:
269
- * summary: List muted selectors
270
- * description: |
271
- * Returns muted selectors sourced directly from SelectorState
272
- * (not the hotspot aggregator), so silenced-but-not-recently-healed
273
- * rows still surface. Each row is enriched with the most recent
274
- * `is_healed=true` SessionLog timestamp for the tuple.
275
- * tags: [Selector Health]
276
- * parameters:
277
- * - in: query
278
- * name: limit
279
- * schema: { type: integer, default: 50, maximum: 200 }
280
- * - in: query
281
- * name: offset
282
- * schema: { type: integer, default: 0 }
283
- * responses:
284
- * 200:
285
- * description: Paginated muted selectors
286
- * content:
287
- * application/json:
288
- * schema:
289
- * type: object
290
- * properties:
291
- * muted: { type: array }
292
- * total: { type: integer }
293
- * limit: { type: integer }
294
- * offset: { type: integer }
295
- * 401: { $ref: '#/components/responses/Unauthorized' }
296
- * 429: { $ref: '#/components/responses/RateLimited' }
297
- */
298
- /**
299
- * @swagger
300
- * /api/healing/state/{strategy}/{value}:
301
- * get:
302
- * summary: Look up SelectorState for a single tuple
303
- * description: |
304
- * URL-encoded strategy + value. Returns `{ state: null }` when no
305
- * row exists — null is meaningful (the selector is implicitly Active).
306
- * tags: [Selector Health]
307
- * parameters:
308
- * - in: path
309
- * name: strategy
310
- * required: true
311
- * schema: { type: string }
312
- * - in: path
313
- * name: value
314
- * required: true
315
- * schema: { type: string }
316
- * responses:
317
- * 200:
318
- * description: SelectorState row, or null
319
- * 401: { $ref: '#/components/responses/Unauthorized' }
320
- * 429: { $ref: '#/components/responses/RateLimited' }
321
- */
322
- /**
323
- * @swagger
324
- * /api/healing/hotspots:
325
- * get:
326
- * summary: List the most-healed selectors (with lifecycle filter)
327
- * description: |
328
- * Status filter is `active` (default), `pending`, `resolved`, `muted`,
329
- * or `all`. The default value transparently hides muted/pending/
330
- * resolved selectors from the live list, the CI gate, and the digest.
331
- * tags: [Selector Health]
332
- * parameters:
333
- * - in: query
334
- * name: windowDays
335
- * schema: { type: integer, default: 30 }
336
- * - in: query
337
- * name: limit
338
- * schema: { type: integer, default: 10 }
339
- * - in: query
340
- * name: status
341
- * schema: { type: string, enum: [active, pending, resolved, muted, all], default: active }
342
- * - in: query
343
- * name: tier
344
- * schema: { type: string }
345
- * - in: query
346
- * name: platform
347
- * schema: { type: string }
348
- * responses:
349
- * 200:
350
- * description: Hotspots with state overlay
351
- * 401: { $ref: '#/components/responses/Unauthorized' }
352
- * 429: { $ref: '#/components/responses/RateLimited' }
353
- */
354
- // =============================================================================
355
- // Health & Operations
356
- // =============================================================================
357
- /**
358
- * @swagger
359
- * /api/health:
360
- * get:
361
- * summary: Liveness probe
362
- * description: |
363
- * Public, unauthenticated, not rate-limited. Returns `{ ok: true }` as soon
364
- * as the Express stack is up — does not check database or device state.
365
- * Suitable for Kubernetes/load-balancer liveness probes.
366
- * tags: [Health & Ops]
367
- * security: []
368
- * responses:
369
- * 200:
370
- * description: Server is up
371
- * content:
372
- * application/json:
373
- * schema:
374
- * type: object
375
- * properties:
376
- * ok: { type: boolean, example: true }
377
- */
378
- /**
379
- * @swagger
380
- * /api/ping:
381
- * get:
382
- * summary: Authenticated ping with build version
383
- * description: Returns `{ pong, version }`. Use to confirm an API key is valid and read the running plugin version.
384
- * tags: [Health & Ops]
385
- * responses:
386
- * 200:
387
- * description: Pong with version
388
- * content:
389
- * application/json:
390
- * schema:
391
- * type: object
392
- * properties:
393
- * pong: { type: boolean, example: true }
394
- * version: { type: string, example: '1.5.0' }
395
- * 401: { $ref: '#/components/responses/Unauthorized' }
396
- * 429: { $ref: '#/components/responses/RateLimited' }
397
- */
398
- /**
399
- * @swagger
400
- * /api/metrics:
401
- * get:
402
- * summary: Prometheus-format metrics
403
- * description: |
404
- * Returns scrape-friendly Prometheus exposition format (`text/plain; version=0.0.4`).
405
- * Auth-gated to avoid operational reconnaissance — point your scraper at this
406
- * endpoint with a `read`-scope token in the `(x-xenon-access-key, x-xenon-token)` pair.
407
- * tags: [Health & Ops]
408
- * responses:
409
- * 200:
410
- * description: Prometheus metrics
411
- * content:
412
- * text/plain:
413
- * schema: { type: string }
414
- * 401: { $ref: '#/components/responses/Unauthorized' }
415
- * 429: { $ref: '#/components/responses/RateLimited' }
416
- */
417
- /**
418
- * @swagger
419
- * /api/cliArgs:
420
- * get:
421
- * summary: Read the plugin CLI arguments in effect
422
- * description: |
423
- * Returns the resolved plugin arguments (host, hub URL, platform, intervals, etc.)
424
- * this process was started with. Requires the ADMIN role and `admin` scope.
425
- * Values are redacted the way the logger redacts them: any key that looks
426
- * secret (database URL, API key, token, password, auth…) reads
427
- * `***REDACTED***`, and so do provider-key-shaped strings.
428
- * tags: [Health & Ops]
429
- * responses:
430
- * 200:
431
- * description: Resolved plugin arguments
432
- * content:
433
- * application/json:
434
- * schema: { type: object, additionalProperties: true }
435
- * 401: { $ref: '#/components/responses/Unauthorized' }
436
- * 403: { $ref: '#/components/responses/Forbidden' }
437
- * 429: { $ref: '#/components/responses/RateLimited' }
438
- */
439
- // =============================================================================
440
- // Authentication
441
- // =============================================================================
442
- /**
443
- * @swagger
444
- * /api/auth/dashboard-session:
445
- * post:
446
- * summary: Exchange an API key for a browser dashboard session cookie
447
- * description: |
448
- * Used by the dashboard UI to convert a one-time API key paste into a
449
- * short-lived session cookie. Public endpoint — no API key header is
450
- * required because the body itself carries the credential. On success,
451
- * sets `xenon_dashboard_session` (HttpOnly, Secure, SameSite=Strict, 24 h
452
- * max-age). Subsequent dashboard requests use the cookie instead of the
453
- * header. CSRF protection: cookie-authed callers must send a matching
454
- * `Origin`/`Referer`.
455
- * tags: [Authentication]
456
- * security: []
457
- * requestBody:
458
- * required: true
459
- * content:
460
- * application/json:
461
- * schema:
462
- * type: object
463
- * required: [apiKey]
464
- * properties:
465
- * apiKey: { type: string, description: 'Plain-text API key' }
466
- * responses:
467
- * 200:
468
- * description: Session cookie set
469
- * content:
470
- * application/json:
471
- * schema:
472
- * type: object
473
- * properties:
474
- * ok: { type: boolean, example: true }
475
- * scopes:
476
- * type: array
477
- * items: { type: string, enum: [read, sessions, devices, admin] }
478
- * 400: { description: 'Missing apiKey field' }
479
- * 401: { description: 'Invalid API key' }
480
- */
481
- // =============================================================================
482
- // Admin — API Keys
483
- // =============================================================================
484
- /**
485
- * @swagger
486
- * /api/apikeys:
487
- * get:
488
- * summary: List all API keys
489
- * description: Returns metadata only — the plain-text key value is never returned after creation. Requires `admin` scope.
490
- * tags: [Admin]
491
- * responses:
492
- * 200:
493
- * description: List of API key records
494
- * content:
495
- * application/json:
496
- * schema:
497
- * type: array
498
- * items:
499
- * type: object
500
- * properties:
501
- * id: { type: string }
502
- * name: { type: string }
503
- * scopes:
504
- * type: array
505
- * items: { type: string, enum: [read, sessions, devices, admin] }
506
- * rateLimit: { type: integer, nullable: true }
507
- * teamId: { type: string, nullable: true }
508
- * createdAt: { type: string, format: date-time }
509
- * lastUsedAt: { type: string, format: date-time, nullable: true }
510
- * 401: { $ref: '#/components/responses/Unauthorized' }
511
- * 403: { $ref: '#/components/responses/Forbidden' }
512
- * 429: { $ref: '#/components/responses/RateLimited' }
513
- * post:
514
- * summary: Mint a new API key
515
- * description: |
516
- * Returns the plain-text key **once** in the response. Store it immediately —
517
- * it cannot be retrieved later. Requires `admin` scope.
518
- * tags: [Admin]
519
- * requestBody:
520
- * required: true
521
- * content:
522
- * application/json:
523
- * schema:
524
- * type: object
525
- * required: [name, scopes]
526
- * properties:
527
- * name: { type: string, example: 'CI runner' }
528
- * scopes:
529
- * type: array
530
- * minItems: 1
531
- * items: { type: string, enum: [read, sessions, devices, admin] }
532
- * rateLimit:
533
- * type: integer
534
- * description: 'Per-window quota override; null inherits the plugin default'
535
- * teamId:
536
- * type: string
537
- * nullable: true
538
- * description: 'Bind this key to a team (so it sees team-owned devices only)'
539
- * responses:
540
- * 200:
541
- * description: Key created
542
- * content:
543
- * application/json:
544
- * schema:
545
- * type: object
546
- * properties:
547
- * id: { type: string }
548
- * key: { type: string, description: 'Plain-text key — shown once' }
549
- * 400: { description: 'Missing name or empty scopes array' }
550
- * 401: { $ref: '#/components/responses/Unauthorized' }
551
- * 403: { $ref: '#/components/responses/Forbidden' }
552
- * 429: { $ref: '#/components/responses/RateLimited' }
553
- */
554
- /**
555
- * @swagger
556
- * /api/apikeys/{id}:
557
- * delete:
558
- * summary: Revoke an API key
559
- * description: Hard-deletes the key. In-flight requests already authenticated continue until completion; subsequent requests with the revoked key get 401. Requires `admin` scope.
560
- * tags: [Admin]
561
- * parameters:
562
- * - in: path
563
- * name: id
564
- * required: true
565
- * schema: { type: string }
566
- * responses:
567
- * 200:
568
- * description: Key revoked
569
- * content:
570
- * application/json:
571
- * schema: { $ref: '#/components/schemas/Success' }
572
- * 401: { $ref: '#/components/responses/Unauthorized' }
573
- * 403: { $ref: '#/components/responses/Forbidden' }
574
- * 429: { $ref: '#/components/responses/RateLimited' }
575
- */
576
- // =============================================================================
577
- // Admin — Teams
578
- // =============================================================================
579
- /**
580
- * @swagger
581
- * /api/teams:
582
- * get:
583
- * summary: List all teams
584
- * description: Requires `admin` scope.
585
- * tags: [Admin]
586
- * responses:
587
- * 200:
588
- * description: List of teams
589
- * content:
590
- * application/json:
591
- * schema:
592
- * type: array
593
- * items:
594
- * type: object
595
- * properties:
596
- * id: { type: string }
597
- * name: { type: string }
598
- * createdAt: { type: string, format: date-time }
599
- * 401: { $ref: '#/components/responses/Unauthorized' }
600
- * 403: { $ref: '#/components/responses/Forbidden' }
601
- * 429: { $ref: '#/components/responses/RateLimited' }
602
- * post:
603
- * summary: Create a team
604
- * description: Team names must be unique. Requires `admin` scope.
605
- * tags: [Admin]
606
- * requestBody:
607
- * required: true
608
- * content:
609
- * application/json:
610
- * schema:
611
- * type: object
612
- * required: [name]
613
- * properties:
614
- * name: { type: string, example: 'Mobile Platform' }
615
- * responses:
616
- * 200:
617
- * description: Team created
618
- * 400: { description: 'Missing name' }
619
- * 409: { description: 'Team name already exists' }
620
- * 401: { $ref: '#/components/responses/Unauthorized' }
621
- * 403: { $ref: '#/components/responses/Forbidden' }
622
- * 429: { $ref: '#/components/responses/RateLimited' }
623
- */
624
- /**
625
- * @swagger
626
- * /api/teams/{id}:
627
- * delete:
628
- * summary: Delete a team
629
- * description: Cascades to membership rows. Devices owned by the team revert to shared (null) ownership. Requires `admin` scope.
630
- * tags: [Admin]
631
- * parameters:
632
- * - in: path
633
- * name: id
634
- * required: true
635
- * schema: { type: string }
636
- * responses:
637
- * 200:
638
- * description: Team deleted
639
- * content:
640
- * application/json:
641
- * schema: { $ref: '#/components/schemas/Success' }
642
- * 401: { $ref: '#/components/responses/Unauthorized' }
643
- * 403: { $ref: '#/components/responses/Forbidden' }
644
- * 429: { $ref: '#/components/responses/RateLimited' }
645
- */
646
- /**
647
- * @swagger
648
- * /api/teams/{id}/members:
649
- * get:
650
- * summary: List team members
651
- * description: Returns the API keys bound to this team. Requires `admin` scope.
652
- * tags: [Admin]
653
- * parameters:
654
- * - in: path
655
- * name: id
656
- * required: true
657
- * schema: { type: string }
658
- * responses:
659
- * 200:
660
- * description: List of members
661
- * content:
662
- * application/json:
663
- * schema:
664
- * type: array
665
- * items:
666
- * type: object
667
- * properties:
668
- * apiKeyId: { type: string }
669
- * role: { type: string, enum: [member, owner] }
670
- * 401: { $ref: '#/components/responses/Unauthorized' }
671
- * 429: { $ref: '#/components/responses/RateLimited' }
672
- * post:
673
- * summary: Add a member to a team
674
- * description: Adds an existing API key as a team member. Defaults to `member` role. Requires `admin` scope.
675
- * tags: [Admin]
676
- * parameters:
677
- * - in: path
678
- * name: id
679
- * required: true
680
- * schema: { type: string }
681
- * requestBody:
682
- * required: true
683
- * content:
684
- * application/json:
685
- * schema:
686
- * type: object
687
- * required: [apiKeyId]
688
- * properties:
689
- * apiKeyId: { type: string }
690
- * role: { type: string, enum: [member, owner], default: member }
691
- * responses:
692
- * 200: { description: 'Member added' }
693
- * 400: { description: 'Missing apiKeyId' }
694
- * 401: { $ref: '#/components/responses/Unauthorized' }
695
- * 403: { $ref: '#/components/responses/Forbidden' }
696
- * 429: { $ref: '#/components/responses/RateLimited' }
697
- */
698
- /**
699
- * @swagger
700
- * /api/teams/{id}/members/{apiKeyId}:
701
- * delete:
702
- * summary: Remove a member from a team
703
- * description: Does not revoke the API key itself — only unbinds it from the team. Requires `admin` scope.
704
- * tags: [Admin]
705
- * parameters:
706
- * - in: path
707
- * name: id
708
- * required: true
709
- * schema: { type: string }
710
- * - in: path
711
- * name: apiKeyId
712
- * required: true
713
- * schema: { type: string }
714
- * responses:
715
- * 200:
716
- * description: Member removed
717
- * content:
718
- * application/json:
719
- * schema: { $ref: '#/components/schemas/Success' }
720
- * 401: { $ref: '#/components/responses/Unauthorized' }
721
- * 403: { $ref: '#/components/responses/Forbidden' }
722
- * 429: { $ref: '#/components/responses/RateLimited' }
723
- */
724
- // =============================================================================
725
- // Admin — Process snapshot
726
- // =============================================================================
727
- /**
728
- * @swagger
729
- * /api/processes:
730
- * get:
731
- * summary: Snapshot of long-running child processes
732
- * description: |
733
- * Lists the long-lived workers Xenon is currently supervising — Appium-driven
734
- * sessions, ADB log tails, MJPEG stream encoders, etc. Useful when triaging
735
- * why a host is hot or a device is wedged. Requires `admin` scope.
736
- * tags: [Admin]
737
- * responses:
738
- * 200:
739
- * description: List of supervised processes
740
- * content:
741
- * application/json:
742
- * schema:
743
- * type: array
744
- * items:
745
- * type: object
746
- * properties:
747
- * id: { type: string }
748
- * sessionId: { type: string, nullable: true }
749
- * udid: { type: string, nullable: true }
750
- * kind: { type: string, example: 'mjpeg-stream' }
751
- * pid: { type: integer }
752
- * uptimeMs: { type: integer }
753
- * 401: { $ref: '#/components/responses/Unauthorized' }
754
- * 403: { $ref: '#/components/responses/Forbidden' }
755
- * 429: { $ref: '#/components/responses/RateLimited' }
756
- */
757
- // =============================================================================
758
- // Network Interceptor
759
- // =============================================================================
760
- /**
761
- * @swagger
762
- * /api/interceptor/sessions/{sessionId}/requests:
763
- * get:
764
- * summary: List intercepted HTTP requests for a session
765
- * description: |
766
- * Returns the full list of requests captured by the per-session interceptor.
767
- * If the session is still active, results are read live; otherwise the
768
- * archived `requests.json` from the session asset directory is replayed.
769
- * Returns 404 when neither a live interceptor nor an archive exists.
770
- * tags: [Network Interceptor]
771
- * parameters:
772
- * - in: path
773
- * name: sessionId
774
- * required: true
775
- * schema: { type: string }
776
- * responses:
777
- * 200:
778
- * description: Request list
779
- * content:
780
- * application/json:
781
- * schema:
782
- * type: object
783
- * properties:
784
- * requests:
785
- * type: array
786
- * items: { type: object, additionalProperties: true }
787
- * 404: { description: 'Interceptor inactive and no archive on disk' }
788
- * 401: { $ref: '#/components/responses/Unauthorized' }
789
- * 429: { $ref: '#/components/responses/RateLimited' }
790
- */
791
- /**
792
- * @swagger
793
- * /api/interceptor/sessions/{sessionId}/requests/{requestId}:
794
- * get:
795
- * summary: Get a single intercepted request with full body
796
- * description: |
797
- * Hydrates the request detail, including any response body that was
798
- * offloaded to disk (`bodyPath`). Use this from the dashboard's request
799
- * drawer or for HAR-equivalent single-record export.
800
- * tags: [Network Interceptor]
801
- * parameters:
802
- * - in: path
803
- * name: sessionId
804
- * required: true
805
- * schema: { type: string }
806
- * - in: path
807
- * name: requestId
808
- * required: true
809
- * schema: { type: string }
810
- * responses:
811
- * 200:
812
- * description: Single request entry
813
- * content:
814
- * application/json:
815
- * schema: { type: object, additionalProperties: true }
816
- * 404: { description: 'Request not found in live or archived state' }
817
- * 401: { $ref: '#/components/responses/Unauthorized' }
818
- * 429: { $ref: '#/components/responses/RateLimited' }
819
- */
820
- /**
821
- * @swagger
822
- * /api/interceptor/sessions/{sessionId}/har:
823
- * get:
824
- * summary: Export session traffic as a HAR file
825
- * description: |
826
- * Returns a HAR 1.2 archive (`application/json`, with `Content-Disposition:
827
- * attachment`). Falls back to the on-disk archive if the session has ended.
828
- * tags: [Network Interceptor]
829
- * parameters:
830
- * - in: path
831
- * name: sessionId
832
- * required: true
833
- * schema: { type: string }
834
- * responses:
835
- * 200:
836
- * description: HAR file
837
- * content:
838
- * application/json:
839
- * schema: { type: object, additionalProperties: true }
840
- * 404: { description: 'No live interceptor and no archived HAR' }
841
- * 401: { $ref: '#/components/responses/Unauthorized' }
842
- * 429: { $ref: '#/components/responses/RateLimited' }
843
- */
844
- /**
845
- * @swagger
846
- * /api/interceptor/sessions/{sessionId}/mocks:
847
- * get:
848
- * summary: List mock rules for a session
849
- * description: Mock rules apply only while the session is live — there is no archive fallback.
850
- * tags: [Network Interceptor]
851
- * parameters:
852
- * - in: path
853
- * name: sessionId
854
- * required: true
855
- * schema: { type: string }
856
- * responses:
857
- * 200:
858
- * description: Mock list
859
- * content:
860
- * application/json:
861
- * schema:
862
- * type: object
863
- * properties:
864
- * mocks:
865
- * type: array
866
- * items: { type: object, additionalProperties: true }
867
- * 404: { description: 'Interceptor not active for this session' }
868
- * 401: { $ref: '#/components/responses/Unauthorized' }
869
- * 429: { $ref: '#/components/responses/RateLimited' }
870
- * post:
871
- * summary: Add a mock rule
872
- * description: |
873
- * Mock rules match by URL pattern and method, then either return a static
874
- * response, rewrite the upstream response, or delay it. Only valid while
875
- * the target session is live.
876
- * tags: [Network Interceptor]
877
- * parameters:
878
- * - in: path
879
- * name: sessionId
880
- * required: true
881
- * schema: { type: string }
882
- * requestBody:
883
- * required: true
884
- * content:
885
- * application/json:
886
- * schema: { type: object, additionalProperties: true }
887
- * responses:
888
- * 201:
889
- * description: Mock created
890
- * content:
891
- * application/json:
892
- * schema:
893
- * type: object
894
- * properties:
895
- * id: { type: string }
896
- * 400: { description: 'Invalid mock rule' }
897
- * 404: { description: 'Interceptor not active' }
898
- * 401: { $ref: '#/components/responses/Unauthorized' }
899
- * 403: { $ref: '#/components/responses/Forbidden' }
900
- * 429: { $ref: '#/components/responses/RateLimited' }
901
- * delete:
902
- * summary: Clear all mock rules for a session
903
- * tags: [Network Interceptor]
904
- * parameters:
905
- * - in: path
906
- * name: sessionId
907
- * required: true
908
- * schema: { type: string }
909
- * responses:
910
- * 200:
911
- * description: Mocks cleared
912
- * content:
913
- * application/json:
914
- * schema: { $ref: '#/components/schemas/Success' }
915
- * 404: { description: 'Interceptor not active' }
916
- * 401: { $ref: '#/components/responses/Unauthorized' }
917
- * 403: { $ref: '#/components/responses/Forbidden' }
918
- * 429: { $ref: '#/components/responses/RateLimited' }
919
- */
920
- /**
921
- * @swagger
922
- * /api/interceptor/sessions/{sessionId}/mocks/{mockId}:
923
- * delete:
924
- * summary: Remove a single mock rule
925
- * tags: [Network Interceptor]
926
- * parameters:
927
- * - in: path
928
- * name: sessionId
929
- * required: true
930
- * schema: { type: string }
931
- * - in: path
932
- * name: mockId
933
- * required: true
934
- * schema: { type: string }
935
- * responses:
936
- * 200:
937
- * description: Mock removed
938
- * content:
939
- * application/json:
940
- * schema:
941
- * type: object
942
- * properties:
943
- * removed: { type: boolean }
944
- * 404: { description: 'Interceptor not active or mock not found' }
945
- * 401: { $ref: '#/components/responses/Unauthorized' }
946
- * 403: { $ref: '#/components/responses/Forbidden' }
947
- * 429: { $ref: '#/components/responses/RateLimited' }
948
- */
949
- // =============================================================================
950
- // Hub-Node channel (pair auth)
951
- // =============================================================================
952
- /**
953
- * @swagger
954
- * /api/register:
955
- * post:
956
- * summary: Node→Hub device inventory push
957
- * description: |
958
- * Hub-node only. Authenticates with the per-node
959
- * `(x-xenon-access-key, x-xenon-token)` pair the node was provisioned
960
- * with on the hub. Nodes call this on startup and on a recurring
961
- * interval (`sendNodeDevicesToHubIntervalMs`, default 30 s) to publish
962
- * their device list. The `type` query param selects the operation:
963
- *
964
- * | type | effect |
965
- * |---------------|---------------------------------------------------------------|
966
- * | `add` | Insert / refresh the supplied devices |
967
- * | `remove` | Drop the supplied devices (e.g. USB unplug) |
968
- * | `unregister` | Drop every device for the calling host (used on graceful exit) |
969
- *
970
- * Not intended for end-user automation. Document here for transparency.
971
- * tags: [Hub-Node]
972
- * security:
973
- * - AccessKeyAuth: []
974
- * TokenAuth: []
975
- * parameters:
976
- * - in: query
977
- * name: type
978
- * required: true
979
- * schema: { type: string, enum: [add, remove, unregister] }
980
- * - in: query
981
- * name: host
982
- * schema: { type: string }
983
- * description: 'Required for `unregister`'
984
- * requestBody:
985
- * content:
986
- * application/json:
987
- * schema:
988
- * oneOf:
989
- * - type: array
990
- * items: { $ref: '#/components/schemas/Device' }
991
- * - type: object
992
- * responses:
993
- * 200:
994
- * description: Inventory accepted
995
- * content:
996
- * application/json:
997
- * schema: { $ref: '#/components/schemas/Success' }
998
- * 401: { description: 'Missing or invalid (accessKey, token) pair' }
999
- * 403: { $ref: '#/components/responses/Forbidden' }
1000
- * 429: { $ref: '#/components/responses/RateLimited' }
1001
- */
1002
- /**
1003
- * @swagger
1004
- * /api/unblock:
1005
- * post:
1006
- * summary: Node→Hub release of a manually-blocked device
1007
- * description: |
1008
- * Hub-node only. Authenticates with the per-node
1009
- * `(x-xenon-access-key, x-xenon-token)` pair. Nodes call this to
1010
- * re-enter a device into the pool after a maintenance lock or a
1011
- * blocked state lifted on the node side. Returns 404 if the
1012
- * (udid, host) tuple is not in the registry.
1013
- * tags: [Hub-Node]
1014
- * security:
1015
- * - AccessKeyAuth: []
1016
- * TokenAuth: []
1017
- * requestBody:
1018
- * required: true
1019
- * content:
1020
- * application/json:
1021
- * schema:
1022
- * type: object
1023
- * required: [udid, host]
1024
- * properties:
1025
- * udid: { type: string }
1026
- * host: { type: string }
1027
- * responses:
1028
- * 200:
1029
- * description: Device unblocked
1030
- * content:
1031
- * application/json:
1032
- * schema: { $ref: '#/components/schemas/Success' }
1033
- * 404: { description: 'Device not found in registry' }
1034
- * 401: { description: 'Missing or invalid (accessKey, token) pair' }
1035
- * 403: { $ref: '#/components/responses/Forbidden' }
1036
- * 429: { $ref: '#/components/responses/RateLimited' }
1037
- */
1038
- // =============================================================================
1039
- // Grid — previously-undocumented routes
1040
- // =============================================================================
1041
- /**
1042
- * @swagger
1043
- * /api/device/tags:
1044
- * post:
1045
- * summary: Replace the tag list on a device
1046
- * description: Tags are user-defined free-form labels (e.g. `flaky`, `lab-row-3`). Replaces — does not append. Requires `devices` scope.
1047
- * tags: [Devices]
1048
- * requestBody:
1049
- * required: true
1050
- * content:
1051
- * application/json:
1052
- * schema:
1053
- * type: object
1054
- * required: [udid, host, tags]
1055
- * properties:
1056
- * udid: { type: string }
1057
- * host: { type: string }
1058
- * tags:
1059
- * type: array
1060
- * items: { type: string }
1061
- * responses:
1062
- * 200:
1063
- * description: Tags updated
1064
- * content:
1065
- * application/json:
1066
- * schema: { $ref: '#/components/schemas/Success' }
1067
- * 400: { description: 'Missing udid, host, or tags array' }
1068
- * 401: { $ref: '#/components/responses/Unauthorized' }
1069
- * 403: { $ref: '#/components/responses/Forbidden' }
1070
- * 429: { $ref: '#/components/responses/RateLimited' }
1071
- */
1072
- /**
1073
- * @swagger
1074
- * /api/device/{udid}/team:
1075
- * put:
1076
- * summary: Assign (or clear) a device's team ownership
1077
- * description: |
1078
- * Pass `teamId: null` to unassign the device (it returns to the shared
1079
- * pool, visible to all keys). Pass a string id to bind it. The team must
1080
- * already exist. Requires `admin` scope.
1081
- * tags: [Devices]
1082
- * parameters:
1083
- * - in: path
1084
- * name: udid
1085
- * required: true
1086
- * schema: { type: string }
1087
- * requestBody:
1088
- * required: true
1089
- * content:
1090
- * application/json:
1091
- * schema:
1092
- * type: object
1093
- * required: [teamId]
1094
- * properties:
1095
- * teamId: { type: string, nullable: true }
1096
- * responses:
1097
- * 200:
1098
- * description: Team ownership updated
1099
- * content:
1100
- * application/json:
1101
- * schema:
1102
- * type: object
1103
- * properties:
1104
- * ok: { type: boolean }
1105
- * updated: { type: integer }
1106
- * 404: { description: 'Team or device not found' }
1107
- * 401: { $ref: '#/components/responses/Unauthorized' }
1108
- * 403: { $ref: '#/components/responses/Forbidden' }
1109
- * 429: { $ref: '#/components/responses/RateLimited' }
1110
- */
1111
- /**
1112
- * @swagger
1113
- * /api/queue/status/{capability_id}:
1114
- * get:
1115
- * summary: Status of a single queued session request
1116
- * description: |
1117
- * Returns the queue position and ETA for a pending session keyed by the
1118
- * capability id you submitted. 404 if the request has already been
1119
- * allocated or expired, or is one the caller can't see in
1120
- * `GET /api/queue`: the same answer as for an unknown id.
1121
- * tags: [Grid]
1122
- * parameters:
1123
- * - in: path
1124
- * name: capability_id
1125
- * required: true
1126
- * schema: { type: string }
1127
- * responses:
1128
- * 200:
1129
- * description: Queue status
1130
- * content:
1131
- * application/json:
1132
- * schema: { type: object, additionalProperties: true }
1133
- * 404: { description: 'Pending session not found' }
1134
- * 401: { $ref: '#/components/responses/Unauthorized' }
1135
- * 429: { $ref: '#/components/responses/RateLimited' }
1136
- */
1137
- /**
1138
- * @swagger
1139
- * /api/node/{host}/status:
1140
- * get:
1141
- * summary: ADB / device status for a specific node
1142
- * description: |
1143
- * If `host` matches the node serving the request, the response is built
1144
- * locally. Otherwise the hub proxies the call to that node's
1145
- * `/api/node/status` endpoint and returns the result. Returns 404 if no
1146
- * devices are registered for the requested host.
1147
- * tags: [Grid]
1148
- * parameters:
1149
- * - in: path
1150
- * name: host
1151
- * required: true
1152
- * schema: { type: string }
1153
- * description: 'Node host (URL or hostname as registered with the hub)'
1154
- * responses:
1155
- * 200:
1156
- * description: Per-device status
1157
- * content:
1158
- * application/json:
1159
- * schema:
1160
- * type: array
1161
- * items:
1162
- * type: object
1163
- * properties:
1164
- * udid: { type: string }
1165
- * host: { type: string }
1166
- * state: { type: string }
1167
- * platform: { type: string, enum: [ios, android] }
1168
- * 404: { description: 'No devices registered for that host' }
1169
- * 401: { $ref: '#/components/responses/Unauthorized' }
1170
- * 429: { $ref: '#/components/responses/RateLimited' }
1171
- */
1172
- // =============================================================================
1173
- // Selector Health — previously-undocumented routes
1174
- // =============================================================================
1175
- /**
1176
- * @swagger
1177
- * /api/healing/events:
1178
- * get:
1179
- * summary: Recent healing events
1180
- * description: |
1181
- * Returns the most recent self-healing events along with a `todayCount`
1182
- * used by the dashboard "today" badge. Newest first; clamped to 1–200.
1183
- * tags: [Selector Health]
1184
- * parameters:
1185
- * - in: query
1186
- * name: limit
1187
- * schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
1188
- * responses:
1189
- * 200:
1190
- * description: Recent events
1191
- * content:
1192
- * application/json:
1193
- * schema:
1194
- * type: object
1195
- * properties:
1196
- * events:
1197
- * type: array
1198
- * items:
1199
- * type: object
1200
- * properties:
1201
- * id: { type: string }
1202
- * sessionId: { type: string }
1203
- * deviceUdid: { type: string }
1204
- * deviceName: { type: string }
1205
- * devicePlatform: { type: string, enum: [ios, android] }
1206
- * commandName: { type: string }
1207
- * originalSelector: { type: string }
1208
- * healedSelector: { type: string }
1209
- * confidence: { type: number }
1210
- * tier: { type: string }
1211
- * isSuccess: { type: boolean }
1212
- * createdAt: { type: string, format: date-time }
1213
- * todayCount: { type: integer }
1214
- * 401: { $ref: '#/components/responses/Unauthorized' }
1215
- * 429: { $ref: '#/components/responses/RateLimited' }
1216
- */
1217
- /**
1218
- * @swagger
1219
- * /api/healing/summary:
1220
- * get:
1221
- * summary: Selector-health KPI summary with prior-period comparison
1222
- * description: |
1223
- * Aggregates over `windowDays` (clamped 1–365, default 30) and emits a
1224
- * parallel "prior" object covering the equivalent immediately-preceding
1225
- * window — use the diff for week-over-week / month-over-month deltas.
1226
- * tags: [Selector Health]
1227
- * parameters:
1228
- * - in: query
1229
- * name: windowDays
1230
- * schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
1231
- * - in: query
1232
- * name: tz
1233
- * schema: { type: integer, minimum: -840, maximum: 840, default: 0 }
1234
- * description: "The caller's offset from UTC in minutes (east positive), so `trend` days are theirs"
1235
- * responses:
1236
- * 200:
1237
- * description: KPI summary
1238
- * content:
1239
- * application/json:
1240
- * schema:
1241
- * type: object
1242
- * properties:
1243
- * windowDays: { type: integer }
1244
- * current:
1245
- * type: object
1246
- * properties:
1247
- * totalHeals: { type: integer }
1248
- * distinctSelectors: { type: integer }
1249
- * sessionsTouched: { type: integer }
1250
- * byTier: { type: object, additionalProperties: { type: integer } }
1251
- * timeSpentMs: { type: integer, description: 'Total duration of the commands that needed healing' }
1252
- * prior:
1253
- * type: object
1254
- * description: 'Same shape as `current` for the immediately preceding window'
1255
- * resolvedCount: { type: integer }
1256
- * pendingCount: { type: integer }
1257
- * trend:
1258
- * type: array
1259
- * description: 'Heals per day of the period, in the caller''s time zone, days with none included'
1260
- * items:
1261
- * type: object
1262
- * properties:
1263
- * t: { type: integer, description: 'When the day began, epoch ms' }
1264
- * heals: { type: integer }
1265
- * aiHeals: { type: integer, description: 'Heals by Visual AI or an LLM' }
1266
- * 401: { $ref: '#/components/responses/Unauthorized' }
1267
- * 429: { $ref: '#/components/responses/RateLimited' }
1268
- */
1269
- /**
1270
- * @swagger
1271
- * /api/healing/selectors:
1272
- * get:
1273
- * summary: One tab of the Selector Health list
1274
- * description: |
1275
- * `fix` groups the period's heals by selector and leaves out selectors
1276
- * being verified, fixed or muted; the other tabs list the selectors with
1277
- * that status (`fixed`: verified within the period). Only selectors
1278
- * healed, at any time, in sessions the caller can see. `counts` follow
1279
- * the period, not the search or filters. A page past the end answers the
1280
- * last page.
1281
- * tags: [Selector Health]
1282
- * parameters:
1283
- * - { in: query, name: tab, schema: { type: string, enum: [fix, verifying, fixed, muted], default: fix } }
1284
- * - { in: query, name: days, schema: { type: integer, minimum: 1, maximum: 365, default: 30 } }
1285
- * - { in: query, name: q, schema: { type: string }, description: 'Text in the selector, or (fix) in its suggested fix' }
1286
- * - { in: query, name: platform, schema: { type: string }, description: 'fix only' }
1287
- * - { in: query, name: method, schema: { type: string }, description: 'Healing method, e.g. LLM; fix only' }
1288
- * - { in: query, name: sort, schema: { type: string, enum: [heals, recent, time], default: heals }, description: 'fix only' }
1289
- * - { in: query, name: page, schema: { type: integer, minimum: 1, default: 1 } }
1290
- * - { in: query, name: pageSize, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
1291
- * responses:
1292
- * 200: { description: '`{ tab, days, page, pageSize, total, counts, canAct, items }`' }
1293
- * 401: { $ref: '#/components/responses/Unauthorized' }
1294
- */
1295
- /**
1296
- * @swagger
1297
- * /api/healing/selectors/detail:
1298
- * get:
1299
- * summary: Everything the Selector Health side panel shows for one selector
1300
- * description: |
1301
- * Its heals in the period (suggested fixes with their share, methods and
1302
- * confidence; heals per day; platforms, builds and devices; the latest 20
1303
- * heals), its status, the latest 50 status changes with who made them,
1304
- * and whether the caller may act. A selector no session the caller can
1305
- * see has healed answers 404, exactly as an unknown one.
1306
- * tags: [Selector Health]
1307
- * parameters:
1308
- * - { in: query, name: selector, required: true, schema: { type: string } }
1309
- * - { in: query, name: strategy, schema: { type: string }, description: "Empty for heals recorded with no strategy" }
1310
- * - { in: query, name: days, schema: { type: integer, minimum: 1, maximum: 365, default: 30 } }
1311
- * - { in: query, name: tz, schema: { type: integer, minimum: -840, maximum: 840, default: 0 } }
1312
- * responses:
1313
- * 200: { description: 'The panel for one selector' }
1314
- * 400: { description: 'No selector given' }
1315
- * 404: { description: '`{ error: "not_found", message: "Selector not found" }`' }
1316
- * 401: { $ref: '#/components/responses/Unauthorized' }
1317
- */
1318
- /**
1319
- * @swagger
1320
- * /api/healing/digest/send:
1321
- * post:
1322
- * summary: On-demand selector-health digest webhook
1323
- * description: |
1324
- * Manually fires the digest that the scheduler normally sends on a fixed
1325
- * cadence. Useful for pre-release "is the selector landscape healthy
1326
- * enough to ship?" gates. Requires `admin` scope.
1327
- * tags: [Selector Health]
1328
- * requestBody:
1329
- * content:
1330
- * application/json:
1331
- * schema:
1332
- * type: object
1333
- * properties:
1334
- * windowDays: { type: integer }
1335
- * limit: { type: integer }
1336
- * minHealCount: { type: integer }
1337
- * responses:
1338
- * 200:
1339
- * description: Digest dispatched
1340
- * content:
1341
- * application/json:
1342
- * schema:
1343
- * type: object
1344
- * properties:
1345
- * sent: { type: integer, description: 'Number of webhook subscribers fired' }
1346
- * windowDays: { type: integer }
1347
- * hotspotsIncluded: { type: integer }
1348
- * 401: { $ref: '#/components/responses/Unauthorized' }
1349
- * 403: { $ref: '#/components/responses/Forbidden' }
1350
- * 429: { $ref: '#/components/responses/RateLimited' }
1351
- */
1352
- // =============================================================================
1353
- // Control — previously-undocumented routes
1354
- // =============================================================================
1355
- /**
1356
- * @swagger
1357
- * /api/control/{udid}/logs:
1358
- * get:
1359
- * summary: Pull recent device logs
1360
- * description: |
1361
- * Android: `adb logcat -d` style snapshot. iOS: recent syslog buffer.
1362
- * Returns plain text wrapped in a JSON `logs` field.
1363
- * tags: [Control]
1364
- * parameters:
1365
- * - in: path
1366
- * name: udid
1367
- * required: true
1368
- * schema: { type: string }
1369
- * responses:
1370
- * 200:
1371
- * description: Device logs
1372
- * content:
1373
- * application/json:
1374
- * schema:
1375
- * type: object
1376
- * properties:
1377
- * logs: { type: string }
1378
- * 404: { description: 'Device not found' }
1379
- * 401: { $ref: '#/components/responses/Unauthorized' }
1380
- * 429: { $ref: '#/components/responses/RateLimited' }
1381
- */
1382
- // =============================================================================
1383
- // Bug Report — one-click bundling of session artifacts
1384
- // =============================================================================
1385
- /**
1386
- * @swagger
1387
- * /api/sessions/{sessionId}/bug-report:
1388
- * post:
1389
- * summary: Generate a bug-report bundle for a session
1390
- * description: |
1391
- * Streams a zip archive containing the session's video, logs, HAR, AI
1392
- * summary, and a manifest.json. In `slice` mode the video is trimmed to
1393
- * the last `windowSec` seconds (default 60). In `full` mode the entire
1394
- * session is included verbatim.
1395
- * tags: [Sessions]
1396
- * parameters:
1397
- * - in: path
1398
- * name: sessionId
1399
- * required: true
1400
- * schema: { type: string }
1401
- * - in: query
1402
- * name: mode
1403
- * required: true
1404
- * schema: { type: string, enum: [slice, full] }
1405
- * - in: query
1406
- * name: windowSec
1407
- * required: false
1408
- * schema: { type: integer, minimum: 5, maximum: 600, default: 60 }
1409
- * description: Slice window length in seconds. Ignored when mode=full.
1410
- * responses:
1411
- * 200:
1412
- * description: zip archive
1413
- * content:
1414
- * application/zip:
1415
- * schema: { type: string, format: binary }
1416
- * 400: { $ref: '#/components/responses/BadRequest' }
1417
- * 401: { $ref: '#/components/responses/Unauthorized' }
1418
- * 403: { $ref: '#/components/responses/Forbidden' }
1419
- * 404: { $ref: '#/components/responses/NotFound' }
1420
- * 429: { $ref: '#/components/responses/RateLimited' }
1421
- */
1422
- /**
1423
- * @swagger
1424
- * /api/recordings:
1425
- * post:
1426
- * summary: Start a multi-device recording group
1427
- * description: Start free-form recording on one or more devices. Atomic — if any UDID is busy or the concurrency cap is exceeded, nothing is started.
1428
- * tags: [Recordings]
1429
- * requestBody:
1430
- * required: true
1431
- * content:
1432
- * application/json:
1433
- * schema:
1434
- * type: object
1435
- * required: [udids]
1436
- * properties:
1437
- * udids:
1438
- * type: array
1439
- * items: { type: string }
1440
- * description: Device UDIDs to record
1441
- * sessionId:
1442
- * type: string
1443
- * description: Optional session ID to associate
1444
- * note:
1445
- * type: string
1446
- * description: Optional label for the recording group
1447
- * responses:
1448
- * 202:
1449
- * description: Recording started
1450
- * content:
1451
- * application/json:
1452
- * schema:
1453
- * type: object
1454
- * properties:
1455
- * groupId: { type: string }
1456
- * recordings:
1457
- * type: array
1458
- * items:
1459
- * type: object
1460
- * properties:
1461
- * id: { type: string }
1462
- * udid: { type: string }
1463
- * status: { type: string }
1464
- * startedAt: { type: string, format: date-time }
1465
- * 409:
1466
- * description: Device busy or concurrency cap reached
1467
- * content:
1468
- * application/json:
1469
- * schema:
1470
- * type: object
1471
- * properties:
1472
- * error: { type: string, enum: [device_busy, concurrency_cap] }
1473
- * busyDevices:
1474
- * type: array
1475
- * items:
1476
- * type: object
1477
- * properties:
1478
- * udid: { type: string }
1479
- * reason: { type: string }
1480
- * sessionId: { type: string }
1481
- * blockId: { type: string }
1482
- * limit: { type: integer }
1483
- * active: { type: integer }
1484
- * message: { type: string }
1485
- * 401: { $ref: '#/components/responses/Unauthorized' }
1486
- * 429: { $ref: '#/components/responses/RateLimited' }
1487
- */
1488
- /**
1489
- * @swagger
1490
- * /api/recordings/{groupId}/add-device:
1491
- * post:
1492
- * summary: Add a device to a running recording group
1493
- * description: Add a single device to an existing, active recording group. Atomic — if the device is busy or the cap is exceeded, nothing is started.
1494
- * tags: [Recordings]
1495
- * parameters:
1496
- * - in: path
1497
- * name: groupId
1498
- * required: true
1499
- * schema: { type: string }
1500
- * description: Recording group ID
1501
- * requestBody:
1502
- * required: true
1503
- * content:
1504
- * application/json:
1505
- * schema:
1506
- * type: object
1507
- * required: [udid]
1508
- * properties:
1509
- * udid:
1510
- * type: string
1511
- * description: Device UDID to add
1512
- * responses:
1513
- * 201:
1514
- * description: Device added to recording group
1515
- * content:
1516
- * application/json:
1517
- * schema:
1518
- * type: object
1519
- * properties:
1520
- * recording:
1521
- * type: object
1522
- * properties:
1523
- * id: { type: string }
1524
- * udid: { type: string }
1525
- * status: { type: string }
1526
- * 409:
1527
- * description: Device busy or concurrency cap reached
1528
- * 401: { $ref: '#/components/responses/Unauthorized' }
1529
- * 429: { $ref: '#/components/responses/RateLimited' }
1530
- */
1531
- /**
1532
- * @swagger
1533
- * /api/recordings/{groupId}/stop:
1534
- * post:
1535
- * summary: Stop all recordings in a group
1536
- * description: Stop all active recordings in the specified group. Each recording is finalized independently.
1537
- * tags: [Recordings]
1538
- * parameters:
1539
- * - in: path
1540
- * name: groupId
1541
- * required: true
1542
- * schema: { type: string }
1543
- * responses:
1544
- * 200:
1545
- * description: Recordings stopped
1546
- * content:
1547
- * application/json:
1548
- * schema:
1549
- * type: object
1550
- * properties:
1551
- * groupId: { type: string }
1552
- * recordings:
1553
- * type: array
1554
- * items:
1555
- * type: object
1556
- * properties:
1557
- * id: { type: string }
1558
- * udid: { type: string }
1559
- * status: { type: string }
1560
- * durationMs: { type: integer }
1561
- * sizeBytes: { type: integer }
1562
- * 401: { $ref: '#/components/responses/Unauthorized' }
1563
- * 429: { $ref: '#/components/responses/RateLimited' }
1564
- */
1565
- /**
1566
- * @swagger
1567
- * /api/recordings/{groupId}/bookmark:
1568
- * post:
1569
- * summary: Add a bookmark to a recording
1570
- * description: Drop a time-stamped bookmark on a recording, optionally with a note.
1571
- * tags: [Recordings]
1572
- * parameters:
1573
- * - in: path
1574
- * name: groupId
1575
- * required: true
1576
- * schema: { type: string }
1577
- * requestBody:
1578
- * required: true
1579
- * content:
1580
- * application/json:
1581
- * schema:
1582
- * type: object
1583
- * required: [recordingId, timecodeMs, label]
1584
- * properties:
1585
- * recordingId: { type: string }
1586
- * timecodeMs: { type: integer }
1587
- * label: { type: string }
1588
- * note: { type: string }
1589
- * responses:
1590
- * 201:
1591
- * description: Bookmark created
1592
- * 401: { $ref: '#/components/responses/Unauthorized' }
1593
- * 429: { $ref: '#/components/responses/RateLimited' }
1594
- */
1595
- /**
1596
- * @swagger
1597
- * /api/recordings/{groupId}/annotation:
1598
- * post:
1599
- * summary: Add an annotation to a recording
1600
- * description: Persist a normalized annotation (shape, geometry, color) on a recording frame.
1601
- * tags: [Recordings]
1602
- * parameters:
1603
- * - in: path
1604
- * name: groupId
1605
- * required: true
1606
- * schema: { type: string }
1607
- * requestBody:
1608
- * required: true
1609
- * content:
1610
- * application/json:
1611
- * schema:
1612
- * type: object
1613
- * required: [recordingId, timecodeMs, shape, geometry, color]
1614
- * properties:
1615
- * recordingId: { type: string }
1616
- * timecodeMs: { type: integer }
1617
- * shape: { type: string, enum: [RECT, CIRCLE, ARROW, TEXT, FREEHAND] }
1618
- * geometry: { type: string, description: JSON string with normalized coordinates }
1619
- * color: { type: string }
1620
- * text: { type: string }
1621
- * author: { type: string }
1622
- * responses:
1623
- * 201:
1624
- * description: Annotation created
1625
- * 401: { $ref: '#/components/responses/Unauthorized' }
1626
- * 429: { $ref: '#/components/responses/RateLimited' }
1627
- */
1628
- /**
1629
- * @swagger
1630
- * /api/recordings/{groupId}:
1631
- * get:
1632
- * summary: Get recording group details
1633
- * description: Retrieve all recordings, bookmarks, and annotations for a recording group.
1634
- * tags: [Recordings]
1635
- * parameters:
1636
- * - in: path
1637
- * name: groupId
1638
- * required: true
1639
- * schema: { type: string }
1640
- * responses:
1641
- * 200:
1642
- * description: Recording group details
1643
- * content:
1644
- * application/json:
1645
- * schema:
1646
- * type: object
1647
- * properties:
1648
- * groupId: { type: string }
1649
- * recordings: { type: array }
1650
- * 401: { $ref: '#/components/responses/Unauthorized' }
1651
- * 429: { $ref: '#/components/responses/RateLimited' }
1652
- */
1653
- /**
1654
- * @swagger
1655
- * /api/recordings/{groupId}/bundle.zip:
1656
- * get:
1657
- * summary: Download proof bundle
1658
- * description: Download a self-contained zip containing videos, bookmarks, annotations, and device metadata.
1659
- * tags: [Recordings]
1660
- * parameters:
1661
- * - in: path
1662
- * name: groupId
1663
- * required: true
1664
- * schema: { type: string }
1665
- * responses:
1666
- * 200:
1667
- * description: Zip archive stream
1668
- * content:
1669
- * application/zip:
1670
- * schema: { type: string, format: binary }
1671
- * 401: { $ref: '#/components/responses/Unauthorized' }
1672
- * 429: { $ref: '#/components/responses/RateLimited' }
1673
- */
1674
- /**
1675
- * @swagger
1676
- * /api/recordings/{groupId}/exports/annotated.mp4:
1677
- * get:
1678
- * summary: Download annotated MP4
1679
- * description: Generate and stream an MP4 with annotations burned into the video pixels. Lazy — only rendered on demand.
1680
- * tags: [Recordings]
1681
- * parameters:
1682
- * - in: path
1683
- * name: groupId
1684
- * required: true
1685
- * schema: { type: string }
1686
- * - in: query
1687
- * name: recordingId
1688
- * required: true
1689
- * schema: { type: string }
1690
- * description: The specific recording to render
1691
- * responses:
1692
- * 200:
1693
- * description: Annotated MP4 stream
1694
- * content:
1695
- * video/mp4:
1696
- * schema: { type: string, format: binary }
1697
- * 400:
1698
- * description: Missing recordingId query param
1699
- * 401: { $ref: '#/components/responses/Unauthorized' }
1700
- * 429: { $ref: '#/components/responses/RateLimited' }
1701
- */