@fleetless/contracts 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -273,6 +273,24 @@
273
273
  "transport": "http",
274
274
  "notes": "HTML. An unknown, spent or expired token renders one \"link no longer valid\" page at `410` — they are one refusal on the wire already, and splitting them here would tell a stranger which tokens ever existed. No rate limiter: the GET changes nothing, and the POST it leads to is limited per IP."
275
275
  },
276
+ {
277
+ "method": "GET",
278
+ "path": "/favicon.svg",
279
+ "section": "client-auth",
280
+ "summary": "Serves the Fleetless icon for the auth portal's and the MCP welcome page's browser tab.",
281
+ "audience": "internal",
282
+ "auth": "none",
283
+ "rateLimited": false,
284
+ "ownerTier": false,
285
+ "status": 200,
286
+ "params": [],
287
+ "query": null,
288
+ "request": null,
289
+ "response": null,
290
+ "errors": [],
291
+ "transport": "http",
292
+ "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."
293
+ },
276
294
  {
277
295
  "method": "POST",
278
296
  "path": "/api/auth/password/reset/confirm",
@@ -1954,7 +1972,7 @@
1954
1972
  "rate_limited"
1955
1973
  ],
1956
1974
  "transport": "http",
1957
- "notes": "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because §3.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which §3.2.1 allows a server to substitute — this authorization server issues `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`."
1975
+ "notes": "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because §3.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which §3.2.1 allows a server to substitute — this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`."
1958
1976
  },
1959
1977
  {
1960
1978
  "method": "GET",
@@ -2103,7 +2121,7 @@
2103
2121
  "response": "oauth-token-response",
2104
2122
  "errors": [],
2105
2123
  "transport": "http",
2106
- "notes": "Only `authorization_code` is supported — there is no refresh grant here, so a session ends when its token expires and the client signs in again. Refusals are RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference. The response carries no `refresh_token`; the shape is the same `oauthTokenResponse` the app flow answers, whose refresh field is optional. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for."
2124
+ "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."
2107
2125
  },
2108
2126
  {
2109
2127
  "method": "GET",
@@ -2473,7 +2491,7 @@
2473
2491
  "not_found"
2474
2492
  ],
2475
2493
  "transport": "http",
2476
- "notes": "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which §3.2.1 allows — `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from — a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not."
2494
+ "notes": "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which §3.2.1 allows — this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from — a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not."
2477
2495
  },
2478
2496
  {
2479
2497
  "method": "GET",
@@ -2522,7 +2540,7 @@
2522
2540
  "response": "oauth-token-response",
2523
2541
  "errors": [],
2524
2542
  "transport": "http",
2525
- "notes": "Only `authorization_code`, PKCE-verified and single-use. There is no refresh grant here either, so a session ends when its token expires and the client signs in again; the shape is the same `oauthTokenResponse` the central endpoint answers, whose refresh field is optional and stays empty. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it — that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash."
2543
+ "notes": "`authorization_code`, PKCE-verified and single-use, and `refresh_token`, which rotates the pair the exchange minted; the refresh token lives ninety days from its last use, is bound to its client and to this app, and a refresh re-reads the app user's status and their standing consent to the client, so a block or a withdrawn consent ends the session at its next refresh at the latest. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it — that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 §5.2's `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash."
2526
2544
  },
2527
2545
  {
2528
2546
  "method": "POST",
@@ -3300,6 +3318,59 @@
3300
3318
  "transport": "http",
3301
3319
  "notes": "For a client caller the grant check runs **before** any existence lookup, with no extra query on either path to time: a denied slug and a nonexistent one must be one answer. That is why an ungranted slug is `403 forbidden` while a granted-but-unconfigured one is `404 unknown_datapoint` and a configured one with no sample yet is `404 no_data` — three facts a caller who is entitled to them needs told apart. The plane built-ins (`bridge_state`, `robot_details`) answer here too, without appearing in any document."
3302
3320
  },
