@myronsi/messenger-api 2.1.0-alpha.12 → 2.1.0-alpha.14

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/dist/openapi.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: Messenger API
4
- version: 2.1.0-alpha.12
4
+ version: 2.1.0-alpha.14
5
5
  summary: REST contract of the Messenger backend (v2).
6
6
  description: |
7
7
  Contract for the Go backend. The WebSocket protocol is described in `websocket/` and
@@ -223,7 +223,7 @@ paths:
223
223
  application/json:
224
224
  schema: { $ref: "#/components/schemas/RegisterResponse" }
225
225
  "400": { $ref: "#/components/responses/BadRequest" }
226
- "401": { $ref: "#/components/responses/Unauthorized" }
226
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
227
227
  "403": { $ref: "#/components/responses/Forbidden" }
228
228
  "409": { $ref: "#/components/responses/Conflict" }
229
229
  "422": { $ref: "#/components/responses/ValidationFailed" }
@@ -266,7 +266,7 @@ paths:
266
266
  application/json:
267
267
  schema: { $ref: "#/components/schemas/RecoveryKeyResult" }
268
268
  "400": { $ref: "#/components/responses/BadRequest" }
269
- "401": { $ref: "#/components/responses/Unauthorized" }
269
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
270
270
  "403": { $ref: "#/components/responses/Forbidden" }
271
271
  "409": { $ref: "#/components/responses/Conflict" }
272
272
  "422": { $ref: "#/components/responses/ValidationFailed" }
@@ -293,56 +293,6 @@ paths:
293
293
  "422": { $ref: "#/components/responses/ValidationFailed" }
294
294
  "426": { $ref: "#/components/responses/ClientOutdated" }
295
295
  "429": { $ref: "#/components/responses/TooManyRequests" }
296
- /auth/recover:
297
- parameters:
298
- - $ref: "#/components/parameters/ClientVersion"
299
- - $ref: "#/components/parameters/ClientApiVersion"
300
- post:
301
- tags: [auth]
302
- operationId: startRecovery
303
- summary: Start account recovery (temporary)
304
- description: Temporary port of the Python recovery flow; it is redesigned in release 1.1.
305
- x-temporary: true
306
- security: []
307
- requestBody:
308
- required: true
309
- content:
310
- application/json:
311
- schema: { $ref: "#/components/schemas/RecoveryRequest" }
312
- responses:
313
- "426": { $ref: "#/components/responses/ClientOutdated" }
314
- "200":
315
- description: Recovery token
316
- content:
317
- application/json:
318
- schema: { $ref: "#/components/schemas/RecoveryResponse" }
319
- "400": { $ref: "#/components/responses/BadRequest" }
320
- "404": { $ref: "#/components/responses/NotFound" }
321
- "429": { $ref: "#/components/responses/TooManyRequests" }
322
- /auth/reset-password:
323
- parameters:
324
- - $ref: "#/components/parameters/ClientVersion"
325
- - $ref: "#/components/parameters/ClientApiVersion"
326
- post:
327
- tags: [auth]
328
- operationId: resetPassword
329
- summary: Set a new password with a recovery token (temporary)
330
- x-temporary: true
331
- security: []
332
- requestBody:
333
- required: true
334
- content:
335
- application/json:
336
- schema: { $ref: "#/components/schemas/ResetPasswordRequest" }
337
- responses:
338
- "426": { $ref: "#/components/responses/ClientOutdated" }
339
- "204":
340
- description: Password changed; all sessions are revoked
341
- "400": { $ref: "#/components/responses/BadRequest" }
342
- "401": { $ref: "#/components/responses/Unauthorized" }
343
- "422": { $ref: "#/components/responses/ValidationFailed" }
344
- "429": { $ref: "#/components/responses/TooManyRequests" }
345
-
346
296
  /ws/ticket:
347
297
  parameters:
348
298
  - $ref: "#/components/parameters/ClientVersion"
@@ -637,7 +587,7 @@ paths:
637
587
  tags: [me]
638
588
  operationId: disableTwoFactor
639
589
  summary: Turn two-factor off
640
- description: "A wrong password or code answers `403` (`invalid_credentials` or `invalid_two_factor_code`); `409` when two-factor is off."
590
+ description: "A wrong password or code answers `403` (`invalid_credentials` or `invalid_two_factor_code`); a correct password without `code` answers `401` with `code: two_factor_required`; `409` when two-factor is off."
641
591
  requestBody:
642
592
  required: true
643
593
  content:
@@ -648,7 +598,7 @@ paths:
648
598
  "204":
649
599
  description: Two-factor authentication disabled
650
600
  "400": { $ref: "#/components/responses/BadRequest" }
651
- "401": { $ref: "#/components/responses/Unauthorized" }
601
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
652
602
  "403": { $ref: "#/components/responses/Forbidden" }
653
603
  "409": { $ref: "#/components/responses/Conflict" }
654
604
  "429": { $ref: "#/components/responses/TooManyRequests" }
@@ -2120,34 +2070,1364 @@ paths:
2120
2070
  "409":
2121
2071
  $ref: "#/components/responses/Conflict"
2122
2072
 
