@fleetless/contracts 1.1.0 → 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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ the wire shapes.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] — 2026-09-18
11
+
12
+ ### Added
13
+
14
+ - **The MCP token request has its refresh grant back.** `oauthTokenRequest` is a discriminated union again: `oauthCodeTokenRequest` (unchanged) or the new `oauthRefreshTokenRequest` — `grant_type: refresh_token`, `refresh_token`, a required `client_id` and an optional RFC 8707 `resource`. Both MCP authorization servers answer it from cloud 0.20.0: every exchange issues a refresh token, every refresh rotates it, and it lives ninety days from its last use. The registration, token-response and metadata descriptions and the four route notes stop promising there is no refresh grant. Nothing previously valid becomes invalid.
15
+
10
16
  ## [1.1.0] — 2026-09-17
11
17
 
12
18
  ### Added
@@ -3382,7 +3382,7 @@
3382
3382
  }
3383
3383
  }
3384
3384
  },
3385
- "description": "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`.",
3385
+ "description": "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`.",
3386
3386
  "requestBody": {
3387
3387
  "required": true,
3388
3388
  "content": {
@@ -3522,7 +3522,7 @@
3522
3522
  }
3523
3523
  }
3524
3524
  },
3525
- "description": "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.",
3525
+ "description": "`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.",
3526
3526
  "requestBody": {
3527
3527
  "required": true,
3528
3528
  "content": {
@@ -3814,7 +3814,7 @@
3814
3814
  }
3815
3815
  }
3816
3816
  },
3817
- "description": "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.",
3817
+ "description": "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.",
3818
3818
  "requestBody": {
3819
3819
  "required": true,
3820
3820
  "content": {
@@ -3973,7 +3973,7 @@
3973
3973
  }
3974
3974
  }
3975
3975
  },
3976
- "description": "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.",
3976
+ "description": "`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.",
3977
3977
  "requestBody": {
3978
3978
  "required": true,
3979
3979
  "content": {
@@ -9953,7 +9953,7 @@
9953
9953
  "refresh_token"
9954
9954
  ]
9955
9955
  },
9956
- "description": "The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here."
9956
+ "description": "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
9957
9957
  },
9958
9958
  "code_challenge_methods_supported": {
9959
9959
  "type": "array",
@@ -13200,7 +13200,7 @@
13200
13200
  ]
13201
13201
  },