3321
+ {
3322
+ "method": "GET",
3323
+ "path": "/api/client/robots",
3324
+ "section": "robots",
3325
+ "summary": "Lists the robots the caller reaches, with bridge state and the published configuration version.",
3326
+ "audience": "client",
3327
+ "auth": "developer_or_client",
3328
+ "rateLimited": false,
3329
+ "ownerTier": false,
3330
+ "status": 200,
3331
+ "params": [],
3332
+ "query": null,
3333
+ "request": null,
3334
+ "response": "client-robot-list-response",
3335
+ "errors": [
3336
+ "unauthorized",
3337
+ "token_expired",
3338
+ "token_revoked",
3339
+ "forbidden"
3340
+ ],
3341
+ "transport": "http",
3342
+ "notes": "**The REST twin of the MCP tool `robots_list`**, and the one robot question no robot-scoped route can answer: which robots may I name at all. An app user sees the robots their app attaches on which their role grants at least one slug or capability; a server key sees every robot its app attaches; a developer bearer sees the organisation's robots. Name order, id as the tiebreak. A robot on which the role grants nothing is absent rather than listed empty — the same answer `robots_list` gives, for the same reason: reach is a grant, not an attachment. Under `/api/client/` because it names no robot; every robot-scoped read stays under `/api/robots/:id/…`."
3343
+ },
3344
+ {
3345
+ "method": "GET",
3346
+ "path": "/api/robots/:id/datasheet",
3347
+ "section": "robots",
3348
+ "summary": "Describes everything the caller's role lets them do on one robot, with parameter schemas.",
3349
+ "audience": "client",
3350
+ "auth": "developer_or_client",
3351
+ "rateLimited": false,
3352
+ "ownerTier": false,
3353
+ "status": 200,
3354
+ "params": [
3355
+ {
3356
+ "name": "id",
3357
+ "description": "The robot's uuid, as `GET /api/client/robots` lists it."
3358
+ }
3359
+ ],
3360
+ "query": null,
3361
+ "request": null,
3362
+ "response": "mcp-robot-datasheet",
3363
+ "errors": [
3364
+ "unauthorized",
3365
+ "token_expired",
3366
+ "token_revoked",
3367
+ "forbidden",
3368
+ "invalid_uuid",
3369
+ "not_found"
3370
+ ],
3371
+ "transport": "http",
3372
+ "notes": "**The REST twin of the MCP tool `robot_describe`**: one answer per robot — every datapoint, action, service, publisher and camera the role grants, each with its `input_schema` where it takes parameters, plus the two capabilities that gate whole features, `action_history` and `assets`. A robot with nothing published answers an empty `exposures` list, never a refusal. A robot the caller does not reach — not attached to their app, or attached with a role that grants nothing on it — answers `404` exactly as one that does not exist. The app-user datapoint and camera listings under this prefix stay; this is the one read that also names actions, services, publishers and capabilities, which is what an app needs before it can draw a screen."
3373
+ },
3303
3374
  {
3304
3375
  "method": "GET",
3305
3376
  "path": "/api/robots/:id/config/draft",
@@ -39,7 +39,7 @@
39
39
  "refresh_token"
40
40
  ]
41
41
  },
42
- "description": "The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here."
42
+ "description": "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
43
43
  },