2123
- components:
2124
- securitySchemes:
2125
- bearerAuth:
2126
- type: http
2127
- scheme: bearer
2128
- bearerFormat: JWT
2129
- description: Short-lived access token from login or refresh.
2130
- cookieAuth:
2131
- type: apiKey
2132
- in: cookie
2133
- name: refresh_token
2134
- description: HttpOnly refresh cookie set by login; scoped to `/api/v2/auth`.
2073
+ /auth/recovery-requests:
2074
+ parameters:
2075
+ - &a1
2076
+ $ref: "#/components/parameters/ClientVersion"
2077
+ - &a2
2078
+ $ref: "#/components/parameters/ClientApiVersion"
2079
+ post:
2080
+ tags:
2081
+ - auth
2082
+ operationId: startDeviceRecovery
2083
+ summary: Request approval from a signed-in device
2084
+ security: []
2085
+ requestBody:
2086
+ required: true
2087
+ content:
2088
+ application/json:
2089
+ schema:
2090
+ $ref: "#/components/schemas/DeviceRecoveryStart"
2091
+ responses:
2092
+ "200":
2093
+ description: Success
2094
+ content:
2095
+ application/json:
2096
+ schema:
2097
+ $ref: "#/components/schemas/DeviceRecoveryResult"
2098
+ "400":
2099
+ $ref: "#/components/responses/BadRequest"
2100
+ "401":
2101
+ $ref: "#/components/responses/Unauthorized"
2102
+ "403":
2103
+ $ref: "#/components/responses/Forbidden"
2104
+ "409":
2105
+ $ref: "#/components/responses/Conflict"
2106
+ "422":
2107
+ $ref: "#/components/responses/ValidationFailed"
2108
+ "426":
2109
+ $ref: "#/components/responses/ClientOutdated"
2110
+ "429":
2111
+ $ref: "#/components/responses/TooManyRequests"
2112
+ /auth/recovery-requests/{requestId}:
2113
+ parameters:
2114
+ - *a1
2115
+ - *a2
2116
+ - &a3
2117
+ name: requestId
2118
+ in: path
2119
+ required: true
2120
+ schema:
2121
+ type: string
2122
+ minLength: 32
2123
+ maxLength: 128
2124
+ pattern: ^[A-Za-z0-9_-]+$
2125
+ description: Secret one-time recovery request capability. Never log or persist
2126
+ it in browser storage.
2127
+ get:
2128
+ tags:
2129
+ - auth
2130
+ operationId: deviceRecoveryStatus
2131
+ summary: Poll the status of a secret recovery request
2132
+ security: []
2133
+ responses:
2134
+ "200":
2135
+ description: Success
2136
+ content:
2137
+ application/json:
2138
+ schema:
2139
+ $ref: "#/components/schemas/DeviceRecoveryStatus"
2140
+ "400":
2141
+ $ref: "#/components/responses/BadRequest"
2142
+ "401":
2143
+ $ref: "#/components/responses/Unauthorized"
2144
+ "403":
2145
+ $ref: "#/components/responses/Forbidden"
2146
+ "409":
2147
+ $ref: "#/components/responses/Conflict"
2148
+ "422":
2149
+ $ref: "#/components/responses/ValidationFailed"
2150
+ "426":
2151
+ $ref: "#/components/responses/ClientOutdated"
2152
+ "429":
2153
+ $ref: "#/components/responses/TooManyRequests"
2154
+ /auth/recovery-requests/{requestId}/approve:
2155
+ parameters:
2156
+ - *a1
2157
+ - *a2
2158
+ - &a4
2159
+ name: requestId
2160
+ in: path
2161
+ required: true
2162
+ schema:
2163
+ type: string
2164
+ minLength: 32
2165
+ maxLength: 128
2166
+ pattern: ^[A-Za-z0-9_-]+$
2167
+ description: Approval handle from the recovery_request event. It is different from the
2168
+ requester's capability and cannot poll or complete the request.
2169
+ post:
2170
+ tags:
2171
+ - auth
2172
+ operationId: approveDeviceRecovery
2173
+ summary: Approve a recovery request using number matching, the password and the second factor
2174
+ requestBody:
2175
+ required: true
2176
+ content:
2177
+ application/json:
2178
+ schema:
2179
+ $ref: "#/components/schemas/DeviceRecoveryApproval"
2180
+ responses:
2181
+ "200":
2182
+ description: Success
2183
+ content:
2184
+ application/json:
2185
+ schema:
2186
+ $ref: "#/components/schemas/DeviceRecoveryStatus"
2187
+ "400":
2188
+ $ref: "#/components/responses/BadRequest"
2189
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
2190
+ "403":
2191
+ $ref: "#/components/responses/Forbidden"
2192
+ "404":
2193
+ $ref: "#/components/responses/NotFound"
2194
+ "409":
2195
+ $ref: "#/components/responses/Conflict"
2196
+ "422":
2197
+ $ref: "#/components/responses/ValidationFailed"
2198
+ "426":
2199
+ $ref: "#/components/responses/ClientOutdated"
2200
+ "429":
2201
+ $ref: "#/components/responses/TooManyRequests"
2202
+ /auth/recovery-requests/{requestId}/deny:
2203
+ parameters:
2204
+ - *a1
2205
+ - *a2
2206
+ - *a4
2207
+ post:
2208
+ tags:
2209
+ - auth
2210
+ operationId: denyDeviceRecovery
2211
+ summary: Deny an unexpected recovery request
2212
+ responses:
2213
+ "200":
2214
+ description: Success
2215
+ content:
2216
+ application/json:
2217
+ schema:
2218
+ $ref: "#/components/schemas/DeviceRecoveryStatus"
2219
+ "400":
2220
+ $ref: "#/components/responses/BadRequest"
2221
+ "401":
2222
+ $ref: "#/components/responses/Unauthorized"
2223
+ "403":
2224
+ $ref: "#/components/responses/Forbidden"
2225
+ "404":
2226
+ $ref: "#/components/responses/NotFound"
2227
+ "409":
2228
+ $ref: "#/components/responses/Conflict"
2229
+ "422":
2230
+ $ref: "#/components/responses/ValidationFailed"
2231
+ "426":
2232
+ $ref: "#/components/responses/ClientOutdated"
2233
+ "429":
2234
+ $ref: "#/components/responses/TooManyRequests"
2235
+ /auth/recovery-requests/{requestId}/complete:
2236
+ parameters:
2237
+ - *a1
2238
+ - *a2
2239
+ - *a3
2240
+ post:
2241
+ tags:
2242
+ - auth
2243
+ operationId: completeDeviceRecovery
2244
+ summary: Consume an approved request and reset the password
2245
+ security: []
2246
+ requestBody:
2247
+ required: true
2248
+ content:
2249
+ application/json:
2250
+ schema:
2251
+ $ref: "#/components/schemas/DeviceRecoveryComplete"
2252
+ responses:
2253
+ "200":
2254
+ description: Success
2255
+ content:
2256
+ application/json:
2257
+ schema:
2258
+ $ref: "#/components/schemas/RegisterResponse"
2259
+ "400":
2260
+ $ref: "#/components/responses/BadRequest"
2261
+ "401":
2262
+ $ref: "#/components/responses/Unauthorized"
2263
+ "403":
2264
+ $ref: "#/components/responses/Forbidden"
2265
+ "409":
2266
+ $ref: "#/components/responses/Conflict"
2267
+ "422":
2268
+ $ref: "#/components/responses/ValidationFailed"
2269
+ "426":
2270
+ $ref: "#/components/responses/ClientOutdated"
2271
+ "429":
2272
+ $ref: "#/components/responses/TooManyRequests"
2273
+ /auth/recovery-requests/{requestId}/cancel:
2274
+ parameters:
2275
+ - *a1
2276
+ - *a2
2277
+ - *a3
2278
+ post:
2279
+ tags:
2280
+ - auth
2281
+ operationId: cancelDeviceRecovery
2282
+ summary: Withdraw a pending recovery request from the requesting browser
2283
+ description: Ends a pending request so it no longer counts towards the limit of three active requests. It
2284
+ answers 204 for every capability, including unknown, decided and expired ones.
2285
+ security: []
2286
+ responses:
2287
+ "204":
2288
+ description: Cancelled or nothing to cancel
2289
+ "400":
2290
+ $ref: "#/components/responses/BadRequest"
2291
+ "426":
2292
+ $ref: "#/components/responses/ClientOutdated"
2293
+ "429":
2294
+ $ref: "#/components/responses/TooManyRequests"
2135
2295
 