13202
13202
  "grant_types": {
13203
- "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.",
13203
+ "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.",
13204
13204
  "type": "array",
13205
13205
  "items": {
13206
13206
  "type": "string",
@@ -13260,7 +13260,7 @@
13260
13260
  "items": {
13261
13261
  "type": "string"
13262
13262
  },
13263
- "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."
13263
+ "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."
13264
13264
  },
13265
13265
  "response_types": {
13266
13266
  "type": "array",
@@ -14874,50 +14874,88 @@
14874
14874
  "additionalProperties": false
14875
14875
  },
14876
14876
  "oauth-token-request": {
14877
- "type": "object",
14878
- "properties": {
14879
- "grant_type": {
14880
- "type": "string",
14881
- "const": "authorization_code",
14882
- "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."
14883
- },
14884
- "code": {
14885
- "type": "string",
14886
- "minLength": 1,
14887
- "maxLength": 500,
14888
- "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."
14889
- },
14890
- "redirect_uri": {
14891
- "type": "string",
14892
- "minLength": 1,
14893
- "maxLength": 2000,
14894
- "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
14895
- },
14896
- "client_id": {
14897
- "type": "string",
14898
- "minLength": 1,
14899
- "maxLength": 200,
14900
- "description": "The client making the exchange, as registered."
14901
- },
14902
- "code_verifier": {
14903
- "type": "string",
14904
- "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
14905
- "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."
14877
+ "oneOf": [
14878
+ {
14879
+ "type": "object",
14880
+ "properties": {
14881
+ "grant_type": {
14882
+ "type": "string",
14883
+ "const": "authorization_code",
14884
+ "description": "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
14885
+ },
14886
+ "code": {
14887
+ "type": "string",
14888
+ "minLength": 1,
14889
+ "maxLength": 500,
14890
+ "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."
14891
+ },
14892
+ "redirect_uri": {
14893
+ "type": "string",
14894
+ "minLength": 1,
14895
+ "maxLength": 2000,
14896
+ "description": "The same redirect URI the authorize request used. It is compared, not merely recorded."
14897
+ },
14898
+ "client_id": {
14899
+ "type": "string",
14900
+ "minLength": 1,
14901
+ "maxLength": 200,
14902
+ "description": "The client making the exchange, as registered."
14903
+ },
14904
+ "code_verifier": {
14905
+ "type": "string",
14906
+ "pattern": "^[A-Za-z0-9\\-._~]{43,128}$",
14907
+ "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."
14908
+ },
14909
+ "resource": {
14910
+ "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.",
14911
+ "type": "string",
14912
+ "format": "uri"
14913
+ }
14914
+ },
14915
+ "required": [
14916
+ "grant_type",
14917
+ "code",
14918
+ "redirect_uri",
14919
+ "client_id",
14920
+ "code_verifier"
14921
+ ],
14922
+ "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."
14906
14923
  },
14907
- "resource": {
14908
- "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.",
14909
- "type": "string",
14910
- "format": "uri"
14924
+ {
14925
+ "type": "object",
14926
+ "properties": {
14927
+ "grant_type": {
14928
+ "type": "string",
14929
+ "const": "refresh_token",
14930
+ "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."
14931
+ },
14932
+ "refresh_token": {
14933
+ "type": "string",
14934
+ "minLength": 1,
14935
+ "maxLength": 500,
14936
+ "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."
14937
+ },
14938
+ "client_id": {
14939
+ "type": "string",
14940
+ "minLength": 1,
14941
+ "maxLength": 200,
14942
+ "description": "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
14943
+ },
14944
+ "resource": {
14945
+ "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.",
14946
+ "type": "string",
14947
+ "format": "uri"
14948
+ }
14949
+ },
14950
+ "required": [
14951
+ "grant_type",
14952
+ "refresh_token",
14953
+ "client_id"
14954
+ ],
14955
+ "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."
14911
14956
  }
14912
- },
14913
- "required": [
14914
- "grant_type",
14915
- "code",
14916
- "redirect_uri",
14917
- "client_id",
14918
- "code_verifier"
14919
14957
  ],
14920
- "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."
14958
+ "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."
14921
14959
  },
14922
14960
  "oauth-token-response": {
14923
14961
  "type": "object",
@@ -14939,7 +14977,7 @@
14939
14977
  "description": "How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds."
14940
14978
  },
14941
14979
  "refresh_token": {
14942
- "description": "The refresh token, when one was issued. It rotates on every use.",
14980
+ "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.",
14943
14981
  "type": "string",
14944
14982
  "minLength": 1
14945
14983
  },
@@ -1972,7 +1972,7 @@
1972
1972
  "rate_limited"
1973
1973
  ],
1974
1974
  "transport": "http",
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 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`."
1976
1976
  },
1977
1977
  {
1978
1978
  "method": "GET",
@@ -2121,7 +2121,7 @@
2121
2121
  "response": "oauth-token-response",
2122
2122
  "errors": [],
2123
2123
  "transport": "http",
2124
- "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."
2125
2125
  },
2126
2126
  {
2127
2127
  "method": "GET",
@@ -2491,7 +2491,7 @@
2491
2491
  "not_found"
2492
2492
  ],
2493
2493
  "transport": "http",
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 — `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."
2495
2495
  },
2496
2496
  {
2497
2497
  "method": "GET",
@@ -2540,7 +2540,7 @@
2540
2540
  "response": "oauth-token-response",
2541
2541
  "errors": [],
2542
2542
  "transport": "http",
2543
- "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."
2544
2544
  },
2545
2545
  {
2546
2546
  "method": "POST",
@@ -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",
@@ -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
  },
package/dist/index.d.ts CHANGED
@@ -47,7 +47,7 @@ export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertL
47
47
  export type { AlertRowCondition, AlertSeverity, AlertState, DatapointAlertRow, AlertListResponse, OrgFiringAlertsResponse, OrgAlertsQuery, DatapointDisplay, PutDatapointDisplayRequest, } from './alerts.js';
48
48
  export { apiError, parameterViolation, parameterInvalidDetails, ERROR_CODES } from './errors.js';
49
49
  export type { ApiError, ParameterViolation, ParameterInvalidDetails, ErrorCode } from './errors.js';
50
- export { oauthErrorCode, oauthError, oauthRedirectResponse, oauthTokenRequest, oauthTokenResponse, redirectUri, codeChallengeMethod, oauthAuthorizeQuery, dynamicClientRegistrationRequest, MCP_DCR_MAX_REDIRECT_URIS, dynamicClientRegistrationResponse, authorizationServerMetadata, protectedResourceMetadata, } from './oauth.js';
51
- 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';
52
52
  export { ROUTES, ROUTE_SECTIONS, IN_HANDLER_ROUTES } from './routes.js';
53
53
  export type { RouteEntry, RouteParam, RouteAudience, RouteAuth, RouteSection, RouteMethod, RouteTransport, } from './routes.js';
package/dist/index.js CHANGED
@@ -47,5 +47,5 @@ export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse,
47
47
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
48
48
  export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertListResponse, orgFiringAlertsResponse, orgAlertsQuery, datapointDisplay, putDatapointDisplayRequest, } from './alerts.js';
49
49
  export { apiError, parameterViolation, parameterInvalidDetails, ERROR_CODES } from './errors.js';
50
- 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';
51
51
  export { ROUTES, ROUTE_SECTIONS, IN_HANDLER_ROUTES } from './routes.js';
package/dist/oauth.d.ts CHANGED
@@ -168,26 +168,20 @@ export declare const dynamicClientRegistrationResponse: z.ZodObject<{
168
168
  }, z.core.$strip>;
169
169
  export type DynamicClientRegistrationResponse = z.infer<typeof dynamicClientRegistrationResponse>;
170
170
  /**
171
- * **The MCP token endpoint's request — one grant, because the servers serve
172
- * one.**
171
+ * **The MCP token endpoint's request — two grants, one per half of a
172
+ * session.**
173
173
  *
174
- * Both authorization servers, central and per-app, exchange through one
175
- * implementation, whose first act is to refuse anything but
176
- * `authorization_code` before a single lookup happens. There is no refresh grant here: a session ends when its token
177
- * expires and the client signs in again.
174
+ * `authorization_code` mints the first access token and a refresh token;
175
+ * `refresh_token` rotates that refresh token into a new pair. Both MCP
176
+ * authorization servers, central and per-app, answer both; the console's own
177
+ * OAuth portal answers the code grant only.
178
178
  *
179
- * **This was a `discriminatedUnion` with a `refresh_token` branch, and that
180
- * branch had no producer left.** It described the app-level OAuth surface,
181
- * which is deleted; an app user's refresh runs through `POST
182
- * /api/client/refresh` and `refreshRequest`, a different wire on a different
183
- * route. Keeping it would have published, to every MCP client author reading
184
- * `/openapi.json`, a grant the endpoint answers `unsupported_grant_type` to.
185
- * The argument the branch carried is worth keeping even though the branch is
186
- * not: **RFC 8707's `resource` has to survive rotation**, because a refresh
187
- * that drops the audience mints a successor with no `aud`, and the validating
188
- * resource then refuses a token the caller obtained legitimately — one token
189
- * lifetime after a login that worked, to somebody who did nothing wrong. If a
190
- * refresh grant is ever added here, it carries `resource`.
179
+ * **RFC 8707's `resource` has to survive rotation**: a refresh that drops the
180
+ * audience mints a successor with no `aud`, and the validating resource then
181
+ * refuses a token the caller obtained legitimately. So the server keeps the
182
+ * audience on the refresh token's own row, and a `resource` named here must
183
+ * match it or the answer is `invalid_target` — before the token is consumed,
184
+ * so a typo costs nothing.
191
185
  *
192
186
  * `code_verifier`'s bounds are RFC 7636 §4.1's, charset included. A verifier
193
187
  * is compared, not parsed, so a length nobody checks is a length an attacker
@@ -201,7 +195,7 @@ export type DynamicClientRegistrationResponse = z.infer<typeof dynamicClientRegi
201
195
  * **stripped**, which bit the test for this schema: `safeParse().success`
202
196
  * cannot tell a present field from an absent one. Assert on the parsed value.
203
197
  */
204
- export declare const oauthTokenRequest: z.ZodObject<{
198
+ export declare const oauthCodeTokenRequest: z.ZodObject<{
205
199
  grant_type: z.ZodLiteral<"authorization_code">;
206
200
  code: z.ZodString;
207
201
  redirect_uri: z.ZodString;
@@ -209,6 +203,27 @@ export declare const oauthTokenRequest: z.ZodObject<{
209
203
  code_verifier: z.ZodString;
210
204
  resource: z.ZodOptional<z.ZodURL>;
211
205
  }, z.core.$strip>;
206
+ export type OauthCodeTokenRequest = z.infer<typeof oauthCodeTokenRequest>;
207
+ export declare const oauthRefreshTokenRequest: z.ZodObject<{
208
+ grant_type: z.ZodLiteral<"refresh_token">;
209
+ refresh_token: z.ZodString;
210
+ client_id: z.ZodString;
211
+ resource: z.ZodOptional<z.ZodURL>;
212
+ }, z.core.$strip>;
213
+ export type OauthRefreshTokenRequest = z.infer<typeof oauthRefreshTokenRequest>;
214
+ export declare const oauthTokenRequest: z.ZodDiscriminatedUnion<[z.ZodObject<{
215
+ grant_type: z.ZodLiteral<"authorization_code">;
216
+ code: z.ZodString;
217
+ redirect_uri: z.ZodString;
218
+ client_id: z.ZodString;
219
+ code_verifier: z.ZodString;
220
+ resource: z.ZodOptional<z.ZodURL>;
221
+ }, z.core.$strip>, z.ZodObject<{
222
+ grant_type: z.ZodLiteral<"refresh_token">;
223
+ refresh_token: z.ZodString;
224
+ client_id: z.ZodString;
225
+ resource: z.ZodOptional<z.ZodURL>;
226
+ }, z.core.$strip>], "grant_type">;
212
227
  export type OauthTokenRequest = z.infer<typeof oauthTokenRequest>;
213
228
  /**
214
229
  * RFC 6749 §5.1's success envelope — **the second deliberate dialect, and this
package/dist/oauth.js CHANGED
@@ -188,7 +188,7 @@ export const dynamicClientRegistrationRequest = z
188
188
  description: '`none`, RFC 7591\'s value for a public client, and the only value either server registers. Any other value is **refused rather than silently downgraded**: a client that believes it holds a secret and does not has a wrong mental model of its own security. There is no client secret to hold — mandatory PKCE (`S256`) is the defence.',
189
189
  }),
190
190
  grant_types: z.array(z.enum(['authorization_code', 'refresh_token'])).optional().meta({
191
- 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.',
191
+ 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.',
192
192
  }),
193
193
  response_types: z.array(z.enum(['code'])).optional().meta({
194
194
  description: 'Accepted for conformance and then **ignored**; the response names `code`, which is the only response type OAuth 2.1 leaves, the implicit grant having been removed.',
@@ -211,7 +211,7 @@ export const dynamicClientRegistrationResponse = z.object({
211
211
  description: 'The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else.',
212
212
  }),
213
213
  grant_types: z.array(z.string()).meta({
214
- 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.',
214
+ 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.',
215
215
  }),
216
216
  response_types: z.array(z.string()).meta({
217
217
  description: 'The response types this client may ask for: `code`.',
@@ -227,26 +227,20 @@ export const dynamicClientRegistrationResponse = z.object({
227
227
  }),
228
228
  });
229
229
  /**
230
- * **The MCP token endpoint's request — one grant, because the servers serve
231
- * one.**
230
+ * **The MCP token endpoint's request — two grants, one per half of a
231
+ * session.**
232
232
  *
233
- * Both authorization servers, central and per-app, exchange through one
234
- * implementation, whose first act is to refuse anything but
235
- * `authorization_code` before a single lookup happens. There is no refresh grant here: a session ends when its token
236
- * expires and the client signs in again.
233
+ * `authorization_code` mints the first access token and a refresh token;
234
+ * `refresh_token` rotates that refresh token into a new pair. Both MCP
235
+ * authorization servers, central and per-app, answer both; the console's own
236
+ * OAuth portal answers the code grant only.
237
237
  *
238
- * **This was a `discriminatedUnion` with a `refresh_token` branch, and that
239
- * branch had no producer left.** It described the app-level OAuth surface,
240
- * which is deleted; an app user's refresh runs through `POST
241
- * /api/client/refresh` and `refreshRequest`, a different wire on a different
242
- * route. Keeping it would have published, to every MCP client author reading
243
- * `/openapi.json`, a grant the endpoint answers `unsupported_grant_type` to.
244
- * The argument the branch carried is worth keeping even though the branch is
245
- * not: **RFC 8707's `resource` has to survive rotation**, because a refresh
246
- * that drops the audience mints a successor with no `aud`, and the validating
247
- * resource then refuses a token the caller obtained legitimately — one token
248
- * lifetime after a login that worked, to somebody who did nothing wrong. If a
249
- * refresh grant is ever added here, it carries `resource`.
238
+ * **RFC 8707's `resource` has to survive rotation**: a refresh that drops the
239
+ * audience mints a successor with no `aud`, and the validating resource then
240
+ * refuses a token the caller obtained legitimately. So the server keeps the
241
+ * audience on the refresh token's own row, and a `resource` named here must
242
+ * match it or the answer is `invalid_target` — before the token is consumed,
243
+ * so a typo costs nothing.
250
244
  *
251
245
  * `code_verifier`'s bounds are RFC 7636 §4.1's, charset included. A verifier
252
246
  * is compared, not parsed, so a length nobody checks is a length an attacker
@@ -260,10 +254,10 @@ export const dynamicClientRegistrationResponse = z.object({
260
254
  * **stripped**, which bit the test for this schema: `safeParse().success`
261
255
  * cannot tell a present field from an absent one. Assert on the parsed value.
262
256
  */
263
- export const oauthTokenRequest = z
257
+ export const oauthCodeTokenRequest = z
264
258
  .object({
265
259
  grant_type: z.literal('authorization_code').meta({
266
- 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.',
260
+ description: '`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token.',
267
261
  }),
268
262
  code: z.string().min(1).max(500).meta({
269
263
  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.',
@@ -284,6 +278,27 @@ export const oauthTokenRequest = z
284
278
  .meta({
285
279
  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.",
286
280
  });
281
+ export const oauthRefreshTokenRequest = z
282
+ .object({
283
+ grant_type: z.literal('refresh_token').meta({
284
+ 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.',
285
+ }),
286
+ refresh_token: z.string().min(1).max(500).meta({
287
+ 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.',
288
+ }),
289
+ client_id: z.string().min(1).max(200).meta({
290
+ description: 'The client the refresh token was issued to, as registered. A refresh token is not transferable between clients.',
291
+ }),
292
+ resource: z.url().optional().meta({
293
+ 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.',
294
+ }),
295
+ })
296
+ .meta({
297
+ 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.",
298
+ });
299
+ export const oauthTokenRequest = z.discriminatedUnion('grant_type', [oauthCodeTokenRequest, oauthRefreshTokenRequest]).meta({
300
+ 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.',
301
+ });
287
302
  /**
288
303
  * RFC 6749 §5.1's success envelope — **the second deliberate dialect, and this
289
304
  * one is a success shape rather than an error shape.**
@@ -311,7 +326,7 @@ export const oauthTokenResponse = z.object({
311
326
  description: 'How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds.',
312
327
  }),
313
328
  refresh_token: z.string().min(1).optional().meta({
314
- description: 'The refresh token, when one was issued. It rotates on every use.',
329
+ 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.',
315
330
  }),
316
331
  scope: z.string().max(500).optional().meta({
317
332
  description: 'The scopes the issued token actually carries, space-separated.',
@@ -335,7 +350,7 @@ export const authorizationServerMetadata = z.object({
335
350
  description: 'The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1.',
336
351
  }),
337
352
  grant_types_supported: z.array(z.enum(['authorization_code', 'refresh_token'])).meta({
338
- description: 'The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here.',
353
+ description: 'The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here.',
339
354
  }),
340
355
  code_challenge_methods_supported: z.array(codeChallengeMethod).meta({
341
356
  description: 'The PKCE challenge methods accepted: `S256` only. `plain` is not offered — a challenge equal to its verifier defends against nothing, and offering it would make a downgrade negotiable.',
package/dist/routes.js CHANGED
@@ -948,8 +948,8 @@ export const ROUTES = [
948
948
  'caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming ' +
949
949
  'client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and ' +
950
950
  '`redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what ' +
951
- 'was actually granted, which §3.2.1 allows a server to substitute — this authorization server issues `authorization_code` only, so a ' +
952
- 'client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. Refusals ' +
951
+ 'was actually granted, which §3.2.1 allows a server to substitute — this authorization server grants `authorization_code` and ' +
952
+ '`refresh_token` to every registration. The registration carries a TTL. Refusals ' +
953
953
  'are `oauthError`; the rate limiter answers `apiError`.',
954
954
  },
955
955
  {
@@ -1023,10 +1023,11 @@ export const ROUTES = [
1023
1023
  audience: 'client', auth: 'none', rateLimited: false, ownerTier: false, status: 200,
1024
1024
  params: [], query: null, request: oauthTokenRequest, response: oauthTokenResponse,
1025
1025
  errors: [], transport: 'http',
1026
- notes: 'Only `authorization_code` is supported — there is no refresh grant here, so a session ends when its token expires and the client signs ' +
1027
- '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 ' +
1028
- '`refresh_token`; the shape is the same `oauthTokenResponse` the app flow answers, whose refresh field is optional. The code is ' +
1029
- 'single-use, PKCE-verified, and its `resource` must match the audience it was authorized for.',
1026
+ notes: '`authorization_code` mints an `mcp_session` access token bound to the central resource and a refresh token; `refresh_token` rotates that pair, ' +
1027
+ 'and the presented refresh token is consumed — a second presentation revokes the session, as on `/api/auth/refresh`. The refresh token lives ninety days ' +
1028
+ '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. ' +
1029
+ '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 ' +
1030
+ '`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.',
1030
1031
  },
1031
1032
  /* ------------------------------- developer auth (the console\'s OAuth portal) */
1032
1033
  {
@@ -1248,8 +1249,8 @@ export const ROUTES = [
1248
1249
  'accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` ' +
1249
1250
  'failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, ' +
1250
1251
  'and what comes back is what was actually ' +
1251
- 'granted, which §3.2.1 allows — `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly ' +
1252
- '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 ' +
1252
+ 'granted, which §3.2.1 allows — this authorization server grants `authorization_code` and `refresh_token` to every registration. ' +
1253
+ 'The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here ' +
1253
1254
  'authorizes at this app\'s endpoint and nowhere else, so a client registered against one app cannot walk into another\'s authorize with ' +
1254
1255
  'it, and a developer who switches MCP off is not left with strangers\' registrations valid somewhere adjacent. \n\nRefusals are ' +
1255
1256
  '`oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the ' +
@@ -1288,9 +1289,8 @@ export const ROUTES = [
1288
1289
  audience: 'client', auth: 'none', rateLimited: false, ownerTier: false, status: 200,
1289
1290
  params: [APP_IDENTIFIER], query: null, request: oauthTokenRequest, response: oauthTokenResponse,
1290
1291
  errors: [], transport: 'http',
1291
- notes: 'Only `authorization_code`, PKCE-verified and single-use. There is no refresh grant here either, so a session ends when its token ' +
1292
- 'expires and the client signs in again; the shape is the same `oauthTokenResponse` the central endpoint answers, whose refresh field is ' +
1293
- 'optional and stays empty. **The `aud` is this app\'s endpoint URL on the canonical public base**, and the code\'s `resource` must match ' +
1292
+ 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. ' +
1293
+ '**The `aud` is this app\'s endpoint URL on the canonical public base**, and the code\'s `resource` must match ' +
1294
1294
  '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 ' +
1295
1295
  '§5.2\'s `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown ' +
1296
1296
  'identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
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",