44
44
  "code_challenge_methods_supported": {
45
45
  "type": "array",
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "id": {
6
+ "type": "string",
7
+ "format": "uuid",
8
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
9
+ "description": "The robot, and what every robot-scoped route takes as its `:id`."
10
+ },
11
+ "name": {
12
+ "type": "string",
13
+ "minLength": 1,
14
+ "maxLength": 63,
15
+ "description": "The robot's display name, at most 63 characters. Free text, changed through `PATCH /api/robots/:id`."
16
+ },
17
+ "created_at": {
18
+ "type": "string",
19
+ "format": "date-time",
20
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
21
+ "description": "When the robot was created, as an ISO 8601 timestamp."
22
+ },
23
+ "bridge_state": {
24
+ "type": "object",
25
+ "properties": {
26
+ "online": {
27
+ "type": "boolean"
28
+ },
29
+ "latency_ms": {
30
+ "anyOf": [
31
+ {
32
+ "type": "number",
33
+ "minimum": 0
34
+ },
35
+ {
36
+ "type": "null"
37
+ }
38
+ ]
39
+ }
40
+ },
41
+ "required": [
42
+ "online",
43
+ "latency_ms"
44
+ ],
45
+ "additionalProperties": false,
46
+ "description": "The built-in `bridge_state` datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is."
47
+ },
48
+ "published_version": {
49
+ "anyOf": [
50
+ {
51
+ "type": "integer",
52
+ "exclusiveMinimum": 0,
53
+ "maximum": 9007199254740991
54
+ },
55
+ {
56
+ "type": "null"
57
+ }
58
+ ],
59
+ "description": "The published configuration version, or `null` when nothing has been published yet. A robot with nothing published is still listed — \"not configured yet\" is a real state, and the caller is entitled to it — and its datasheet answers an empty exposure list."
60
+ }
61
+ },
62
+ "required": [
63
+ "id",
64
+ "name",
65
+ "created_at",
66
+ "bridge_state",
67
+ "published_version"
68
+ ],
69
+ "additionalProperties": false
70
+ }
@@ -0,0 +1,83 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "robots": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "properties": {
10
+ "id": {
11
+ "type": "string",
12
+ "format": "uuid",
13
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
14
+ "description": "The robot, and what every robot-scoped route takes as its `:id`."
15
+ },
16
+ "name": {
17
+ "type": "string",
18
+ "minLength": 1,
19
+ "maxLength": 63,
20
+ "description": "The robot's display name, at most 63 characters. Free text, changed through `PATCH /api/robots/:id`."
21
+ },
22
+ "created_at": {
23
+ "type": "string",
24
+ "format": "date-time",
25
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
26
+ "description": "When the robot was created, as an ISO 8601 timestamp."
27
+ },
28
+ "bridge_state": {
29
+ "type": "object",
30
+ "properties": {
31
+ "online": {
32
+ "type": "boolean"
33
+ },
34
+ "latency_ms": {
35
+ "anyOf": [
36
+ {
37
+ "type": "number",
38
+ "minimum": 0
39
+ },
40
+ {
41
+ "type": "null"
42
+ }
43
+ ]
44
+ }
45
+ },
46
+ "required": [
47
+ "online",
48
+ "latency_ms"
49
+ ],
50
+ "additionalProperties": false,
51
+ "description": "The built-in `bridge_state` datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is."
52
+ },
53
+ "published_version": {
54
+ "anyOf": [
55
+ {
56
+ "type": "integer",
57
+ "exclusiveMinimum": 0,
58
+ "maximum": 9007199254740991
59
+ },
60
+ {
61
+ "type": "null"
62
+ }
63
+ ],
64
+ "description": "The published configuration version, or `null` when nothing has been published yet. A robot with nothing published is still listed — \"not configured yet\" is a real state, and the caller is entitled to it — and its datasheet answers an empty exposure list."
65
+ }
66
+ },
67
+ "required": [
68
+ "id",
69
+ "name",
70
+ "created_at",
71
+ "bridge_state",
72
+ "published_version"
73
+ ],
74
+ "additionalProperties": false
75
+ },
76
+ "description": "Every robot the caller reaches, in name order with the id as the tiebreak. An app user reaches the robots their app attaches on which their role grants at least one slug or capability; a server key reaches every robot its app attaches; a developer reaches every robot of the organisation."
77
+ }
78
+ },
79
+ "required": [
80
+ "robots"
81
+ ],
82
+ "additionalProperties": false
83
+ }
@@ -27,7 +27,7 @@
27
27
  ]