2136
- headers:
2137
- RefreshCookie:
2138
- description: HttpOnly, Secure, SameSite refresh cookie scoped to `/api/v2/auth/refresh`, so the browser sends it only to the refresh endpoint. Login and refresh set it; logout clears it (expired cookie).
2139
- schema: { type: string }
2296
+ /auth/qr-login:
2297
+ parameters:
2298
+ - $ref: "#/components/parameters/ClientVersion"
2299
+ - $ref: "#/components/parameters/ClientApiVersion"
2300
+ post:
2301
+ tags: [auth]
2302
+ operationId: createQrLogin
2303
+ summary: Start a sign-in with a QR code
2304
+ description: |
2305
+ Called by a device that is not signed in. Show `url` as a QR code and poll
2306
+ `POST /auth/qr-login/{requestId}/poll` with `poll_token` until the status is final. The request expires
2307
+ after 2 minutes. Keep `poll_token` in memory only; it is the secret that receives the session.
2308
+ security: []
2309
+ responses:
2310
+ "201":
2311
+ description: Created
2312
+ content:
2313
+ application/json:
2314
+ schema: { $ref: "#/components/schemas/QrLoginStart" }
2315
+ example:
2316
+ request_id: 3q2-7wEAAAD9W8b0kZ3lYfHk6gF1n9cQmR2pVx4sTtU
2317
+ poll_token: example-poll-token
2318
+ url: https://messenger.example.com/qr/3q2-7wEAAAD9W8b0kZ3lYfHk6gF1n9cQmR2pVx4sTtU
2319
+ expires_at: "2026-11-06T12:02:00Z"
2320
+ "400": { $ref: "#/components/responses/BadRequest" }
2321
+ "426": { $ref: "#/components/responses/ClientOutdated" }
2322
+ "429": { $ref: "#/components/responses/TooManyRequests" }
2323
+ /auth/qr-login/{requestId}:
2324
+ parameters:
2325
+ - $ref: "#/components/parameters/ClientVersion"
2326
+ - $ref: "#/components/parameters/ClientApiVersion"
2327
+ - $ref: "#/components/parameters/QrLoginRequestId"
2328
+ get:
2329
+ tags: [auth]
2330
+ operationId: getQrLogin
2331
+ summary: Show who asks to sign in with a QR code
2332
+ description: |
2333
+ Called by a signed-in device after it opened the link of the QR code. The first call moves the request from
2334
+ `pending` to `scanned` and binds it to the caller: other accounts get `404` from then on. Show `device` and
2335
+ `location` and let the user confirm that the sign-in is their own before approving. Unknown, expired and
2336
+ used requests answer `404`.
2337
+ responses:
2338
+ "200":
2339
+ description: Success
2340
+ content:
2341
+ application/json:
2342
+ schema: { $ref: "#/components/schemas/QrLoginRequest" }
2343
+ example:
2344
+ request_id: 3q2-7wEAAAD9W8b0kZ3lYfHk6gF1n9cQmR2pVx4sTtU
2345
+ status: scanned
2346
+ device: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Firefox/140.0
2347
+ location: { city: Berlin, country_code: DE, country: Germany }
2348
+ created_at: "2026-11-06T12:00:00Z"
2349
+ expires_at: "2026-11-06T12:02:00Z"
2350
+ "400": { $ref: "#/components/responses/BadRequest" }
2351
+ "401": { $ref: "#/components/responses/Unauthorized" }
2352
+ "404": { $ref: "#/components/responses/NotFound" }
2353
+ "426": { $ref: "#/components/responses/ClientOutdated" }
2354
+ "429": { $ref: "#/components/responses/TooManyRequests" }
2355
+ /auth/qr-login/{requestId}/poll:
2356
+ parameters:
2357
+ - $ref: "#/components/parameters/ClientVersion"
2358
+ - $ref: "#/components/parameters/ClientApiVersion"
2359
+ - $ref: "#/components/parameters/QrLoginRequestId"
2360
+ post:
2361
+ tags: [auth]
2362
+ operationId: pollQrLogin
2363
+ summary: Poll a QR sign-in and receive the session once approved
2364
+ description: |
2365
+ Called by the device that created the request, about once per second (at most 120 polls per 2 minutes per
2366
+ client address). While the request is open the answer is `pending` or `scanned`. The first poll after
2367
+ approval signs the device in exactly like a successful `POST /auth/login`: it sets the refresh cookie and
2368
+ returns `status: approved` with the token fields. Later polls, unknown requests, a wrong `poll_token` and
2369
+ expired requests all answer `expired`.
2370
+ security: []
2371
+ requestBody:
2372
+ required: true
2373
+ content:
2374
+ application/json:
2375
+ schema: { $ref: "#/components/schemas/QrLoginPollRequest" }
2376
+ responses:
2377
+ "200":
2378
+ description: The current status, plus the session when it was just approved
2379
+ headers:
2380
+ Set-Cookie: { $ref: "#/components/headers/RefreshCookie" }
2381
+ content:
2382
+ application/json:
2383
+ schema: { $ref: "#/components/schemas/QrLoginPollResult" }
2384
+ examples:
2385
+ waiting:
2386
+ value: { status: scanned }
2387
+ approved:
2388
+ value:
2389
+ status: approved
2390
+ access_token: example-access-token
2391
+ token_type: Bearer
2392
+ expires_in: 900
2393
+ "400": { $ref: "#/components/responses/BadRequest" }
2394
+ "426": { $ref: "#/components/responses/ClientOutdated" }
2395
+ "429": { $ref: "#/components/responses/TooManyRequests" }
2396
+ /auth/qr-login/{requestId}/approve:
2397
+ parameters:
2398
+ - $ref: "#/components/parameters/ClientVersion"
2399
+ - $ref: "#/components/parameters/ClientApiVersion"
2400
+ - $ref: "#/components/parameters/QrLoginRequestId"
2401
+ post:
2402
+ tags: [auth]
2403
+ operationId: approveQrLogin
2404
+ summary: Sign the device of a QR code into this account
2405
+ description: |
2406
+ Single use. The request must be `pending` or `scanned`, not expired and not bound to another account
2407
+ (`404` otherwise; `409` when it was already approved or denied). The calling session must be active and
2408
+ used within `RECOVERY_APPROVAL_MAX_AGE` (`403 forbidden` otherwise). With two-factor on, send `code` (an
2409
+ authenticator code or an unused recovery code); without it the answer is `401 two_factor_required` and the
2410
+ request stays approvable. A wrong code answers `400 invalid_credentials`. The body may be omitted.
2411
+ requestBody:
2412
+ required: false
2413
+ content:
2414
+ application/json:
2415
+ schema: { $ref: "#/components/schemas/QrLoginApproval" }
2416
+ example: { code: "123456" }
2417
+ responses:
2418
+ "204": { description: Approved; the new device receives its session on its next poll }
2419
+ "400": { $ref: "#/components/responses/BadRequest" }
2420
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
2421
+ "403": { $ref: "#/components/responses/Forbidden" }
2422
+ "404": { $ref: "#/components/responses/NotFound" }
2423
+ "409": { $ref: "#/components/responses/Conflict" }
2424
+ "426": { $ref: "#/components/responses/ClientOutdated" }
2425
+ "429": { $ref: "#/components/responses/TooManyRequests" }
2426
+ /auth/qr-login/{requestId}/deny:
2427
+ parameters:
2428
+ - $ref: "#/components/parameters/ClientVersion"
2429
+ - $ref: "#/components/parameters/ClientApiVersion"
2430
+ - $ref: "#/components/parameters/QrLoginRequestId"
2431
+ post:
2432
+ tags: [auth]
2433
+ operationId: denyQrLogin
2434
+ summary: Refuse a QR sign-in
2435
+ description: The new device then polls `denied`. Same `404` and `409` rules as approving.
2436
+ responses:
2437
+ "204": { description: Denied }
2438
+ "400": { $ref: "#/components/responses/BadRequest" }
2439
+ "401": { $ref: "#/components/responses/Unauthorized" }
2440
+ "404": { $ref: "#/components/responses/NotFound" }
2441
+ "409": { $ref: "#/components/responses/Conflict" }
2442
+ "426": { $ref: "#/components/responses/ClientOutdated" }
2443
+ "429": { $ref: "#/components/responses/TooManyRequests" }
2140
2444
 
