@fleetless/contracts 5.3.0-next.1 → 6.0.0-next.2
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/CHANGELOG.md +108 -33
- package/artifacts/openapi.json +2053 -784
- package/artifacts/routes.json +1680 -323
- package/artifacts/schema/accept-team-invite-request.schema.json +13 -7
- package/artifacts/schema/app-auth-config.schema.json +108 -4
- package/artifacts/schema/app-hosted-pages.schema.json +33 -0
- package/artifacts/schema/app-invitation.schema.json +5 -12
- package/artifacts/schema/app-mail-template-list-response.schema.json +4 -3
- package/artifacts/schema/app-mail-template.schema.json +3 -2
- package/artifacts/schema/app-sign-in-methods.schema.json +19 -0
- package/artifacts/schema/app-user-list-response.schema.json +36 -0
- package/artifacts/schema/app-user.schema.json +36 -0
- package/artifacts/schema/audit-query.schema.json +0 -6
- package/artifacts/schema/auth-me-response.schema.json +27 -0
- package/artifacts/schema/auth-ok.schema.json +13 -1
- package/artifacts/schema/client-accept-invitation-request.schema.json +3 -4
- package/artifacts/schema/client-identity.schema.json +13 -1
- package/artifacts/schema/client-login-code-request.schema.json +24 -0
- package/artifacts/schema/client-login-code-verify-request.schema.json +30 -0
- package/artifacts/schema/client-provider-list-response.schema.json +22 -2
- package/artifacts/schema/client-register-request.schema.json +3 -4
- package/artifacts/schema/client-sign-in-result.schema.json +55 -0
- package/artifacts/schema/client-two-factor-disable-request.schema.json +15 -0
- package/artifacts/schema/client-two-factor-setup-confirm-request.schema.json +20 -0
- package/artifacts/schema/client-two-factor-setup-confirm-response.schema.json +49 -0
- package/artifacts/schema/client-two-factor-setup-request.schema.json +12 -0
- package/artifacts/schema/client-two-factor-verify-request.schema.json +25 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +1 -1
- package/artifacts/schema/create-passkey-request.schema.json +25 -0
- package/artifacts/schema/create-passkey-response.schema.json +84 -0
- package/artifacts/schema/developer-passkey.schema.json +56 -0
- package/artifacts/schema/developer-two-factor.schema.json +105 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +22 -0
- package/artifacts/schema/fleetless-user.schema.json +22 -0
- package/artifacts/schema/invalid-code-details.schema.json +16 -0
- package/artifacts/schema/job-actor.schema.json +1 -15
- package/artifacts/schema/job-run-list-response.schema.json +1 -15
- package/artifacts/schema/job-run.schema.json +1 -15
- package/artifacts/schema/org.schema.json +5 -0
- package/artifacts/schema/patch-org-request.schema.json +5 -3
- package/artifacts/schema/patch-org-response.schema.json +5 -0
- package/artifacts/schema/put-app-auth-look-request.schema.json +22 -0
- package/artifacts/schema/put-app-auth-mcp-request.schema.json +1 -14
- package/artifacts/schema/put-app-auth-sign-in-request.schema.json +39 -0
- package/artifacts/schema/put-app-auth-urls-request.schema.json +31 -4
- package/artifacts/schema/recovery-codes-response.schema.json +20 -0
- package/artifacts/schema/{role-rename-request.schema.json → rename-passkey-request.schema.json} +2 -2
- package/artifacts/schema/role-list-response.schema.json +1 -1
- package/artifacts/schema/role.schema.json +1 -1
- package/artifacts/schema/totp-confirm-request.schema.json +15 -0
- package/artifacts/schema/totp-confirm-response.schema.json +27 -0
- package/artifacts/schema/two-factor-challenge.schema.json +24 -0
- package/artifacts/schema/two-factor-setup-response.schema.json +21 -0
- package/artifacts/schema/webauthn-options-response.schema.json +18 -0
- package/dist/app-users.d.ts +154 -35
- package/dist/app-users.js +177 -46
- package/dist/apps.d.ts +2 -38
- package/dist/apps.js +3 -46
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +0 -13
- package/dist/client-auth.d.ts +133 -24
- package/dist/client-auth.js +139 -28
- package/dist/config.d.ts +2 -2
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +37 -38
- package/dist/identity.d.ts +175 -128
- package/dist/identity.js +192 -106
- package/dist/index.d.ts +11 -13
- package/dist/index.js +12 -10
- package/dist/jobs.d.ts +0 -3
- package/dist/jobs.js +0 -16
- package/dist/protocol.d.ts +1 -1
- package/dist/realtime.d.ts +1 -0
- package/dist/rest.d.ts +2 -2
- package/dist/rest.js +17 -28
- package/dist/routes.d.ts +26 -0
- package/dist/routes.js +528 -234
- package/package.json +1 -1
- package/artifacts/schema/developer-login-request.schema.json +0 -19
- package/artifacts/schema/feedback-request.schema.json +0 -34
- package/artifacts/schema/feedback-response.schema.json +0 -27
- package/artifacts/schema/password-reset-confirm.schema.json +0 -19
- package/artifacts/schema/password-reset-request.schema.json +0 -14
- package/artifacts/schema/role-delete-query.schema.json +0 -13
- package/artifacts/schema/role-in-use-details.schema.json +0 -28
- package/artifacts/schema/sign-up-request.schema.json +0 -26
- package/artifacts/schema/sign-up-response.schema.json +0 -127
- package/dist/feedback.d.ts +0 -42
- package/dist/feedback.js +0 -35
package/artifacts/routes.json
CHANGED
|
@@ -76,29 +76,6 @@
|
|
|
76
76
|
"transport": "http",
|
|
77
77
|
"notes": "Answers `{ ok, dependencies: { database, storage, liveKit } }` — a cloud-local shape, not a wire contract, so nothing here pins it. The status is always `200`: each dependency is probed independently and a failure is reported in the body rather than thrown, because `/healthz` must never itself be a reason the process looks down. Read `ok`, not the status code."
|
|
78
78
|
},
|
|
79
|
-
{
|
|
80
|
-
"method": "POST",
|
|
81
|
-
"path": "/api/auth/signup",
|
|
82
|
-
"section": "developer-auth",
|
|
83
|
-
"summary": "Creates an org and its founding Owner, and answers a developer session.",
|
|
84
|
-
"audience": "developer",
|
|
85
|
-
"auth": "none",
|
|
86
|
-
"rateLimited": true,
|
|
87
|
-
"ownerTier": false,
|
|
88
|
-
"status": 201,
|
|
89
|
-
"params": [],
|
|
90
|
-
"query": null,
|
|
91
|
-
"request": "sign-up-request",
|
|
92
|
-
"response": "sign-up-response",
|
|
93
|
-
"errors": [
|
|
94
|
-
"rate_limited",
|
|
95
|
-
"signup_closed",
|
|
96
|
-
"validation_error",
|
|
97
|
-
"email_taken"
|
|
98
|
-
],
|
|
99
|
-
"transport": "http",
|
|
100
|
-
"notes": "While the deployment runs in closed beta this answers `403 signup_closed` before it looks at the body — there is nothing for a validation message, or an `email_taken` answer, to be right about when nothing will be created. Email is globally unique, so an address already registered in any org is refused."
|
|
101
|
-
},
|
|
102
79
|
{
|
|
103
80
|
"method": "POST",
|
|
104
81
|
"path": "/api/auth/refresh",
|
|
@@ -120,7 +97,7 @@
|
|
|
120
97
|
"token_revoked"
|
|
121
98
|
],
|
|
122
99
|
"transport": "http",
|
|
123
|
-
"notes": "The whole family is re-checked here, not just the token: an account that has been removed from the org, or whose `token_version` was bumped by
|
|
100
|
+
"notes": "The whole family is re-checked here, not just the token: an account that has been removed from the org, or whose `token_version` was bumped by an owner's two-factor reset, cannot mint a fresh console token and answers `token_revoked`. Refusing that only on the other routes would leave a session that is dead everywhere but here."
|
|
124
101
|
},
|
|
125
102
|
{
|
|
126
103
|
"method": "POST",
|
|
@@ -188,10 +165,10 @@
|
|
|
188
165
|
"notes": "No Owner tier: this can only ever touch the caller's own row, so there is nothing for a tier check to gate. Saving the name already held writes nothing and records no audit event — the org activity stream reaches every developer with the console open, and an event for a no-op would misreport that something changed."
|
|
189
166
|
},
|
|
190
167
|
{
|
|
191
|
-
"method": "
|
|
192
|
-
"path": "/api/auth/
|
|
168
|
+
"method": "GET",
|
|
169
|
+
"path": "/api/auth/two-factor",
|
|
193
170
|
"section": "developer-auth",
|
|
194
|
-
"summary": "
|
|
171
|
+
"summary": "Answers the calling developer's passkeys, authenticator, recovery codes left and the org's policy.",
|
|
195
172
|
"audience": "developer",
|
|
196
173
|
"auth": "developer",
|
|
197
174
|
"rateLimited": false,
|
|
@@ -199,119 +176,232 @@
|
|
|
199
176
|
"status": 200,
|
|
200
177
|
"params": [],
|
|
201
178
|
"query": null,
|
|
202
|
-
"request":
|
|
203
|
-
"response": "
|
|
179
|
+
"request": null,
|
|
180
|
+
"response": "developer-two-factor",
|
|
204
181
|
"errors": [
|
|
205
182
|
"unauthorized",
|
|
206
183
|
"token_expired",
|
|
207
|
-
"token_revoked"
|
|
208
|
-
"validation_error",
|
|
209
|
-
"invalid_credentials"
|
|
184
|
+
"token_revoked"
|
|
210
185
|
],
|
|
211
186
|
"transport": "http",
|
|
212
|
-
"notes": "
|
|
187
|
+
"notes": "What Settings › Profile › Security draws. No key material, secret or code travels here — the passkeys are names and dates, the authenticator is a date, the recovery codes are a count."
|
|
213
188
|
},
|
|
214
189
|
{
|
|
215
190
|
"method": "POST",
|
|
216
|
-
"path": "/api/auth/
|
|
191
|
+
"path": "/api/auth/passkeys/options",
|
|
217
192
|
"section": "developer-auth",
|
|
218
|
-
"summary": "
|
|
193
|
+
"summary": "Answers the WebAuthn creation options for registering a passkey.",
|
|
219
194
|
"audience": "developer",
|
|
220
|
-
"auth": "
|
|
221
|
-
"rateLimited":
|
|
195
|
+
"auth": "developer",
|
|
196
|
+
"rateLimited": false,
|
|
222
197
|
"ownerTier": false,
|
|
223
|
-
"status":
|
|
198
|
+
"status": 200,
|
|
224
199
|
"params": [],
|
|
225
200
|
"query": null,
|
|
226
|
-
"request":
|
|
227
|
-
"response":
|
|
201
|
+
"request": null,
|
|
202
|
+
"response": "webauthn-options-response",
|
|
228
203
|
"errors": [
|
|
229
|
-
"
|
|
230
|
-
"
|
|
204
|
+
"unauthorized",
|
|
205
|
+
"token_expired",
|
|
206
|
+
"token_revoked"
|
|
231
207
|
],
|
|
232
208
|
"transport": "http",
|
|
233
|
-
"notes": "
|
|
209
|
+
"notes": "Hand `options` to the browser's WebAuthn API as it is. The relying party is `fleetless.dev`, so the passkey works on the auth portal and in the console alike; user verification is required and the credential is discoverable, so it can sign the person in without an address. The passkeys the caller already has are excluded. The challenge is single-use and expires with the ceremony."
|
|
234
210
|
},
|
|
235
211
|
{
|
|
236
|
-
"method": "
|
|
237
|
-
"path": "/
|
|
238
|
-
"section": "
|
|
239
|
-
"summary": "
|
|
240
|
-
"audience": "
|
|
241
|
-
"auth": "
|
|
212
|
+
"method": "POST",
|
|
213
|
+
"path": "/api/auth/passkeys",
|
|
214
|
+
"section": "developer-auth",
|
|
215
|
+
"summary": "Registers a passkey from the browser's answer to the creation options.",
|
|
216
|
+
"audience": "developer",
|
|
217
|
+
"auth": "developer",
|
|
242
218
|
"rateLimited": false,
|
|
243
219
|
"ownerTier": false,
|
|
244
|
-
"status":
|
|
220
|
+
"status": 201,
|
|
245
221
|
"params": [],
|
|
246
222
|
"query": null,
|
|
247
|
-
"request":
|
|
248
|
-
"response":
|
|
249
|
-
"errors": [
|
|
223
|
+
"request": "create-passkey-request",
|
|
224
|
+
"response": "create-passkey-response",
|
|
225
|
+
"errors": [
|
|
226
|
+
"unauthorized",
|
|
227
|
+
"token_expired",
|
|
228
|
+
"token_revoked",
|
|
229
|
+
"validation_error"
|
|
230
|
+
],
|
|
250
231
|
"transport": "http",
|
|
251
|
-
"notes": "
|
|
232
|
+
"notes": "A ceremony that does not verify — a wrong challenge, origin or relying party, no user verification — is `400 validation_error` naming `credential`. When this is the account's first second factor, ten recovery codes are issued and answered once; otherwise `recovery_codes` is `null` and the existing ones stay valid. Audited as `developer.two_factor_added` with `details.kind` `passkey`."
|
|
252
233
|
},
|
|
253
234
|
{
|
|
254
|
-
"method": "
|
|
255
|
-
"path": "/
|
|
256
|
-
"section": "
|
|
257
|
-
"summary": "
|
|
258
|
-
"audience": "
|
|
259
|
-
"auth": "
|
|
235
|
+
"method": "PATCH",
|
|
236
|
+
"path": "/api/auth/passkeys/:id",
|
|
237
|
+
"section": "developer-auth",
|
|
238
|
+
"summary": "Renames one of the caller's passkeys.",
|
|
239
|
+
"audience": "developer",
|
|
240
|
+
"auth": "developer",
|
|
260
241
|
"rateLimited": false,
|
|
261
242
|
"ownerTier": false,
|
|
262
243
|
"status": 200,
|
|
263
244
|
"params": [
|
|
264
245
|
{
|
|
265
|
-
"name": "
|
|
266
|
-
"description": "The
|
|
246
|
+
"name": "id",
|
|
247
|
+
"description": "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`."
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
"query": null,
|
|
251
|
+
"request": "rename-passkey-request",
|
|
252
|
+
"response": "developer-passkey",
|
|
253
|
+
"errors": [
|
|
254
|
+
"unauthorized",
|
|
255
|
+
"token_expired",
|
|
256
|
+
"token_revoked",
|
|
257
|
+
"invalid_uuid",
|
|
258
|
+
"validation_error",
|
|
259
|
+
"not_found"
|
|
260
|
+
],
|
|
261
|
+
"transport": "http"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"method": "DELETE",
|
|
265
|
+
"path": "/api/auth/passkeys/:id",
|
|
266
|
+
"section": "developer-auth",
|
|
267
|
+
"summary": "Removes one of the caller's passkeys.",
|
|
268
|
+
"audience": "developer",
|
|
269
|
+
"auth": "developer",
|
|
270
|
+
"rateLimited": false,
|
|
271
|
+
"ownerTier": false,
|
|
272
|
+
"status": 204,
|
|
273
|
+
"params": [
|
|
274
|
+
{
|
|
275
|
+
"name": "id",
|
|
276
|
+
"description": "The passkey's uuid, as listed by `GET /api/auth/two-factor`; another person's passkey answers `404`."
|
|
267
277
|
}
|
|
268
278
|
],
|
|
269
279
|
"query": null,
|
|
270
280
|
"request": null,
|
|
271
281
|
"response": null,
|
|
272
|
-
"errors": [
|
|
282
|
+
"errors": [
|
|
283
|
+
"unauthorized",
|
|
284
|
+
"token_expired",
|
|
285
|
+
"token_revoked",
|
|
286
|
+
"invalid_uuid",
|
|
287
|
+
"not_found",
|
|
288
|
+
"target_state_conflict"
|
|
289
|
+
],
|
|
273
290
|
"transport": "http",
|
|
274
|
-
"notes": "
|
|
291
|
+
"notes": "`409 target_state_conflict` names `two_factor` with rule `required_by_org` when this is the caller's last second factor and the organisation requires one. Removing the last one otherwise also voids the recovery codes. Audited as `developer.two_factor_removed` with `details.kind` `passkey`."
|
|
275
292
|
},
|
|
276
293
|
{
|
|
277
|
-
"method": "
|
|
278
|
-
"path": "/
|
|
279
|
-
"section": "
|
|
280
|
-
"summary": "
|
|
281
|
-
"audience": "
|
|
282
|
-
"auth": "
|
|
294
|
+
"method": "POST",
|
|
295
|
+
"path": "/api/auth/totp",
|
|
296
|
+
"section": "developer-auth",
|
|
297
|
+
"summary": "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
298
|
+
"audience": "developer",
|
|
299
|
+
"auth": "developer",
|
|
283
300
|
"rateLimited": false,
|
|
284
301
|
"ownerTier": false,
|
|
285
302
|
"status": 200,
|
|
286
303
|
"params": [],
|
|
287
304
|
"query": null,
|
|
288
305
|
"request": null,
|
|
289
|
-
"response":
|
|
290
|
-
"errors": [
|
|
306
|
+
"response": "two-factor-setup-response",
|
|
307
|
+
"errors": [
|
|
308
|
+
"unauthorized",
|
|
309
|
+
"token_expired",
|
|
310
|
+
"token_revoked"
|
|
311
|
+
],
|
|
291
312
|
"transport": "http",
|
|
292
|
-
"notes": "
|
|
313
|
+
"notes": "The secret is pending until `POST /api/auth/totp/confirm` accepts a code from it; a second call replaces a pending secret. A developer who already has an authenticator keeps it until the new one is confirmed, which is how `Replace…` works."
|
|
293
314
|
},
|
|
294
315
|
{
|
|
295
316
|
"method": "POST",
|
|
296
|
-
"path": "/api/auth/
|
|
317
|
+
"path": "/api/auth/totp/confirm",
|
|
297
318
|
"section": "developer-auth",
|
|
298
|
-
"summary": "
|
|
319
|
+
"summary": "Confirms the pending authenticator with a code it shows now.",
|
|
299
320
|
"audience": "developer",
|
|
300
|
-
"auth": "
|
|
321
|
+
"auth": "developer",
|
|
301
322
|
"rateLimited": true,
|
|
302
323
|
"ownerTier": false,
|
|
303
|
-
"status":
|
|
324
|
+
"status": 200,
|
|
304
325
|
"params": [],
|
|
305
326
|
"query": null,
|
|
306
|
-
"request": "
|
|
307
|
-
"response":
|
|
327
|
+
"request": "totp-confirm-request",
|
|
328
|
+
"response": "totp-confirm-response",
|
|
308
329
|
"errors": [
|
|
330
|
+
"unauthorized",
|
|
331
|
+
"token_expired",
|
|
332
|
+
"token_revoked",
|
|
309
333
|
"rate_limited",
|
|
310
334
|
"validation_error",
|
|
335
|
+
"invalid_code",
|
|
311
336
|
"token_spent"
|
|
312
337
|
],
|
|
313
338
|
"transport": "http",
|
|
314
|
-
"notes": "
|
|
339
|
+
"notes": "A code that does not match the pending secret is `400 invalid_code`; no pending setup is `410 token_spent`. On success the new authenticator replaces any earlier one. Ten recovery codes are answered when it is the account's first second factor, otherwise `null`. Audited as `developer.two_factor_added` with `details.kind` `authenticator`."
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"method": "DELETE",
|
|
343
|
+
"path": "/api/auth/totp",
|
|
344
|
+
"section": "developer-auth",
|
|
345
|
+
"summary": "Removes the caller's authenticator app.",
|
|
346
|
+
"audience": "developer",
|
|
347
|
+
"auth": "developer",
|
|
348
|
+
"rateLimited": false,
|
|
349
|
+
"ownerTier": false,
|
|
350
|
+
"status": 204,
|
|
351
|
+
"params": [],
|
|
352
|
+
"query": null,
|
|
353
|
+
"request": null,
|
|
354
|
+
"response": null,
|
|
355
|
+
"errors": [
|
|
356
|
+
"unauthorized",
|
|
357
|
+
"token_expired",
|
|
358
|
+
"token_revoked",
|
|
359
|
+
"not_found",
|
|
360
|
+
"target_state_conflict"
|
|
361
|
+
],
|
|
362
|
+
"transport": "http",
|
|
363
|
+
"notes": "`404 not_found` when there is no authenticator. `409 target_state_conflict` names `two_factor` with rule `required_by_org` when it is the caller's last second factor and the organisation requires one. Audited as `developer.two_factor_removed` with `details.kind` `authenticator`."
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"method": "POST",
|
|
367
|
+
"path": "/api/auth/recovery-codes",
|
|
368
|
+
"section": "developer-auth",
|
|
369
|
+
"summary": "Issues ten new recovery codes and voids the old ones.",
|
|
370
|
+
"audience": "developer",
|
|
371
|
+
"auth": "developer",
|
|
372
|
+
"rateLimited": false,
|
|
373
|
+
"ownerTier": false,
|
|
374
|
+
"status": 200,
|
|
375
|
+
"params": [],
|
|
376
|
+
"query": null,
|
|
377
|
+
"request": null,
|
|
378
|
+
"response": "recovery-codes-response",
|
|
379
|
+
"errors": [
|
|
380
|
+
"unauthorized",
|
|
381
|
+
"token_expired",
|
|
382
|
+
"token_revoked",
|
|
383
|
+
"target_state_conflict"
|
|
384
|
+
],
|
|
385
|
+
"transport": "http",
|
|
386
|
+
"notes": "The codes are shown this once. `409 target_state_conflict` names `two_factor` with rule `off` when the caller has no second factor: recovery codes only stand in for one. Audited as `developer.recovery_codes_generated`."
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"method": "GET",
|
|
390
|
+
"path": "/favicon.svg",
|
|
391
|
+
"section": "client-auth",
|
|
392
|
+
"summary": "Serves the Fleetless icon for the auth portal's and the MCP welcome page's browser tab.",
|
|
393
|
+
"audience": "internal",
|
|
394
|
+
"auth": "none",
|
|
395
|
+
"rateLimited": false,
|
|
396
|
+
"ownerTier": false,
|
|
397
|
+
"status": 200,
|
|
398
|
+
"params": [],
|
|
399
|
+
"query": null,
|
|
400
|
+
"request": null,
|
|
401
|
+
"response": null,
|
|
402
|
+
"errors": [],
|
|
403
|
+
"transport": "http",
|
|
404
|
+
"notes": "An SVG, not JSON. Those pages carry a Content-Security-Policy that admits no `data:` image, so the icon is a file on their own origin — the one source `img-src 'self'` names. Cached for a day: the bytes change when the brand does, not per deploy."
|
|
315
405
|
},
|
|
316
406
|
{
|
|
317
407
|
"method": "POST",
|
|
@@ -574,7 +664,7 @@
|
|
|
574
664
|
"validation_error"
|
|
575
665
|
],
|
|
576
666
|
"transport": "http",
|
|
577
|
-
"notes": "The body is `{ \"name\": string }` — non-empty, trimmed, at most
|
|
667
|
+
"notes": "The body is `{ \"name\": string }` — non-empty, trimmed, at most 120 characters — and is deliberately not a contract shape: contracts define the `role` this answers with, not this one trivial request. **The answer is a bare `role`, not an envelope**, unlike the listing beside it."
|
|
578
668
|
},
|
|
579
669
|
{
|
|
580
670
|
"method": "GET",
|
|
@@ -704,77 +794,6 @@
|
|
|
704
794
|
"transport": "http",
|
|
705
795
|
"notes": "Built by the same builder the MCP server's own `robot_describe` uses, so the two cannot drift. It answers what the role *would* be offered and consults nothing about any user's actual MCP entitlement. A robot the role grants nothing on still appears, with an empty `exposures` — dropping it would read as \"not attached\", which is a different fact."
|
|
706
796
|
},
|
|
707
|
-
{
|
|
708
|
-
"method": "PATCH",
|
|
709
|
-
"path": "/api/apps/:id/roles/:roleId",
|
|
710
|
-
"section": "apps",
|
|
711
|
-
"summary": "Renames a role; its users keep it.",
|
|
712
|
-
"audience": "developer",
|
|
713
|
-
"auth": "developer",
|
|
714
|
-
"rateLimited": false,
|
|
715
|
-
"ownerTier": false,
|
|
716
|
-
"status": 200,
|
|
717
|
-
"params": [
|
|
718
|
-
{
|
|
719
|
-
"name": "id",
|
|
720
|
-
"description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
|
|
721
|
-
},
|
|
722
|
-
{
|
|
723
|
-
"name": "roleId",
|
|
724
|
-
"description": "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`."
|
|
725
|
-
}
|
|
726
|
-
],
|
|
727
|
-
"query": null,
|
|
728
|
-
"request": "role-rename-request",
|
|
729
|
-
"response": "role",
|
|
730
|
-
"errors": [
|
|
731
|
-
"unauthorized",
|
|
732
|
-
"token_expired",
|
|
733
|
-
"token_revoked",
|
|
734
|
-
"invalid_uuid",
|
|
735
|
-
"not_found",
|
|
736
|
-
"validation_error",
|
|
737
|
-
"role_name_taken"
|
|
738
|
-
],
|
|
739
|
-
"transport": "http",
|
|
740
|
-
"notes": "Names are unique per app, compared exactly as stored after trimming. Built-in roles can be renamed."
|
|
741
|
-
},
|
|
742
|
-
{
|
|
743
|
-
"method": "DELETE",
|
|
744
|
-
"path": "/api/apps/:id/roles/:roleId",
|
|
745
|
-
"section": "apps",
|
|
746
|
-
"summary": "Deletes a role, moving its users, pending invitations and default-role status to another role.",
|
|
747
|
-
"audience": "developer",
|
|
748
|
-
"auth": "developer",
|
|
749
|
-
"rateLimited": false,
|
|
750
|
-
"ownerTier": false,
|
|
751
|
-
"status": 204,
|
|
752
|
-
"params": [
|
|
753
|
-
{
|
|
754
|
-
"name": "id",
|
|
755
|
-
"description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
|
|
756
|
-
},
|
|
757
|
-
{
|
|
758
|
-
"name": "roleId",
|
|
759
|
-
"description": "The role's uuid, from `GET /api/apps/:id/roles`; a role of another app answers `404`."
|
|
760
|
-
}
|
|
761
|
-
],
|
|
762
|
-
"query": "role-delete-query",
|
|
763
|
-
"request": null,
|
|
764
|
-
"response": null,
|
|
765
|
-
"errors": [
|
|
766
|
-
"unauthorized",
|
|
767
|
-
"token_expired",
|
|
768
|
-
"token_revoked",
|
|
769
|
-
"invalid_uuid",
|
|
770
|
-
"not_found",
|
|
771
|
-
"validation_error",
|
|
772
|
-
"role_in_use",
|
|
773
|
-
"last_role"
|
|
774
|
-
],
|
|
775
|
-
"transport": "http",
|
|
776
|
-
"notes": "Without `move_to`, a role that app users or pending invitations hold, or that is the app's default, answers `409 role_in_use` with `{ users, invitations, is_default }`. With `move_to` — another role of the same app, else `400 validation_error` — one transaction moves `app_users.role_id`, pending invitations and `default_role_id`, then deletes the role and its permissions. The app's only role answers `409 last_role`. Built-in roles can be deleted like any other."
|
|
777
|
-
},
|
|
778
797
|
{
|
|
779
798
|
"method": "POST",
|
|
780
799
|
"path": "/api/apps/:id/server-keys",
|
|
@@ -1095,21 +1114,22 @@
|
|
|
1095
1114
|
"token_revoked",
|
|
1096
1115
|
"invalid_uuid",
|
|
1097
1116
|
"not_found",
|
|
1098
|
-
"target_state_conflict"
|
|
1117
|
+
"target_state_conflict",
|
|
1118
|
+
"method_not_allowed"
|
|
1099
1119
|
],
|
|
1100
1120
|
"transport": "http",
|
|
1101
|
-
"notes": "The support door beside `POST /api/client/password/reset
|
|
1121
|
+
"notes": "The support door beside `POST /api/client/password/reset`, refused like it with `403 method_not_allowed` while the app has the password method off: the same one-hour token and the same link, triggered by a developer for a user who asked them rather than the form. **No enumeration discipline applies** — the caller is authenticated into the app and can read the user list — so this one answers what actually happened: `{ \"mail\": mailStatus }`, where `not_configured` is a deployment without a mailer and `failed` is the state worth somebody's attention. The link points at the app's `reset_url`, or at the hosted reset page when the app has configured none. `409 target_state_conflict` names `password` with rule `not_set` for an account that has none — an OIDC-only app user, whom a reset link would hand a second, quieter door — and `status` with rule `blocked` for a blocked one, since `POST /api/client/password/reset` mails a blocked account nothing and the two doors may not disagree. Setting the password directly is deliberately not offered; a developer who could would hold their customers' credentials."
|
|
1102
1122
|
},
|
|
1103
1123
|
{
|
|
1104
|
-
"method": "
|
|
1105
|
-
"path": "/api/apps/:id/users/:userId/
|
|
1124
|
+
"method": "DELETE",
|
|
1125
|
+
"path": "/api/apps/:id/users/:userId/two-factor",
|
|
1106
1126
|
"section": "apps",
|
|
1107
|
-
"summary": "
|
|
1127
|
+
"summary": "Removes an app user's authenticator and recovery codes and ends every session they hold.",
|
|
1108
1128
|
"audience": "developer",
|
|
1109
1129
|
"auth": "developer",
|
|
1110
1130
|
"rateLimited": false,
|
|
1111
1131
|
"ownerTier": false,
|
|
1112
|
-
"status":
|
|
1132
|
+
"status": 204,
|
|
1113
1133
|
"params": [
|
|
1114
1134
|
{
|
|
1115
1135
|
"name": "id",
|
|
@@ -1122,7 +1142,40 @@
|
|
|
1122
1142
|
],
|
|
1123
1143
|
"query": null,
|
|
1124
1144
|
"request": null,
|
|
1125
|
-
"response":
|
|
1145
|
+
"response": null,
|
|
1146
|
+
"errors": [
|
|
1147
|
+
"unauthorized",
|
|
1148
|
+
"token_expired",
|
|
1149
|
+
"token_revoked",
|
|
1150
|
+
"invalid_uuid",
|
|
1151
|
+
"not_found"
|
|
1152
|
+
],
|
|
1153
|
+
"transport": "http",
|
|
1154
|
+
"notes": "The support door for a person who lost their authenticator and their recovery codes. The authenticator and every recovery code go, and so does every session of the account — whoever held one may be the reason for the reset. **A user with no second factor answers `204` too**: that is the end state being asked for. When the app requires two-factor, the person sets it up again at their next sign-in, before any session exists. Audited as `app_user.two_factor_reset`."
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
"method": "GET",
|
|
1158
|
+
"path": "/api/apps/:id/users/:userId/mcp-grants",
|
|
1159
|
+
"section": "apps",
|
|
1160
|
+
"summary": "Lists the MCP clients one app user has consented to.",
|
|
1161
|
+
"audience": "developer",
|
|
1162
|
+
"auth": "developer",
|
|
1163
|
+
"rateLimited": false,
|
|
1164
|
+
"ownerTier": false,
|
|
1165
|
+
"status": 200,
|
|
1166
|
+
"params": [
|
|
1167
|
+
{
|
|
1168
|
+
"name": "id",
|
|
1169
|
+
"description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
"name": "userId",
|
|
1173
|
+
"description": "The app user's uuid, from `GET /api/apps/:id/users`; a user of another app answers `404`."
|
|
1174
|
+
}
|
|
1175
|
+
],
|
|
1176
|
+
"query": null,
|
|
1177
|
+
"request": null,
|
|
1178
|
+
"response": "mcp-consent-grant-list-response",
|
|
1126
1179
|
"errors": [
|
|
1127
1180
|
"unauthorized",
|
|
1128
1181
|
"token_expired",
|
|
@@ -1230,7 +1283,7 @@
|
|
|
1230
1283
|
"rate_limited"
|
|
1231
1284
|
],
|
|
1232
1285
|
"transport": "http",
|
|
1233
|
-
"notes": "**An app user, not a team member.** `POST /api/org/invitations` is the other space and leads to the console; this link leads into the developer's own app. The role is resolved and stored now, so a later change to `default_role_id` does not re-aim a link already sent. An invitation **always bypasses `allowed_domains`**. \n\nThe answer carries `accept_url
|
|
1286
|
+
"notes": "**An app user, not a team member.** `POST /api/org/invitations` is the other space and leads to the console; this link leads into the developer's own app. The role is resolved and stored now, so a later change to `default_role_id` does not re-aim a link already sent. An invitation **always bypasses `allowed_domains`**. \n\nThe answer carries `accept_url`: the app's `invite_url` with the token in it, or the Fleetless-hosted invitation page when the app has configured none — so mailing it is never refused for a missing URL. `409 target_state_conflict` names `default_role_id` when `role_id` is absent and the app has no default role, or its default no longer resolves: an invitation that names no role has nothing to hand its acceptor, so it is refused here rather than at the acceptance a week later. `409 email_taken` is an address the app already has as a user; `404 not_found` is the app or a `role_id` that is not one of its roles. \n\nCreating shares the reissue route's ceiling of **five invitation mails a minute per app**, answering `429 rate_limited` with `retry_after_ms`: re-creating an invitation for one address replaces it and mails again, so a limit that bound only reissue would be a limit on the wrong door."
|
|
1234
1287
|
},
|
|
1235
1288
|
{
|
|
1236
1289
|
"method": "POST",
|
|
@@ -1467,7 +1520,7 @@
|
|
|
1467
1520
|
"method": "GET",
|
|
1468
1521
|
"path": "/api/apps/:id/auth-config",
|
|
1469
1522
|
"section": "apps",
|
|
1470
|
-
"summary": "Reads the app's auth settings:
|
|
1523
|
+
"summary": "Reads the app's auth settings: sign-in methods, two-factor, registration, pages, the hosted look and the MCP switch.",
|
|
1471
1524
|
"audience": "developer",
|
|
1472
1525
|
"auth": "developer",
|
|
1473
1526
|
"rateLimited": false,
|
|
@@ -1490,7 +1543,7 @@
|
|
|
1490
1543
|
"not_found"
|
|
1491
1544
|
],
|
|
1492
1545
|
"transport": "http",
|
|
1493
|
-
"notes": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a client-supplied value would be a lie about when the row last changed."
|
|
1546
|
+
"notes": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a client-supplied value would be a lie about when the row last changed. `hosted_pages` and `hosted_logo_url` are read-only as well: the cloud mints both from the auth portal's base URL and the app's identifier."
|
|
1494
1547
|
},
|
|
1495
1548
|
{
|
|
1496
1549
|
"method": "PUT",
|
|
@@ -1520,13 +1573,43 @@
|
|
|
1520
1573
|
"not_found"
|
|
1521
1574
|
],
|
|
1522
1575
|
"transport": "http",
|
|
1523
|
-
"notes": "**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. \n\nThe merge is server-side against the stored row, so this write never disturbs
|
|
1576
|
+
"notes": "**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. \n\nThe merge is server-side against the stored row, so this write never disturbs another slice."
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
"method": "PUT",
|
|
1580
|
+
"path": "/api/apps/:id/auth-config/sign-in",
|
|
1581
|
+
"section": "apps",
|
|
1582
|
+
"summary": "Replaces how the app's users sign in and whether they give a second factor.",
|
|
1583
|
+
"audience": "developer",
|
|
1584
|
+
"auth": "developer",
|
|
1585
|
+
"rateLimited": false,
|
|
1586
|
+
"ownerTier": false,
|
|
1587
|
+
"status": 200,
|
|
1588
|
+
"params": [
|
|
1589
|
+
{
|
|
1590
|
+
"name": "id",
|
|
1591
|
+
"description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
|
|
1592
|
+
}
|
|
1593
|
+
],
|
|
1594
|
+
"query": null,
|
|
1595
|
+
"request": "put-app-auth-sign-in-request",
|
|
1596
|
+
"response": "app-auth-config",
|
|
1597
|
+
"errors": [
|
|
1598
|
+
"unauthorized",
|
|
1599
|
+
"token_expired",
|
|
1600
|
+
"token_revoked",
|
|
1601
|
+
"invalid_uuid",
|
|
1602
|
+
"validation_error",
|
|
1603
|
+
"not_found"
|
|
1604
|
+
],
|
|
1605
|
+
"transport": "http",
|
|
1606
|
+
"notes": "**A replace, not a merge, and `.strict()`**: `sign_in_methods` and `two_factor` both arrive or the write is refused. Both methods off is `400 validation_error` naming `sign_in_methods.password` — an app needs at least one door besides its identity providers. \n\nTurning a method off refuses its routes with `method_not_allowed` from the next request on; a stored password stays stored. Setting `two_factor` to `required` signs nobody out: each person without an authenticator sets one up at their next sign-in, before any session exists. Audited with both old and new values. The merge is server-side against the stored row, so this write never disturbs another slice."
|
|
1524
1607
|
},
|
|
1525
1608
|
{
|
|
1526
1609
|
"method": "PUT",
|
|
1527
1610
|
"path": "/api/apps/:id/auth-config/urls",
|
|
1528
1611
|
"section": "apps",
|
|
1529
|
-
"summary": "Replaces the
|
|
1612
|
+
"summary": "Replaces the app's home page and the four pages Fleetless's mails and MCP sign-in point at.",
|
|
1530
1613
|
"audience": "developer",
|
|
1531
1614
|
"auth": "developer",
|
|
1532
1615
|
"rateLimited": false,
|
|
@@ -1550,13 +1633,13 @@
|
|
|
1550
1633
|
"not_found"
|
|
1551
1634
|
],
|
|
1552
1635
|
"transport": "http",
|
|
1553
|
-
"notes": "**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `
|
|
1636
|
+
"notes": "**A replace, not a merge, and `.strict()`**: `app_url`, `invite_url`, `verify_url`, `reset_url` and `mcp_login_url` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\nEach may be `null`, and then the Fleetless-hosted page in `hosted_pages` stands in for it: nothing is refused for a missing URL. `400 validation_error` is where the field rules land: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once — a second occurrence leaves one literal in a mailed link, refused here rather than failing silently once the mail is sent — and `app_url` takes the same host rule with no placeholder. `mcp_login_url` moved here from the `mcp` slice, because one screen owns all four pages. \n\nThe merge is server-side against the stored row, so this write never disturbs another slice."
|
|
1554
1637
|
},
|
|
1555
1638
|
{
|
|
1556
1639
|
"method": "PUT",
|
|
1557
1640
|
"path": "/api/apps/:id/auth-config/mcp",
|
|
1558
1641
|
"section": "apps",
|
|
1559
|
-
"summary": "
|
|
1642
|
+
"summary": "Turns the app's MCP endpoint on or off.",
|
|
1560
1643
|
"audience": "developer",
|
|
1561
1644
|
"auth": "developer",
|
|
1562
1645
|
"rateLimited": false,
|
|
@@ -1580,7 +1663,97 @@
|
|
|
1580
1663
|
"not_found"
|
|
1581
1664
|
],
|
|
1582
1665
|
"transport": "http",
|
|
1583
|
-
"notes": "**A replace, not a merge, and `.strict()`**: `mcp_enabled`
|
|
1666
|
+
"notes": "**A replace, not a merge, and `.strict()`**: `mcp_enabled` arrives or the write is refused. It used to take `mcp_login_url` as well, because on without a URL refused every sign-in; the hosted MCP sign-in now stands in for an unset URL, and the URL moved to the `urls` slice. A body still carrying it is `400 validation_error`. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's. The merge is server-side against the stored row, so this write never disturbs another slice."
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
"method": "PUT",
|
|
1670
|
+
"path": "/api/apps/:id/auth-config/look",
|
|
1671
|
+
"section": "apps",
|
|
1672
|
+
"summary": "Replaces the hosted pages' accent colour.",
|
|
1673
|
+
"audience": "developer",
|
|
1674
|
+
"auth": "developer",
|
|
1675
|
+
"rateLimited": false,
|
|
1676
|
+
"ownerTier": false,
|
|
1677
|
+
"status": 200,
|
|
1678
|
+
"params": [
|
|
1679
|
+
{
|
|
1680
|
+
"name": "id",
|
|
1681
|
+
"description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
|
|
1682
|
+
}
|
|
1683
|
+
],
|
|
1684
|
+
"query": null,
|
|
1685
|
+
"request": "put-app-auth-look-request",
|
|
1686
|
+
"response": "app-auth-config",
|
|
1687
|
+
"errors": [
|
|
1688
|
+
"unauthorized",
|
|
1689
|
+
"token_expired",
|
|
1690
|
+
"token_revoked",
|
|
1691
|
+
"invalid_uuid",
|
|
1692
|
+
"validation_error",
|
|
1693
|
+
"not_found"
|
|
1694
|
+
],
|
|
1695
|
+
"transport": "http",
|
|
1696
|
+
"notes": "**A replace, and `.strict()`**: `hosted_accent` arrives, `#rrggbb` in lowercase, or `null` for the neutral shell's own accent. The logo is its own write, `PUT /api/apps/:id/auth-config/logo`, because it is an image rather than a field. The merge is server-side against the stored row, so this write never disturbs another slice."
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
"method": "PUT",
|
|
1700
|
+
"path": "/api/apps/:id/auth-config/logo",
|
|
1701
|
+
"section": "apps",
|
|
1702
|
+
"summary": "Stores the logo the hosted pages show above the app's name.",
|
|
1703
|
+
"audience": "developer",
|
|
1704
|
+
"auth": "developer",
|
|
1705
|
+
"rateLimited": false,
|
|
1706
|
+
"ownerTier": false,
|
|
1707
|
+
"status": 200,
|
|
1708
|
+
"params": [
|
|
1709
|
+
{
|
|
1710
|
+
"name": "id",
|
|
1711
|
+
"description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
|
|
1712
|
+
}
|
|
1713
|
+
],
|
|
1714
|
+
"query": null,
|
|
1715
|
+
"request": null,
|
|
1716
|
+
"response": "app-auth-config",
|
|
1717
|
+
"errors": [
|
|
1718
|
+
"unauthorized",
|
|
1719
|
+
"token_expired",
|
|
1720
|
+
"token_revoked",
|
|
1721
|
+
"invalid_uuid",
|
|
1722
|
+
"validation_error",
|
|
1723
|
+
"not_found",
|
|
1724
|
+
"unsupported_media_type"
|
|
1725
|
+
],
|
|
1726
|
+
"transport": "http",
|
|
1727
|
+
"notes": "The body is the **raw image**, not JSON, so it has no request schema: `Content-Type` is one of `HOSTED_LOGO_TYPES` (`image/png`, `image/svg+xml`) and anything else is `415 unsupported_media_type`. At most `HOSTED_LOGO_MAX_BYTES` (100 KB); a larger body, or one that is not the image its type names, is `400 validation_error`. A new logo replaces the stored one. The hosted pages load it from `hosted_logo_url` as an image only, and the cloud serves an SVG sandboxed, so a script inside one never runs."
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
"method": "DELETE",
|
|
1731
|
+
"path": "/api/apps/:id/auth-config/logo",
|
|
1732
|
+
"section": "apps",
|
|
1733
|
+
"summary": "Removes the logo from the hosted pages.",
|
|
1734
|
+
"audience": "developer",
|
|
1735
|
+
"auth": "developer",
|
|
1736
|
+
"rateLimited": false,
|
|
1737
|
+
"ownerTier": false,
|
|
1738
|
+
"status": 200,
|
|
1739
|
+
"params": [
|
|
1740
|
+
{
|
|
1741
|
+
"name": "id",
|
|
1742
|
+
"description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
|
|
1743
|
+
}
|
|
1744
|
+
],
|
|
1745
|
+
"query": null,
|
|
1746
|
+
"request": null,
|
|
1747
|
+
"response": "app-auth-config",
|
|
1748
|
+
"errors": [
|
|
1749
|
+
"unauthorized",
|
|
1750
|
+
"token_expired",
|
|
1751
|
+
"token_revoked",
|
|
1752
|
+
"invalid_uuid",
|
|
1753
|
+
"not_found"
|
|
1754
|
+
],
|
|
1755
|
+
"transport": "http",
|
|
1756
|
+
"notes": "Answers the whole configuration, with `hosted_logo_url` now `null`; the hosted pages show the app's name alone. An app with no logo answers the same: that is the end state being asked for."
|
|
1584
1757
|
},
|
|
1585
1758
|
{
|
|
1586
1759
|
"method": "GET",
|
|
@@ -1609,7 +1782,7 @@
|
|
|
1609
1782
|
"not_found"
|
|
1610
1783
|
],
|
|
1611
1784
|
"transport": "http",
|
|
1612
|
-
"notes": "Answers `{ \"templates\": [appMailTemplate, …] }` with **only the kinds that have a custom template** — at most
|
|
1785
|
+
"notes": "Answers `{ \"templates\": [appMailTemplate, …] }` with **only the kinds that have a custom template** — at most four. A kind that does not appear is one using the Fleetless default text, which is an ordinary state and not a missing row. Mails to *Fleetless* users, a team invitation or a console sign-in code, are not in this list and are deliberately not customisable: they are about this platform, not about the developer's product."
|
|
1613
1786
|
},
|
|
1614
1787
|
{
|
|
1615
1788
|
"method": "GET",
|
|
@@ -1628,7 +1801,7 @@
|
|
|
1628
1801
|
},
|
|
1629
1802
|
{
|
|
1630
1803
|
"name": "kind",
|
|
1631
|
-
"description": "Which of the
|
|
1804
|
+
"description": "Which of the four mails this template replaces — a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`."
|
|
1632
1805
|
}
|
|
1633
1806
|
],
|
|
1634
1807
|
"query": null,
|
|
@@ -1662,7 +1835,7 @@
|
|
|
1662
1835
|
},
|
|
1663
1836
|
{
|
|
1664
1837
|
"name": "kind",
|
|
1665
|
-
"description": "Which of the
|
|
1838
|
+
"description": "Which of the four mails this template replaces — a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`."
|
|
1666
1839
|
}
|
|
1667
1840
|
],
|
|
1668
1841
|
"query": null,
|
|
@@ -1698,7 +1871,7 @@
|
|
|
1698
1871
|
},
|
|
1699
1872
|
{
|
|
1700
1873
|
"name": "kind",
|
|
1701
|
-
"description": "Which of the
|
|
1874
|
+
"description": "Which of the four mails this template replaces — a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`."
|
|
1702
1875
|
}
|
|
1703
1876
|
],
|
|
1704
1877
|
"query": null,
|
|
@@ -1732,7 +1905,7 @@
|
|
|
1732
1905
|
},
|
|
1733
1906
|
{
|
|
1734
1907
|
"name": "kind",
|
|
1735
|
-
"description": "Which of the
|
|
1908
|
+
"description": "Which of the four mails this template replaces — a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`."
|
|
1736
1909
|
}
|
|
1737
1910
|
],
|
|
1738
1911
|
"query": null,
|
|
@@ -1768,7 +1941,7 @@
|
|
|
1768
1941
|
},
|
|
1769
1942
|
{
|
|
1770
1943
|
"name": "kind",
|
|
1771
|
-
"description": "Which of the
|
|
1944
|
+
"description": "Which of the four mails this template replaces — a `mailTemplateKind`: `invite`, `verify`, `reset` or `login_code`."
|
|
1772
1945
|
}
|
|
1773
1946
|
],
|
|
1774
1947
|
"query": null,
|
|
@@ -1949,7 +2122,7 @@
|
|
|
1949
2122
|
"method": "POST",
|
|
1950
2123
|
"path": "/api/org/invitations/accept",
|
|
1951
2124
|
"section": "users",
|
|
1952
|
-
"summary": "Spends an invitation token and creates the
|
|
2125
|
+
"summary": "Spends an invitation token and creates the account it was addressed to.",
|
|
1953
2126
|
"audience": "developer",
|
|
1954
2127
|
"auth": "none",
|
|
1955
2128
|
"rateLimited": true,
|
|
@@ -1966,7 +2139,7 @@
|
|
|
1966
2139
|
"email_taken"
|
|
1967
2140
|
],
|
|
1968
2141
|
"transport": "http",
|
|
1969
|
-
"notes": "**`204`, not a session.** The console signs in through its own OAuth portal, so a session minted here would be a second credential door for one account — and every security property would then have to be right in two places. Unknown, expired and already-accepted tokens collapse into `410 token_spent`. A browser form post gets the rendered \"you're in\" page instead."
|
|
2142
|
+
"notes": "**`204`, not a session.** The console signs in through its own OAuth portal, so a session minted here would be a second credential door for one account — and every security property would then have to be right in two places. **No password**: the mailed link proves the address, so accepting needs no code either, and the new member signs in by emailed code from then on. When the organisation requires two-factor, the member sets one up at their first sign-in. Unknown, expired and already-accepted tokens collapse into `410 token_spent`. A browser form post gets the rendered \"you're in\" page instead."
|
|
1970
2143
|
},
|
|
1971
2144
|
{
|
|
1972
2145
|
"method": "GET",
|
|
@@ -1989,7 +2162,7 @@
|
|
|
1989
2162
|
"response": null,
|
|
1990
2163
|
"errors": [],
|
|
1991
2164
|
"transport": "http",
|
|
1992
|
-
"notes": "HTML, served by the cloud from the auth portal origin; the form on it posts to `POST /api/org/invitations/accept`. An unknown, spent or expired token renders the \"link no longer valid\" page at `410`, which
|
|
2165
|
+
"notes": "HTML, served by the cloud from the auth portal origin; the form on it posts to `POST /api/org/invitations/accept`. **The GET spends nothing** — a mail scanner opening the link must not accept the invitation — only the form's POST does. An unknown, spent or expired token renders the \"link no longer valid\" page at `410`, which says to ask the organisation for a new invitation: the person holding a dead link cannot re-issue it."
|
|
1993
2166
|
},
|
|
1994
2167
|
{
|
|
1995
2168
|
"method": "PATCH",
|
|
@@ -2084,11 +2257,42 @@
|
|
|
2084
2257
|
"transport": "http",
|
|
2085
2258
|
"notes": "Owner tier, unconditionally — this is the route the whole owner-exclusive list is about. A uuid that is not a Fleetless user of this org answers `404 not_found`, the same as one that does not exist anywhere: the `409 target_state_conflict` documented here until the two-space cut had exactly one producer, the Org Admins membership check, and went with it. Demoting the last Owner is `409 last_owner`, decided by a row lock inside the writing transaction rather than by a read beforehand. Setting the tier already held changes nothing and writes no audit event. No session is revoked: a tier is re-read from the row on every request, so no issued token carries a stale copy of it."
|
|
2086
2259
|
},
|
|
2260
|
+
{
|
|
2261
|
+
"method": "DELETE",
|
|
2262
|
+
"path": "/api/org/users/:id/two-factor",
|
|
2263
|
+
"section": "users",
|
|
2264
|
+
"summary": "Removes a team member's passkeys, authenticator and recovery codes and ends their sessions.",
|
|
2265
|
+
"audience": "developer",
|
|
2266
|
+
"auth": "developer",
|
|
2267
|
+
"rateLimited": false,
|
|
2268
|
+
"ownerTier": true,
|
|
2269
|
+
"status": 204,
|
|
2270
|
+
"params": [
|
|
2271
|
+
{
|
|
2272
|
+
"name": "id",
|
|
2273
|
+
"description": "The Fleetless user's uuid, as listed by `GET /api/org/users`."
|
|
2274
|
+
}
|
|
2275
|
+
],
|
|
2276
|
+
"query": null,
|
|
2277
|
+
"request": null,
|
|
2278
|
+
"response": null,
|
|
2279
|
+
"errors": [
|
|
2280
|
+
"unauthorized",
|
|
2281
|
+
"token_expired",
|
|
2282
|
+
"token_revoked",
|
|
2283
|
+
"tier_required",
|
|
2284
|
+
"invalid_uuid",
|
|
2285
|
+
"not_found",
|
|
2286
|
+
"target_state_conflict"
|
|
2287
|
+
],
|
|
2288
|
+
"transport": "http",
|
|
2289
|
+
"notes": "Owner tier: the door for a member who lost every second factor and every recovery code. Every session of the member ends, and when the organisation requires two-factor they set one up again at their next sign-in. **An owner cannot reset their own** — `409 target_state_conflict` naming `user_id` with rule `self`; Settings › Profile is where they change it. A member with no second factor answers `204` too. Audited as `developer.two_factor_reset`, naming the owner who did it."
|
|
2290
|
+
},
|
|
2087
2291
|
{
|
|
2088
2292
|
"method": "PATCH",
|
|
2089
2293
|
"path": "/api/org",
|
|
2090
2294
|
"section": "org",
|
|
2091
|
-
"summary": "Renames the org.",
|
|
2295
|
+
"summary": "Renames the org, requires two-factor for its members, or both.",
|
|
2092
2296
|
"audience": "developer",
|
|
2093
2297
|
"auth": "developer",
|
|
2094
2298
|
"rateLimited": false,
|
|
@@ -2106,7 +2310,7 @@
|
|
|
2106
2310
|
"validation_error"
|
|
2107
2311
|
],
|
|
2108
2312
|
"transport": "http",
|
|
2109
|
-
"notes": "Answers `{ \"org\": org }`. Owner tier, and the gate runs before the body is looked at, so a malformed
|
|
2313
|
+
"notes": "Answers `{ \"org\": org }`. Owner tier, and the gate runs before the body is looked at, so a malformed patch and a forbidden one answer the same way — `403 tier_required` for a developer, whichever field they sent. An empty body is `400 validation_error`. Writing the values already held writes nothing and records no audit event. \n\n`require_two_factor` on signs nobody out: each member without a passkey or authenticator sets one up at their next sign-in, before any session exists, on the console and the central MCP endpoint alike. Server keys and robot bridges are not people and are not affected. Audited as `org.two_factor_required_changed`."
|
|
2110
2314
|
},
|
|
2111
2315
|
{
|
|
2112
2316
|
"method": "GET",
|
|
@@ -2180,7 +2384,7 @@
|
|
|
2180
2384
|
"response": null,
|
|
2181
2385
|
"errors": [],
|
|
2182
2386
|
"transport": "http",
|
|
2183
|
-
"notes": "**The query schema is what this endpoint accepts, not what it parses**: the handler reads it parameter by parameter because the answers differ, and one parse would collapse them. Client and `redirect_uri` are validated first and a failure there never redirects, the same open-redirect discipline the app flow applies; those refusals are `oauthError`. Exact `redirect_uri` matching for both client kinds — the loopback-port wildcard of RFC 8252 §7.3 belongs to the one central client alone, whose URIs are configured ahead of time and cannot name an ephemeral port. A client that registered itself seconds ago can name the port it bound, and widening the wildcard there would only widen where a stolen `client_id` may send a browser. Nothing about the person is decided here — the next card asks for an email address and the
|
|
2387
|
+
"notes": "**The query schema is what this endpoint accepts, not what it parses**: the handler reads it parameter by parameter because the answers differ, and one parse would collapse them. Client and `redirect_uri` are validated first and a failure there never redirects, the same open-redirect discipline the app flow applies; those refusals are `oauthError`. Exact `redirect_uri` matching for both client kinds — the loopback-port wildcard of RFC 8252 §7.3 belongs to the one central client alone, whose URIs are configured ahead of time and cannot name an ephemeral port. A client that registered itself seconds ago can name the port it bound, and widening the wildcard there would only widen where a stolen `client_id` may send a browser. Nothing about the person is decided here — the next card asks for an email address, or a passkey, and the steps after it resolve the account; this route knows only the client."
|
|
2184
2388
|
},
|
|
2185
2389
|
{
|
|
2186
2390
|
"method": "GET",
|
|
@@ -2203,13 +2407,13 @@
|
|
|
2203
2407
|
"response": null,
|
|
2204
2408
|
"errors": [],
|
|
2205
2409
|
"transport": "http",
|
|
2206
|
-
"notes": "HTML, and a GET rather than the body of the authorize response — so it is reloadable, bookmarkable and survives a back button, which the inline page it replaced was not. An expired, consumed, unknown or hand-edited interaction renders one page at `410`, and so does a client whose dynamic registration lapsed in between."
|
|
2410
|
+
"notes": "HTML, and a GET rather than the body of the authorize response — so it is reloadable, bookmarkable and survives a back button, which the inline page it replaced was not. An expired, consumed, unknown or hand-edited interaction renders one page at `410`, and so does a client whose dynamic registration lapsed in between. It offers the email field and `Sign in with a passkey`, and opening it binds the interaction to this browser with the browser-proof cookie, which every sign-in step after it checks."
|
|
2207
2411
|
},
|
|
2208
2412
|
{
|
|
2209
2413
|
"method": "POST",
|
|
2210
2414
|
"path": "/mcp/oauth/identify",
|
|
2211
2415
|
"section": "mcp",
|
|
2212
|
-
"summary": "Takes the email address and hands back the
|
|
2416
|
+
"summary": "Takes the email address, mails a sign-in code and hands back the code step.",
|
|
2213
2417
|
"audience": "internal",
|
|
2214
2418
|
"auth": "none",
|
|
2215
2419
|
"rateLimited": true,
|
|
@@ -2222,16 +2426,17 @@
|
|
|
2222
2426
|
"errors": [
|
|
2223
2427
|
"rate_limited",
|
|
2224
2428
|
"validation_error",
|
|
2225
|
-
"token_spent"
|
|
2429
|
+
"token_spent",
|
|
2430
|
+
"wrong_browser"
|
|
2226
2431
|
],
|
|
2227
2432
|
"transport": "http",
|
|
2228
|
-
"notes": "The
|
|
2433
|
+
"notes": "The page answers `Check your email` **for every address**: a known one gets `Your Fleetless sign-in code`, six digits valid ten minutes; an unknown one gets a mail saying no Fleetless account uses it, with a link to sign up (or the waiting list while sign-up is closed). So the page never reveals who has an account, and the address is trimmed and compared case-insensitively. A request within sixty seconds of the last one for the same address renders the same page without a second mail. The browser-proof cookie is set here when the interaction has none yet, and checked when it has. A browser form post gets the code card; a JSON caller gets `{ \"next\": \"/mcp/oauth/code\" }`, which has no schema. A dead interaction is `410 token_spent`."
|
|
2229
2434
|
},
|
|
2230
2435
|
{
|
|
2231
2436
|
"method": "POST",
|
|
2232
|
-
"path": "/mcp/oauth/
|
|
2437
|
+
"path": "/mcp/oauth/code",
|
|
2233
2438
|
"section": "mcp",
|
|
2234
|
-
"summary": "Checks the
|
|
2439
|
+
"summary": "Checks the emailed code and finishes the sign-in, or hands back the second step.",
|
|
2235
2440
|
"audience": "internal",
|
|
2236
2441
|
"auth": "none",
|
|
2237
2442
|
"rateLimited": true,
|
|
@@ -2245,16 +2450,17 @@
|
|
|
2245
2450
|
"rate_limited",
|
|
2246
2451
|
"validation_error",
|
|
2247
2452
|
"token_spent",
|
|
2248
|
-
"
|
|
2453
|
+
"wrong_browser",
|
|
2454
|
+
"invalid_code"
|
|
2249
2455
|
],
|
|
2250
2456
|
"transport": "http",
|
|
2251
|
-
"notes": "
|
|
2457
|
+
"notes": "A wrong code renders the code card again with the attempts left (`400 invalid_code`, `details.attempts_left`); a code spent, past its ten minutes or out of its five attempts is `410 token_spent` and a new one has to be asked for. Spaces inside a typed code are removed before it is checked. With no second factor to give, a browser gets a `303` to the consent screen for a self-registered client, or straight to the callback for the central one, and a JSON caller that URL as `redirect_to`. Otherwise the next step: a JSON caller gets `{ \"next\" }` naming `/mcp/oauth/two-factor` — the person has a passkey or an authenticator — or `/mcp/oauth/two-factor/setup` — the organisation requires one and the person has none — and a browser the page itself. Audited as `developer.login` with `details.method` `email_code` once the sign-in completes."
|
|
2252
2458
|
},
|
|
2253
2459
|
{
|
|
2254
2460
|
"method": "GET",
|
|
2255
|
-
"path": "/mcp/oauth/
|
|
2461
|
+
"path": "/mcp/oauth/two-factor/:id",
|
|
2256
2462
|
"section": "mcp",
|
|
2257
|
-
"summary": "Serves the
|
|
2463
|
+
"summary": "Serves the second step: the authenticator code, or the passkey prompt.",
|
|
2258
2464
|
"audience": "internal",
|
|
2259
2465
|
"auth": "none",
|
|
2260
2466
|
"rateLimited": false,
|
|
@@ -2263,7 +2469,7 @@
|
|
|
2263
2469
|
"params": [
|
|
2264
2470
|
{
|
|
2265
2471
|
"name": "id",
|
|
2266
|
-
"description": "The interaction id
|
|
2472
|
+
"description": "The interaction id of this sign-in; the step before redirects the browser here."
|
|
2267
2473
|
}
|
|
2268
2474
|
],
|
|
2269
2475
|
"query": null,
|
|
@@ -2271,13 +2477,36 @@
|
|
|
2271
2477
|
"response": null,
|
|
2272
2478
|
"errors": [],
|
|
2273
2479
|
"transport": "http",
|
|
2274
|
-
"notes": "HTML.
|
|
2480
|
+
"notes": "HTML. Six boxes for the authenticator code, `Use a passkey`, and `Use a recovery code`; a developer with passkeys only sees the passkey prompt directly. The browser-proof cookie is checked on this GET too. A dead interaction renders the `410` page."
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
"method": "GET",
|
|
2484
|
+
"path": "/mcp/oauth/two-factor/:id/recovery",
|
|
2485
|
+
"section": "mcp",
|
|
2486
|
+
"summary": "Serves the recovery-code page of the second step.",
|
|
2487
|
+
"audience": "internal",
|
|
2488
|
+
"auth": "none",
|
|
2489
|
+
"rateLimited": false,
|
|
2490
|
+
"ownerTier": false,
|
|
2491
|
+
"status": 200,
|
|
2492
|
+
"params": [
|
|
2493
|
+
{
|
|
2494
|
+
"name": "id",
|
|
2495
|
+
"description": "The interaction id of this sign-in; the step before redirects the browser here."
|
|
2496
|
+
}
|
|
2497
|
+
],
|
|
2498
|
+
"query": null,
|
|
2499
|
+
"request": null,
|
|
2500
|
+
"response": null,
|
|
2501
|
+
"errors": [],
|
|
2502
|
+
"transport": "http",
|
|
2503
|
+
"notes": "HTML. One field for a recovery code, and the way out when none is left: an owner of the organisation can reset the member's two-factor in Settings › Team."
|
|
2275
2504
|
},
|
|
2276
2505
|
{
|
|
2277
2506
|
"method": "POST",
|
|
2278
|
-
"path": "/mcp/oauth/
|
|
2507
|
+
"path": "/mcp/oauth/two-factor",
|
|
2279
2508
|
"section": "mcp",
|
|
2280
|
-
"summary": "
|
|
2509
|
+
"summary": "Checks an authenticator code or a recovery code and finishes the sign-in.",
|
|
2281
2510
|
"audience": "internal",
|
|
2282
2511
|
"auth": "none",
|
|
2283
2512
|
"rateLimited": true,
|
|
@@ -2290,52 +2519,64 @@
|
|
|
2290
2519
|
"errors": [
|
|
2291
2520
|
"rate_limited",
|
|
2292
2521
|
"validation_error",
|
|
2293
|
-
"token_spent"
|
|
2522
|
+
"token_spent",
|
|
2523
|
+
"wrong_browser",
|
|
2524
|
+
"invalid_code"
|
|
2294
2525
|
],
|
|
2295
2526
|
"transport": "http",
|
|
2296
|
-
"notes": "
|
|
2527
|
+
"notes": "The form carries either `code` or `recovery_code`. **An authenticator code is accepted at most once**, so the same code sent twice finishes one sign-in. A recovery code is spent by its use. Five wrong codes end the interaction (`410 token_spent`). A browser gets a `303` to the consent screen for a self-registered client, or straight to the callback for the central one; a JSON caller that URL as `redirect_to`."
|
|
2297
2528
|
},
|
|
2298
2529
|
{
|
|
2299
2530
|
"method": "POST",
|
|
2300
|
-
"path": "/mcp/oauth/
|
|
2531
|
+
"path": "/mcp/oauth/passkey/options",
|
|
2301
2532
|
"section": "mcp",
|
|
2302
|
-
"summary": "
|
|
2303
|
-
"audience": "
|
|
2533
|
+
"summary": "Answers the WebAuthn request options for a passkey sign-in or second step.",
|
|
2534
|
+
"audience": "internal",
|
|
2304
2535
|
"auth": "none",
|
|
2305
|
-
"rateLimited":
|
|
2536
|
+
"rateLimited": true,
|
|
2306
2537
|
"ownerTier": false,
|
|
2307
2538
|
"status": 200,
|
|
2308
2539
|
"params": [],
|
|
2309
2540
|
"query": null,
|
|
2310
|
-
"request":
|
|
2311
|
-
"response": "
|
|
2312
|
-
"errors": [
|
|
2541
|
+
"request": null,
|
|
2542
|
+
"response": "webauthn-options-response",
|
|
2543
|
+
"errors": [
|
|
2544
|
+
"rate_limited",
|
|
2545
|
+
"token_spent",
|
|
2546
|
+
"wrong_browser"
|
|
2547
|
+
],
|
|
2313
2548
|
"transport": "http",
|
|
2314
|
-
"notes": "
|
|
2549
|
+
"notes": "Before an address is known the options name no credential, so the browser offers every discoverable passkey for `fleetless.dev` (`Sign in with a passkey`); after the code step they name the account's own passkeys. User verification is required. The challenge is bound to the interaction and single-use. **This is the first step of a passkey sign-in**, which has no email step: the browser-proof cookie is set here when the interaction has none yet, and checked when it has — so `POST /mcp/oauth/passkey` can require it."
|
|
2315
2550
|
},
|
|
2316
2551
|
{
|
|
2317
|
-
"method": "
|
|
2318
|
-
"path": "/
|
|
2319
|
-
"section": "
|
|
2320
|
-
"summary": "
|
|
2552
|
+
"method": "POST",
|
|
2553
|
+
"path": "/mcp/oauth/passkey",
|
|
2554
|
+
"section": "mcp",
|
|
2555
|
+
"summary": "Checks a passkey assertion; a passkey completes the sign-in on its own.",
|
|
2321
2556
|
"audience": "internal",
|
|
2322
2557
|
"auth": "none",
|
|
2323
|
-
"rateLimited":
|
|
2558
|
+
"rateLimited": true,
|
|
2324
2559
|
"ownerTier": false,
|
|
2325
|
-
"status":
|
|
2560
|
+
"status": 200,
|
|
2326
2561
|
"params": [],
|
|
2327
2562
|
"query": null,
|
|
2328
2563
|
"request": null,
|
|
2329
|
-
"response":
|
|
2330
|
-
"errors": [
|
|
2564
|
+
"response": "oauth-redirect-response",
|
|
2565
|
+
"errors": [
|
|
2566
|
+
"rate_limited",
|
|
2567
|
+
"validation_error",
|
|
2568
|
+
"token_spent",
|
|
2569
|
+
"wrong_browser",
|
|
2570
|
+
"invalid_credentials"
|
|
2571
|
+
],
|
|
2331
2572
|
"transport": "http",
|
|
2332
|
-
"notes": "
|
|
2573
|
+
"notes": "**A passkey is a full sign-in**: it proves possession and user verification, two factors, so it skips the emailed code and the second step — used as the second step, it finishes it. An assertion that does not verify, or names no passkey of an account, is `401 invalid_credentials`, the same answer for both. When the organisation requires two-factor, a passkey satisfies it. A browser gets a `303` to the consent screen for a self-registered client, or straight to the callback for the central one; a JSON caller that URL as `redirect_to`. Audited as `developer.login` with `details.method` `passkey`."
|
|
2333
2574
|
},
|
|
2334
2575
|
{
|
|
2335
2576
|
"method": "GET",
|
|
2336
|
-
"path": "/
|
|
2337
|
-
"section": "
|
|
2338
|
-
"summary": "Serves the \"
|
|
2577
|
+
"path": "/mcp/oauth/two-factor/setup/:id",
|
|
2578
|
+
"section": "mcp",
|
|
2579
|
+
"summary": "Serves the \"your organisation requires two-factor\" choice between a passkey and an authenticator.",
|
|
2339
2580
|
"audience": "internal",
|
|
2340
2581
|
"auth": "none",
|
|
2341
2582
|
"rateLimited": false,
|
|
@@ -2344,64 +2585,1045 @@
|
|
|
2344
2585
|
"params": [
|
|
2345
2586
|
{
|
|
2346
2587
|
"name": "id",
|
|
2347
|
-
"description": "The interaction id
|
|
2588
|
+
"description": "The interaction id of this sign-in; the step before redirects the browser here."
|
|
2589
|
+
}
|
|
2590
|
+
],
|
|
2591
|
+
"query": null,
|
|
2592
|
+
"request": null,
|
|
2593
|
+
"response": null,
|
|
2594
|
+
"errors": [],
|
|
2595
|
+
"transport": "http",
|
|
2596
|
+
"notes": "HTML. Reached when the organisation requires two-factor and the person has none; no session exists until the setup is done. Two options, the passkey recommended because it also signs the person in without an emailed code. `Signed in as <email> · Sign out` under it."
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
"method": "POST",
|
|
2600
|
+
"path": "/mcp/oauth/two-factor/setup/totp",
|
|
2601
|
+
"section": "mcp",
|
|
2602
|
+
"summary": "Starts an authenticator setup inside the sign-in and hands back its QR code and key.",
|
|
2603
|
+
"audience": "internal",
|
|
2604
|
+
"auth": "none",
|
|
2605
|
+
"rateLimited": true,
|
|
2606
|
+
"ownerTier": false,
|
|
2607
|
+
"status": 200,
|
|
2608
|
+
"params": [],
|
|
2609
|
+
"query": null,
|
|
2610
|
+
"request": null,
|
|
2611
|
+
"response": "two-factor-setup-response",
|
|
2612
|
+
"errors": [
|
|
2613
|
+
"rate_limited",
|
|
2614
|
+
"token_spent",
|
|
2615
|
+
"wrong_browser"
|
|
2616
|
+
],
|
|
2617
|
+
"transport": "http",
|
|
2618
|
+
"notes": "A browser gets the page with the QR code, the key and the confirm field; a JSON caller the secret and `otpauth_url`. Nothing is stored as confirmed yet."
|
|
2619
|
+
},
|
|
2620
|
+
{
|
|
2621
|
+
"method": "POST",
|
|
2622
|
+
"path": "/mcp/oauth/two-factor/setup/totp/confirm",
|
|
2623
|
+
"section": "mcp",
|
|
2624
|
+
"summary": "Confirms the new authenticator and hands back the ten recovery codes.",
|
|
2625
|
+
"audience": "internal",
|
|
2626
|
+
"auth": "none",
|
|
2627
|
+
"rateLimited": true,
|
|
2628
|
+
"ownerTier": false,
|
|
2629
|
+
"status": 200,
|
|
2630
|
+
"params": [],
|
|
2631
|
+
"query": null,
|
|
2632
|
+
"request": null,
|
|
2633
|
+
"response": "recovery-codes-response",
|
|
2634
|
+
"errors": [
|
|
2635
|
+
"rate_limited",
|
|
2636
|
+
"validation_error",
|
|
2637
|
+
"token_spent",
|
|
2638
|
+
"wrong_browser",
|
|
2639
|
+
"invalid_code"
|
|
2640
|
+
],
|
|
2641
|
+
"transport": "http",
|
|
2642
|
+
"notes": "A code that does not match is `400 invalid_code`. On success the authenticator is on and ten recovery codes are shown once; `I saved my recovery codes` then finishes the sign-in. Audited as `developer.two_factor_added` with `details.kind` `authenticator`."
|
|
2643
|
+
},
|
|
2644
|
+
{
|
|
2645
|
+
"method": "POST",
|
|
2646
|
+
"path": "/mcp/oauth/two-factor/setup/passkey/options",
|
|
2647
|
+
"section": "mcp",
|
|
2648
|
+
"summary": "Answers the WebAuthn creation options for a passkey set up inside the sign-in.",
|
|
2649
|
+
"audience": "internal",
|
|
2650
|
+
"auth": "none",
|
|
2651
|
+
"rateLimited": true,
|
|
2652
|
+
"ownerTier": false,
|
|
2653
|
+
"status": 200,
|
|
2654
|
+
"params": [],
|
|
2655
|
+
"query": null,
|
|
2656
|
+
"request": null,
|
|
2657
|
+
"response": "webauthn-options-response",
|
|
2658
|
+
"errors": [
|
|
2659
|
+
"rate_limited",
|
|
2660
|
+
"token_spent",
|
|
2661
|
+
"wrong_browser"
|
|
2662
|
+
],
|
|
2663
|
+
"transport": "http",
|
|
2664
|
+
"notes": "The same options `POST /api/auth/passkeys/options` answers a signed-in developer: relying party `fleetless.dev`, user verification required, a discoverable credential."
|
|
2665
|
+
},
|
|
2666
|
+
{
|
|
2667
|
+
"method": "POST",
|
|
2668
|
+
"path": "/mcp/oauth/two-factor/setup/passkey",
|
|
2669
|
+
"section": "mcp",
|
|
2670
|
+
"summary": "Registers the passkey and hands back the ten recovery codes.",
|
|
2671
|
+
"audience": "internal",
|
|
2672
|
+
"auth": "none",
|
|
2673
|
+
"rateLimited": true,
|
|
2674
|
+
"ownerTier": false,
|
|
2675
|
+
"status": 200,
|
|
2676
|
+
"params": [],
|
|
2677
|
+
"query": null,
|
|
2678
|
+
"request": null,
|
|
2679
|
+
"response": "recovery-codes-response",
|
|
2680
|
+
"errors": [
|
|
2681
|
+
"rate_limited",
|
|
2682
|
+
"validation_error",
|
|
2683
|
+
"token_spent",
|
|
2684
|
+
"wrong_browser"
|
|
2685
|
+
],
|
|
2686
|
+
"transport": "http",
|
|
2687
|
+
"notes": "A ceremony that does not verify is `400 validation_error`. On success the passkey is stored, named after the browser's device where it says so, and ten recovery codes are shown once. Audited as `developer.two_factor_added` with `details.kind` `passkey`."
|
|
2688
|
+
},
|
|
2689
|
+
{
|
|
2690
|
+
"method": "POST",
|
|
2691
|
+
"path": "/mcp/oauth/two-factor/setup/done",
|
|
2692
|
+
"section": "mcp",
|
|
2693
|
+
"summary": "Finishes the sign-in once the recovery codes are saved.",
|
|
2694
|
+
"audience": "internal",
|
|
2695
|
+
"auth": "none",
|
|
2696
|
+
"rateLimited": true,
|
|
2697
|
+
"ownerTier": false,
|
|
2698
|
+
"status": 200,
|
|
2699
|
+
"params": [],
|
|
2700
|
+
"query": null,
|
|
2701
|
+
"request": null,
|
|
2702
|
+
"response": "oauth-redirect-response",
|
|
2703
|
+
"errors": [
|
|
2704
|
+
"rate_limited",
|
|
2705
|
+
"token_spent",
|
|
2706
|
+
"wrong_browser"
|
|
2707
|
+
],
|
|
2708
|
+
"transport": "http",
|
|
2709
|
+
"notes": "`I saved my recovery codes` gates the button on the page; the step itself only checks that a second factor now exists. A browser gets a `303` to the consent screen for a self-registered client, or straight to the callback for the central one; a JSON caller that URL as `redirect_to`."
|
|
2710
|
+
},
|
|
2711
|
+
{
|
|
2712
|
+
"method": "GET",
|
|
2713
|
+
"path": "/mcp/oauth/consent/:id",
|
|
2714
|
+
"section": "mcp",
|
|
2715
|
+
"summary": "Serves the consent screen for an MCP client that registered itself.",
|
|
2716
|
+
"audience": "internal",
|
|
2717
|
+
"auth": "none",
|
|
2718
|
+
"rateLimited": false,
|
|
2719
|
+
"ownerTier": false,
|
|
2720
|
+
"status": 200,
|
|
2721
|
+
"params": [
|
|
2722
|
+
{
|
|
2723
|
+
"name": "id",
|
|
2724
|
+
"description": "The interaction id from the sign-in; the last sign-in step redirects the browser here."
|
|
2725
|
+
}
|
|
2726
|
+
],
|
|
2727
|
+
"query": null,
|
|
2728
|
+
"request": null,
|
|
2729
|
+
"response": null,
|
|
2730
|
+
"errors": [],
|
|
2731
|
+
"transport": "http",
|
|
2732
|
+
"notes": "HTML. The browser-proof cookie is checked on this GET, not only on the POST. The **central** client never reaches this screen and renders the `410` page instead: it is configured by the operator, so there is no self-registered stranger for a person to weigh up."
|
|
2733
|
+
},
|
|
2734
|
+
{
|
|
2735
|
+
"method": "POST",
|
|
2736
|
+
"path": "/mcp/oauth/consent",
|
|
2737
|
+
"section": "mcp",
|
|
2738
|
+
"summary": "Records the allow-or-deny and sends the browser back to the MCP client.",
|
|
2739
|
+
"audience": "internal",
|
|
2740
|
+
"auth": "none",
|
|
2741
|
+
"rateLimited": true,
|
|
2742
|
+
"ownerTier": false,
|
|
2743
|
+
"status": 200,
|
|
2744
|
+
"params": [],
|
|
2745
|
+
"query": null,
|
|
2746
|
+
"request": null,
|
|
2747
|
+
"response": "oauth-redirect-response",
|
|
2748
|
+
"errors": [
|
|
2749
|
+
"rate_limited",
|
|
2750
|
+
"validation_error",
|
|
2751
|
+
"token_spent"
|
|
2752
|
+
],
|
|
2753
|
+
"transport": "http",
|
|
2754
|
+
"notes": "Fail-closed exactly as the app flow's consent POST is: the body carries the pressed button's `decision`, and anything that is not the Allow value — a missing field included — denies. A denial still answers a `redirect_to`, carrying `error=access_denied` back to the client, because a client that is refused must learn so from its own callback rather than from a page nobody sent it."
|
|
2755
|
+
},
|
|
2756
|
+
{
|
|
2757
|
+
"method": "POST",
|
|
2758
|
+
"path": "/mcp/oauth/token",
|
|
2759
|
+
"section": "mcp",
|
|
2760
|
+
"summary": "Exchanges an MCP authorization code for an access token.",
|
|
2761
|
+
"audience": "client",
|
|
2762
|
+
"auth": "none",
|
|
2763
|
+
"rateLimited": false,
|
|
2764
|
+
"ownerTier": false,
|
|
2765
|
+
"status": 200,
|
|
2766
|
+
"params": [],
|
|
2767
|
+
"query": null,
|
|
2768
|
+
"request": "oauth-token-request",
|
|
2769
|
+
"response": "oauth-token-response",
|
|
2770
|
+
"errors": [],
|
|
2771
|
+
"transport": "http",
|
|
2772
|
+
"notes": "`authorization_code` mints an `mcp_session` access token bound to the central resource and a refresh token; `refresh_token` rotates that pair, and the presented refresh token is consumed — a second presentation revokes the session, as on `/api/auth/refresh`. The refresh token lives ninety days from its last use and is bound to the `client_id` it was issued to. A refresh re-reads the Fleetless user, so a removed account cannot refresh. Refusals are RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for; a `resource` on a refresh must match the session's audience, and is checked before the token is consumed."
|
|
2773
|
+
},
|
|
2774
|
+
{
|
|
2775
|
+
"method": "GET",
|
|
2776
|
+
"path": "/console/oauth/authorize",
|
|
2777
|
+
"section": "developer-auth",
|
|
2778
|
+
"summary": "Starts a console sign-in and redirects the browser to the identify card.",
|
|
2779
|
+
"audience": "internal",
|
|
2780
|
+
"auth": "none",
|
|
2781
|
+
"rateLimited": false,
|
|
2782
|
+
"ownerTier": false,
|
|
2783
|
+
"status": 302,
|
|
2784
|
+
"params": [],
|
|
2785
|
+
"query": null,
|
|
2786
|
+
"request": null,
|
|
2787
|
+
"response": null,
|
|
2788
|
+
"errors": [],
|
|
2789
|
+
"transport": "http",
|
|
2790
|
+
"notes": "The authorization-code leg of the console's own OAuth flow: PKCE `S256` is required and `redirect_uri` must match a configured console callback exactly, never a prefix. Refusals use RFC 6749's flat `oauthError` shape, not the `apiError` envelope, so they carry none of the codes in this reference. A bad `redirect_uri` never redirects — until the URI is known-good, sending a browser to it is the attack; later errors go back to the callback as query parameters. `?prompt=create` starts at sign-up rather than sign-in."
|
|
2791
|
+
},
|
|
2792
|
+
{
|
|
2793
|
+
"method": "GET",
|
|
2794
|
+
"path": "/console/oauth/interaction/:id",
|
|
2795
|
+
"section": "developer-auth",
|
|
2796
|
+
"summary": "Serves the \"what is your email address\" card of a console sign-in.",
|
|
2797
|
+
"audience": "internal",
|
|
2798
|
+
"auth": "none",
|
|
2799
|
+
"rateLimited": false,
|
|
2800
|
+
"ownerTier": false,
|
|
2801
|
+
"status": 200,
|
|
2802
|
+
"params": [
|
|
2803
|
+
{
|
|
2804
|
+
"name": "id",
|
|
2805
|
+
"description": "The interaction id minted by `GET /console/oauth/authorize`, which redirects the browser here."
|
|
2806
|
+
}
|
|
2807
|
+
],
|
|
2808
|
+
"query": null,
|
|
2809
|
+
"request": null,
|
|
2810
|
+
"response": null,
|
|
2811
|
+
"errors": [],
|
|
2812
|
+
"transport": "http",
|
|
2813
|
+
"notes": "HTML: the email field, `Email me a code`, and `Sign in with a passkey`. Opening it binds the interaction to this browser with the browser-proof cookie, which every sign-in step after it checks. An expired, consumed, unknown or hand-edited interaction renders one page at `410`: which of the four it was is not a fact a stranger may learn, and to the person it is one fact anyway. The page resolves nothing about the address typed into it, so there is no enumeration oracle here at all."
|
|
2814
|
+
},
|
|
2815
|
+
{
|
|
2816
|
+
"method": "POST",
|
|
2817
|
+
"path": "/console/oauth/identify",
|
|
2818
|
+
"section": "developer-auth",
|
|
2819
|
+
"summary": "Takes the email address, mails a sign-in code and hands back the code step.",
|
|
2820
|
+
"audience": "internal",
|
|
2821
|
+
"auth": "none",
|
|
2822
|
+
"rateLimited": true,
|
|
2823
|
+
"ownerTier": false,
|
|
2824
|
+
"status": 200,
|
|
2825
|
+
"params": [],
|
|
2826
|
+
"query": null,
|
|
2827
|
+
"request": null,
|
|
2828
|
+
"response": null,
|
|
2829
|
+
"errors": [
|
|
2830
|
+
"rate_limited",
|
|
2831
|
+
"validation_error",
|
|
2832
|
+
"token_spent",
|
|
2833
|
+
"wrong_browser"
|
|
2834
|
+
],
|
|
2835
|
+
"transport": "http",
|
|
2836
|
+
"notes": "The page answers `Check your email` **for every address**: a known one gets `Your Fleetless sign-in code`, six digits valid ten minutes; an unknown one gets a mail saying no Fleetless account uses it, with a link to sign up (or the waiting list while sign-up is closed). So the page never reveals who has an account, and the address is trimmed and compared case-insensitively. A request within sixty seconds of the last one for the same address renders the same page without a second mail. The browser-proof cookie is set here when the interaction has none yet, and checked when it has. A browser form post gets the code card; a JSON caller gets `{ \"next\": \"/console/oauth/code\" }`, which has no schema. A dead interaction is `410 token_spent`."
|
|
2837
|
+
},
|
|
2838
|
+
{
|
|
2839
|
+
"method": "POST",
|
|
2840
|
+
"path": "/console/oauth/code",
|
|
2841
|
+
"section": "developer-auth",
|
|
2842
|
+
"summary": "Checks the emailed code and finishes the sign-in, or hands back the second step.",
|
|
2843
|
+
"audience": "internal",
|
|
2844
|
+
"auth": "none",
|
|
2845
|
+
"rateLimited": true,
|
|
2846
|
+
"ownerTier": false,
|
|
2847
|
+
"status": 200,
|
|
2848
|
+
"params": [],
|
|
2849
|
+
"query": null,
|
|
2850
|
+
"request": null,
|
|
2851
|
+
"response": "oauth-redirect-response",
|
|
2852
|
+
"errors": [
|
|
2853
|
+
"rate_limited",
|
|
2854
|
+
"validation_error",
|
|
2855
|
+
"token_spent",
|
|
2856
|
+
"wrong_browser",
|
|
2857
|
+
"invalid_code"
|
|
2858
|
+
],
|
|
2859
|
+
"transport": "http",
|
|
2860
|
+
"notes": "A wrong code renders the code card again with the attempts left (`400 invalid_code`, `details.attempts_left`); a code spent, past its ten minutes or out of its five attempts is `410 token_spent` and a new one has to be asked for. Spaces inside a typed code are removed before it is checked. With no second factor to give, a browser gets a `303` to the console callback with the authorization code, and a JSON caller that URL as `redirect_to`. Otherwise the next step: a JSON caller gets `{ \"next\" }` naming `/console/oauth/two-factor` — the person has a passkey or an authenticator — or `/console/oauth/two-factor/setup` — the organisation requires one and the person has none — and a browser the page itself. Audited as `developer.login` with `details.method` `email_code` once the sign-in completes."
|
|
2861
|
+
},
|
|
2862
|
+
{
|
|
2863
|
+
"method": "GET",
|
|
2864
|
+
"path": "/console/oauth/two-factor/:id",
|
|
2865
|
+
"section": "developer-auth",
|
|
2866
|
+
"summary": "Serves the second step: the authenticator code, or the passkey prompt.",
|
|
2867
|
+
"audience": "internal",
|
|
2868
|
+
"auth": "none",
|
|
2869
|
+
"rateLimited": false,
|
|
2870
|
+
"ownerTier": false,
|
|
2871
|
+
"status": 200,
|
|
2872
|
+
"params": [
|
|
2873
|
+
{
|
|
2874
|
+
"name": "id",
|
|
2875
|
+
"description": "The interaction id of this sign-in; the step before redirects the browser here."
|
|
2876
|
+
}
|
|
2877
|
+
],
|
|
2878
|
+
"query": null,
|
|
2879
|
+
"request": null,
|
|
2880
|
+
"response": null,
|
|
2881
|
+
"errors": [],
|
|
2882
|
+
"transport": "http",
|
|
2883
|
+
"notes": "HTML. Six boxes for the authenticator code, `Use a passkey`, and `Use a recovery code`; a developer with passkeys only sees the passkey prompt directly. The browser-proof cookie is checked on this GET too. A dead interaction renders the `410` page."
|
|
2884
|
+
},
|
|
2885
|
+
{
|
|
2886
|
+
"method": "GET",
|
|
2887
|
+
"path": "/console/oauth/two-factor/:id/recovery",
|
|
2888
|
+
"section": "developer-auth",
|
|
2889
|
+
"summary": "Serves the recovery-code page of the second step.",
|
|
2890
|
+
"audience": "internal",
|
|
2891
|
+
"auth": "none",
|
|
2892
|
+
"rateLimited": false,
|
|
2893
|
+
"ownerTier": false,
|
|
2894
|
+
"status": 200,
|
|
2895
|
+
"params": [
|
|
2896
|
+
{
|
|
2897
|
+
"name": "id",
|
|
2898
|
+
"description": "The interaction id of this sign-in; the step before redirects the browser here."
|
|
2899
|
+
}
|
|
2900
|
+
],
|
|
2901
|
+
"query": null,
|
|
2902
|
+
"request": null,
|
|
2903
|
+
"response": null,
|
|
2904
|
+
"errors": [],
|
|
2905
|
+
"transport": "http",
|
|
2906
|
+
"notes": "HTML. One field for a recovery code, and the way out when none is left: an owner of the organisation can reset the member's two-factor in Settings › Team."
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
"method": "POST",
|
|
2910
|
+
"path": "/console/oauth/two-factor",
|
|
2911
|
+
"section": "developer-auth",
|
|
2912
|
+
"summary": "Checks an authenticator code or a recovery code and finishes the sign-in.",
|
|
2913
|
+
"audience": "internal",
|
|
2914
|
+
"auth": "none",
|
|
2915
|
+
"rateLimited": true,
|
|
2916
|
+
"ownerTier": false,
|
|
2917
|
+
"status": 200,
|
|
2918
|
+
"params": [],
|
|
2919
|
+
"query": null,
|
|
2920
|
+
"request": null,
|
|
2921
|
+
"response": "oauth-redirect-response",
|
|
2922
|
+
"errors": [
|
|
2923
|
+
"rate_limited",
|
|
2924
|
+
"validation_error",
|
|
2925
|
+
"token_spent",
|
|
2926
|
+
"wrong_browser",
|
|
2927
|
+
"invalid_code"
|
|
2928
|
+
],
|
|
2929
|
+
"transport": "http",
|
|
2930
|
+
"notes": "The form carries either `code` or `recovery_code`. **An authenticator code is accepted at most once**, so the same code sent twice finishes one sign-in. A recovery code is spent by its use. Five wrong codes end the interaction (`410 token_spent`). A browser gets a `303` to the console callback with the authorization code; a JSON caller that URL as `redirect_to`."
|
|
2931
|
+
},
|
|
2932
|
+
{
|
|
2933
|
+
"method": "POST",
|
|
2934
|
+
"path": "/console/oauth/passkey/options",
|
|
2935
|
+
"section": "developer-auth",
|
|
2936
|
+
"summary": "Answers the WebAuthn request options for a passkey sign-in or second step.",
|
|
2937
|
+
"audience": "internal",
|
|
2938
|
+
"auth": "none",
|
|
2939
|
+
"rateLimited": true,
|
|
2940
|
+
"ownerTier": false,
|
|
2941
|
+
"status": 200,
|
|
2942
|
+
"params": [],
|
|
2943
|
+
"query": null,
|
|
2944
|
+
"request": null,
|
|
2945
|
+
"response": "webauthn-options-response",
|
|
2946
|
+
"errors": [
|
|
2947
|
+
"rate_limited",
|
|
2948
|
+
"token_spent",
|
|
2949
|
+
"wrong_browser"
|
|
2950
|
+
],
|
|
2951
|
+
"transport": "http",
|
|
2952
|
+
"notes": "Before an address is known the options name no credential, so the browser offers every discoverable passkey for `fleetless.dev` (`Sign in with a passkey`); after the code step they name the account's own passkeys. User verification is required. The challenge is bound to the interaction and single-use. **This is the first step of a passkey sign-in**, which has no email step: the browser-proof cookie is set here when the interaction has none yet, and checked when it has — so `POST /console/oauth/passkey` can require it."
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
"method": "POST",
|
|
2956
|
+
"path": "/console/oauth/passkey",
|
|
2957
|
+
"section": "developer-auth",
|
|
2958
|
+
"summary": "Checks a passkey assertion; a passkey completes the sign-in on its own.",
|
|
2959
|
+
"audience": "internal",
|
|
2960
|
+
"auth": "none",
|
|
2961
|
+
"rateLimited": true,
|
|
2962
|
+
"ownerTier": false,
|
|
2963
|
+
"status": 200,
|
|
2964
|
+
"params": [],
|
|
2965
|
+
"query": null,
|
|
2966
|
+
"request": null,
|
|
2967
|
+
"response": "oauth-redirect-response",
|
|
2968
|
+
"errors": [
|
|
2969
|
+
"rate_limited",
|
|
2970
|
+
"validation_error",
|
|
2971
|
+
"token_spent",
|
|
2972
|
+
"wrong_browser",
|
|
2973
|
+
"invalid_credentials"
|
|
2974
|
+
],
|
|
2975
|
+
"transport": "http",
|
|
2976
|
+
"notes": "**A passkey is a full sign-in**: it proves possession and user verification, two factors, so it skips the emailed code and the second step — used as the second step, it finishes it. An assertion that does not verify, or names no passkey of an account, is `401 invalid_credentials`, the same answer for both. When the organisation requires two-factor, a passkey satisfies it. A browser gets a `303` to the console callback with the authorization code; a JSON caller that URL as `redirect_to`. Audited as `developer.login` with `details.method` `passkey`."
|
|
2977
|
+
},
|
|
2978
|
+
{
|
|
2979
|
+
"method": "GET",
|
|
2980
|
+
"path": "/console/oauth/two-factor/setup/:id",
|
|
2981
|
+
"section": "developer-auth",
|
|
2982
|
+
"summary": "Serves the \"your organisation requires two-factor\" choice between a passkey and an authenticator.",
|
|
2983
|
+
"audience": "internal",
|
|
2984
|
+
"auth": "none",
|
|
2985
|
+
"rateLimited": false,
|
|
2986
|
+
"ownerTier": false,
|
|
2987
|
+
"status": 200,
|
|
2988
|
+
"params": [
|
|
2989
|
+
{
|
|
2990
|
+
"name": "id",
|
|
2991
|
+
"description": "The interaction id of this sign-in; the step before redirects the browser here."
|
|
2992
|
+
}
|
|
2993
|
+
],
|
|
2994
|
+
"query": null,
|
|
2995
|
+
"request": null,
|
|
2996
|
+
"response": null,
|
|
2997
|
+
"errors": [],
|
|
2998
|
+
"transport": "http",
|
|
2999
|
+
"notes": "HTML. Reached when the organisation requires two-factor and the person has none; no session exists until the setup is done. Two options, the passkey recommended because it also signs the person in without an emailed code. `Signed in as <email> · Sign out` under it."
|
|
3000
|
+
},
|
|
3001
|
+
{
|
|
3002
|
+
"method": "POST",
|
|
3003
|
+
"path": "/console/oauth/two-factor/setup/totp",
|
|
3004
|
+
"section": "developer-auth",
|
|
3005
|
+
"summary": "Starts an authenticator setup inside the sign-in and hands back its QR code and key.",
|
|
3006
|
+
"audience": "internal",
|
|
3007
|
+
"auth": "none",
|
|
3008
|
+
"rateLimited": true,
|
|
3009
|
+
"ownerTier": false,
|
|
3010
|
+
"status": 200,
|
|
3011
|
+
"params": [],
|
|
3012
|
+
"query": null,
|
|
3013
|
+
"request": null,
|
|
3014
|
+
"response": "two-factor-setup-response",
|
|
3015
|
+
"errors": [
|
|
3016
|
+
"rate_limited",
|
|
3017
|
+
"token_spent",
|
|
3018
|
+
"wrong_browser"
|
|
3019
|
+
],
|
|
3020
|
+
"transport": "http",
|
|
3021
|
+
"notes": "A browser gets the page with the QR code, the key and the confirm field; a JSON caller the secret and `otpauth_url`. Nothing is stored as confirmed yet."
|
|
3022
|
+
},
|
|
3023
|
+
{
|
|
3024
|
+
"method": "POST",
|
|
3025
|
+
"path": "/console/oauth/two-factor/setup/totp/confirm",
|
|
3026
|
+
"section": "developer-auth",
|
|
3027
|
+
"summary": "Confirms the new authenticator and hands back the ten recovery codes.",
|
|
3028
|
+
"audience": "internal",
|
|
3029
|
+
"auth": "none",
|
|
3030
|
+
"rateLimited": true,
|
|
3031
|
+
"ownerTier": false,
|
|
3032
|
+
"status": 200,
|
|
3033
|
+
"params": [],
|
|
3034
|
+
"query": null,
|
|
3035
|
+
"request": null,
|
|
3036
|
+
"response": "recovery-codes-response",
|
|
3037
|
+
"errors": [
|
|
3038
|
+
"rate_limited",
|
|
3039
|
+
"validation_error",
|
|
3040
|
+
"token_spent",
|
|
3041
|
+
"wrong_browser",
|
|
3042
|
+
"invalid_code"
|
|
3043
|
+
],
|
|
3044
|
+
"transport": "http",
|
|
3045
|
+
"notes": "A code that does not match is `400 invalid_code`. On success the authenticator is on and ten recovery codes are shown once; `I saved my recovery codes` then finishes the sign-in. Audited as `developer.two_factor_added` with `details.kind` `authenticator`."
|
|
3046
|
+
},
|
|
3047
|
+
{
|
|
3048
|
+
"method": "POST",
|
|
3049
|
+
"path": "/console/oauth/two-factor/setup/passkey/options",
|
|
3050
|
+
"section": "developer-auth",
|
|
3051
|
+
"summary": "Answers the WebAuthn creation options for a passkey set up inside the sign-in.",
|
|
3052
|
+
"audience": "internal",
|
|
3053
|
+
"auth": "none",
|
|
3054
|
+
"rateLimited": true,
|
|
3055
|
+
"ownerTier": false,
|
|
3056
|
+
"status": 200,
|
|
3057
|
+
"params": [],
|
|
3058
|
+
"query": null,
|
|
3059
|
+
"request": null,
|
|
3060
|
+
"response": "webauthn-options-response",
|
|
3061
|
+
"errors": [
|
|
3062
|
+
"rate_limited",
|
|
3063
|
+
"token_spent",
|
|
3064
|
+
"wrong_browser"
|
|
3065
|
+
],
|
|
3066
|
+
"transport": "http",
|
|
3067
|
+
"notes": "The same options `POST /api/auth/passkeys/options` answers a signed-in developer: relying party `fleetless.dev`, user verification required, a discoverable credential."
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
"method": "POST",
|
|
3071
|
+
"path": "/console/oauth/two-factor/setup/passkey",
|
|
3072
|
+
"section": "developer-auth",
|
|
3073
|
+
"summary": "Registers the passkey and hands back the ten recovery codes.",
|
|
3074
|
+
"audience": "internal",
|
|
3075
|
+
"auth": "none",
|
|
3076
|
+
"rateLimited": true,
|
|
3077
|
+
"ownerTier": false,
|
|
3078
|
+
"status": 200,
|
|
3079
|
+
"params": [],
|
|
3080
|
+
"query": null,
|
|
3081
|
+
"request": null,
|
|
3082
|
+
"response": "recovery-codes-response",
|
|
3083
|
+
"errors": [
|
|
3084
|
+
"rate_limited",
|
|
3085
|
+
"validation_error",
|
|
3086
|
+
"token_spent",
|
|
3087
|
+
"wrong_browser"
|
|
3088
|
+
],
|
|
3089
|
+
"transport": "http",
|
|
3090
|
+
"notes": "A ceremony that does not verify is `400 validation_error`. On success the passkey is stored, named after the browser's device where it says so, and ten recovery codes are shown once. Audited as `developer.two_factor_added` with `details.kind` `passkey`."
|
|
3091
|
+
},
|
|
3092
|
+
{
|
|
3093
|
+
"method": "POST",
|
|
3094
|
+
"path": "/console/oauth/two-factor/setup/done",
|
|
3095
|
+
"section": "developer-auth",
|
|
3096
|
+
"summary": "Finishes the sign-in once the recovery codes are saved.",
|
|
3097
|
+
"audience": "internal",
|
|
3098
|
+
"auth": "none",
|
|
3099
|
+
"rateLimited": true,
|
|
3100
|
+
"ownerTier": false,
|
|
3101
|
+
"status": 200,
|
|
3102
|
+
"params": [],
|
|
3103
|
+
"query": null,
|
|
3104
|
+
"request": null,
|
|
3105
|
+
"response": "oauth-redirect-response",
|
|
3106
|
+
"errors": [
|
|
3107
|
+
"rate_limited",
|
|
3108
|
+
"token_spent",
|
|
3109
|
+
"wrong_browser"
|
|
3110
|
+
],
|
|
3111
|
+
"transport": "http",
|
|
3112
|
+
"notes": "`I saved my recovery codes` gates the button on the page; the step itself only checks that a second factor now exists. A browser gets a `303` to the console callback with the authorization code; a JSON caller that URL as `redirect_to`."
|
|
3113
|
+
},
|
|
3114
|
+
{
|
|
3115
|
+
"method": "GET",
|
|
3116
|
+
"path": "/console/oauth/signup/:id",
|
|
3117
|
+
"section": "developer-auth",
|
|
3118
|
+
"summary": "Serves step one of console sign-up, the email card.",
|
|
3119
|
+
"audience": "internal",
|
|
3120
|
+
"auth": "none",
|
|
3121
|
+
"rateLimited": false,
|
|
3122
|
+
"ownerTier": false,
|
|
3123
|
+
"status": 200,
|
|
3124
|
+
"params": [
|
|
3125
|
+
{
|
|
3126
|
+
"name": "id",
|
|
3127
|
+
"description": "The interaction id minted by `GET /console/oauth/authorize` with `?prompt=create`."
|
|
3128
|
+
}
|
|
3129
|
+
],
|
|
3130
|
+
"query": null,
|
|
3131
|
+
"request": null,
|
|
3132
|
+
"response": null,
|
|
3133
|
+
"errors": [],
|
|
3134
|
+
"transport": "http",
|
|
3135
|
+
"notes": "HTML: the email field and `Email me a code`, the first of three steps — email, code, organization. While the deployment runs in closed beta this renders the \"sign-up is closed\" card at `403` instead, keeping the interaction alive and pointing back at sign-in and the waiting list — the person may well already have an account."
|
|
3136
|
+
},
|
|
3137
|
+
{
|
|
3138
|
+
"method": "POST",
|
|
3139
|
+
"path": "/console/oauth/signup",
|
|
3140
|
+
"section": "developer-auth",
|
|
3141
|
+
"summary": "Takes the sign-up email, mails a code and hands back the code step.",
|
|
3142
|
+
"audience": "internal",
|
|
3143
|
+
"auth": "none",
|
|
3144
|
+
"rateLimited": true,
|
|
3145
|
+
"ownerTier": false,
|
|
3146
|
+
"status": 200,
|
|
3147
|
+
"params": [],
|
|
3148
|
+
"query": null,
|
|
3149
|
+
"request": null,
|
|
3150
|
+
"response": null,
|
|
3151
|
+
"errors": [
|
|
3152
|
+
"rate_limited",
|
|
3153
|
+
"token_spent",
|
|
3154
|
+
"signup_closed",
|
|
3155
|
+
"validation_error",
|
|
3156
|
+
"email_taken"
|
|
3157
|
+
],
|
|
3158
|
+
"transport": "http",
|
|
3159
|
+
"notes": "A browser form post gets the code card; a JSON caller gets `{ \"next\", \"email\" }`, which has no schema. **No password**: the code mailed here, six digits valid ten minutes, proves the address. A per-interaction proof cookie is set here — it is what stops a third party from finishing a sign-up somebody else started. Sign-up is the one surface whose job is to say an address is taken, so `409 email_taken` is not a leak here."
|
|
3160
|
+
},
|
|
3161
|
+
{
|
|
3162
|
+
"method": "POST",
|
|
3163
|
+
"path": "/console/oauth/signup/code",
|
|
3164
|
+
"section": "developer-auth",
|
|
3165
|
+
"summary": "Checks the sign-up code and hands back the organization step.",
|
|
3166
|
+
"audience": "internal",
|
|
3167
|
+
"auth": "none",
|
|
3168
|
+
"rateLimited": true,
|
|
3169
|
+
"ownerTier": false,
|
|
3170
|
+
"status": 200,
|
|
3171
|
+
"params": [],
|
|
3172
|
+
"query": null,
|
|
3173
|
+
"request": null,
|
|
3174
|
+
"response": null,
|
|
3175
|
+
"errors": [
|
|
3176
|
+
"rate_limited",
|
|
3177
|
+
"token_spent",
|
|
3178
|
+
"signup_closed",
|
|
3179
|
+
"wrong_browser",
|
|
3180
|
+
"validation_error",
|
|
3181
|
+
"invalid_code"
|
|
3182
|
+
],
|
|
3183
|
+
"transport": "http",
|
|
3184
|
+
"notes": "A wrong code renders the code card again with the attempts left (`400 invalid_code`); a spent, expired or exhausted one is `410 token_spent`. The step before must have run in **this** browser (`401 wrong_browser`). A browser form post gets the organization card, the address shown `confirmed`; a JSON caller gets `{ \"next\" }`, which has no schema."
|
|
3185
|
+
},
|
|
3186
|
+
{
|
|
3187
|
+
"method": "POST",
|
|
3188
|
+
"path": "/console/oauth/signup/organization",
|
|
3189
|
+
"section": "developer-auth",
|
|
3190
|
+
"summary": "Takes the organization name and creates the org and its founding Owner.",
|
|
3191
|
+
"audience": "internal",
|
|
3192
|
+
"auth": "none",
|
|
3193
|
+
"rateLimited": true,
|
|
3194
|
+
"ownerTier": false,
|
|
3195
|
+
"status": 200,
|
|
3196
|
+
"params": [],
|
|
3197
|
+
"query": null,
|
|
3198
|
+
"request": null,
|
|
3199
|
+
"response": "oauth-redirect-response",
|
|
3200
|
+
"errors": [
|
|
3201
|
+
"rate_limited",
|
|
3202
|
+
"token_spent",
|
|
3203
|
+
"signup_closed",
|
|
3204
|
+
"wrong_browser",
|
|
3205
|
+
"validation_error",
|
|
3206
|
+
"email_taken"
|
|
3207
|
+
],
|
|
3208
|
+
"transport": "http",
|
|
3209
|
+
"notes": "The form carries `org_name` only. The org and its founding Owner are created in one transaction, and only after the code step confirmed the address. Both steps before must have run in **this** browser: a missing or mismatched proof cookie is `401 wrong_browser` and the person is sent back to step one. `409 email_taken` when the address was taken meanwhile. A browser form post gets a `303` to the console callback; a JSON caller gets `redirect_to` at `200`. This is the only way an organisation is created: `POST /api/auth/signup` is gone, because without a password it would hand a session to anybody who names an address."
|
|
3210
|
+
},
|
|
3211
|
+
{
|
|
3212
|
+
"method": "POST",
|
|
3213
|
+
"path": "/console/oauth/token",
|
|
3214
|
+
"section": "developer-auth",
|
|
3215
|
+
"summary": "Exchanges the console's authorization code for a developer session.",
|
|
3216
|
+
"audience": "internal",
|
|
3217
|
+
"auth": "none",
|
|
3218
|
+
"rateLimited": false,
|
|
3219
|
+
"ownerTier": false,
|
|
3220
|
+
"status": 200,
|
|
3221
|
+
"params": [],
|
|
3222
|
+
"query": null,
|
|
3223
|
+
"request": null,
|
|
3224
|
+
"response": "session-tokens",
|
|
3225
|
+
"errors": [],
|
|
3226
|
+
"transport": "http",
|
|
3227
|
+
"notes": "Called by the console's own server, never by a browser. Refusals use RFC 6749 §5.2's flat `oauthError` shape — it is a token endpoint, and that is the dialect a caller of one expects — so it emits none of the codes in this reference. Proof of possession is checked before the replay check, and the single-use consume is atomic, so exactly one caller ever mints. The Org Admins membership is re-read here: the code was minted earlier, and a user moved out in between must not get a console session."
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
"method": "GET",
|
|
3231
|
+
"path": "/app/:appIdentifier/logo",
|
|
3232
|
+
"section": "client-auth",
|
|
3233
|
+
"summary": "Serves the app's logo for its hosted pages.",
|
|
3234
|
+
"audience": "internal",
|
|
3235
|
+
"auth": "none",
|
|
3236
|
+
"rateLimited": false,
|
|
3237
|
+
"ownerTier": false,
|
|
3238
|
+
"status": 200,
|
|
3239
|
+
"params": [
|
|
3240
|
+
{
|
|
3241
|
+
"name": "appIdentifier",
|
|
3242
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3243
|
+
}
|
|
3244
|
+
],
|
|
3245
|
+
"query": null,
|
|
3246
|
+
"request": null,
|
|
3247
|
+
"response": null,
|
|
3248
|
+
"errors": [],
|
|
3249
|
+
"transport": "http",
|
|
3250
|
+
"notes": "The stored PNG or SVG, as written through `PUT /api/apps/:id/auth-config/logo`; `404` when the app has none. **Served sandboxed**: `X-Content-Type-Options: nosniff` and `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox`, so a script inside an SVG never runs, even opened directly. The hosted pages embed it as an image only."
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
"method": "GET",
|
|
3254
|
+
"path": "/app/:appIdentifier/mcp/:interaction",
|
|
3255
|
+
"section": "client-auth",
|
|
3256
|
+
"summary": "Serves the hosted MCP sign-in, the page an unset `mcp_login_url` falls back to.",
|
|
3257
|
+
"audience": "internal",
|
|
3258
|
+
"auth": "none",
|
|
3259
|
+
"rateLimited": false,
|
|
3260
|
+
"ownerTier": false,
|
|
3261
|
+
"status": 200,
|
|
3262
|
+
"params": [
|
|
3263
|
+
{
|
|
3264
|
+
"name": "appIdentifier",
|
|
3265
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3266
|
+
},
|
|
3267
|
+
{
|
|
3268
|
+
"name": "interaction",
|
|
3269
|
+
"description": "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
3270
|
+
}
|
|
3271
|
+
],
|
|
3272
|
+
"query": null,
|
|
3273
|
+
"request": null,
|
|
3274
|
+
"response": null,
|
|
3275
|
+
"errors": [],
|
|
3276
|
+
"transport": "http",
|
|
3277
|
+
"notes": "HTML: the app's identity providers, then the sign-in the app offers — email and password with `Email me a sign-in code instead`, or email and `Email me a code` for a code-only app — and `Create one` when self-registration is on. A dead interaction renders the `410` page. The browser-proof cookie is set here."
|
|
3278
|
+
},
|
|
3279
|
+
{
|
|
3280
|
+
"method": "POST",
|
|
3281
|
+
"path": "/app/:appIdentifier/mcp/:interaction/password",
|
|
3282
|
+
"section": "client-auth",
|
|
3283
|
+
"summary": "Checks the email and password of the hosted MCP sign-in.",
|
|
3284
|
+
"audience": "internal",
|
|
3285
|
+
"auth": "none",
|
|
3286
|
+
"rateLimited": true,
|
|
3287
|
+
"ownerTier": false,
|
|
3288
|
+
"status": 200,
|
|
3289
|
+
"params": [
|
|
3290
|
+
{
|
|
3291
|
+
"name": "appIdentifier",
|
|
3292
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3293
|
+
},
|
|
3294
|
+
{
|
|
3295
|
+
"name": "interaction",
|
|
3296
|
+
"description": "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
3297
|
+
}
|
|
3298
|
+
],
|
|
3299
|
+
"query": null,
|
|
3300
|
+
"request": null,
|
|
3301
|
+
"response": null,
|
|
3302
|
+
"errors": [
|
|
3303
|
+
"rate_limited"
|
|
3304
|
+
],
|
|
3305
|
+
"transport": "http",
|
|
3306
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. One refusal for every miss, as `POST /api/client/login` answers. With a second factor to give, the two-factor page follows; otherwise the consent page."
|
|
3307
|
+
},
|
|
3308
|
+
{
|
|
3309
|
+
"method": "POST",
|
|
3310
|
+
"path": "/app/:appIdentifier/mcp/:interaction/code",
|
|
3311
|
+
"section": "client-auth",
|
|
3312
|
+
"summary": "Mails a sign-in code for the hosted MCP sign-in and renders the code page.",
|
|
3313
|
+
"audience": "internal",
|
|
3314
|
+
"auth": "none",
|
|
3315
|
+
"rateLimited": true,
|
|
3316
|
+
"ownerTier": false,
|
|
3317
|
+
"status": 200,
|
|
3318
|
+
"params": [
|
|
3319
|
+
{
|
|
3320
|
+
"name": "appIdentifier",
|
|
3321
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3322
|
+
},
|
|
3323
|
+
{
|
|
3324
|
+
"name": "interaction",
|
|
3325
|
+
"description": "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
3326
|
+
}
|
|
3327
|
+
],
|
|
3328
|
+
"query": null,
|
|
3329
|
+
"request": null,
|
|
3330
|
+
"response": null,
|
|
3331
|
+
"errors": [
|
|
3332
|
+
"rate_limited"
|
|
3333
|
+
],
|
|
3334
|
+
"transport": "http",
|
|
3335
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. The same page for a known and an unknown address, as `POST /api/client/login/code` answers; six boxes, `Resend code in 0:42`, and `Use password instead` where the app has passwords on."
|
|
3336
|
+
},
|
|
3337
|
+
{
|
|
3338
|
+
"method": "POST",
|
|
3339
|
+
"path": "/app/:appIdentifier/mcp/:interaction/code/verify",
|
|
3340
|
+
"section": "client-auth",
|
|
3341
|
+
"summary": "Checks the emailed code of the hosted MCP sign-in.",
|
|
3342
|
+
"audience": "internal",
|
|
3343
|
+
"auth": "none",
|
|
3344
|
+
"rateLimited": true,
|
|
3345
|
+
"ownerTier": false,
|
|
3346
|
+
"status": 200,
|
|
3347
|
+
"params": [
|
|
3348
|
+
{
|
|
3349
|
+
"name": "appIdentifier",
|
|
3350
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3351
|
+
},
|
|
3352
|
+
{
|
|
3353
|
+
"name": "interaction",
|
|
3354
|
+
"description": "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
3355
|
+
}
|
|
3356
|
+
],
|
|
3357
|
+
"query": null,
|
|
3358
|
+
"request": null,
|
|
3359
|
+
"response": null,
|
|
3360
|
+
"errors": [
|
|
3361
|
+
"rate_limited"
|
|
3362
|
+
],
|
|
3363
|
+
"transport": "http",
|
|
3364
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. A wrong code renders the code page with the attempts left; a spent one asks for a new code. With a second factor to give, the two-factor page follows; otherwise the consent page."
|
|
3365
|
+
},
|
|
3366
|
+
{
|
|
3367
|
+
"method": "POST",
|
|
3368
|
+
"path": "/app/:appIdentifier/mcp/:interaction/consent",
|
|
3369
|
+
"section": "client-auth",
|
|
3370
|
+
"summary": "Records the allow-or-deny of the hosted MCP sign-in and sends the browser back to the client.",
|
|
3371
|
+
"audience": "internal",
|
|
3372
|
+
"auth": "none",
|
|
3373
|
+
"rateLimited": true,
|
|
3374
|
+
"ownerTier": false,
|
|
3375
|
+
"status": 200,
|
|
3376
|
+
"params": [
|
|
3377
|
+
{
|
|
3378
|
+
"name": "appIdentifier",
|
|
3379
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3380
|
+
},
|
|
3381
|
+
{
|
|
3382
|
+
"name": "interaction",
|
|
3383
|
+
"description": "The interaction id `GET /mcp/:appIdentifier/oauth/authorize` put into the hosted MCP sign-in URL."
|
|
3384
|
+
}
|
|
3385
|
+
],
|
|
3386
|
+
"query": null,
|
|
3387
|
+
"request": null,
|
|
3388
|
+
"response": null,
|
|
3389
|
+
"errors": [
|
|
3390
|
+
"rate_limited"
|
|
3391
|
+
],
|
|
3392
|
+
"transport": "http",
|
|
3393
|
+
"notes": "Answers a `303` to the MCP client's callback, carrying the code or `error=access_denied`, as `POST /api/client/mcp/interactions/:id/approve` and `…/deny` do. Fail-closed: anything but the Allow value denies. The browser-proof cookie set at the sign-in must match, or the `wrong_browser` page renders."
|
|
3394
|
+
},
|
|
3395
|
+
{
|
|
3396
|
+
"method": "POST",
|
|
3397
|
+
"path": "/app/:appIdentifier/two-factor",
|
|
3398
|
+
"section": "client-auth",
|
|
3399
|
+
"summary": "Checks an authenticator or recovery code on a hosted page and finishes the sign-in it interrupted.",
|
|
3400
|
+
"audience": "internal",
|
|
3401
|
+
"auth": "none",
|
|
3402
|
+
"rateLimited": true,
|
|
3403
|
+
"ownerTier": false,
|
|
3404
|
+
"status": 200,
|
|
3405
|
+
"params": [
|
|
3406
|
+
{
|
|
3407
|
+
"name": "appIdentifier",
|
|
3408
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3409
|
+
}
|
|
3410
|
+
],
|
|
3411
|
+
"query": null,
|
|
3412
|
+
"request": null,
|
|
3413
|
+
"response": null,
|
|
3414
|
+
"errors": [
|
|
3415
|
+
"rate_limited"
|
|
3416
|
+
],
|
|
3417
|
+
"transport": "http",
|
|
3418
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. The form carries the challenge the step before answered and either `code` or `recovery_code`, checked as `POST /api/client/two-factor/verify` checks them. Success continues where the sign-in was going: the MCP consent, or the done page."
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
"method": "POST",
|
|
3422
|
+
"path": "/app/:appIdentifier/two-factor/setup",
|
|
3423
|
+
"section": "client-auth",
|
|
3424
|
+
"summary": "Starts an authenticator setup on a hosted page, when the app requires two-factor.",
|
|
3425
|
+
"audience": "internal",
|
|
3426
|
+
"auth": "none",
|
|
3427
|
+
"rateLimited": true,
|
|
3428
|
+
"ownerTier": false,
|
|
3429
|
+
"status": 200,
|
|
3430
|
+
"params": [
|
|
3431
|
+
{
|
|
3432
|
+
"name": "appIdentifier",
|
|
3433
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3434
|
+
}
|
|
3435
|
+
],
|
|
3436
|
+
"query": null,
|
|
3437
|
+
"request": null,
|
|
3438
|
+
"response": null,
|
|
3439
|
+
"errors": [
|
|
3440
|
+
"rate_limited"
|
|
3441
|
+
],
|
|
3442
|
+
"transport": "http",
|
|
3443
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. Renders the QR code, the key with `Copy key`, and the confirm field, for the challenge the step before answered."
|
|
3444
|
+
},
|
|
3445
|
+
{
|
|
3446
|
+
"method": "POST",
|
|
3447
|
+
"path": "/app/:appIdentifier/two-factor/setup/confirm",
|
|
3448
|
+
"section": "client-auth",
|
|
3449
|
+
"summary": "Confirms the new authenticator on a hosted page and shows the ten recovery codes.",
|
|
3450
|
+
"audience": "internal",
|
|
3451
|
+
"auth": "none",
|
|
3452
|
+
"rateLimited": true,
|
|
3453
|
+
"ownerTier": false,
|
|
3454
|
+
"status": 200,
|
|
3455
|
+
"params": [
|
|
3456
|
+
{
|
|
3457
|
+
"name": "appIdentifier",
|
|
3458
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3459
|
+
}
|
|
3460
|
+
],
|
|
3461
|
+
"query": null,
|
|
3462
|
+
"request": null,
|
|
3463
|
+
"response": null,
|
|
3464
|
+
"errors": [
|
|
3465
|
+
"rate_limited"
|
|
3466
|
+
],
|
|
3467
|
+
"transport": "http",
|
|
3468
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. A wrong code renders the setup page again. On success the ten recovery codes are shown once, with `Copy` and `Download .txt`; `I saved my recovery codes` gates `Continue`."
|
|
3469
|
+
},
|
|
3470
|
+
{
|
|
3471
|
+
"method": "GET",
|
|
3472
|
+
"path": "/app/:appIdentifier/invite/:token",
|
|
3473
|
+
"section": "client-auth",
|
|
3474
|
+
"summary": "Serves the hosted invitation page, the one an unset `invite_url` falls back to.",
|
|
3475
|
+
"audience": "internal",
|
|
3476
|
+
"auth": "none",
|
|
3477
|
+
"rateLimited": false,
|
|
3478
|
+
"ownerTier": false,
|
|
3479
|
+
"status": 200,
|
|
3480
|
+
"params": [
|
|
3481
|
+
{
|
|
3482
|
+
"name": "appIdentifier",
|
|
3483
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3484
|
+
},
|
|
3485
|
+
{
|
|
3486
|
+
"name": "token",
|
|
3487
|
+
"description": "The opaque token from the mailed link; it is never sent as a query parameter."
|
|
3488
|
+
}
|
|
3489
|
+
],
|
|
3490
|
+
"query": null,
|
|
3491
|
+
"request": null,
|
|
3492
|
+
"response": null,
|
|
3493
|
+
"errors": [],
|
|
3494
|
+
"transport": "http",
|
|
3495
|
+
"notes": "HTML. Renders a form; only its `POST` spends the token, so a mail scanner opening the link changes nothing. Asks for a name, and a password only while the app has passwords on; announces the two-factor setup when the app requires it. A spent, expired or unknown token renders the `410` page with the next step for its kind."
|
|
3496
|
+
},
|
|
3497
|
+
{
|
|
3498
|
+
"method": "POST",
|
|
3499
|
+
"path": "/app/:appIdentifier/invite",
|
|
3500
|
+
"section": "client-auth",
|
|
3501
|
+
"summary": "Accepts an invitation from the hosted invitation page.",
|
|
3502
|
+
"audience": "internal",
|
|
3503
|
+
"auth": "none",
|
|
3504
|
+
"rateLimited": true,
|
|
3505
|
+
"ownerTier": false,
|
|
3506
|
+
"status": 200,
|
|
3507
|
+
"params": [
|
|
3508
|
+
{
|
|
3509
|
+
"name": "appIdentifier",
|
|
3510
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3511
|
+
}
|
|
3512
|
+
],
|
|
3513
|
+
"query": null,
|
|
3514
|
+
"request": null,
|
|
3515
|
+
"response": null,
|
|
3516
|
+
"errors": [
|
|
3517
|
+
"rate_limited"
|
|
3518
|
+
],
|
|
3519
|
+
"transport": "http",
|
|
3520
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. Spends the token as `POST /api/client/invitations/accept` does; the two-factor setup follows when the app requires it, then the done page."
|
|
3521
|
+
},
|
|
3522
|
+
{
|
|
3523
|
+
"method": "GET",
|
|
3524
|
+
"path": "/app/:appIdentifier/reset/:token",
|
|
3525
|
+
"section": "client-auth",
|
|
3526
|
+
"summary": "Serves the hosted new-password page, the one an unset `reset_url` falls back to.",
|
|
3527
|
+
"audience": "internal",
|
|
3528
|
+
"auth": "none",
|
|
3529
|
+
"rateLimited": false,
|
|
3530
|
+
"ownerTier": false,
|
|
3531
|
+
"status": 200,
|
|
3532
|
+
"params": [
|
|
3533
|
+
{
|
|
3534
|
+
"name": "appIdentifier",
|
|
3535
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3536
|
+
},
|
|
3537
|
+
{
|
|
3538
|
+
"name": "token",
|
|
3539
|
+
"description": "The opaque token from the mailed link; it is never sent as a query parameter."
|
|
3540
|
+
}
|
|
3541
|
+
],
|
|
3542
|
+
"query": null,
|
|
3543
|
+
"request": null,
|
|
3544
|
+
"response": null,
|
|
3545
|
+
"errors": [],
|
|
3546
|
+
"transport": "http",
|
|
3547
|
+
"notes": "HTML. Renders a form; only its `POST` spends the token, so a mail scanner opening the link changes nothing. Says that two-factor stays on. A spent, expired or unknown token renders the `410` page with the next step for its kind."
|
|
3548
|
+
},
|
|
3549
|
+
{
|
|
3550
|
+
"method": "POST",
|
|
3551
|
+
"path": "/app/:appIdentifier/reset",
|
|
3552
|
+
"section": "client-auth",
|
|
3553
|
+
"summary": "Sets the new password from the hosted new-password page.",
|
|
3554
|
+
"audience": "internal",
|
|
3555
|
+
"auth": "none",
|
|
3556
|
+
"rateLimited": true,
|
|
3557
|
+
"ownerTier": false,
|
|
3558
|
+
"status": 200,
|
|
3559
|
+
"params": [
|
|
3560
|
+
{
|
|
3561
|
+
"name": "appIdentifier",
|
|
3562
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
2348
3563
|
}
|
|
2349
3564
|
],
|
|
2350
3565
|
"query": null,
|
|
2351
3566
|
"request": null,
|
|
2352
3567
|
"response": null,
|
|
2353
|
-
"errors": [
|
|
3568
|
+
"errors": [
|
|
3569
|
+
"rate_limited"
|
|
3570
|
+
],
|
|
2354
3571
|
"transport": "http",
|
|
2355
|
-
"notes": "HTML
|
|
3572
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. Spends the token as `POST /api/client/password/reset/confirm` does; a person with an authenticator gives a code before the done page."
|
|
2356
3573
|
},
|
|
2357
3574
|
{
|
|
2358
|
-
"method": "
|
|
2359
|
-
"path": "/
|
|
2360
|
-
"section": "
|
|
2361
|
-
"summary": "
|
|
3575
|
+
"method": "GET",
|
|
3576
|
+
"path": "/app/:appIdentifier/forgot",
|
|
3577
|
+
"section": "client-auth",
|
|
3578
|
+
"summary": "Serves the hosted \"forgot your password\" page.",
|
|
2362
3579
|
"audience": "internal",
|
|
2363
3580
|
"auth": "none",
|
|
2364
|
-
"rateLimited":
|
|
3581
|
+
"rateLimited": false,
|
|
2365
3582
|
"ownerTier": false,
|
|
2366
3583
|
"status": 200,
|
|
2367
|
-
"params": [
|
|
3584
|
+
"params": [
|
|
3585
|
+
{
|
|
3586
|
+
"name": "appIdentifier",
|
|
3587
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3588
|
+
}
|
|
3589
|
+
],
|
|
2368
3590
|
"query": null,
|
|
2369
3591
|
"request": null,
|
|
2370
3592
|
"response": null,
|
|
2371
|
-
"errors": [
|
|
2372
|
-
"rate_limited",
|
|
2373
|
-
"token_spent"
|
|
2374
|
-
],
|
|
3593
|
+
"errors": [],
|
|
2375
3594
|
"transport": "http",
|
|
2376
|
-
"notes": "
|
|
3595
|
+
"notes": "HTML: the address field. Reached from the hosted sign-in and the dead-link page."
|
|
2377
3596
|
},
|
|
2378
3597
|
{
|
|
2379
3598
|
"method": "POST",
|
|
2380
|
-
"path": "/
|
|
2381
|
-
"section": "
|
|
2382
|
-
"summary": "
|
|
3599
|
+
"path": "/app/:appIdentifier/forgot",
|
|
3600
|
+
"section": "client-auth",
|
|
3601
|
+
"summary": "Mails a reset link from the hosted \"forgot your password\" page.",
|
|
2383
3602
|
"audience": "internal",
|
|
2384
3603
|
"auth": "none",
|
|
2385
3604
|
"rateLimited": true,
|
|
2386
3605
|
"ownerTier": false,
|
|
2387
3606
|
"status": 200,
|
|
2388
|
-
"params": [
|
|
3607
|
+
"params": [
|
|
3608
|
+
{
|
|
3609
|
+
"name": "appIdentifier",
|
|
3610
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3611
|
+
}
|
|
3612
|
+
],
|
|
2389
3613
|
"query": null,
|
|
2390
3614
|
"request": null,
|
|
2391
|
-
"response":
|
|
3615
|
+
"response": null,
|
|
2392
3616
|
"errors": [
|
|
2393
|
-
"rate_limited"
|
|
2394
|
-
"token_spent",
|
|
2395
|
-
"invalid_credentials"
|
|
3617
|
+
"rate_limited"
|
|
2396
3618
|
],
|
|
2397
3619
|
"transport": "http",
|
|
2398
|
-
"notes": "
|
|
3620
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. The same \"check your mail\" page for a known and an unknown address, as `POST /api/client/password/reset` answers."
|
|
2399
3621
|
},
|
|
2400
3622
|
{
|
|
2401
3623
|
"method": "GET",
|
|
2402
|
-
"path": "/
|
|
2403
|
-
"section": "
|
|
2404
|
-
"summary": "Serves
|
|
3624
|
+
"path": "/app/:appIdentifier/sign-up",
|
|
3625
|
+
"section": "client-auth",
|
|
3626
|
+
"summary": "Serves the hosted sign-up page, when the app has self-registration on.",
|
|
2405
3627
|
"audience": "internal",
|
|
2406
3628
|
"auth": "none",
|
|
2407
3629
|
"rateLimited": false,
|
|
@@ -2409,8 +3631,8 @@
|
|
|
2409
3631
|
"status": 200,
|
|
2410
3632
|
"params": [
|
|
2411
3633
|
{
|
|
2412
|
-
"name": "
|
|
2413
|
-
"description": "The
|
|
3634
|
+
"name": "appIdentifier",
|
|
3635
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
2414
3636
|
}
|
|
2415
3637
|
],
|
|
2416
3638
|
"query": null,
|
|
@@ -2418,74 +3640,84 @@
|
|
|
2418
3640
|
"response": null,
|
|
2419
3641
|
"errors": [],
|
|
2420
3642
|
"transport": "http",
|
|
2421
|
-
"notes": "HTML
|
|
3643
|
+
"notes": "HTML: the address, a password while the app has passwords on, and an optional name. With self-registration off it renders the \"registration closed\" page."
|
|
2422
3644
|
},
|
|
2423
3645
|
{
|
|
2424
3646
|
"method": "POST",
|
|
2425
|
-
"path": "/
|
|
2426
|
-
"section": "
|
|
2427
|
-
"summary": "
|
|
3647
|
+
"path": "/app/:appIdentifier/sign-up",
|
|
3648
|
+
"section": "client-auth",
|
|
3649
|
+
"summary": "Creates an account from the hosted sign-up page and mails the confirmation link.",
|
|
2428
3650
|
"audience": "internal",
|
|
2429
3651
|
"auth": "none",
|
|
2430
3652
|
"rateLimited": true,
|
|
2431
3653
|
"ownerTier": false,
|
|
2432
3654
|
"status": 200,
|
|
2433
|
-
"params": [
|
|
3655
|
+
"params": [
|
|
3656
|
+
{
|
|
3657
|
+
"name": "appIdentifier",
|
|
3658
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3659
|
+
}
|
|
3660
|
+
],
|
|
2434
3661
|
"query": null,
|
|
2435
3662
|
"request": null,
|
|
2436
3663
|
"response": null,
|
|
2437
3664
|
"errors": [
|
|
2438
|
-
"rate_limited"
|
|
2439
|
-
"token_spent",
|
|
2440
|
-
"signup_closed",
|
|
2441
|
-
"validation_error",
|
|
2442
|
-
"email_taken"
|
|
3665
|
+
"rate_limited"
|
|
2443
3666
|
],
|
|
2444
3667
|
"transport": "http",
|
|
2445
|
-
"notes": "
|
|
3668
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. Registers as `POST /api/client/register` does, and renders the same \"check your mail\" page for a new and a known address."
|
|
2446
3669
|
},
|
|
2447
3670
|
{
|
|
2448
|
-
"method": "
|
|
2449
|
-
"path": "/
|
|
2450
|
-
"section": "
|
|
2451
|
-
"summary": "
|
|
3671
|
+
"method": "GET",
|
|
3672
|
+
"path": "/app/:appIdentifier/verify/:token",
|
|
3673
|
+
"section": "client-auth",
|
|
3674
|
+
"summary": "Serves the hosted email-confirmation page, the one an unset `verify_url` falls back to.",
|
|
2452
3675
|
"audience": "internal",
|
|
2453
3676
|
"auth": "none",
|
|
2454
|
-
"rateLimited":
|
|
3677
|
+
"rateLimited": false,
|
|
2455
3678
|
"ownerTier": false,
|
|
2456
3679
|
"status": 200,
|
|
2457
|
-
"params": [
|
|
3680
|
+
"params": [
|
|
3681
|
+
{
|
|
3682
|
+
"name": "appIdentifier",
|
|
3683
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3684
|
+
},
|
|
3685
|
+
{
|
|
3686
|
+
"name": "token",
|
|
3687
|
+
"description": "The opaque token from the mailed link; it is never sent as a query parameter."
|
|
3688
|
+
}
|
|
3689
|
+
],
|
|
2458
3690
|
"query": null,
|
|
2459
3691
|
"request": null,
|
|
2460
|
-
"response":
|
|
2461
|
-
"errors": [
|
|
2462
|
-
"rate_limited",
|
|
2463
|
-
"token_spent",
|
|
2464
|
-
"signup_closed",
|
|
2465
|
-
"wrong_browser",
|
|
2466
|
-
"validation_error",
|
|
2467
|
-
"email_taken"
|
|
2468
|
-
],
|
|
3692
|
+
"response": null,
|
|
3693
|
+
"errors": [],
|
|
2469
3694
|
"transport": "http",
|
|
2470
|
-
"notes": "
|
|
3695
|
+
"notes": "HTML. Renders a form; only its `POST` spends the token, so a mail scanner opening the link changes nothing. A spent, expired or unknown token renders the `410` page with the next step for its kind."
|
|
2471
3696
|
},
|
|
2472
3697
|
{
|
|
2473
3698
|
"method": "POST",
|
|
2474
|
-
"path": "/
|
|
2475
|
-
"section": "
|
|
2476
|
-
"summary": "
|
|
3699
|
+
"path": "/app/:appIdentifier/verify",
|
|
3700
|
+
"section": "client-auth",
|
|
3701
|
+
"summary": "Confirms the address from the hosted email-confirmation page.",
|
|
2477
3702
|
"audience": "internal",
|
|
2478
3703
|
"auth": "none",
|
|
2479
|
-
"rateLimited":
|
|
3704
|
+
"rateLimited": true,
|
|
2480
3705
|
"ownerTier": false,
|
|
2481
3706
|
"status": 200,
|
|
2482
|
-
"params": [
|
|
3707
|
+
"params": [
|
|
3708
|
+
{
|
|
3709
|
+
"name": "appIdentifier",
|
|
3710
|
+
"description": "The app's public identifier — `app.identifier`, what the console prints beside its copy button. It is not a secret: it appears in this path, in both metadata documents, and in the URL an app user pastes into their AI tool."
|
|
3711
|
+
}
|
|
3712
|
+
],
|
|
2483
3713
|
"query": null,
|
|
2484
3714
|
"request": null,
|
|
2485
|
-
"response":
|
|
2486
|
-
"errors": [
|
|
3715
|
+
"response": null,
|
|
3716
|
+
"errors": [
|
|
3717
|
+
"rate_limited"
|
|
3718
|
+
],
|
|
2487
3719
|
"transport": "http",
|
|
2488
|
-
"notes": "
|
|
3720
|
+
"notes": "HTML: the next page on success, the same page with the problem named on a refusal. Spends the token as `POST /api/client/verify-email` does; the two-factor setup follows when the app requires it, then the done page."
|
|
2489
3721
|
},
|
|
2490
3722
|
{
|
|
2491
3723
|
"method": "GET",
|
|
@@ -2655,7 +3887,7 @@
|
|
|
2655
3887
|
"not_found"
|
|
2656
3888
|
],
|
|
2657
3889
|
"transport": "http",
|
|
2658
|
-
"notes": "RFC 8414, for the issuer `<PUBLIC_API_BASE_URL>/mcp/<identifier>` — the same path rule as the document above, and the same `404` for a switched-off app. `registration_endpoint` is present for the reason the central document states: a client that finds it registers itself and never asks a person for a `client_id`. \n\n**`issuer`, `token_endpoint` and the resource identifier are minted from the canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request's `Host`**, because a client checks a minted token's `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an auth-portal origin
|
|
3890
|
+
"notes": "RFC 8414, for the issuer `<PUBLIC_API_BASE_URL>/mcp/<identifier>` — the same path rule as the document above, and the same `404` for a switched-off app. `registration_endpoint` is present for the reason the central document states: a client that finds it registers itself and never asks a person for a `client_id`. \n\n**`issuer`, `token_endpoint` and the resource identifier are minted from the canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request's `Host`**, because a client checks a minted token's `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an auth-portal origin**: the authorization step renders no page itself. It redirects to the app's own `mcp_login_url`, or to the hosted MCP sign-in on the auth portal when the app has configured none."
|
|
2659
3891
|
},
|
|
2660
3892
|
{
|
|
2661
3893
|
"method": "POST",
|
|
@@ -2687,7 +3919,7 @@
|
|
|
2687
3919
|
"method": "GET",
|
|
2688
3920
|
"path": "/mcp/:appIdentifier/oauth/authorize",
|
|
2689
3921
|
"section": "mcp",
|
|
2690
|
-
"summary": "Starts an MCP sign-in and redirects the browser to the app's own login page.",
|
|
3922
|
+
"summary": "Starts an MCP sign-in and redirects the browser to the app's own login page, or to the hosted one.",
|
|
2691
3923
|
"audience": "client",
|
|
2692
3924
|
"auth": "none",
|
|
2693
3925
|
"rateLimited": false,
|
|
@@ -2703,11 +3935,10 @@
|
|
|
2703
3935
|
"request": null,
|
|
2704
3936
|
"response": null,
|
|
2705
3937
|
"errors": [
|
|
2706
|
-
"not_found"
|
|
2707
|
-
"target_state_conflict"
|
|
3938
|
+
"not_found"
|
|
2708
3939
|
],
|
|
2709
3940
|
"transport": "http",
|
|
2710
|
-
"notes": "The same query as `GET /mcp/oauth/authorize`, read the same way — parameter by parameter, because the answers differ and one parse would collapse them. \n\n**
|
|
3941
|
+
"notes": "The same query as `GET /mcp/oauth/authorize`, read the same way — parameter by parameter, because the answers differ and one parse would collapse them. \n\n**This route renders no page.** It writes an interaction — ten minutes, as the OIDC ones live — and redirects to `appAuthConfig.mcp_login_url` with `{interaction}` filled in. The app then authenticates the person with its own UI, reads `GET /api/client/mcp/interactions/:id` to show the client's claimed name and the scopes it asked for, and calls approve or deny. **An app with no `mcp_login_url` is redirected to the hosted MCP sign-in** (`GET /app/:appIdentifier/mcp/:interaction`), which runs the same steps on the auth portal; nothing is refused for a missing URL. \n\nClient and `redirect_uri` are validated first and a failure there never redirects — the open-redirect discipline `GET /mcp/oauth/authorize` and `GET /api/client/oidc/:slug/start` both keep — and those refusals are RFC 6749's flat `oauthError`, which is why none of them appear above. `redirect_uri` is matched **exactly** against the registration, with no loopback-port wildcard: every client here registered itself minutes ago and can name the port it bound, so a wildcard would only widen where a stolen `client_id` may send a browser. \n\nThe code above is the `apiError` envelope because it is a refusal about the **app**, decided before an OAuth parameter is looked at. **`404 not_found` covers an identifier no app carries AND an app with MCP switched off** — the same single answer the two metadata documents, `register` and the transport give. An earlier draft answered `403 mcp_disabled` here, on the argument that a client which registered while the switch was on is owed the difference between \"turned off\" and \"mistyped\"; that argument does not survive the caller being anonymous. This route takes no credential, so the extra code was readable by anyone who could type an identifier, and it handed back precisely the existence distinction every neighbouring route collapses. `mcp_disabled` survives only where the caller has already proved they belong to the app — the two decision routes under `/api/client/mcp/interactions/:id`."
|
|
2711
3942
|
},
|
|
2712
3943
|
{
|
|
2713
3944
|
"method": "POST",
|
|
@@ -2745,14 +3976,62 @@
|
|
|
2745
3976
|
"params": [],
|
|
2746
3977
|
"query": null,
|
|
2747
3978
|
"request": "client-login-request",
|
|
2748
|
-
"response": "
|
|
3979
|
+
"response": "client-sign-in-result",
|
|
2749
3980
|
"errors": [
|
|
2750
3981
|
"rate_limited",
|
|
2751
3982
|
"validation_error",
|
|
2752
|
-
"invalid_credentials"
|
|
3983
|
+
"invalid_credentials",
|
|
3984
|
+
"method_not_allowed"
|
|
3985
|
+
],
|
|
3986
|
+
"transport": "http",
|
|
3987
|
+
"notes": "One refusal for every miss — unknown app, unknown address, wrong password, a `blocked` account and one still `pending_verification` — because the caller supplies the `app_identifier` unauthenticated, so \"this app knows this user\" is not a fact the answer may carry. The argon2 verify is paid unconditionally, including for an unknown app identifier, so response time is not an oracle either. \n\n**The answer is a `clientSignInResult`**: session tokens, or a `twoFactorChallenge` when the person has a confirmed authenticator or the app requires one — then no session exists until `POST /api/client/two-factor/verify` or the setup is done. `403 method_not_allowed` when the app has the password method off; it names the app's policy, not a person."
|
|
3988
|
+
},
|
|
3989
|
+
{
|
|
3990
|
+
"method": "POST",
|
|
3991
|
+
"path": "/api/client/login/code",
|
|
3992
|
+
"section": "client-auth",
|
|
3993
|
+
"summary": "Mails a six-digit sign-in code, and answers the same whether or not the address exists.",
|
|
3994
|
+
"audience": "client",
|
|
3995
|
+
"auth": "none",
|
|
3996
|
+
"rateLimited": true,
|
|
3997
|
+
"ownerTier": false,
|
|
3998
|
+
"status": 202,
|
|
3999
|
+
"params": [],
|
|
4000
|
+
"query": null,
|
|
4001
|
+
"request": "client-login-code-request",
|
|
4002
|
+
"response": null,
|
|
4003
|
+
"errors": [
|
|
4004
|
+
"rate_limited",
|
|
4005
|
+
"validation_error",
|
|
4006
|
+
"not_found",
|
|
4007
|
+
"method_not_allowed"
|
|
4008
|
+
],
|
|
4009
|
+
"transport": "http",
|
|
4010
|
+
"notes": "**`202` and an empty body for every request the policy allows**, in status, body and timing, whether or not the address names an active account of this app — a decoy like `POST /api/client/resend-verification`, so this is no enumeration oracle. A mail goes out for an `active` account and for one still `pending_verification` — spending the code proves the address, as the verification link would — and never for a `blocked` one or an unknown address. The code is six digits, valid ten minutes, takes five wrong attempts, and a new request expires the previous one for the same address; a request within sixty seconds of the last sends no second mail. The address is trimmed and compared case-insensitively. `404 not_found` is the **app identifier**, never the address; `403 method_not_allowed` when the app has the email-code method off. Limited per app, address and IP, so it cannot be used to mail somebody repeatedly."
|
|
4011
|
+
},
|
|
4012
|
+
{
|
|
4013
|
+
"method": "POST",
|
|
4014
|
+
"path": "/api/client/login/code/verify",
|
|
4015
|
+
"section": "client-auth",
|
|
4016
|
+
"summary": "Spends a mailed sign-in code and answers a session or a two-factor challenge.",
|
|
4017
|
+
"audience": "client",
|
|
4018
|
+
"auth": "none",
|
|
4019
|
+
"rateLimited": true,
|
|
4020
|
+
"ownerTier": false,
|
|
4021
|
+
"status": 200,
|
|
4022
|
+
"params": [],
|
|
4023
|
+
"query": null,
|
|
4024
|
+
"request": "client-login-code-verify-request",
|
|
4025
|
+
"response": "client-sign-in-result",
|
|
4026
|
+
"errors": [
|
|
4027
|
+
"rate_limited",
|
|
4028
|
+
"validation_error",
|
|
4029
|
+
"invalid_code",
|
|
4030
|
+
"token_spent",
|
|
4031
|
+
"method_not_allowed"
|
|
2753
4032
|
],
|
|
2754
4033
|
"transport": "http",
|
|
2755
|
-
"notes": "
|
|
4034
|
+
"notes": "A wrong code is `400 invalid_code` with `details.attempts_left` (`invalidCodeDetails`). A code that is spent, past its ten minutes, out of attempts, or was never mailed is `410 token_spent` — one answer, because telling them apart would say whether a code was ever sent to that address; the recovery is the same, ask for a new code. The address is trimmed and compared case-insensitively, so the address typed at the request and here need not match in case. \n\n**The answer is a `clientSignInResult`**, like the password login: tokens, or a `twoFactorChallenge` when the person has an authenticator or the app requires one. A pending-verification account that spends a code is activated — reading a mail at that address is the proof verification asks for."
|
|
2756
4035
|
},
|
|
2757
4036
|
{
|
|
2758
4037
|
"method": "POST",
|
|
@@ -2778,7 +4057,7 @@
|
|
|
2778
4057
|
"quota_exceeded"
|
|
2779
4058
|
],
|
|
2780
4059
|
"transport": "http",
|
|
2781
|
-
"notes": "**`202` and an empty body for every request policy allows** — a new address, one this app already knows and one it does not answer identically, in status, body and timing. An answer that depended on existence would be the account-enumeration oracle the whole client family is built to avoid. The account cannot log in until the mailed link is spent; `POST /api/client/verify-email` is what does that. \n\n**An address on an account still `pending_verification` is re-registered, not ignored.** The password and display name from this call replace what is stored, every outstanding verification link for the address stops working, and a fresh one is mailed. Otherwise whoever typed an address first would own the password of the account its real owner later verifies. An address on an `active` account changes nothing and sends nothing — that account has already been proven, and its way back in is `POST /api/client/password/reset`. Neither case is visible in the answer. \n\nThe refusals it *does* make are about policy or about what the caller typed, never about a person. `403 registration_closed` when the app has self-registration off and `403 domain_not_allowed` when the address is outside `allowed_domains`: both are the developer's own configuration, and a stranger learns the app's policy rather than who is in it. **A password under twelve characters is part of that `400 validation_error`** and not a code of its own — the minimum is the `password` field's schema rule, and the error names the field, which is what a form needs to mark it. `404 not_found` names an **app identifier no app carries**, and never an address: an app identifier is already public (it is in the MCP metadata path and in the developer's own URLs), while collapsing it into `registration_closed` sent a developer who mistyped their own identifier hunting a configuration bug that was not there. `409 target_state_conflict` when the app has
|
|
4060
|
+
"notes": "**`202` and an empty body for every request policy allows** — a new address, one this app already knows and one it does not answer identically, in status, body and timing. An answer that depended on existence would be the account-enumeration oracle the whole client family is built to avoid. The account cannot log in until the mailed link is spent; `POST /api/client/verify-email` is what does that. \n\n**An address on an account still `pending_verification` is re-registered, not ignored.** The password and display name from this call replace what is stored, every outstanding verification link for the address stops working, and a fresh one is mailed. Otherwise whoever typed an address first would own the password of the account its real owner later verifies. An address on an `active` account changes nothing and sends nothing — that account has already been proven, and its way back in is `POST /api/client/password/reset`. Neither case is visible in the answer. \n\nThe refusals it *does* make are about policy or about what the caller typed, never about a person. `403 registration_closed` when the app has self-registration off and `403 domain_not_allowed` when the address is outside `allowed_domains`: both are the developer's own configuration, and a stranger learns the app's policy rather than who is in it. **A password under twelve characters is part of that `400 validation_error`** and not a code of its own — the minimum is the `password` field's schema rule, and the error names the field, which is what a form needs to mark it. The same `400` names `password` when one is missing while the app's password method is on, or sent while it is off: an email-code-only app registers people without one. `404 not_found` names an **app identifier no app carries**, and never an address: an app identifier is already public (it is in the MCP metadata path and in the developer's own URLs), while collapsing it into `registration_closed` sent a developer who mistyped their own identifier hunting a configuration bug that was not there. `409 target_state_conflict` when the app has no default role — there would be no role to give the person. An app with no `verify_url` is not refused: the mailed link points at the hosted confirmation page instead. \n\n**`409 quota_exceeded` when the org is at its `max_end_users` limit**, counted across every app of the org. It is the one refusal here that is answered **before the address is looked at** — and that ordering is the point rather than an implementation detail: a quota checked after the existence branch would answer `202` for an address the app already knows and `409` for one it does not, which is precisely the enumeration oracle every other line of this route exists to close. At the quota, every registration is refused identically, including one that would only have re-mailed a pending account's link."
|
|
2782
4061
|
},
|
|
2783
4062
|
{
|
|
2784
4063
|
"method": "POST",
|
|
@@ -2793,14 +4072,14 @@
|
|
|
2793
4072
|
"params": [],
|
|
2794
4073
|
"query": null,
|
|
2795
4074
|
"request": "client-verify-email-request",
|
|
2796
|
-
"response": "
|
|
4075
|
+
"response": "client-sign-in-result",
|
|
2797
4076
|
"errors": [
|
|
2798
4077
|
"rate_limited",
|
|
2799
4078
|
"validation_error",
|
|
2800
4079
|
"token_spent"
|
|
2801
4080
|
],
|
|
2802
4081
|
"transport": "http",
|
|
2803
|
-
"notes": "**The answer is a session, not a `204
|
|
4082
|
+
"notes": "**The answer is a session, not a `204`** — or, as on every sign-in step, a `twoFactorChallenge` when the app requires two-factor (`clientSignInResult`). Somebody who has just proved they can read the mail should not be asked to type their password again on the next screen, and the app has an access token to carry them into it. The token is spent first and the account is activated second, as **two writes**: the spend is the atomic one, so a link opened twice cannot mint two sessions, but a process that died between them would leave a spent token on an account still `pending_verification`, whose recovery is `POST /api/client/resend-verification`. Spending the token also proves the address, so a later `PATCH` may return the account to `active` after a block. \n\n**One refusal for every token that does not work: `410 token_spent`** — unknown, past its twenty-four hours, or already used. There is one code because distinguishing them would tell a stranger whether a token ever existed, and because the recovery is the same in all three cases: ask for a fresh link with `POST /api/client/resend-verification`. An app rendering this refusal should offer that and nothing conditional on which of the three it was."
|
|
2804
4083
|
},
|
|
2805
4084
|
{
|
|
2806
4085
|
"method": "POST",
|
|
@@ -2841,10 +4120,11 @@
|
|
|
2841
4120
|
"errors": [
|
|
2842
4121
|
"rate_limited",
|
|
2843
4122
|
"validation_error",
|
|
2844
|
-
"not_found"
|
|
4123
|
+
"not_found",
|
|
4124
|
+
"method_not_allowed"
|
|
2845
4125
|
],
|
|
2846
4126
|
"transport": "http",
|
|
2847
|
-
"notes": "
|
|
4127
|
+
"notes": "The pair of app identifier and address is the identifier: an app user's address is unique only within their app. `403 method_not_allowed` when the app has the password method off — a reset link whose confirmation would be refused is not mailed; the code names the app's policy, not a person. Status, body and timing are identical for a known and an unknown address. An account with no password — one created through an identity provider — is mailed nothing and still answers `202`. `404 not_found` is the **app identifier**, never the address. The link points at the app's `reset_url`, or at the hosted reset page when the app has configured none."
|
|
2848
4128
|
},
|
|
2849
4129
|
{
|
|
2850
4130
|
"method": "POST",
|
|
@@ -2859,14 +4139,15 @@
|
|
|
2859
4139
|
"params": [],
|
|
2860
4140
|
"query": null,
|
|
2861
4141
|
"request": "client-password-reset-confirm-request",
|
|
2862
|
-
"response": "
|
|
4142
|
+
"response": "client-sign-in-result",
|
|
2863
4143
|
"errors": [
|
|
2864
4144
|
"rate_limited",
|
|
2865
4145
|
"validation_error",
|
|
2866
|
-
"token_spent"
|
|
4146
|
+
"token_spent",
|
|
4147
|
+
"method_not_allowed"
|
|
2867
4148
|
],
|
|
2868
4149
|
"transport": "http",
|
|
2869
|
-
"notes": "**Every refresh family of that account is revoked**, then a fresh pair is minted for the caller — a forgotten password is one of the two states where somebody else may be holding a live session, and the person completing the reset is the one who should keep theirs. The account is activated if it was still `pending_verification`: reading a mail at that address is the same proof verification asks for. \n\n**One refusal for every token that does not work: `410 token_spent`** — unknown, past its hour, or already used. There is one code because distinguishing them would tell a stranger whether a token ever existed, and the recovery is identical either way: ask for a new link. A replacement password under twelve characters is a `400 validation_error` naming the `new_password` field — the twelve-character minimum is that field's schema rule, and it is refused the way any other malformed field is."
|
|
4150
|
+
"notes": "**A new password does not bypass the second factor**: a person with an authenticator, or in an app that requires one, gets a `twoFactorChallenge` instead of tokens (`clientSignInResult`), and the authenticator stays on. `403 method_not_allowed` when the app has the password method off. \n\n**Every refresh family of that account is revoked**, then a fresh pair is minted for the caller — a forgotten password is one of the two states where somebody else may be holding a live session, and the person completing the reset is the one who should keep theirs. The account is activated if it was still `pending_verification`: reading a mail at that address is the same proof verification asks for. \n\n**One refusal for every token that does not work: `410 token_spent`** — unknown, past its hour, or already used. There is one code because distinguishing them would tell a stranger whether a token ever existed, and the recovery is identical either way: ask for a new link. A replacement password under twelve characters is a `400 validation_error` naming the `new_password` field — the twelve-character minimum is that field's schema rule, and it is refused the way any other malformed field is."
|
|
2870
4151
|
},
|
|
2871
4152
|
{
|
|
2872
4153
|
"method": "POST",
|
|
@@ -2881,7 +4162,7 @@
|
|
|
2881
4162
|
"params": [],
|
|
2882
4163
|
"query": null,
|
|
2883
4164
|
"request": "client-accept-invitation-request",
|
|
2884
|
-
"response": "
|
|
4165
|
+
"response": "client-sign-in-result",
|
|
2885
4166
|
"errors": [
|
|
2886
4167
|
"rate_limited",
|
|
2887
4168
|
"validation_error",
|
|
@@ -2891,7 +4172,7 @@
|
|
|
2891
4172
|
"quota_exceeded"
|
|
2892
4173
|
],
|
|
2893
4174
|
"transport": "http",
|
|
2894
|
-
"notes": "**An app invitation, not a team one.** `POST /api/org/invitations/accept` is the other space and answers `204`; this one answers a session, because the person is landing in the developer's app and there is no second door for them to sign in through. The role is the one the invitation fixed at creation, so a later change to the app's default role does not re-aim a link already in somebody's inbox, and the invitation **bypasses `allowed_domains`** — a developer inviting somebody by hand has already made the decision the whitelist automates. \n\n**One refusal for every token that does not work: `410 token_spent`** — unknown, expired past the seven days, revoked by the developer, or already accepted. There is one code because telling them apart would say whether a token ever existed, and because the one thing the holder of a dead link can do is ask the developer for a new one, whichever of the four it was. A chosen password under twelve characters is part of the `400 validation_error`, naming the `password` field. `409 email_taken` is an address this app has acquired since the invitation was written **as an account that is already in use** — the invitation stays outstanding rather than being spent, so the developer can revoke it or point the person at the login. An address that registered itself and is still `pending_verification` is not that state: accepting sets the password the invitee just chose, activates the account and gives it the invitation's role, because reading the invitation mail proves the address the verification link was waiting on. \n\n`409 target_state_conflict` names `role_id` with rule `not_set` when the role the invitation was fixed to has since been deleted and the app has no default role to fall back on: there is no access to hand the acceptor, and creating an account with none would be worse than saying so. \n\n**`409 quota_exceeded` when accepting would CREATE an account and the org is at its `max_end_users` limit**, counted across every app of the org. An invitation that names a row the developer already created, and one whose address is held by an unfinished self-registration, both finish an account that already counts — those are not refused, because the org is not one account larger afterwards. The token is not spent by the refusal: the developer can raise the limit, or delete somebody, and the same link still works."
|
|
4175
|
+
"notes": "**An app invitation, not a team one.** `POST /api/org/invitations/accept` is the other space and answers `204`; this one answers a session, because the person is landing in the developer's app and there is no second door for them to sign in through. The role is the one the invitation fixed at creation, so a later change to the app's default role does not re-aim a link already in somebody's inbox, and the invitation **bypasses `allowed_domains`** — a developer inviting somebody by hand has already made the decision the whitelist automates. The answer is a `clientSignInResult`: a `twoFactorChallenge` instead of tokens when the app requires two-factor. `password` is required while the app's password method is on and refused while it is off, both as `400 validation_error` naming the field. \n\n**One refusal for every token that does not work: `410 token_spent`** — unknown, expired past the seven days, revoked by the developer, or already accepted. There is one code because telling them apart would say whether a token ever existed, and because the one thing the holder of a dead link can do is ask the developer for a new one, whichever of the four it was. A chosen password under twelve characters is part of the `400 validation_error`, naming the `password` field. `409 email_taken` is an address this app has acquired since the invitation was written **as an account that is already in use** — the invitation stays outstanding rather than being spent, so the developer can revoke it or point the person at the login. An address that registered itself and is still `pending_verification` is not that state: accepting sets the password the invitee just chose, activates the account and gives it the invitation's role, because reading the invitation mail proves the address the verification link was waiting on. \n\n`409 target_state_conflict` names `role_id` with rule `not_set` when the role the invitation was fixed to has since been deleted and the app has no default role to fall back on: there is no access to hand the acceptor, and creating an account with none would be worse than saying so. \n\n**`409 quota_exceeded` when accepting would CREATE an account and the org is at its `max_end_users` limit**, counted across every app of the org. An invitation that names a row the developer already created, and one whose address is held by an unfinished self-registration, both finish an account that already counts — those are not refused, because the org is not one account larger afterwards. The token is not spent by the refusal: the developer can raise the limit, or delete somebody, and the same link still works."
|
|
2895
4176
|
},
|
|
2896
4177
|
{
|
|
2897
4178
|
"method": "POST",
|
|
@@ -2958,10 +4239,11 @@
|
|
|
2958
4239
|
"forbidden",
|
|
2959
4240
|
"validation_error",
|
|
2960
4241
|
"invalid_credentials",
|
|
2961
|
-
"target_state_conflict"
|
|
4242
|
+
"target_state_conflict",
|
|
4243
|
+
"method_not_allowed"
|
|
2962
4244
|
],
|
|
2963
4245
|
"transport": "http",
|
|
2964
|
-
"notes": "The guard admits all three caller kinds, but a password belongs to an app user specifically — a developer bearer or a server key reaching this is `401 unauthorized`. Every other session of the account ends; the answer is the replacement pair, so the tab that made the change stays signed in. An app user belongs to one app, so \"every session\" is this app's. An account that has **no password** — an OIDC-only app user, which the schema admits — answers `409 target_state_conflict` naming the `password` field with rule `not_set`, not `401`: the session is live and the token is fine, it is the account that has nothing to change, and telling such a caller to sign in again sends them round a loop that ends here."
|
|
4246
|
+
"notes": "`403 method_not_allowed` when the app has the password method off: a stored password stays stored but is not in use, so it is not changed either. The guard admits all three caller kinds, but a password belongs to an app user specifically — a developer bearer or a server key reaching this is `401 unauthorized`. Every other session of the account ends; the answer is the replacement pair, so the tab that made the change stays signed in. An app user belongs to one app, so \"every session\" is this app's. An account that has **no password** — an OIDC-only app user, which the schema admits — answers `409 target_state_conflict` naming the `password` field with rule `not_set`, not `401`: the session is live and the token is fine, it is the account that has nothing to change, and telling such a caller to sign in again sends them round a loop that ends here."
|
|
2965
4247
|
},
|
|
2966
4248
|
{
|
|
2967
4249
|
"method": "GET",
|
|
@@ -2986,6 +4268,105 @@
|
|
|
2986
4268
|
"transport": "http",
|
|
2987
4269
|
"notes": "The one route that answers for all three caller kinds — a developer bearer, an app-user bearer and a server key — which is why the shape names each of `developer_id`, `app_user_id` and `server_key_id` and fills exactly one."
|
|
2988
4270
|
},
|
|
4271
|
+
{
|
|
4272
|
+
"method": "POST",
|
|
4273
|
+
"path": "/api/client/two-factor/verify",
|
|
4274
|
+
"section": "client-auth",
|
|
4275
|
+
"summary": "Answers a two-factor challenge with an authenticator or recovery code, and answers the session.",
|
|
4276
|
+
"audience": "client",
|
|
4277
|
+
"auth": "none",
|
|
4278
|
+
"rateLimited": true,
|
|
4279
|
+
"ownerTier": false,
|
|
4280
|
+
"status": 200,
|
|
4281
|
+
"params": [],
|
|
4282
|
+
"query": null,
|
|
4283
|
+
"request": "client-two-factor-verify-request",
|
|
4284
|
+
"response": "session-tokens",
|
|
4285
|
+
"errors": [
|
|
4286
|
+
"rate_limited",
|
|
4287
|
+
"validation_error",
|
|
4288
|
+
"invalid_code",
|
|
4289
|
+
"token_spent"
|
|
4290
|
+
],
|
|
4291
|
+
"transport": "http",
|
|
4292
|
+
"notes": "The challenge is the one a sign-in step answered with `two_factor_required`; it lives five minutes and takes five wrong codes, after which it is `410 token_spent` and the sign-in starts over. A wrong code is `400 invalid_code` with `details.attempts_left`. **A code is accepted at most once**: the same authenticator code sent twice, even at the same moment, signs in exactly once. A recovery code is spent by its use and audited as `app_user.recovery_code_used`. Exactly one of `code` and `recovery_code`, or `400 validation_error`."
|
|
4293
|
+
},
|
|
4294
|
+
{
|
|
4295
|
+
"method": "POST",
|
|
4296
|
+
"path": "/api/client/two-factor/setup",
|
|
4297
|
+
"section": "client-auth",
|
|
4298
|
+
"summary": "Starts an authenticator setup and answers its secret and otpauth URL.",
|
|
4299
|
+
"audience": "client",
|
|
4300
|
+
"auth": "in_handler",
|
|
4301
|
+
"rateLimited": true,
|
|
4302
|
+
"ownerTier": false,
|
|
4303
|
+
"status": 200,
|
|
4304
|
+
"params": [],
|
|
4305
|
+
"query": null,
|
|
4306
|
+
"request": "client-two-factor-setup-request",
|
|
4307
|
+
"requestOptional": true,
|
|
4308
|
+
"response": "two-factor-setup-response",
|
|
4309
|
+
"errors": [
|
|
4310
|
+
"rate_limited",
|
|
4311
|
+
"validation_error",
|
|
4312
|
+
"token_spent",
|
|
4313
|
+
"unauthorized",
|
|
4314
|
+
"target_state_conflict"
|
|
4315
|
+
],
|
|
4316
|
+
"transport": "http",
|
|
4317
|
+
"notes": "**Two ways in, decided in the handler.** During sign-in the body carries the `two_factor_setup_required` challenge, and that is the credential; from the app's own account settings the app user's bearer is, with no challenge and an empty or missing body. Neither is `401 unauthorized`, and a dead challenge is `410 token_spent`. `409 target_state_conflict` names `two_factor` with rule `off` when the app's policy is `off`. The secret is not in use until `POST /api/client/two-factor/setup/confirm` accepts a code from it; a second call replaces a pending secret, and an account that already has an authenticator keeps it until the new one is confirmed."
|
|
4318
|
+
},
|
|
4319
|
+
{
|
|
4320
|
+
"method": "POST",
|
|
4321
|
+
"path": "/api/client/two-factor/setup/confirm",
|
|
4322
|
+
"section": "client-auth",
|
|
4323
|
+
"summary": "Confirms the new authenticator with a code and answers the recovery codes and a session.",
|
|
4324
|
+
"audience": "client",
|
|
4325
|
+
"auth": "in_handler",
|
|
4326
|
+
"rateLimited": true,
|
|
4327
|
+
"ownerTier": false,
|
|
4328
|
+
"status": 200,
|
|
4329
|
+
"params": [],
|
|
4330
|
+
"query": null,
|
|
4331
|
+
"request": "client-two-factor-setup-confirm-request",
|
|
4332
|
+
"response": "client-two-factor-setup-confirm-response",
|
|
4333
|
+
"errors": [
|
|
4334
|
+
"rate_limited",
|
|
4335
|
+
"validation_error",
|
|
4336
|
+
"invalid_code",
|
|
4337
|
+
"token_spent",
|
|
4338
|
+
"unauthorized"
|
|
4339
|
+
],
|
|
4340
|
+
"transport": "http",
|
|
4341
|
+
"notes": "The same two ways in as `setup`. A code that does not match the pending secret is `400 invalid_code`; no pending setup, or a dead challenge, is `410 token_spent`. On success the authenticator is on, ten recovery codes are issued — shown this once, any earlier set void — and the answer carries a session: the one the sign-in was waiting for, or, from account settings, a fresh one while every other session of the account ends. Audited as `app_user.two_factor_enabled`."
|
|
4342
|
+
},
|
|
4343
|
+
{
|
|
4344
|
+
"method": "DELETE",
|
|
4345
|
+
"path": "/api/client/two-factor",
|
|
4346
|
+
"section": "client-auth",
|
|
4347
|
+
"summary": "Turns the signed-in app user's authenticator off.",
|
|
4348
|
+
"audience": "client",
|
|
4349
|
+
"auth": "developer_or_client",
|
|
4350
|
+
"rateLimited": true,
|
|
4351
|
+
"ownerTier": false,
|
|
4352
|
+
"status": 204,
|
|
4353
|
+
"params": [],
|
|
4354
|
+
"query": null,
|
|
4355
|
+
"request": "client-two-factor-disable-request",
|
|
4356
|
+
"response": null,
|
|
4357
|
+
"errors": [
|
|
4358
|
+
"unauthorized",
|
|
4359
|
+
"token_expired",
|
|
4360
|
+
"token_revoked",
|
|
4361
|
+
"forbidden",
|
|
4362
|
+
"rate_limited",
|
|
4363
|
+
"validation_error",
|
|
4364
|
+
"invalid_code",
|
|
4365
|
+
"target_state_conflict"
|
|
4366
|
+
],
|
|
4367
|
+
"transport": "http",
|
|
4368
|
+
"notes": "The app user's own door; a developer bearer or a server key is `401 unauthorized`, because the factor is the person's. A current code proves they still hold the authenticator: a stolen session alone cannot remove it. The authenticator and every recovery code go. `409 target_state_conflict` names `two_factor` with rule `required` while the app requires two-factor, and with rule `off` when there is none to remove. Audited as `app_user.two_factor_disabled`. The developer's support door is `DELETE /api/apps/:id/users/:userId/two-factor`."
|
|
4369
|
+
},
|
|
2989
4370
|
{
|
|
2990
4371
|
"method": "GET",
|
|
2991
4372
|
"path": "/api/client/providers",
|
|
@@ -3056,7 +4437,7 @@
|
|
|
3056
4437
|
"rate_limited"
|
|
3057
4438
|
],
|
|
3058
4439
|
"transport": "http",
|
|
3059
|
-
"notes": "**One callback URL for every app and every provider**, and the value of `appAuthConfig.oidc_callback_url` — the string a developer registers at their IdP. `CLIENT_OIDC_CALLBACK_PATH` in `client-auth.ts` is the single spelling of this path; the URL is that path on the cloud's canonical public base, never a friendly alias, because the provider compares the redirect target against the one string it was given. \n\n**Rate limited per ip, generously.** The first draft left this route unlimited on the argument that the caller is an identity provider redirecting somebody's browser, so a limiter would drop real sign-ins on the strength of traffic none of those people sent. The cost of that argument is a `state` obtained from one `start` being replayable for the interaction's full ten minutes, unbounded and unauthenticated, with every replay driving a server-side POST to the developer's token endpoint and one audit row into their org — an amplifier against a third party. The interaction is now spent on **every** terminal outcome, refusals included, which closes the replay itself; the limiter is the ceiling on how fast the attempts may arrive at all. It is per ip and sized for a browser, so a person completing a sign-in never meets it. `429 rate_limited` is the one `apiError` this route can answer, and it is not a sign-in outcome — it is a refusal to begin the work, which is why it does not ride back to the app as an `?error=`. \n\nThe query is the **provider's** rather than a Fleetless shape, and `clientOidcCallbackQuery` describes it **without being strict**: `state` always, `code` on success, `error` and `error_description` on the provider's own refusal, and whatever else that provider adds — RFC 9207's `iss`, a `session_state`, a vendor field. Refusing those would refuse conforming providers, the trap `POST /mcp/oauth/register` documents avoiding. `state` is the required field because it is the only one Fleetless minted. \n\n**It lists `rate_limited` and no other code, because every sign-in outcome it has is a redirect.** Success and failure alike are a `302` to the app's own `redirect_uri`: `?code=…&state=…` when a session was resolved, `?error=<clientOidcErrorCode>&state=…` when it was not, so the app renders its own message and can bind either answer to the request it started.
|
|
4440
|
+
"notes": "**One callback URL for every app and every provider**, and the value of `appAuthConfig.oidc_callback_url` — the string a developer registers at their IdP. `CLIENT_OIDC_CALLBACK_PATH` in `client-auth.ts` is the single spelling of this path; the URL is that path on the cloud's canonical public base, never a friendly alias, because the provider compares the redirect target against the one string it was given. \n\n**Rate limited per ip, generously.** The first draft left this route unlimited on the argument that the caller is an identity provider redirecting somebody's browser, so a limiter would drop real sign-ins on the strength of traffic none of those people sent. The cost of that argument is a `state` obtained from one `start` being replayable for the interaction's full ten minutes, unbounded and unauthenticated, with every replay driving a server-side POST to the developer's token endpoint and one audit row into their org — an amplifier against a third party. The interaction is now spent on **every** terminal outcome, refusals included, which closes the replay itself; the limiter is the ceiling on how fast the attempts may arrive at all. It is per ip and sized for a browser, so a person completing a sign-in never meets it. `429 rate_limited` is the one `apiError` this route can answer, and it is not a sign-in outcome — it is a refusal to begin the work, which is why it does not ride back to the app as an `?error=`. \n\nThe query is the **provider's** rather than a Fleetless shape, and `clientOidcCallbackQuery` describes it **without being strict**: `state` always, `code` on success, `error` and `error_description` on the provider's own refusal, and whatever else that provider adds — RFC 9207's `iss`, a `session_state`, a vendor field. Refusing those would refuse conforming providers, the trap `POST /mcp/oauth/register` documents avoiding. `state` is the required field because it is the only one Fleetless minted. \n\n**It lists `rate_limited` and no other code, because every sign-in outcome it has is a redirect.** Success and failure alike are a `302` to the app's own `redirect_uri`: `?code=…&state=…` when a session was resolved, `?error=<clientOidcErrorCode>&state=…` when it was not, so the app renders its own message and can bind either answer to the request it started. This route renders no page for an outcome. \n\n**The one exception is a `state` that resolves to no interaction** — unknown, hand-edited, or past its ten minutes. Then there is no confirmed redirect target to carry the answer to, and bouncing a browser to an unvalidated one is the hole the whole flow is arranged to avoid, so the cloud renders an HTML problem page at `400`. It is HTML rather than an `apiError`, which is why no code is listed: a code here would document an envelope no caller receives, and this manifest's other HTML pages (`GET /mcp/oauth/interaction/:id`, `GET /console/oauth/interaction/:id`) say their status in prose for the same reason."
|
|
3060
4441
|
},
|
|
3061
4442
|
{
|
|
3062
4443
|
"method": "POST",
|
|
@@ -4892,30 +6273,6 @@
|
|
|
4892
6273
|
"transport": "http",
|
|
4893
6274
|
"notes": "A window longer than `USAGE_WINDOW_MAX_DAYS` is refused naming the field, not silently capped: a caller who asked for more than the platform will answer is owed a refusal, not a shorter answer they will mistake for the whole picture. `from_day <= to_day` is a cross-field rule no JSON Schema can express and is enforced here. The window is echoed back."
|
|
4894
6275
|
},
|
|
4895
|
-
{
|
|
4896
|
-
"method": "POST",
|
|
4897
|
-
"path": "/api/feedback",
|
|
4898
|
-
"section": "org",
|
|
4899
|
-
"summary": "Sends a message from a developer to the people who build Fleetless.",
|
|
4900
|
-
"audience": "developer",
|
|
4901
|
-
"auth": "developer",
|
|
4902
|
-
"rateLimited": true,
|
|
4903
|
-
"ownerTier": false,
|
|
4904
|
-
"status": 202,
|
|
4905
|
-
"params": [],
|
|
4906
|
-
"query": null,
|
|
4907
|
-
"request": "feedback-request",
|
|
4908
|
-
"response": "feedback-response",
|
|
4909
|
-
"errors": [
|
|
4910
|
-
"unauthorized",
|
|
4911
|
-
"token_expired",
|
|
4912
|
-
"token_revoked",
|
|
4913
|
-
"validation_error",
|
|
4914
|
-
"rate_limited"
|
|
4915
|
-
],
|
|
4916
|
-
"transport": "http",
|
|
4917
|
-
"notes": "The message is stored before any mail is tried, so `202` means it is kept whatever `mail` says: `sent`, `failed`, or `not_configured` when this cloud has no feedback address. At most 10 messages per developer per hour; the 11th answers `429 rate_limited` with `retry_after_ms`. Replies come by mail, to the sender's address."
|
|
4918
|
-
},
|
|
4919
6276
|
{
|
|
4920
6277
|
"method": "POST",
|
|
4921
6278
|
"path": "/api/bridge/assets",
|