28
28
  },
29
29
  "grant_types": {
30
- "description": "Accepted for conformance with RFC 7591 and then **ignored**. What comes back is what was actually granted, which §3.2.1 permits a server to substitute: `authorization_code` and nothing else, so a client that asks for `refresh_token` is registered and told plainly that it did not get one.",
30
+ "description": "Accepted for conformance with RFC 7591 and then **ignored**: both MCP authorization servers grant `authorization_code` and `refresh_token` to every registration, and the answer states what was granted (§3.2.1) rather than what was asked.",
31
31
  "type": "array",
32
32
  "items": {
33
33
  "type": "string",
@@ -28,7 +28,7 @@
28
28
  "items": {
29
29
  "type": "string"
30
30
  },
31
- "description": "The grants this client may use. Always exactly `[\"authorization_code\"]` — a client that asked for `refresh_token` is registered and told here that it did not get one, which is the substitution RFC 7591 §3.2.1 permits."
31
+ "description": "The grants this client may use. Always exactly `[\"authorization_code\", \"refresh_token\"]` — an exchange mints a refresh token and the token endpoint rotates it."
32
32
  },
33
33
  "response_types": {
34
34
  "type": "array",
@@ -1,47 +1,85 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "type": "object",
4
- "properties": {
5
- "grant_type": {
6
- "type": "string",
7
- "const": "authorization_code",
8
- "description": "Always `authorization_code`: this request exchanges the code from the authorize redirect for tokens. Any other value — `refresh_token` included — is `unsupported_grant_type`, refused before the code is looked up."
3
+ "oneOf": [
4
+ {
5
+ "type": "object",
6
+ "properties": {
7
+ "grant_type": {
8
+ "type": "string",
9
+ "const": "authorization_code",
10
+ "description": "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
11
+ },
12
+ "code": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "maxLength": 500,
16
+ "description": "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
17
+ },
18
+ "redirect_uri": {
19
+ "type": "string",
20
+ "minLength": 1,
21
+ "maxLength": 2000,
22
+ "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
23
+ },
24
+ "client_id": {
25
+ "type": "string",
26
+ "minLength": 1,
27
+ "maxLength": 200,
28
+ "description": "The client making the exchange, as registered."
29
+ },
30
+ "code_verifier": {
31
+ "type": "string",
32
+ "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
33
+ "description": "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 §4.1 — it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
34
+ },
35
+ "resource": {
36
+ "description": "The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else — which is what keeps a token minted for one app out of another app's endpoint.",
37
+ "type": "string",
38
+ "format": "uri"
39
+ }
40
+ },
41
+ "required": [
42
+ "grant_type",
43
+ "code",
44
+ "redirect_uri",
45
+ "client_id",
46
+ "code_verifier"
47
+ ],
48
+ "description": "RFC 6749 §4.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per §4.1.3, though the server accepts a JSON body too."
9
49
  },
10
- "code": {
11
- "type": "string",
12
- "minLength": 1,
13
- "maxLength": 500,
14
- "description": "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
15
- },
16
- "redirect_uri": {
17
- "type": "string",
18
- "minLength": 1,
19
- "maxLength": 2000,
20
- "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
21
- },
22
- "client_id": {
23
- "type": "string",
24
- "minLength": 1,
25
- "maxLength": 200,
26
- "description": "The client making the exchange, as registered."
27
- },
28
- "code_verifier": {
29
- "type": "string",
30
- "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
31
- "description": "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 §4.1 — it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
32
- },
33
- "resource": {
34
- "description": "The resource the token is requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else — which is what keeps a token minted for one app out of another app's endpoint.",
35
- "type": "string",
36
- "format": "uri"
50
+ {
51
+ "type": "object",
52
+ "properties": {
53
+ "grant_type": {
54
+ "type": "string",
55
+ "const": "refresh_token",
56
+ "description": "`refresh_token`: this request rotates a refresh token into a new access token and a new refresh token. The presented token is consumed; presenting it again revokes the whole session."
57
+ },
58
+ "refresh_token": {
59
+ "type": "string",
60
+ "minLength": 1,
61
+ "maxLength": 500,
62
+ "description": "The refresh token from the last token response. Bound to the client that received it and to one identity space: presented by another client, or at the other MCP server, it is `invalid_grant` and stays unconsumed."
63
+ },
64
+ "client_id": {
65
+ "type": "string",
66
+ "minLength": 1,
67
+ "maxLength": 200,
68
+ "description": "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
69
+ },
70
+ "resource": {
71
+ "description": "The resource the new token is for, per RFC 8707. Optional; when named it must be the audience the session was issued for, or the answer is `invalid_target` and the refresh token is left untouched. The successor carries the same audience either way.",
72
+ "type": "string",
73
+ "format": "uri"
74
+ }
75
+ },
76
+ "required": [
77
+ "grant_type",
78
+ "refresh_token",
79
+ "client_id"
80
+ ],
81
+ "description": "RFC 6749 §6's refresh, as either MCP authorization server reads it. Every use rotates: the answer carries a new refresh token and the presented one is dead."
37
82
  }
38
- },
39
- "required": [
40
- "grant_type",
41
- "code",
42
- "redirect_uri",
43
- "client_id",
44
- "code_verifier"
45
83
  ],
46
- "description": "RFC 6749 §4.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per §4.1.3, though the server accepts a JSON body too."
84
+ "description": "What an MCP token endpoint accepts: the authorization-code exchange, or a refresh. Any other `grant_type` is `unsupported_grant_type`, refused before a lookup happens."
47
85
  }
@@ -19,7 +19,7 @@
19
19
  "description": "How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds."
20
20
  },
21
21
  "refresh_token": {
22
- "description": "The refresh token, when one was issued. It rotates on every use.",
22
+ "description": "The refresh token. Both MCP token endpoints issue one on every exchange and every refresh; it rotates on every use, lives ninety days from its last use, and dies with the account's sessions — a block, a password change, a withdrawn consent. The console's own OAuth portal issues none.",
23
23
  "type": "string",
24
24
  "minLength": 1
25
25
  },
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ /**
4
+ * One robot as `GET /api/client/robots` lists it — the REST twin of the MCP
5
+ * tool `robots_list`, and the one robot question no robot-scoped route can
6
+ * answer: which robots may I name at all.
7
+ *
8
+ * Deliberately not `robotListItem`: that one carries `exposes`, the per-kind
9
+ * counts a developer's list shows, which are a configuration fact rather than
10
+ * something an app user's role grants. What an app user is entitled to is the
11
+ * robot, its bridge state, and whether anything is published on it yet.
12
+ */
13
+ export declare const clientRobotListItem: z.ZodObject<{
14
+ bridge_state: z.ZodObject<{
15
+ online: z.ZodBoolean;
16
+ latency_ms: z.ZodNullable<z.ZodNumber>;
17
+ }, z.core.$strip>;
18
+ published_version: z.ZodNullable<z.ZodNumber>;
19
+ id: z.ZodUUID;
20
+ name: z.ZodString;
21
+ created_at: z.ZodISODateTime;
22
+ }, z.core.$strip>;
23
+ export type ClientRobotListItem = z.infer<typeof clientRobotListItem>;
24
+ /** What `GET /api/client/robots` answers. Never null: a caller who reaches nothing gets an empty array, and an absent key would make "nothing" and "not answered" the same reading. */
25
+ export declare const clientRobotListResponse: z.ZodObject<{
26
+ robots: z.ZodArray<z.ZodObject<{
27
+ bridge_state: z.ZodObject<{
28
+ online: z.ZodBoolean;
29
+ latency_ms: z.ZodNullable<z.ZodNumber>;
30
+ }, z.core.$strip>;
31
+ published_version: z.ZodNullable<z.ZodNumber>;
32
+ id: z.ZodUUID;
33
+ name: z.ZodString;
34
+ created_at: z.ZodISODateTime;
35
+ }, z.core.$strip>>;
36
+ }, z.core.$strip>;
37
+ export type ClientRobotListResponse = z.infer<typeof clientRobotListResponse>;
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ import { bridgeState } from './protocol.js';
4
+ import { robot } from './rest.js';
5
+ /* ------------------------------------------ the robots an app user reaches */
6
+ /**
7
+ * One robot as `GET /api/client/robots` lists it — the REST twin of the MCP
8
+ * tool `robots_list`, and the one robot question no robot-scoped route can
9
+ * answer: which robots may I name at all.
10
+ *
11
+ * Deliberately not `robotListItem`: that one carries `exposes`, the per-kind
12
+ * counts a developer's list shows, which are a configuration fact rather than
13
+ * something an app user's role grants. What an app user is entitled to is the
14
+ * robot, its bridge state, and whether anything is published on it yet.
15
+ */
16
+ export const clientRobotListItem = z.object({
17
+ ...robot.shape,
18
+ bridge_state: bridgeState.meta({
19
+ description: 'The built-in `bridge_state` datapoint as the cloud observes it right now: whether the bridge is connected, and its latency when it is.',
20
+ }),
21
+ published_version: z.number().int().positive().nullable().meta({
22
+ description: 'The published configuration version, or `null` when nothing has been published yet. A robot with nothing published is still listed — "not configured yet" is a real state, and the caller is entitled to it — and its datasheet answers an empty exposure list.',
23
+ }),
24
+ });
25
+ /** What `GET /api/client/robots` answers. Never null: a caller who reaches nothing gets an empty array, and an absent key would make "nothing" and "not answered" the same reading. */
26
+ export const clientRobotListResponse = z.object({
27
+ robots: z.array(clientRobotListItem).meta({
28
+ description: 'Every robot the caller reaches, in name order with the id as the tiebreak. An app user reaches the robots their app attaches on which their role grants at least one slug or capability; a server key reaches every robot its app attaches; a developer reaches every robot of the organisation.',
29
+ }),
30
+ });
package/dist/index.d.ts CHANGED
@@ -35,6 +35,8 @@ export { appIdentifier, app, appListResponse, createAppRequest, updateAppRequest
35
35
  export type { App, AppListResponse, CreateAppRequest, UpdateAppRequest, ServerKey, ServerKeyListResponse, CreateServerKeyResponse, Role, RoleListResponse, RolePermissions, } from './apps.js';
36
36
  export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRegisterRequest, clientVerifyEmailRequest, clientResendVerificationRequest, clientPasswordResetRequest, clientPasswordResetConfirmRequest, clientAcceptInvitationRequest, CLIENT_OIDC_CALLBACK_PATH, clientProviderListQuery, clientProviderListResponse, clientOidcStartQuery, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcErrorCode, clientMcpInteraction, clientMcpInteractionDecisionResponse, mcpConsentGrant, mcpConsentGrantListResponse, clientIdentity, } from './client-auth.js';
37
37
  export type { ClientLoginRequest, ClientRefreshRequest, ClientLogoutRequest, ClientRegisterRequest, ClientVerifyEmailRequest, ClientResendVerificationRequest, ClientPasswordResetRequest, ClientPasswordResetConfirmRequest, ClientAcceptInvitationRequest, ClientProviderListQuery, ClientProviderListResponse, ClientOidcStartQuery, ClientOidcCallbackQuery, ClientOidcExchangeRequest, ClientOidcErrorCode, ClientMcpInteraction, ClientMcpInteractionDecisionResponse, McpConsentGrant, McpConsentGrantListResponse, ClientIdentity, } from './client-auth.js';
38
+ export { clientRobotListItem, clientRobotListResponse } from './client-robots.js';
39
+ export type { ClientRobotListItem, ClientRobotListResponse } from './client-robots.js';
38
40
  export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthConfigRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
39
41
  export type { AppUserStatus, AppUser, AppUserListResponse, CreateAppUserRequest, PatchAppUserRequest, CreateAppInvitationRequest, AppInvitation, PendingAppInvitation, AppInvitationListResponse, AppOidcProvider, AppOidcProviderListResponse, CreateAppOidcProviderRequest, PatchAppOidcProviderRequest, AppAuthConfig, PutAppAuthConfigRequest, MailTemplateKind, AppMailTemplate, AppMailTemplateListResponse, PutAppMailTemplateRequest, MailTemplatePreviewRequest, MailTemplatePreviewResponse, MailTemplateProblemDetails, MailOutcome, } from './app-users.js';
40
42
  export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetTooLargeDetails, assetSyncBusyDetails, ASSET_UPLOAD_MAX_BYTES, } from './assets.js';
@@ -45,7 +47,7 @@ export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertL
45
47
  export type { AlertRowCondition, AlertSeverity, AlertState, DatapointAlertRow, AlertListResponse, OrgFiringAlertsResponse, OrgAlertsQuery, DatapointDisplay, PutDatapointDisplayRequest, } from './alerts.js';
46
48
  export { apiError, parameterViolation, parameterInvalidDetails, ERROR_CODES } from './errors.js';
47
49
  export type { ApiError, ParameterViolation, ParameterInvalidDetails, ErrorCode } from './errors.js';
48
- export { oauthErrorCode, oauthError, oauthRedirectResponse, oauthTokenRequest, oauthTokenResponse, redirectUri, codeChallengeMethod, oauthAuthorizeQuery, dynamicClientRegistrationRequest, MCP_DCR_MAX_REDIRECT_URIS, dynamicClientRegistrationResponse, authorizationServerMetadata, protectedResourceMetadata, } from './oauth.js';
49
- export type { OauthErrorCode, OauthError, OauthRedirectResponse, OauthTokenRequest, OauthTokenResponse, RedirectUri, OauthAuthorizeQuery, DynamicClientRegistrationRequest, DynamicClientRegistrationResponse, AuthorizationServerMetadata, ProtectedResourceMetadata, } from './oauth.js';
50
+ export { oauthErrorCode, oauthError, oauthRedirectResponse, oauthCodeTokenRequest, oauthRefreshTokenRequest, oauthTokenRequest, oauthTokenResponse, redirectUri, codeChallengeMethod, oauthAuthorizeQuery, dynamicClientRegistrationRequest, MCP_DCR_MAX_REDIRECT_URIS, dynamicClientRegistrationResponse, authorizationServerMetadata, protectedResourceMetadata, } from './oauth.js';
51
+ export type { OauthErrorCode, OauthError, OauthRedirectResponse, OauthCodeTokenRequest, OauthRefreshTokenRequest, OauthTokenRequest, OauthTokenResponse, RedirectUri, OauthAuthorizeQuery, DynamicClientRegistrationRequest, DynamicClientRegistrationResponse, AuthorizationServerMetadata, ProtectedResourceMetadata, } from './oauth.js';
50
52
  export { ROUTES, ROUTE_SECTIONS, IN_HANDLER_ROUTES } from './routes.js';
51
53
  export type { RouteEntry, RouteParam, RouteAudience, RouteAuth, RouteSection, RouteMethod, RouteTransport, } from './routes.js';
package/dist/index.js CHANGED
@@ -40,11 +40,12 @@ mailStatus, tierRequiredDetails, passwordChangeRequest, passwordResetRequest, pa
40
40
  authMeResponse, patchOrgRequest, patchAuthMeRequest, } from './identity.js';