2141
- parameters:
2142
- Limit:
2143
- name: limit
2144
- in: query
2145
- description: Page size
2146
- schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
2147
- After:
2148
- name: after
2149
- in: query
2150
- description: Opaque `next_cursor` of the previous page
2445
+ /auth/passkeys/register/options:
2446
+ parameters: &a1
2447
+ - &a2
2448
+ $ref: "#/components/parameters/ClientVersion"
2449
+ - &a3
2450
+ $ref: "#/components/parameters/ClientApiVersion"
2451
+ post:
2452
+ tags:
2453
+ - auth
2454
+ operationId: passkeyRegistrationOptions
2455
+ summary: Begin passkey registration after password and second-factor verification
2456
+ requestBody:
2457
+ required: true
2458
+ content:
2459
+ application/json:
2460
+ schema:
2461
+ $ref: "#/components/schemas/PasskeyRegistration"
2462
+ responses:
2463
+ "200":
2464
+ description: Success
2465
+ content:
2466
+ application/json:
2467
+ schema:
2468
+ $ref: "#/components/schemas/PasskeyOptions"
2469
+ "400":
2470
+ description: Bad request
2471
+ content:
2472
+ application/problem+json:
2473
+ schema:
2474
+ $ref: "#/components/schemas/Problem"
2475
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
2476
+ "403":
2477
+ description: Forbidden
2478
+ content:
2479
+ application/problem+json:
2480
+ schema:
2481
+ $ref: "#/components/schemas/Problem"
2482
+ "404":
2483
+ description: Not found
2484
+ content:
2485
+ application/problem+json:
2486
+ schema:
2487
+ $ref: "#/components/schemas/Problem"
2488
+ "409":
2489
+ description: Conflict
2490
+ content:
2491
+ application/problem+json:
2492
+ schema:
2493
+ $ref: "#/components/schemas/Problem"
2494
+ "422":
2495
+ description: Invalid input
2496
+ content:
2497
+ application/problem+json:
2498
+ schema:
2499
+ $ref: "#/components/schemas/Problem"
2500
+ "426":
2501
+ description: Client outdated
2502
+ content:
2503
+ application/problem+json:
2504
+ schema:
2505
+ $ref: "#/components/schemas/Problem"
2506
+ "429":
2507
+ description: Rate limited
2508
+ content:
2509
+ application/problem+json:
2510
+ schema:
2511
+ $ref: "#/components/schemas/Problem"
2512
+ "503":
2513
+ description: Passkeys are not configured
2514
+ content:
2515
+ application/problem+json:
2516
+ schema:
2517
+ $ref: "#/components/schemas/Problem"
2518
+ /auth/passkeys/register/verify:
2519
+ parameters: *a1
2520
+ post:
2521
+ tags:
2522
+ - auth
2523
+ operationId: verifyPasskeyRegistration
2524
+ summary: Verify and store a passkey
2525
+ requestBody:
2526
+ required: true
2527
+ content:
2528
+ application/json:
2529
+ schema:
2530
+ $ref: "#/components/schemas/PasskeyVerification"
2531
+ responses:
2532
+ "204":
2533
+ description: Success
2534
+ "400":
2535
+ description: Bad request
2536
+ content:
2537
+ application/problem+json:
2538
+ schema:
2539
+ $ref: "#/components/schemas/Problem"
2540
+ "401":
2541
+ description: Unauthorized
2542
+ content:
2543
+ application/problem+json:
2544
+ schema:
2545
+ $ref: "#/components/schemas/Problem"
2546
+ "403":
2547
+ description: Forbidden
2548
+ content:
2549
+ application/problem+json:
2550
+ schema:
2551
+ $ref: "#/components/schemas/Problem"
2552
+ "404":
2553
+ description: Not found
2554
+ content:
2555
+ application/problem+json:
2556
+ schema:
2557
+ $ref: "#/components/schemas/Problem"
2558
+ "409":
2559
+ description: Conflict
2560
+ content:
2561
+ application/problem+json:
2562
+ schema:
2563
+ $ref: "#/components/schemas/Problem"
2564
+ "422":
2565
+ description: Invalid input
2566
+ content:
2567
+ application/problem+json:
2568
+ schema:
2569
+ $ref: "#/components/schemas/Problem"
2570
+ "426":
2571
+ description: Client outdated
2572
+ content:
2573
+ application/problem+json:
2574
+ schema:
2575
+ $ref: "#/components/schemas/Problem"
2576
+ "429":
2577
+ description: Rate limited
2578
+ content:
2579
+ application/problem+json:
2580
+ schema:
2581
+ $ref: "#/components/schemas/Problem"
2582
+ "503":
2583
+ description: Passkeys are not configured
2584
+ content:
2585
+ application/problem+json:
2586
+ schema:
2587
+ $ref: "#/components/schemas/Problem"
2588
+ /auth/passkeys:
2589
+ parameters: *a1
2590
+ get:
2591
+ tags:
2592
+ - auth
2593
+ operationId: listPasskeys
2594
+ summary: List the account passkeys
2595
+ responses:
2596
+ "200":
2597
+ description: Success
2598
+ content:
2599
+ application/json:
2600
+ schema:
2601
+ $ref: "#/components/schemas/PasskeyPage"
2602
+ "400":
2603
+ description: Bad request
2604
+ content:
2605
+ application/problem+json:
2606
+ schema:
2607
+ $ref: "#/components/schemas/Problem"
2608
+ "401":
2609
+ description: Unauthorized
2610
+ content:
2611
+ application/problem+json:
2612
+ schema:
2613
+ $ref: "#/components/schemas/Problem"
2614
+ "403":
2615
+ description: Forbidden
2616
+ content:
2617
+ application/problem+json:
2618
+ schema:
2619
+ $ref: "#/components/schemas/Problem"
2620
+ "404":
2621
+ description: Not found
2622
+ content:
2623
+ application/problem+json:
2624
+ schema:
2625
+ $ref: "#/components/schemas/Problem"
2626
+ "409":
2627
+ description: Conflict
2628
+ content:
2629
+ application/problem+json:
2630
+ schema:
2631
+ $ref: "#/components/schemas/Problem"
2632
+ "422":
2633
+ description: Invalid input
2634
+ content:
2635
+ application/problem+json:
2636
+ schema:
2637
+ $ref: "#/components/schemas/Problem"
2638
+ "426":
2639
+ description: Client outdated
2640
+ content:
2641
+ application/problem+json:
2642
+ schema:
2643
+ $ref: "#/components/schemas/Problem"
2644
+ "429":
2645
+ description: Rate limited
2646
+ content:
2647
+ application/problem+json:
2648
+ schema:
2649
+ $ref: "#/components/schemas/Problem"
2650
+ "503":
2651
+ description: Passkeys are not configured
2652
+ content:
2653
+ application/problem+json:
2654
+ schema:
2655
+ $ref: "#/components/schemas/Problem"
2656
+ /auth/passkeys/{passkeyId}:
2657
+ parameters:
2658
+ - *a2
2659
+ - *a3
2660
+ - name: passkeyId
2661
+ in: path
2662
+ required: true
2663
+ schema:
2664
+ type: string
2665
+ minLength: 1
2666
+ maxLength: 2048
2667
+ pattern: ^[A-Za-z0-9_-]+$
2668
+ patch:
2669
+ tags:
2670
+ - auth
2671
+ operationId: renamePasskey
2672
+ summary: Rename a passkey
2673
+ requestBody:
2674
+ required: true
2675
+ content:
2676
+ application/json:
2677
+ schema:
2678
+ $ref: "#/components/schemas/PasskeyRename"
2679
+ responses:
2680
+ "204":
2681
+ description: Success
2682
+ "400":
2683
+ description: Bad request
2684
+ content:
2685
+ application/problem+json:
2686
+ schema:
2687
+ $ref: "#/components/schemas/Problem"
2688
+ "401":
2689
+ description: Unauthorized
2690
+ content:
2691
+ application/problem+json:
2692
+ schema:
2693
+ $ref: "#/components/schemas/Problem"
2694
+ "403":
2695
+ description: Forbidden
2696
+ content:
2697
+ application/problem+json:
2698
+ schema:
2699
+ $ref: "#/components/schemas/Problem"
2700
+ "404":
2701
+ description: Not found
2702
+ content:
2703
+ application/problem+json:
2704
+ schema:
2705
+ $ref: "#/components/schemas/Problem"
2706
+ "409":
2707
+ description: Conflict
2708
+ content:
2709
+ application/problem+json:
2710
+ schema:
2711
+ $ref: "#/components/schemas/Problem"
2712
+ "422":
2713
+ description: Invalid input
2714
+ content:
2715
+ application/problem+json:
2716
+ schema:
2717
+ $ref: "#/components/schemas/Problem"
2718
+ "426":
2719
+ description: Client outdated
2720
+ content:
2721
+ application/problem+json:
2722
+ schema:
2723
+ $ref: "#/components/schemas/Problem"
2724
+ "429":
2725
+ description: Rate limited
2726
+ content:
2727
+ application/problem+json:
2728
+ schema:
2729
+ $ref: "#/components/schemas/Problem"
2730
+ "503":
2731
+ description: Passkeys are not configured
2732
+ content:
2733
+ application/problem+json:
2734
+ schema:
2735
+ $ref: "#/components/schemas/Problem"
2736
+ delete:
2737
+ tags:
2738
+ - auth
2739
+ operationId: removePasskey
2740
+ summary: Remove a passkey; the last credential requires the password
2741
+ requestBody:
2742
+ required: true
2743
+ content:
2744
+ application/json:
2745
+ schema:
2746
+ $ref: "#/components/schemas/PasskeyRemoval"
2747
+ responses:
2748
+ "204":
2749
+ description: Success
2750
+ "400":
2751
+ description: Bad request
2752
+ content:
2753
+ application/problem+json:
2754
+ schema:
2755
+ $ref: "#/components/schemas/Problem"
2756
+ "401":
2757
+ description: Unauthorized
2758
+ content:
2759
+ application/problem+json:
2760
+ schema:
2761
+ $ref: "#/components/schemas/Problem"
2762
+ "403":
2763
+ description: Forbidden
2764
+ content:
2765
+ application/problem+json:
2766
+ schema:
2767
+ $ref: "#/components/schemas/Problem"
2768
+ "404":
2769
+ description: Not found
2770
+ content:
2771
+ application/problem+json:
2772
+ schema:
2773
+ $ref: "#/components/schemas/Problem"
2774
+ "409":
2775
+ description: Conflict
2776
+ content:
2777
+ application/problem+json:
2778
+ schema:
2779
+ $ref: "#/components/schemas/Problem"
2780
+ "422":
2781
+ description: Invalid input
2782
+ content:
2783
+ application/problem+json:
2784
+ schema:
2785
+ $ref: "#/components/schemas/Problem"
2786
+ "426":
2787
+ description: Client outdated
2788
+ content:
2789
+ application/problem+json:
2790
+ schema:
2791
+ $ref: "#/components/schemas/Problem"
2792
+ "429":
2793
+ description: Rate limited
2794
+ content:
2795
+ application/problem+json:
2796
+ schema:
2797
+ $ref: "#/components/schemas/Problem"
2798
+ "503":
2799
+ description: Passkeys are not configured
2800
+ content:
2801
+ application/problem+json:
2802
+ schema:
2803
+ $ref: "#/components/schemas/Problem"
2804
+ /auth/passkeys/login/options:
2805
+ parameters: *a1
2806
+ post:
2807
+ tags:
2808
+ - auth
2809
+ operationId: passkeyLoginOptions
2810
+ summary: Begin discoverable passkey sign-in
2811
+ security: []
2812
+ requestBody:
2813
+ required: true
2814
+ content:
2815
+ application/json:
2816
+ schema:
2817
+ $ref: "#/components/schemas/PasskeyLoginOptions"
2818
+ responses:
2819
+ "200":
2820
+ description: Success
2821
+ content:
2822
+ application/json:
2823
+ schema:
2824
+ $ref: "#/components/schemas/PasskeyOptions"
2825
+ "400":
2826
+ description: Bad request
2827
+ content:
2828
+ application/problem+json:
2829
+ schema:
2830
+ $ref: "#/components/schemas/Problem"
2831
+ "401":
2832
+ description: Unauthorized
2833
+ content:
2834
+ application/problem+json:
2835
+ schema:
2836
+ $ref: "#/components/schemas/Problem"
2837
+ "403":
2838
+ description: Forbidden
2839
+ content:
2840
+ application/problem+json:
2841
+ schema:
2842
+ $ref: "#/components/schemas/Problem"
2843
+ "404":
2844
+ description: Not found
2845
+ content:
2846
+ application/problem+json:
2847
+ schema:
2848
+ $ref: "#/components/schemas/Problem"
2849
+ "409":
2850
+ description: Conflict
2851
+ content:
2852
+ application/problem+json:
2853
+ schema:
2854
+ $ref: "#/components/schemas/Problem"
2855
+ "422":
2856
+ description: Invalid input
2857
+ content:
2858
+ application/problem+json:
2859
+ schema:
2860
+ $ref: "#/components/schemas/Problem"
2861
+ "426":
2862
+ description: Client outdated
2863
+ content:
2864
+ application/problem+json:
2865
+ schema:
2866
+ $ref: "#/components/schemas/Problem"
2867
+ "429":
2868
+ description: Rate limited
2869
+ content:
2870
+ application/problem+json:
2871
+ schema:
2872
+ $ref: "#/components/schemas/Problem"
2873
+ "503":
2874
+ description: Passkeys are not configured
2875
+ content:
2876
+ application/problem+json:
2877
+ schema:
2878
+ $ref: "#/components/schemas/Problem"
2879
+ /auth/passkeys/login/verify:
2880
+ parameters: *a1
2881
+ post:
2882
+ tags:
2883
+ - auth
2884
+ operationId: verifyPasskeyLogin
2885
+ summary: Verify a passkey and create a session
2886
+ security: []
2887
+ requestBody:
2888
+ required: true
2889
+ content:
2890
+ application/json:
2891
+ schema:
2892
+ $ref: "#/components/schemas/PasskeyVerification"
2893
+ responses:
2894
+ "200":
2895
+ description: Success
2896
+ content:
2897
+ application/json:
2898
+ schema:
2899
+ $ref: "#/components/schemas/TokenResponse"
2900
+ "400":
2901
+ description: Bad request
2902
+ content:
2903
+ application/problem+json:
2904
+ schema:
2905
+ $ref: "#/components/schemas/Problem"
2906
+ "401":
2907
+ description: Unauthorized
2908
+ content:
2909
+ application/problem+json:
2910
+ schema:
2911
+ $ref: "#/components/schemas/Problem"
2912
+ "403":
2913
+ description: Forbidden
2914
+ content:
2915
+ application/problem+json:
2916
+ schema:
2917
+ $ref: "#/components/schemas/Problem"
2918
+ "404":
2919
+ description: Not found
2920
+ content:
2921
+ application/problem+json:
2922
+ schema:
2923
+ $ref: "#/components/schemas/Problem"
2924
+ "409":
2925
+ description: Conflict
2926
+ content:
2927
+ application/problem+json:
2928
+ schema:
2929
+ $ref: "#/components/schemas/Problem"
2930
+ "422":
2931
+ description: Invalid input
2932
+ content:
2933
+ application/problem+json:
2934
+ schema:
2935
+ $ref: "#/components/schemas/Problem"
2936
+ "426":
2937
+ description: Client outdated
2938
+ content:
2939
+ application/problem+json:
2940
+ schema:
2941
+ $ref: "#/components/schemas/Problem"
2942
+ "429":
2943
+ description: Rate limited
2944
+ content:
2945
+ application/problem+json:
2946
+ schema:
2947
+ $ref: "#/components/schemas/Problem"
2948
+ "503":
2949
+ description: Passkeys are not configured
2950
+ content:
2951
+ application/problem+json:
2952
+ schema:
2953
+ $ref: "#/components/schemas/Problem"
2954
+
2955
+ /me/recovery-status:
2956
+ parameters: &a1
2957
+ - $ref: "#/components/parameters/ClientVersion"
2958
+ - $ref: "#/components/parameters/ClientApiVersion"
2959
+ get:
2960
+ tags:
2961
+ - auth
2962
+ operationId: getRecoveryOverview
2963
+ summary: List recovery methods and their current setup status
2964
+ responses:
2965
+ "200":
2966
+ description: Success
2967
+ content:
2968
+ application/json:
2969
+ schema:
2970
+ $ref: "#/components/schemas/RecoveryOverview"
2971
+ "400":
2972
+ description: Invalid recovery credentials
2973
+ content:
2974
+ application/problem+json:
2975
+ schema:
2976
+ $ref: "#/components/schemas/Problem"
2977
+ "401":
2978
+ description: Unauthorized
2979
+ content:
2980
+ application/problem+json:
2981
+ schema:
2982
+ $ref: "#/components/schemas/Problem"
2983
+ "403":
2984
+ description: Forbidden
2985
+ content:
2986
+ application/problem+json:
2987
+ schema:
2988
+ $ref: "#/components/schemas/Problem"
2989
+ "409":
2990
+ description: Security delay or conflicting state
2991
+ content:
2992
+ application/problem+json:
2993
+ schema:
2994
+ $ref: "#/components/schemas/Problem"
2995
+ "422":
2996
+ description: Invalid input
2997
+ content:
2998
+ application/problem+json:
2999
+ schema:
3000
+ $ref: "#/components/schemas/Problem"
3001
+ "426":
3002
+ description: Client outdated
3003
+ content:
3004
+ application/problem+json:
3005
+ schema:
3006
+ $ref: "#/components/schemas/Problem"
3007
+ "429":
3008
+ description: Rate limited
3009
+ content:
3010
+ application/problem+json:
3011
+ schema:
3012
+ $ref: "#/components/schemas/Problem"
3013
+ "503":
3014
+ description: Email recovery is not configured
3015
+ content:
3016
+ application/problem+json:
3017
+ schema:
3018
+ $ref: "#/components/schemas/Problem"
3019
+ /me/email:
3020
+ parameters: *a1
3021
+ get:
3022
+ tags:
3023
+ - auth
3024
+ operationId: getRecoveryEmail
3025
+ summary: Get private recovery email status
3026
+ responses:
3027
+ "200":
3028
+ description: Success
3029
+ content:
3030
+ application/json:
3031
+ schema:
3032
+ $ref: "#/components/schemas/RecoveryEmailStatus"
3033
+ "400":
3034
+ description: Invalid recovery credentials
3035
+ content:
3036
+ application/problem+json:
3037
+ schema:
3038
+ $ref: "#/components/schemas/Problem"
3039
+ "401":
3040
+ description: Unauthorized
3041
+ content:
3042
+ application/problem+json:
3043
+ schema:
3044
+ $ref: "#/components/schemas/Problem"
3045
+ "403":
3046
+ description: Forbidden
3047
+ content:
3048
+ application/problem+json:
3049
+ schema:
3050
+ $ref: "#/components/schemas/Problem"
3051
+ "409":
3052
+ description: Security delay or conflicting state
3053
+ content:
3054
+ application/problem+json:
3055
+ schema:
3056
+ $ref: "#/components/schemas/Problem"
3057
+ "422":
3058
+ description: Invalid input
3059
+ content:
3060
+ application/problem+json:
3061
+ schema:
3062
+ $ref: "#/components/schemas/Problem"
3063
+ "426":
3064
+ description: Client outdated
3065
+ content:
3066
+ application/problem+json:
3067
+ schema:
3068
+ $ref: "#/components/schemas/Problem"
3069
+ "429":
3070
+ description: Rate limited
3071
+ content:
3072
+ application/problem+json:
3073
+ schema:
3074
+ $ref: "#/components/schemas/Problem"
3075
+ "503":
3076
+ description: Email recovery is not configured
3077
+ content:
3078
+ application/problem+json:
3079
+ schema:
3080
+ $ref: "#/components/schemas/Problem"
3081
+ put:
3082
+ tags:
3083
+ - auth
3084
+ operationId: setRecoveryEmail
3085
+ summary: Set or resend a private recovery email for verification
3086
+ requestBody:
3087
+ required: true
3088
+ content:
3089
+ application/json:
3090
+ schema:
3091
+ $ref: "#/components/schemas/SetRecoveryEmail"
3092
+ responses:
3093
+ "204":
3094
+ description: Success
3095
+ "400":
3096
+ description: Invalid recovery credentials
3097
+ content:
3098
+ application/problem+json:
3099
+ schema:
3100
+ $ref: "#/components/schemas/Problem"
3101
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
3102
+ "403":
3103
+ description: Forbidden
3104
+ content:
3105
+ application/problem+json:
3106
+ schema:
3107
+ $ref: "#/components/schemas/Problem"
3108
+ "409":
3109
+ description: Security delay or conflicting state
3110
+ content:
3111
+ application/problem+json:
3112
+ schema:
3113
+ $ref: "#/components/schemas/Problem"
3114
+ "422":
3115
+ description: Invalid input
3116
+ content:
3117
+ application/problem+json:
3118
+ schema:
3119
+ $ref: "#/components/schemas/Problem"
3120
+ "426":
3121
+ description: Client outdated
3122
+ content:
3123
+ application/problem+json:
3124
+ schema:
3125
+ $ref: "#/components/schemas/Problem"
3126
+ "429":
3127
+ description: Rate limited
3128
+ content:
3129
+ application/problem+json:
3130
+ schema:
3131
+ $ref: "#/components/schemas/Problem"
3132
+ "503":
3133
+ description: Email recovery is not configured
3134
+ content:
3135
+ application/problem+json:
3136
+ schema:
3137
+ $ref: "#/components/schemas/Problem"
3138
+ delete:
3139
+ tags:
3140
+ - auth
3141
+ operationId: removeRecoveryEmail
3142
+ summary: Remove the recovery email after password verification
3143
+ requestBody:
3144
+ required: true
3145
+ content:
3146
+ application/json:
3147
+ schema:
3148
+ $ref: "#/components/schemas/RemoveRecoveryEmail"
3149
+ responses:
3150
+ "204":
3151
+ description: Success
3152
+ "400":
3153
+ description: Invalid recovery credentials
3154
+ content:
3155
+ application/problem+json:
3156
+ schema:
3157
+ $ref: "#/components/schemas/Problem"
3158
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
3159
+ "403":
3160
+ description: Forbidden
3161
+ content:
3162
+ application/problem+json:
3163
+ schema:
3164
+ $ref: "#/components/schemas/Problem"
3165
+ "409":
3166
+ description: Security delay or conflicting state
3167
+ content:
3168
+ application/problem+json:
3169
+ schema:
3170
+ $ref: "#/components/schemas/Problem"
3171
+ "422":
3172
+ description: Invalid input
3173
+ content:
3174
+ application/problem+json:
3175
+ schema:
3176
+ $ref: "#/components/schemas/Problem"
3177
+ "426":
3178
+ description: Client outdated
3179
+ content:
3180
+ application/problem+json:
3181
+ schema:
3182
+ $ref: "#/components/schemas/Problem"
3183
+ "429":
3184
+ description: Rate limited
3185
+ content:
3186
+ application/problem+json:
3187
+ schema:
3188
+ $ref: "#/components/schemas/Problem"
3189
+ "503":
3190
+ description: Email recovery is not configured
3191
+ content:
3192
+ application/problem+json:
3193
+ schema:
3194
+ $ref: "#/components/schemas/Problem"
3195
+ /auth/email/verify:
3196
+ parameters: *a1
3197
+ post:
3198
+ tags:
3199
+ - auth
3200
+ operationId: verifyRecoveryEmail
3201
+ summary: Consume a one-time email verification token
3202
+ security: []
3203
+ requestBody:
3204
+ required: true
3205
+ content:
3206
+ application/json:
3207
+ schema:
3208
+ $ref: "#/components/schemas/VerifyRecoveryEmail"
3209
+ responses:
3210
+ "204":
3211
+ description: Success
3212
+ "400":
3213
+ description: Invalid recovery credentials
3214
+ content:
3215
+ application/problem+json:
3216
+ schema:
3217
+ $ref: "#/components/schemas/Problem"
3218
+ "401":
3219
+ description: Unauthorized
3220
+ content:
3221
+ application/problem+json:
3222
+ schema:
3223
+ $ref: "#/components/schemas/Problem"
3224
+ "403":
3225
+ description: Forbidden
3226
+ content:
3227
+ application/problem+json:
3228
+ schema:
3229
+ $ref: "#/components/schemas/Problem"
3230
+ "409":
3231
+ description: Security delay or conflicting state
3232
+ content:
3233
+ application/problem+json:
3234
+ schema:
3235
+ $ref: "#/components/schemas/Problem"
3236
+ "422":
3237
+ description: Invalid input
3238
+ content:
3239
+ application/problem+json:
3240
+ schema:
3241
+ $ref: "#/components/schemas/Problem"
3242
+ "426":
3243
+ description: Client outdated
3244
+ content:
3245
+ application/problem+json:
3246
+ schema:
3247
+ $ref: "#/components/schemas/Problem"
3248
+ "429":
3249
+ description: Rate limited
3250
+ content:
3251
+ application/problem+json:
3252
+ schema:
3253
+ $ref: "#/components/schemas/Problem"
3254
+ "503":
3255
+ description: Email recovery is not configured
3256
+ content:
3257
+ application/problem+json:
3258
+ schema:
3259
+ $ref: "#/components/schemas/Problem"
3260
+ /auth/password-reset/request:
3261
+ parameters: *a1
3262
+ post:
3263
+ tags:
3264
+ - auth
3265
+ operationId: requestEmailReset
3266
+ summary: Queue a recovery email without revealing whether an account exists
3267
+ security: []
3268
+ requestBody:
3269
+ required: true
3270
+ content:
3271
+ application/json:
3272
+ schema:
3273
+ $ref: "#/components/schemas/EmailResetRequest"
3274
+ responses:
3275
+ "202":
3276
+ description: Success
3277
+ content:
3278
+ application/json:
3279
+ schema:
3280
+ $ref: "#/components/schemas/EmailResetAccepted"
3281
+ "400":
3282
+ description: Invalid recovery credentials
3283
+ content:
3284
+ application/problem+json:
3285
+ schema:
3286
+ $ref: "#/components/schemas/Problem"
3287
+ "401":
3288
+ description: Unauthorized
3289
+ content:
3290
+ application/problem+json:
3291
+ schema:
3292
+ $ref: "#/components/schemas/Problem"
3293
+ "403":
3294
+ description: Forbidden
3295
+ content:
3296
+ application/problem+json:
3297
+ schema:
3298
+ $ref: "#/components/schemas/Problem"
3299
+ "409":
3300
+ description: Security delay or conflicting state
3301
+ content:
3302
+ application/problem+json:
3303
+ schema:
3304
+ $ref: "#/components/schemas/Problem"
3305
+ "422":
3306
+ description: Invalid input
3307
+ content:
3308
+ application/problem+json:
3309
+ schema:
3310
+ $ref: "#/components/schemas/Problem"
3311
+ "426":
3312
+ description: Client outdated
3313
+ content:
3314
+ application/problem+json:
3315
+ schema:
3316
+ $ref: "#/components/schemas/Problem"
3317
+ "429":
3318
+ description: Rate limited
3319
+ content:
3320
+ application/problem+json:
3321
+ schema:
3322
+ $ref: "#/components/schemas/Problem"
3323
+ "503":
3324
+ description: Email recovery is not configured
3325
+ content:
3326
+ application/problem+json:
3327
+ schema:
3328
+ $ref: "#/components/schemas/Problem"
3329
+ /auth/password-reset/confirm:
3330
+ parameters: *a1
3331
+ post:
3332
+ tags:
3333
+ - auth
3334
+ operationId: confirmEmailReset
3335
+ summary: Reset a password using a one-time email token and the current second
3336
+ factor
3337
+ security: []
3338
+ requestBody:
3339
+ required: true
3340
+ content:
3341
+ application/json:
3342
+ schema:
3343
+ $ref: "#/components/schemas/EmailResetConfirmation"
3344
+ responses:
3345
+ "200":
3346
+ description: Success
3347
+ content:
3348
+ application/json:
3349
+ schema:
3350
+ $ref: "#/components/schemas/RegisterResponse"
3351
+ "400":
3352
+ description: Invalid recovery credentials
3353
+ content:
3354
+ application/problem+json:
3355
+ schema:
3356
+ $ref: "#/components/schemas/Problem"
3357
+ "401": { $ref: "#/components/responses/TwoFactorRequired" }
3358
+ "403":
3359
+ description: Forbidden
3360
+ content:
3361
+ application/problem+json:
3362
+ schema:
3363
+ $ref: "#/components/schemas/Problem"
3364
+ "404":
3365
+ $ref: "#/components/responses/NotFound"
3366
+ "409":
3367
+ description: Security delay or conflicting state
3368
+ content:
3369
+ application/problem+json:
3370
+ schema:
3371
+ $ref: "#/components/schemas/Problem"
3372
+ "422":
3373
+ description: Invalid input
3374
+ content:
3375
+ application/problem+json:
3376
+ schema:
3377
+ $ref: "#/components/schemas/Problem"
3378
+ "426":
3379
+ description: Client outdated
3380
+ content:
3381
+ application/problem+json:
3382
+ schema:
3383
+ $ref: "#/components/schemas/Problem"
3384
+ "429":
3385
+ description: Rate limited
3386
+ content:
3387
+ application/problem+json:
3388
+ schema:
3389
+ $ref: "#/components/schemas/Problem"
3390
+ "503":
3391
+ description: Email recovery is not configured
3392
+ content:
3393
+ application/problem+json:
3394
+ schema:
3395
+ $ref: "#/components/schemas/Problem"
3396
+
3397
+ components:
3398
+ securitySchemes:
3399
+ bearerAuth:
3400
+ type: http
3401
+ scheme: bearer
3402
+ bearerFormat: JWT
3403
+ description: Short-lived access token from login or refresh.
3404
+ cookieAuth:
3405
+ type: apiKey
3406
+ in: cookie
3407
+ name: refresh_token
3408
+ description: HttpOnly refresh cookie set by login; scoped to `/api/v2/auth`.
3409
+
3410
+ headers:
3411
+ RefreshCookie:
3412
+ description: HttpOnly, Secure, SameSite refresh cookie scoped to `/api/v2/auth/refresh`, so the browser sends it only to the refresh endpoint. Login and refresh set it; logout clears it (expired cookie).
3413
+ schema: { type: string }
3414
+
3415
+ parameters:
3416
+ QrLoginRequestId:
3417
+ name: requestId
3418
+ in: path
3419
+ required: true
3420
+ description: ID of a QR sign-in request, from the QR code link `/qr/<request_id>`.
3421
+ schema: { type: string, minLength: 32, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }
3422
+ Limit:
3423
+ name: limit
3424
+ in: query
3425
+ description: Page size
3426
+ schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
3427
+ After:
3428
+ name: after
3429
+ in: query
3430
+ description: Opaque `next_cursor` of the previous page
2151
3431
  schema: { $ref: "#/components/schemas/Cursor" }
