@fleetless/contracts 1.0.2 → 1.0.4
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 +48 -0
- package/artifacts/openapi.json +11 -11
- package/artifacts/routes.json +12 -12
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
- package/dist/alerts.d.ts +4 -4
- package/dist/alerts.js +5 -5
- package/dist/app-users.d.ts +11 -13
- package/dist/app-users.js +12 -14
- package/dist/apps.js +2 -1
- package/dist/audit.d.ts +3 -4
- package/dist/audit.js +3 -4
- package/dist/client-auth.d.ts +5 -5
- package/dist/client-auth.js +5 -5
- package/dist/common.d.ts +2 -2
- package/dist/common.js +2 -2
- package/dist/config-issues.d.ts +19 -22
- package/dist/config-issues.js +10 -11
- package/dist/config.d.ts +6 -7
- package/dist/config.js +64 -77
- package/dist/errors.js +22 -29
- package/dist/identity.d.ts +6 -6
- package/dist/identity.js +6 -6
- package/dist/index.js +2 -2
- package/dist/jobs.d.ts +4 -4
- package/dist/jobs.js +4 -4
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +2 -2
- package/dist/oauth.d.ts +8 -9
- package/dist/oauth.js +20 -24
- package/dist/realtime.js +1 -1
- package/dist/rest.d.ts +1 -1
- package/dist/rest.js +4 -4
- package/dist/routes.js +38 -40
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -37,7 +37,7 @@ export const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
|
37
37
|
*
|
|
38
38
|
* **Not parameterised, and that is now a statement rather than the absence of
|
|
39
39
|
* one.** The central endpoint serves the org's team with the console tool
|
|
40
|
-
* family
|
|
40
|
+
* family; an app's users reach a different endpoint, whose
|
|
41
41
|
* path `mcpAppEndpointPath` builds. Two constants for two audiences, so a call
|
|
42
42
|
* site says which it means instead of an argument deciding it.
|
|
43
43
|
*
|
|
@@ -49,7 +49,7 @@ export const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
|
49
49
|
*/
|
|
50
50
|
export const MCP_ENDPOINT_PATH = '/mcp';
|
|
51
51
|
/**
|
|
52
|
-
* The path of **one app's** MCP server
|
|
52
|
+
* The path of **one app's** MCP server — what an app user pastes into
|
|
53
53
|
* their AI tool, served only while the app's `appAuthConfig.mcp_enabled` is on.
|
|
54
54
|
*
|
|
55
55
|
* A helper rather than a template literal at four call sites, for
|
package/dist/oauth.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
* **OAuth 2.1, and it remains only for MCP
|
|
4
|
+
* **OAuth 2.1, and it remains only for MCP.**
|
|
5
5
|
*
|
|
6
6
|
* This file used to describe two front doors: an app's end users signing in
|
|
7
7
|
* through a Fleetless-hosted, app-branded login page, and MCP clients signing
|
|
@@ -172,10 +172,9 @@ export type DynamicClientRegistrationResponse = z.infer<typeof dynamicClientRegi
|
|
|
172
172
|
* **The MCP token endpoint's request — one grant, because the servers serve
|
|
173
173
|
* one.**
|
|
174
174
|
*
|
|
175
|
-
* Both authorization servers, central and per-app, exchange through
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* happens. There is no refresh grant here: a session ends when its token
|
|
175
|
+
* Both authorization servers, central and per-app, exchange through one
|
|
176
|
+
* implementation, whose first act is to refuse anything but
|
|
177
|
+
* `authorization_code` before a single lookup happens. There is no refresh grant here: a session ends when its token
|
|
179
178
|
* expires and the client signs in again.
|
|
180
179
|
*
|
|
181
180
|
* **This was a `discriminatedUnion` with a `refresh_token` branch, and that
|
|
@@ -302,7 +301,7 @@ export type OauthRedirectResponse = z.infer<typeof oauthRedirectResponse>;
|
|
|
302
301
|
* shape and the documentation, not the error path.
|
|
303
302
|
*
|
|
304
303
|
* **Four route entries point at it**: `GET /mcp/oauth/authorize` and
|
|
305
|
-
* `GET /mcp/:appIdentifier/oauth/authorize
|
|
304
|
+
* `GET /mcp/:appIdentifier/oauth/authorize`, which read the same wire.
|
|
306
305
|
* They spent a release naming nothing — the app-level `/oauth/authorize` this
|
|
307
306
|
* was written for was deleted, and `query: null` was read as "there is no
|
|
308
307
|
* query here" rather than as "the handler reads it by hand" — and the eight
|
|
@@ -325,10 +324,10 @@ export type OauthAuthorizeQuery = z.infer<typeof oauthAuthorizeQuery>;
|
|
|
325
324
|
*
|
|
326
325
|
* It carried one parameter, `app_identifier`, on the argument that RFC 7591's
|
|
327
326
|
* registration body has no field for it and one endpoint could serve every
|
|
328
|
-
* app. It was kept — explicitly, in its own doc comment —
|
|
329
|
-
* registration
|
|
327
|
+
* app. It was kept — explicitly, in its own doc comment — for a per-app MCP
|
|
328
|
+
* registration that was said to need exactly this parameter.
|
|
330
329
|
*
|
|
331
|
-
* **That
|
|
330
|
+
* **That registration shipped and needed no such parameter.** `POST
|
|
332
331
|
* /mcp/:appIdentifier/oauth/register` puts the app in the **path**, built by
|
|
333
332
|
* `MCP_APP_PATHS`, and `resolveAppMcpTarget` reads it from `request.params`;
|
|
334
333
|
* `registerMcpDynamicClient` never looks at a query at all. So the one reason
|
package/dist/oauth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
* **OAuth 2.1, and it remains only for MCP
|
|
4
|
+
* **OAuth 2.1, and it remains only for MCP.**
|
|
5
5
|
*
|
|
6
6
|
* This file used to describe two front doors: an app's end users signing in
|
|
7
7
|
* through a Fleetless-hosted, app-branded login page, and MCP clients signing
|
|
@@ -77,8 +77,8 @@ export const oauthError = z.object({
|
|
|
77
77
|
* makes the two indistinguishable to the caller, and *a field that cannot
|
|
78
78
|
* express a distinction produces a workaround somewhere else*. Answering in
|
|
79
79
|
* `apiError` instead would keep the distinction and hand an RFC-compliant
|
|
80
|
-
* client a body it cannot parse
|
|
81
|
-
* to provide.
|
|
80
|
+
* client a body it cannot parse, which is the conformance this dialect
|
|
81
|
+
* exists to provide.
|
|
82
82
|
*
|
|
83
83
|
* So both: `error` is what a standard client reads, `fleetless_code` is what
|
|
84
84
|
* our own tooling switches on. RFC 6749 §5.2 permits additional members, and
|
|
@@ -107,8 +107,7 @@ export const redirectUri = z
|
|
|
107
107
|
.refine((v) => {
|
|
108
108
|
// Parsed, not prefix-matched. `startsWith('https://')` alone accepts the
|
|
109
109
|
// literal string `https://` and anything else that merely opens with
|
|
110
|
-
// those characters — a shape check standing in for a value check
|
|
111
|
-
// is the failure this project keeps meeting under other names.
|
|
110
|
+
// those characters — a shape check standing in for a value check.
|
|
112
111
|
let url;
|
|
113
112
|
try {
|
|
114
113
|
url = new URL(v);
|
|
@@ -232,10 +231,9 @@ export const dynamicClientRegistrationResponse = z.object({
|
|
|
232
231
|
* **The MCP token endpoint's request — one grant, because the servers serve
|
|
233
232
|
* one.**
|
|
234
233
|
*
|
|
235
|
-
* Both authorization servers, central and per-app, exchange through
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* happens. There is no refresh grant here: a session ends when its token
|
|
234
|
+
* Both authorization servers, central and per-app, exchange through one
|
|
235
|
+
* implementation, whose first act is to refuse anything but
|
|
236
|
+
* `authorization_code` before a single lookup happens. There is no refresh grant here: a session ends when its token
|
|
239
237
|
* expires and the client signs in again.
|
|
240
238
|
*
|
|
241
239
|
* **This was a `discriminatedUnion` with a `refresh_token` branch, and that
|
|
@@ -398,14 +396,13 @@ export const oauthRedirectResponse = z.object({
|
|
|
398
396
|
* differently, and it worked — but every path it held belonged to the app-level
|
|
399
397
|
* OAuth flow (`authorize`, `token`, `register`, `consent`, `login`,
|
|
400
398
|
* `impersonate`, `idpCallback`) or to the stub resource's metadata documents,
|
|
401
|
-
* and OAuth 2.1 now remains only for MCP
|
|
402
|
-
*
|
|
403
|
-
* by one file rather than by two repositories.
|
|
399
|
+
* and OAuth 2.1 now remains only for MCP. The MCP authorization server builds
|
|
400
|
+
* its own paths, where they are read by one file rather than by two.
|
|
404
401
|
*
|
|
405
|
-
* Deleted rather than left
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
402
|
+
* Deleted rather than left holding entries whose routes are gone, which is the
|
|
403
|
+
* defect it was created to prevent and then reproduced: an entry naming a route
|
|
404
|
+
* the server had deleted stood for months with nothing noticing. A constant
|
|
405
|
+
* whose every value
|
|
409
406
|
* names a deleted route is that failure at full size.
|
|
410
407
|
*/
|
|
411
408
|
/**
|
|
@@ -421,7 +418,7 @@ export const oauthRedirectResponse = z.object({
|
|
|
421
418
|
* shape and the documentation, not the error path.
|
|
422
419
|
*
|
|
423
420
|
* **Four route entries point at it**: `GET /mcp/oauth/authorize` and
|
|
424
|
-
* `GET /mcp/:appIdentifier/oauth/authorize
|
|
421
|
+
* `GET /mcp/:appIdentifier/oauth/authorize`, which read the same wire.
|
|
425
422
|
* They spent a release naming nothing — the app-level `/oauth/authorize` this
|
|
426
423
|
* was written for was deleted, and `query: null` was read as "there is no
|
|
427
424
|
* query here" rather than as "the handler reads it by hand" — and the eight
|
|
@@ -455,10 +452,9 @@ export const oauthAuthorizeQuery = z
|
|
|
455
452
|
// **No `scope`, because this authorization server issues none.** The field
|
|
456
453
|
// was here describing itself as "carried onto the interaction and read
|
|
457
454
|
// again at consent"; neither authorize handler reads it, the interaction
|
|
458
|
-
// row has no column for it, and the consent screen answers `scopes: []
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// as carried and in fact dropped is worse than one that is absent.
|
|
455
|
+
// row has no column for it, and the consent screen answers `scopes: []`.
|
|
456
|
+
// A parameter documented as carried and in fact dropped is worse than one
|
|
457
|
+
// that is absent.
|
|
462
458
|
})
|
|
463
459
|
.meta({
|
|
464
460
|
description: 'The authorization request an MCP client sends, per RFC 6749 §4.1.1 with mandatory PKCE. The handler reads it parameter by parameter rather than through one parse, because the answers differ: `client_id` and `redirect_uri` are refused flat, with no redirect, since until both are confirmed there is no trusted target to bounce a browser to, and everything after them is reported to the client\'s own callback as query parameters.',
|
|
@@ -468,10 +464,10 @@ export const oauthAuthorizeQuery = z
|
|
|
468
464
|
*
|
|
469
465
|
* It carried one parameter, `app_identifier`, on the argument that RFC 7591's
|
|
470
466
|
* registration body has no field for it and one endpoint could serve every
|
|
471
|
-
* app. It was kept — explicitly, in its own doc comment —
|
|
472
|
-
* registration
|
|
467
|
+
* app. It was kept — explicitly, in its own doc comment — for a per-app MCP
|
|
468
|
+
* registration that was said to need exactly this parameter.
|
|
473
469
|
*
|
|
474
|
-
* **That
|
|
470
|
+
* **That registration shipped and needed no such parameter.** `POST
|
|
475
471
|
* /mcp/:appIdentifier/oauth/register` puts the app in the **path**, built by
|
|
476
472
|
* `MCP_APP_PATHS`, and `resolveAppMcpTarget` reads it from `request.params`;
|
|
477
473
|
* `registerMcpDynamicClient` never looks at a query at all. So the one reason
|
package/dist/realtime.js
CHANGED
|
@@ -295,7 +295,7 @@ export const liveSessionEndReason = z.enum([
|
|
|
295
295
|
/**
|
|
296
296
|
* The cloud ended it and cannot say which of the above applied. **Kept
|
|
297
297
|
* deliberately**: a channel that cannot say "I do not know" will say
|
|
298
|
-
* something false instead,
|
|
298
|
+
* something false instead, which is the costlier failure in
|
|
299
299
|
* the camera path alone.
|
|
300
300
|
*/
|
|
301
301
|
'unknown',
|
package/dist/rest.d.ts
CHANGED
|
@@ -829,7 +829,7 @@ export type ServiceCallResponse = z.infer<typeof serviceCallResponse>;
|
|
|
829
829
|
* **This union exists so the route can name a response at all.** The entry
|
|
830
830
|
* carried `response: null` while the handler demonstrably answers something,
|
|
831
831
|
* which reads in the generated reference as *this route returns nothing* —
|
|
832
|
-
*
|
|
832
|
+
* a documented absence. A `null` there should
|
|
833
833
|
* mean `204`, and on this route it did not.
|
|
834
834
|
*/
|
|
835
835
|
export declare const invokeOrServiceResponse: z.ZodUnion<readonly [z.ZodObject<{
|
package/dist/rest.js
CHANGED
|
@@ -416,7 +416,7 @@ export const serviceCallResponse = z.object({
|
|
|
416
416
|
* **This union exists so the route can name a response at all.** The entry
|
|
417
417
|
* carried `response: null` while the handler demonstrably answers something,
|
|
418
418
|
* which reads in the generated reference as *this route returns nothing* —
|
|
419
|
-
*
|
|
419
|
+
* a documented absence. A `null` there should
|
|
420
420
|
* mean `204`, and on this route it did not.
|
|
421
421
|
*/
|
|
422
422
|
export const invokeOrServiceResponse = z.union([invokeResponse, serviceCallResponse]);
|
|
@@ -546,7 +546,7 @@ export const jobResponse = z.object({
|
|
|
546
546
|
* surface minted it cannot be routed correctly by anything.
|
|
547
547
|
*
|
|
548
548
|
* So the link shapes are fixed here rather than in whichever repo builds them.
|
|
549
|
-
* **They moved to the auth portal
|
|
549
|
+
* **They moved to the auth portal**: the
|
|
550
550
|
* console serves no credential page at all any more, and `{portal}` is the
|
|
551
551
|
* cloud's `AUTH_PUBLIC_URL` — `auth.fleetless.dev` where the deployment has
|
|
552
552
|
* that vhost, the cloud's own base where it does not, since the cloud renders
|
|
@@ -558,7 +558,7 @@ export const jobResponse = z.object({
|
|
|
558
558
|
* | team invitation | `{portal}/accept-invite/{token}` |
|
|
559
559
|
*
|
|
560
560
|
* **An app user's links are not in this table, and cannot be** (2026-09-05,
|
|
561
|
-
*
|
|
561
|
+
* Fleetless renders an app user no page, so there is no `{portal}` path
|
|
562
562
|
* to name: the link points into the **developer's own app**, at the template
|
|
563
563
|
* they configured (`appAuthConfig.invite_url`, `verify_url`, `reset_url`), with
|
|
564
564
|
* the token substituted for `{token}`. That is why those fields are validated
|
|
@@ -569,7 +569,7 @@ export const jobResponse = z.object({
|
|
|
569
569
|
* not a row to restore"*. It was designed; the answer was that the row belongs
|
|
570
570
|
* to the developer and not to this table.
|
|
571
571
|
*
|
|
572
|
-
* The strings themselves live
|
|
572
|
+
* The strings themselves live server-side, read by the
|
|
573
573
|
* route that serves each page AND by the builder that mails it — one constant,
|
|
574
574
|
* because the defect this table records happened again after it was written:
|
|
575
575
|
* the mailer, the page and this table can each spell a path differently, and
|
package/dist/routes.js
CHANGED
|
@@ -63,45 +63,43 @@ export const IN_HANDLER_ROUTES = [
|
|
|
63
63
|
* **The three refusals every `auth: 'developer'` route inherits from its guard**,
|
|
64
64
|
* spelled once rather than retyped eighty times.
|
|
65
65
|
*
|
|
66
|
-
* They are the arms of
|
|
67
|
-
*
|
|
66
|
+
* They are the arms of the developer guard: no bearer or an unverifiable one is
|
|
67
|
+
* `401 unauthorized`, an expired one `401 token_expired`, a
|
|
68
68
|
* vanished account or a bumped `token_version` `401 token_revoked`.
|
|
69
69
|
*
|
|
70
70
|
* **Three, not four: `403 forbidden` went with the Org Admins group.** It stood
|
|
71
71
|
* for "an account that is no longer in the org's Org Admins group", and the
|
|
72
|
-
* two
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
72
|
+
* two identity spaces leave a Fleetless user who IS the team, and the guard's
|
|
73
|
+
* refusal reasons are `'expired' | 'revoked' | 'invalid'` — 401 codes only. A
|
|
74
|
+
* removed team member gets `401 token_revoked`; a reader of the API reference
|
|
75
|
+
* who branched on
|
|
76
76
|
* `forbidden` to render "you lost console access" was branching on an answer no
|
|
77
77
|
* developer-guarded route can send. The `forbidden` producers that remain
|
|
78
78
|
* (`history.ts`, `commands.ts`, `cameras.ts`, `robots.ts`, `mcp.ts`) all sit on
|
|
79
79
|
* `developer_or_client` or MCP surfaces, which is why `CLIENT_GUARD` keeps it.
|
|
80
80
|
*
|
|
81
81
|
* **Not `invalid_token`.** That code exists in `ERROR_CODES` and this guard has
|
|
82
|
-
* never sent it; the three above are what
|
|
83
|
-
* Said plainly because
|
|
84
|
-
*
|
|
85
|
-
* CLAUDE.md's list.
|
|
82
|
+
* never sent it; the three above are what the token refusal actually maps to.
|
|
83
|
+
* Said plainly, because a documented refusal a caller cannot receive is worse
|
|
84
|
+
* than an undocumented one: a consumer branches on it and the branch is dead.
|
|
86
85
|
*/
|
|
87
86
|
const DEVELOPER_GUARD = ['unauthorized', 'token_expired', 'token_revoked'];
|
|
88
87
|
/**
|
|
89
|
-
* The same, for `auth: 'developer_or_client'` —
|
|
90
|
-
*
|
|
91
|
-
* through one `resolveAnyToken`.
|
|
88
|
+
* The same, for `auth: 'developer_or_client'` — one guard resolving a developer
|
|
89
|
+
* bearer, an end-user bearer **or** a server key through a single token lookup.
|
|
92
90
|
*
|
|
93
|
-
* **The same four codes, not five.**
|
|
94
|
-
* `account_blocked`,
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `'revoked'` and `'forbidden'`.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
91
|
+
* **The same four codes, not five.** The token refusal has a fifth arm,
|
|
92
|
+
* `account_blocked`, which this list must not carry. Nothing reaches it: the
|
|
93
|
+
* refusal type admits `'blocked'`, but no site in
|
|
94
|
+
* the cloud constructs one — the only reasons ever returned are `'invalid'`,
|
|
95
|
+
* `'revoked'` and `'forbidden'`. Access is withdrawn by removing an assignment
|
|
96
|
+
* rather than by blocking an account, so an app user who loses access loses it
|
|
97
|
+
* because no assignment resolves, and there is no blocked state left to
|
|
98
|
+
* re-check.
|
|
101
99
|
*
|
|
102
100
|
* Listing it would have documented a refusal no caller can receive — the same
|
|
103
|
-
* mistake as the `invalid_token` above
|
|
104
|
-
*
|
|
101
|
+
* mistake as the `invalid_token` above. No check here catches it, because a
|
|
102
|
+
* code in `ERROR_CODES` satisfies every one of them.
|
|
105
103
|
*
|
|
106
104
|
* It keeps `forbidden`, which `DEVELOPER_GUARD` no longer carries: this guard's
|
|
107
105
|
* routes have live 403 producers — a slug the role does not grant
|
|
@@ -420,7 +418,7 @@ export const ROUTES = [
|
|
|
420
418
|
'listing beside it. `POST /api/client/register` answers `202` to the same fact, because there the caller is a stranger. `404 not_found` ' +
|
|
421
419
|
'is the app, or a `role_id` that is not a role of it — a role of another app is refused rather than stored, since a user holding one ' +
|
|
422
420
|
'would carry rights nothing in this app can resolve. **The password policy answers `400 validation_error`**, not a code of its own: the ' +
|
|
423
|
-
'twelve-character minimum is the `password` field\'s schema rule, and every route
|
|
421
|
+
'twelve-character minimum is the `password` field\'s schema rule, and every route that takes a password refuses a ' +
|
|
424
422
|
'short one exactly the way it refuses any other malformed field. An account created here is `active` immediately: a developer entering ' +
|
|
425
423
|
'somebody by hand has made the decision the verification mail automates, and its address counts as proven. `409 target_state_conflict` ' +
|
|
426
424
|
'names `default_role_id` when `role_id` is absent and the app has no default role, or its default names a role that no longer resolves ' +
|
|
@@ -590,7 +588,7 @@ export const ROUTES = [
|
|
|
590
588
|
'`POST /api/client/invitations/accept`, the same answer one that expired or never existed gets — the developer withdrew it deliberately, ' +
|
|
591
589
|
'and an answer saying so would tell whoever still holds the link that it was once real.',
|
|
592
590
|
},
|
|
593
|
-
/*
|
|
591
|
+
/* ------------------------------------------- the app's OIDC providers */
|
|
594
592
|
{
|
|
595
593
|
method: 'GET', path: '/api/apps/:id/oidc-providers', section: 'apps',
|
|
596
594
|
summary: "Lists every OIDC provider configured on the app, enabled or not.",
|
|
@@ -975,7 +973,7 @@ export const ROUTES = [
|
|
|
975
973
|
audience: 'internal', auth: 'none', rateLimited: true, ownerTier: false, status: 200,
|
|
976
974
|
params: [], query: null, request: null, response: null,
|
|
977
975
|
errors: ['rate_limited', 'validation_error', 'token_spent'], transport: 'http',
|
|
978
|
-
notes: 'The identifier-first step, with nothing left to identify: Fleetless users are password-only
|
|
976
|
+
notes: 'The identifier-first step, with nothing left to identify: Fleetless users are password-only, so **this step does not ' +
|
|
979
977
|
'read the address at all** — it renders the password card for a known address, an unknown one and an empty one alike, and the login ' +
|
|
980
978
|
'step below answers the same `401` for all three. That is a property of the shape rather than of two branches agreeing: there is no ' +
|
|
981
979
|
'lookup here whose result could differ. A browser form post gets the password card; a JSON caller gets `{ "next" }`, which has no ' +
|
|
@@ -1133,10 +1131,10 @@ export const ROUTES = [
|
|
|
1133
1131
|
'a token belongs to its own app\'s endpoint. The code is `forbidden` rather than `mcp_disabled` because nothing is switched off — the ' +
|
|
1134
1132
|
'caller is at the wrong server — and per-app sign-in mints exactly such tokens, so the two states must not share a word. ' +
|
|
1135
1133
|
'`mcp_access_denied` is gone with the per-user override and the ' +
|
|
1136
|
-
'group flag it read: every Fleetless user has MCP access here
|
|
1134
|
+
'group flag it read: every Fleetless user has MCP access here. Stateless: a fresh transport per request, no session id, nothing ' +
|
|
1137
1135
|
'survives the call.',
|
|
1138
1136
|
},
|
|
1139
|
-
/*
|
|
1137
|
+
/* --------------------------------------------- mcp (one app's own server) */
|
|
1140
1138
|
{
|
|
1141
1139
|
method: 'POST', path: MCP_APP.endpoint, section: 'mcp',
|
|
1142
1140
|
summary: "One app's MCP endpoint: the same stateless Streamable HTTP transport, carrying that app's robots.",
|
|
@@ -1174,11 +1172,11 @@ export const ROUTES = [
|
|
|
1174
1172
|
notes: 'MCP\'s Streamable HTTP gives this path three verbs: `POST` carries JSON-RPC, `GET` opens the server-initiated SSE stream, and `DELETE` ' +
|
|
1175
1173
|
'ends a session. This server has no sessions — the argument is in `MCP_PROTOCOL_VERSION`\'s own note, and a per-process session map is ' +
|
|
1176
1174
|
'what breaks at the second cloud instance — so `GET` and `DELETE` answer `405`, which is what a client is built to fall back from. ' +
|
|
1177
|
-
'\n\n**The `405` is this cloud\'s own answer, not the SDK\'s**, and the
|
|
1175
|
+
'\n\n**The `405` is this cloud\'s own answer, not the SDK\'s**, and the two differ: MCP SDK 1.30.0 opens an SSE stream on ' +
|
|
1178
1176
|
'`GET` (`handleGetRequest`) and answers `200` on `DELETE` (`handleDeleteRequest`), neither of which a stateless server has any ' +
|
|
1179
1177
|
'business doing, so the cloud writes the `405` itself in the transport\'s own JSON-RPC error shape with `Allow: POST`. \n\n**The row exists so that the `405` is not a `404`.** An unregistered verb answers ' +
|
|
1180
1178
|
'`404`, and at a path whose last segment is an app identifier a `404` already means *no such app* — one answer for two states, which is ' +
|
|
1181
|
-
'
|
|
1179
|
+
'one answer for two states. Registering the verb lets the endpoint say "this app\'s server is here; this verb is not ' +
|
|
1182
1180
|
'part of it". The central `/mcp` registers neither verb and does not need to: its path takes no parameter, so nothing can misread its ' +
|
|
1183
1181
|
'`404`. \n\n**The `405` body is the transport\'s JSON-RPC error object, not the `apiError` envelope.** The three codes above are the ' +
|
|
1184
1182
|
'refusals that come *first* — the app, its switch, then the bearer, in the order `POST` describes — and they are `apiError` because ' +
|
|
@@ -1228,7 +1226,7 @@ export const ROUTES = [
|
|
|
1228
1226
|
'canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request\'s `Host`**, because a client checks a minted ' +
|
|
1229
1227
|
'token\'s `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an ' +
|
|
1230
1228
|
'auth-portal origin**, and there is nothing here for one to serve: this authorization step renders no Fleetless page at all. It ' +
|
|
1231
|
-
'redirects to the app\'s own `mcp_login_url
|
|
1229
|
+
'redirects to the app\'s own `mcp_login_url`, which is on the developer\'s origin already.',
|
|
1232
1230
|
},
|
|
1233
1231
|
{
|
|
1234
1232
|
method: 'POST', path: MCP_APP.register, section: 'mcp',
|
|
@@ -1236,7 +1234,7 @@ export const ROUTES = [
|
|
|
1236
1234
|
audience: 'client', auth: 'none', rateLimited: true, ownerTier: false, status: 201,
|
|
1237
1235
|
params: [APP_IDENTIFIER], query: null, request: dynamicClientRegistrationRequest, response: dynamicClientRegistrationResponse,
|
|
1238
1236
|
errors: ['rate_limited', 'not_found'], transport: 'http',
|
|
1239
|
-
notes: 'RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` —
|
|
1237
|
+
notes: 'RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a ' +
|
|
1240
1238
|
'second answer to "is this redirect URI acceptable" would agree with the first only by luck. The request schema is what the endpoint ' +
|
|
1241
1239
|
'accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` ' +
|
|
1242
1240
|
'failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, ' +
|
|
@@ -1256,7 +1254,7 @@ export const ROUTES = [
|
|
|
1256
1254
|
params: [APP_IDENTIFIER], query: oauthAuthorizeQuery, request: null, response: null,
|
|
1257
1255
|
errors: ['not_found', 'target_state_conflict'], transport: 'http',
|
|
1258
1256
|
notes: 'The same query as `GET /mcp/oauth/authorize`, read the same way — parameter by parameter, because the answers differ and one parse ' +
|
|
1259
|
-
'would collapse them. \n\n**Fleetless renders no page here
|
|
1257
|
+
'would collapse them. \n\n**Fleetless renders no page here**, and that is the whole of it. The route writes an interaction — ten minutes, as the OIDC ones live ' +
|
|
1260
1258
|
'— and redirects to `appAuthConfig.mcp_login_url` with `{interaction}` filled in. The app then authenticates the person with its own ' +
|
|
1261
1259
|
'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 ' +
|
|
1262
1260
|
'deny. \n\nClient and `redirect_uri` are validated first and a failure there never redirects — the open-redirect discipline `GET ' +
|
|
@@ -1273,7 +1271,7 @@ export const ROUTES = [
|
|
|
1273
1271
|
'under `/api/client/mcp/interactions/:id`. `409 ' +
|
|
1274
1272
|
'target_state_conflict` names `mcp_login_url` with rule `not_set`: MCP is enabled and no page is configured to send the person to. It ' +
|
|
1275
1273
|
'is the same code and the same shape `send_mail` answers for an unconfigured `invite_url`, and the refusal is the honest one — ' +
|
|
1276
|
-
'Fleetless has nowhere to redirect, and rendering a page of its own
|
|
1274
|
+
'Fleetless has nowhere to redirect, and rendering a page of its own would contradict the rule that Fleetless shows an app user no page.',
|
|
1277
1275
|
},
|
|
1278
1276
|
{
|
|
1279
1277
|
method: 'POST', path: MCP_APP.token, section: 'mcp',
|
|
@@ -1516,7 +1514,7 @@ export const ROUTES = [
|
|
|
1516
1514
|
'\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 ' +
|
|
1517
1515
|
'a `302` to the app\'s own ' +
|
|
1518
1516
|
'`redirect_uri`: `?code=…&state=…` when a session was resolved, `?error=<clientOidcErrorCode>&state=…` when it was not, so the app ' +
|
|
1519
|
-
'renders its own message and can bind either answer to the request it started. Fleetless shows an app user no page
|
|
1517
|
+
'renders its own message and can bind either answer to the request it started. Fleetless shows an app user no page. \n\n**The one ' +
|
|
1520
1518
|
'exception is a `state` that resolves to no interaction** — unknown, hand-edited, or past its ten minutes. Then there is no confirmed ' +
|
|
1521
1519
|
'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, ' +
|
|
1522
1520
|
'so the cloud renders an HTML problem page at `400`. That is the only Fleetless-rendered surface an app user can reach. It is HTML ' +
|
|
@@ -1539,7 +1537,7 @@ export const ROUTES = [
|
|
|
1539
1537
|
'is, not how vague the answer is: a mailed one-time code is a credential, an interaction id names a pending request, and the two ' +
|
|
1540
1538
|
'deserve different advice on the app\'s own page.',
|
|
1541
1539
|
},
|
|
1542
|
-
/*
|
|
1540
|
+
/* ------------------------------- the app's own MCP consent screen */
|
|
1543
1541
|
{
|
|
1544
1542
|
method: 'GET', path: '/api/client/mcp/interactions/:id', section: 'client-auth',
|
|
1545
1543
|
summary: 'Reads a pending MCP authorization so the app can draw its own consent screen.',
|
|
@@ -1574,7 +1572,7 @@ export const ROUTES = [
|
|
|
1574
1572
|
query: null, request: null, response: clientMcpInteractionDecisionResponse,
|
|
1575
1573
|
errors: [...CLIENT_GUARD, 'rate_limited', 'interaction_expired', 'mcp_disabled'], transport: 'http',
|
|
1576
1574
|
notes: 'The person is already signed in **at the app**, by whatever means that app uses, and this is the app telling Fleetless what they ' +
|
|
1577
|
-
'decided. Fleetless never sees that sign-in
|
|
1575
|
+
'decided. Fleetless never sees that sign-in. \n\nThe guard admits all three caller kinds and the handler ' +
|
|
1578
1576
|
'takes one: a developer bearer or a server key reaching this is `401 unauthorized`, because a consent is a person\'s and a server key ' +
|
|
1579
1577
|
'is not a person — the same shape `POST /api/client/password/change` has. `403 mcp_disabled` is the app\'s switch, re-read here as it is ' +
|
|
1580
1578
|
'on every request — and it is the one refusal on this surface that names the switch, because reaching it needs an app-user session ' +
|
|
@@ -1615,7 +1613,7 @@ export const ROUTES = [
|
|
|
1615
1613
|
params: [], query: null, request: null, response: mcpConsentGrantListResponse,
|
|
1616
1614
|
errors: [...CLIENT_GUARD], transport: 'http',
|
|
1617
1615
|
notes: '**So the developer\'s app can offer a "connected apps" screen of its own**, which is the only place an end user could ever be shown ' +
|
|
1618
|
-
'this: Fleetless renders no page for an app\'s users
|
|
1616
|
+
'this: Fleetless renders no page for an app\'s users, and the console is the developer\'s tool rather than their customers\'. ' +
|
|
1619
1617
|
'\n\nThe answer is about the bearer\'s own account and takes no user id — there is no id to pass and therefore nothing to pass the ' +
|
|
1620
1618
|
'wrong one. The guard admits all three caller kinds because it is shared, and the handler takes one: a developer bearer or a server ' +
|
|
1621
1619
|
'key is `401 unauthorized`, the shape `POST /api/client/password/change` has, because a consent is a person\'s and a server key is not ' +
|
|
@@ -1937,7 +1935,7 @@ export const ROUTES = [
|
|
|
1937
1935
|
errors: [...CLIENT_GUARD, 'invalid_uuid', 'not_found', 'capability_required', 'validation_error'], transport: 'http',
|
|
1938
1936
|
notes: 'Needs the `action_history` capability, and **this route is what makes that switch mean something** — it was unkeepable while nothing ' +
|
|
1939
1937
|
'durable recorded what had run. Two residuals worth stating rather than implying away. `history` is a syntactically valid slug and ' +
|
|
1940
|
-
'
|
|
1938
|
+
'The router matches a static segment first, so a robot with a service literally slugged `history` can no longer be **read** through ' +
|
|
1941
1939
|
'`GET /api/robots/:id/jobs/:slug`; invoking, cancelling and the listing are unaffected. And a run row names its actor by email address, ' +
|
|
1942
1940
|
'so an end user holding this capability learns which other people have been driving the machine. `robot_id` in the query is shared with ' +
|
|
1943
1941
|
'the org-wide read; a *different* one here is refused rather than quietly answered about the robot in the path.',
|
|
@@ -2278,7 +2276,7 @@ export const ROUTES = [
|
|
|
2278
2276
|
'upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than ' +
|
|
2279
2277
|
'following it: a `preHandler` would already have buffered the whole file. The announced size is refused there too, before a single byte ' +
|
|
2280
2278
|
'is read — it is an announcement and not a proof, so it only ever rejects early and never accepts early, and a body that lies small is ' +
|
|
2281
|
-
'still caught by the real length check. Past both,
|
|
2279
|
+
'still caught by the real length check. Past both, the server\'s own body limit answers a bare `413 bad_request` with neither ceiling nor ' +
|
|
2282
2280
|
'size in it. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler ' +
|
|
2283
2281
|
'registered on this route.',
|
|
2284
2282
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetless/contracts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Fleetless wire contracts: the bridge-cloud protocol, the REST API schemas and the error codes, as zod schemas with generated JSON Schema and OpenAPI artifacts.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dehne Robotik GmbH",
|