41
41
  export { appIdentifier, app, appListResponse, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
42
42
  export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRegisterRequest, clientVerifyEmailRequest, clientResendVerificationRequest, clientPasswordResetRequest, clientPasswordResetConfirmRequest, clientAcceptInvitationRequest, CLIENT_OIDC_CALLBACK_PATH, clientProviderListQuery, clientProviderListResponse, clientOidcStartQuery, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcErrorCode, clientMcpInteraction, clientMcpInteractionDecisionResponse, mcpConsentGrant, mcpConsentGrantListResponse, clientIdentity, } from './client-auth.js';
43
+ export { clientRobotListItem, clientRobotListResponse } from './client-robots.js';
43
44
  // The per-app identity space.
44
45
  export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthConfigRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
45
46
  export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetTooLargeDetails, assetSyncBusyDetails, ASSET_UPLOAD_MAX_BYTES, } from './assets.js';
46
47
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
47
48
  export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertListResponse, orgFiringAlertsResponse, orgAlertsQuery, datapointDisplay, putDatapointDisplayRequest, } from './alerts.js';
48
49
  export { apiError, parameterViolation, parameterInvalidDetails, ERROR_CODES } from './errors.js';
49
- export { oauthErrorCode, oauthError, oauthRedirectResponse, oauthTokenRequest, oauthTokenResponse, redirectUri, codeChallengeMethod, oauthAuthorizeQuery, dynamicClientRegistrationRequest, MCP_DCR_MAX_REDIRECT_URIS, dynamicClientRegistrationResponse, authorizationServerMetadata, protectedResourceMetadata, } from './oauth.js';
50
+ export { oauthErrorCode, oauthError, oauthRedirectResponse, oauthCodeTokenRequest, oauthRefreshTokenRequest, oauthTokenRequest, oauthTokenResponse, redirectUri, codeChallengeMethod, oauthAuthorizeQuery, dynamicClientRegistrationRequest, MCP_DCR_MAX_REDIRECT_URIS, dynamicClientRegistrationResponse, authorizationServerMetadata, protectedResourceMetadata, } from './oauth.js';
50
51
  export { ROUTES, ROUTE_SECTIONS, IN_HANDLER_ROUTES } from './routes.js';