2152
3432
  ChatId:
2153
3433
  name: chat_id
@@ -2209,6 +3489,17 @@ components:
2209
3489
  content:
2210
3490
  application/problem+json:
2211
3491
  schema: { $ref: "#/components/schemas/Problem" }
3492
+ TwoFactorRequired:
3493
+ description: |
3494
+ Missing, invalid or expired credentials, or `code: two_factor_required`. `two_factor_required` means every
3495
+ other credential in the request was valid, the account has two-factor on, and no `code` was sent. Nothing
3496
+ single-use (recovery key, email token, recovery request) was consumed: send the same request again with
3497
+ `code`. Invalid other credentials get the usual error of the operation instead, whatever the two-factor
3498
+ state, and a wrong `code` keeps its usual error. On operations that need an access token this 401 does not
3499
+ mean the session is lost; tell the two apart by `code`.
3500
+ content:
3501
+ application/problem+json:
3502
+ schema: { $ref: "#/components/schemas/Problem" }
2212
3503
  Forbidden:
2213
3504
  description: Authenticated but not allowed
2214
3505
  content:
@@ -2510,20 +3801,25 @@ components:
2510
3801
  type: array
2511
3802
  items:
2512
3803
  type: object
2513
- required: [id, type, created_at]
3804
+ required: [id, type, created_at, location]
2514
3805
  properties:
2515
3806
  id: { $ref: "#/components/schemas/Id" }
2516
3807
  type: { type: string }
2517
3808
  created_at: { type: string, format: date-time }
3809
+ user_agent: { type: [string, "null"], maxLength: 512 }
3810
+ location:
3811
+ description: Approximate location of the client when the event happened; `null` when unknown.
3812
+ oneOf:
3813
+ - $ref: "#/components/schemas/Location"
3814
+ - type: "null"
2518
3815
  RegisterResponse:
2519
3816
  allOf:
2520
3817
  - $ref: "#/components/schemas/TokenResponse"
2521
3818
  - type: object
2522
- description: Recovery parts of the temporary recovery flow (redesigned in release 1.1).
3819
+ description: The one-time recovery key shown after registration.
2523
3820
  properties:
2524
3821
  recovery_key: { type: string, description: Shown once; only a keyed hash is stored }
2525
- device_part: { type: string }
2526
- qr_part: { type: string }
3822
+ username: { $ref: "#/components/schemas/Username" }
2527
3823
  TwoFactorChallenge:
2528
3824
  type: object
2529
3825
  required: [two_factor_required, login_challenge]
@@ -2534,24 +3830,6 @@ components:
2534
3830
  oneOf:
2535
3831
  - $ref: "#/components/schemas/TokenResponse"
2536
3832
  - $ref: "#/components/schemas/TwoFactorChallenge"
2537
- RecoveryRequest:
2538
- type: object
2539
- required: [username]
2540
- properties:
2541
- username: { $ref: "#/components/schemas/Username" }
2542
- device_part: { type: string }
2543
- qr_part: { type: string }
2544
- RecoveryResponse:
2545
- type: object
2546
- required: [recovery_token]
2547
- properties:
2548
- recovery_token: { type: string }
2549
- ResetPasswordRequest:
2550
- type: object
2551
- required: [recovery_token, new_password]
2552
- properties:
2553
- recovery_token: { type: string }
2554
- new_password: { type: string, minLength: 8, maxLength: 128, writeOnly: true }
2555
3833
  WebSocketTicket:
2556
3834
  type: object
2557
3835
  required: [ticket, expires_in]
@@ -2598,7 +3876,7 @@ components:
2598
3876
  session_duration_days: { type: integer, minimum: 1, maximum: 365 }
2599
3877
  Session:
2600
3878
  type: object
2601
- required: [id, created_at, last_used_at, is_current]
3879
+ required: [id, created_at, last_used_at, is_current, location]
2602
3880
  properties:
2603
3881
  id: { type: string, format: uuid }
2604
3882
  device:
@@ -2608,6 +3886,25 @@ components:
2608
3886
  last_used_at: { $ref: "#/components/schemas/Timestamp" }
2609
3887
  expires_at: { $ref: "#/components/schemas/Timestamp" }
2610
3888
  is_current: { type: boolean }