package/dist/mcp.d.ts CHANGED
@@ -169,7 +169,12 @@ export declare const mcpCapabilities: z.ZodObject<{
169
169
  assets: z.ZodBoolean;
170
170
  }, z.core.$strip>;
171
171
  export type McpCapabilities = z.infer<typeof mcpCapabilities>;
172
- /** What one caller may do on one robot — the answer to `robot_describe`. */
172
+ /**
173
+ * What one caller may do on one robot — the answer to `robot_describe`, and
174
+ * since 1.1.0 to `GET /api/robots/:id/datasheet` as well. One schema for both
175
+ * surfaces on purpose: an app and an AI tool read the same description of the
176
+ * same grant. The `mcp` prefix is history, not scope.
177
+ */
173
178
  export declare const mcpRobotDatasheet: z.ZodObject<{
174
179
  robot_id: z.ZodUUID;
175
180
  robot_name: z.ZodString;
package/dist/mcp.js CHANGED
@@ -124,7 +124,12 @@ export const mcpCapabilities = z.object({
124
124
  action_history: z.boolean(),
125
125
  assets: z.boolean(),
126
126
  });
127
- /** What one caller may do on one robot — the answer to `robot_describe`. */
127
+ /**
128
+ * What one caller may do on one robot — the answer to `robot_describe`, and
129
+ * since 1.1.0 to `GET /api/robots/:id/datasheet` as well. One schema for both
130
+ * surfaces on purpose: an app and an AI tool read the same description of the
131
+ * same grant. The `mcp` prefix is history, not scope.
132
+ */
128
133
  export const mcpRobotDatasheet = z.object({
129
134
  robot_id: z.uuid(),
130
135
  robot_name: z.string().min(1).max(200),