3889
+ location:
3890
+ description: Approximate location of the device when it signed in or last refreshed its token; `null` when unknown.
3891
+ oneOf:
3892
+ - $ref: "#/components/schemas/Location"
3893
+ - type: "null"
3894
+ Location:
3895
+ type: object
3896
+ description: |
3897
+ Approximate location of a client address, looked up in an offline GeoIP database (IP geolocation by
3898
+ DB-IP, https://db-ip.com, CC BY 4.0). The address itself is never exposed. Each field is `null` when the
3899
+ database does not know it; clients localise the country name from `country_code`.
3900
+ required: [city, country_code, country]
3901
+ properties:
3902
+ city: { type: [string, "null"], description: City name in English }
3903
+ country_code:
3904
+ type: [string, "null"]
3905
+ pattern: "^[A-Z]{2}$"
3906
+ description: ISO 3166-1 alpha-2 country code
3907
+ country: { type: [string, "null"], description: Country name in English }
2611
3908
  TwoFactorSetup:
2612
3909
  type: object
2613
3910
  required: [secret, otpauth_uri]
@@ -2629,7 +3926,7 @@ components:
2629
3926
  description: One-time codes for signing in without the authenticator app. Shown only once.
2630
3927
  DisableTwoFactorRequest:
2631
3928
  type: object
2632
- required: [password, code]
3929
+ required: [password]
2633
3930
  properties:
2634
3931
  password: { type: string, maxLength: 128, writeOnly: true }
2635
3932
  code:
@@ -3249,3 +4546,319 @@ components:
3249
4546
  type: [string, "null"]
3250
4547
  description: First message of a direct-message request
3251
4548
  created_at: { $ref: "#/components/schemas/Timestamp" }
4549
+
4550
+ DeviceRecoveryStart:
4551
+ type: object
4552
+ required:
4553
+ - username
4554
+ properties:
4555
+ username:
4556
+ $ref: "#/components/schemas/Username"
4557
+ DeviceRecoveryResult:
4558
+ type: object
4559
+ required:
4560
+ - request_id
4561
+ - match_number
4562
+ - expires_at
4563
+ properties:
4564
+ request_id:
4565
+ type: string
4566
+ minLength: 32
4567
+ maxLength: 128
4568
+ pattern: ^[A-Za-z0-9_-]+$
4569
+ match_number:
4570
+ type: integer
4571
+ minimum: 10
4572
+ maximum: 99
4573
+ expires_at:
4574
+ type: string
4575
+ format: date-time
4576
+ DeviceRecoveryStatus:
4577
+ type: object
4578
+ required:
4579
+ - status
4580
+ properties:
4581
+ status:
4582
+ type: string
4583
+ enum:
4584
+ - pending
4585
+ - approved
4586
+ - denied
4587
+ - used
4588
+ - expired
4589
+ DeviceRecoveryApproval:
4590
+ type: object
4591
+ required:
4592
+ - match_number
4593
+ - password
4594
+ properties:
4595
+ match_number:
4596
+ type: integer
4597
+ minimum: 10
4598
+ maximum: 99
4599
+ password:
4600
+ type: string
4601
+ maxLength: 1024
4602
+ code:
4603
+ type: string
4604
+ maxLength: 128
4605
+ DeviceRecoveryComplete:
4606
+ type: object
4607
+ required:
4608
+ - new_password
4609
+ properties:
4610
+ new_password:
4611
+ type: string
4612
+ minLength: 8
4613
+ maxLength: 128
4614
+ writeOnly: true
4615
+
4616
+ QrLoginStatus:
4617
+ type: string
4618
+ description: |
4619
+ `pending` until a signed-in device opens the request, `scanned` once it did, then `approved` or `denied`.
4620
+ `expired` when the request timed out or was used (only in poll answers).
4621
+ enum: [pending, scanned, approved, denied, expired]
4622
+ QrLoginStart:
4623
+ type: object
4624
+ required: [request_id, poll_token, url, expires_at]
4625
+ properties:
4626
+ request_id:
4627
+ type: string
4628
+ minLength: 32
4629
+ maxLength: 128
4630
+ pattern: ^[A-Za-z0-9_-]+$
4631
+ poll_token:
4632
+ type: string
4633
+ minLength: 32
4634
+ maxLength: 128
4635
+ pattern: ^[A-Za-z0-9_-]+$
4636
+ description: Secret of the requesting device; only a keyed hash is stored.
4637
+ url:
4638
+ type: string
4639
+ description: "`<RECOVERY_WEB_URL>/qr/<request_id>`, the content of the QR code."
4640
+ expires_at: { $ref: "#/components/schemas/Timestamp" }
4641
+ QrLoginRequest:
4642
+ type: object
4643
+ required: [request_id, status, device, location, created_at, expires_at]
4644
+ properties:
4645
+ request_id: { type: string }
4646
+ status: { $ref: "#/components/schemas/QrLoginStatus" }
4647
+ device:
4648
+ type: [string, "null"]
4649
+ maxLength: 512
4650
+ description: User agent of the device that asks to sign in; `null` when it sent none.
4651
+ location:
4652
+ description: Approximate location of the device that asks to sign in; `null` when unknown.
4653
+ oneOf:
4654
+ - $ref: "#/components/schemas/Location"
4655
+ - type: "null"
4656
+ created_at: { $ref: "#/components/schemas/Timestamp" }
4657
+ expires_at: { $ref: "#/components/schemas/Timestamp" }
4658
+ QrLoginPollRequest:
4659
+ type: object
4660
+ required: [poll_token]
4661
+ properties:
4662
+ poll_token: { type: string, maxLength: 128, writeOnly: true }
4663
+ QrLoginPollResult:
4664
+ oneOf:
4665
+ - type: object
4666
+ required: [status]
4667
+ properties:
4668
+ status:
4669
+ type: string
4670
+ enum: [pending, scanned, denied, expired]
4671
+ - allOf:
4672
+ - $ref: "#/components/schemas/TokenResponse"
4673
+ - type: object
4674
+ required: [status]
4675
+ properties:
4676
+ status: { type: string, const: approved }
4677
+ QrLoginApproval:
4678
+ type: object
4679
+ properties:
4680
+ code:
4681
+ type: string
4682
+ maxLength: 128
4683
+ description: An authenticator code or an unused recovery code; required when two-factor is on.
4684
+
4685
+ PasskeyOptions:
4686
+ type: object
4687
+ required:
4688
+ - challenge_id
4689
+ - public_key
4690
+ properties:
4691
+ challenge_id: &a1
4692
+ type: string
4693
+ minLength: 32
4694
+ maxLength: 128
4695
+ public_key: &a2
4696
+ type: object
4697
+ additionalProperties: true
4698
+ PasskeyRegistration:
4699
+ type: object
4700
+ required:
4701
+ - password
4702
+ - name
4703
+ properties:
4704
+ password: &a4
4705
+ type: string
4706
+ maxLength: 128
4707
+ writeOnly: true
4708
+ code:
4709
+ type: string
4710
+ maxLength: 128
4711
+ name: &a3
4712
+ type: string
4713
+ minLength: 1
4714
+ maxLength: 64
4715
+ PasskeyVerification:
4716
+ type: object
4717
+ required:
4718
+ - challenge_id
4719
+ - credential
4720
+ properties:
4721
+ challenge_id: *a1
4722
+ credential: *a2
4723
+ PasskeyLoginOptions:
4724
+ type: object
4725
+ required: []
4726
+ properties:
4727
+ username:
4728
+ $ref: "#/components/schemas/Username"
4729
+ PasskeyRename:
4730
+ type: object
4731
+ required:
4732
+ - name
4733
+ properties:
4734
+ name: *a3
4735
+ PasskeyRemoval:
4736
+ type: object
4737
+ required: []
4738
+ properties:
4739
+ password: *a4
4740
+ Passkey:
4741
+ type: object
4742
+ required:
4743
+ - id
4744
+ - name
4745
+ - created_at
4746
+ - last_used_at
4747
+ properties:
4748
+ id:
4749
+ type: string
4750
+ name: *a3
4751
+ created_at:
4752
+ type: string
4753
+ format: date-time
4754
+ last_used_at:
4755
+ type:
4756
+ - string
4757
+ - "null"
4758
+ format: date-time
4759
+ PasskeyPage:
4760
+ type: object
4761
+ required:
4762
+ - items
4763
+ properties:
4764
+ items:
4765
+ type: array
4766
+ items:
4767
+ $ref: "#/components/schemas/Passkey"
4768
+
4769
+ RecoveryEmailStatus:
4770
+ type: object
4771
+ required:
4772
+ - address
4773
+ - state
4774
+ - enabled
4775
+ properties:
4776
+ address:
4777
+ type: string
4778
+ state:
4779
+ type: string
4780
+ enum:
4781
+ - not_added
4782
+ - pending
4783
+ - verified
4784
+ enabled:
4785
+ type: boolean
4786
+ RecoveryOverview:
4787
+ type: object
4788
+ required:
4789
+ - recovery_key
4790
+ - other_devices
4791
+ - passkeys
4792
+ - passkeys_enabled
4793
+ - email
4794
+ properties:
4795
+ recovery_key:
4796
+ $ref: "#/components/schemas/RecoveryKeyStatus"
4797
+ other_devices:
4798
+ type: integer
4799
+ minimum: 0
4800
+ passkeys:
4801
+ type: integer
4802
+ minimum: 0
4803
+ passkeys_enabled:
4804
+ type: boolean
4805
+ email:
4806
+ $ref: "#/components/schemas/RecoveryEmailStatus"
4807
+ SetRecoveryEmail:
4808
+ type: object
4809
+ required:
4810
+ - email
4811
+ - password
4812
+ properties:
4813
+ email:
4814
+ type: string
4815
+ format: email
4816
+ maxLength: 254
4817
+ password: &a1
4818
+ type: string
4819
+ minLength: 8
4820
+ maxLength: 128
4821
+ writeOnly: true
4822
+ code: &a2
4823
+ type: string
4824
+ maxLength: 128
4825
+ RemoveRecoveryEmail:
4826
+ type: object
4827
+ required:
4828
+ - password
4829
+ properties:
4830
+ password: *a1
4831
+ code: *a2
4832
+ VerifyRecoveryEmail:
4833
+ type: object
4834
+ required:
4835
+ - token
4836
+ properties:
4837
+ token: &a3
4838
+ type: string
4839
+ minLength: 32
4840
+ maxLength: 128
4841
+ EmailResetRequest:
4842
+ type: object
4843
+ required:
4844
+ - username_or_email
4845
+ properties:
4846
+ username_or_email:
4847
+ type: string
4848
+ maxLength: 254
4849
+ EmailResetConfirmation:
4850
+ type: object
4851
+ required:
4852
+ - token
4853
+ - new_password
4854
+ properties:
4855
+ token: *a3
4856
+ new_password: *a1
4857
+ code: *a2
4858
+ EmailResetAccepted:
4859
+ type: object
4860
+ required:
4861
+ - message
4862
+ properties:
4863
+ message:
4864
+ type: string