@fleetless/contracts 1.0.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.
Files changed (287) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +17 -0
  4. package/README.md +88 -0
  5. package/artifacts/constants.json +24 -0
  6. package/artifacts/openapi.json +17219 -0
  7. package/artifacts/routes.json +4605 -0
  8. package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
  9. package/artifacts/schema/action-config.schema.json +198 -0
  10. package/artifacts/schema/alert-list-response.schema.json +172 -0
  11. package/artifacts/schema/api-error.schema.json +20 -0
  12. package/artifacts/schema/app-auth-config.schema.json +106 -0
  13. package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
  14. package/artifacts/schema/app-invitation.schema.json +69 -0
  15. package/artifacts/schema/app-list-response.schema.json +82 -0
  16. package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
  17. package/artifacts/schema/app-mail-template.schema.json +54 -0
  18. package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
  19. package/artifacts/schema/app-oidc-provider.schema.json +80 -0
  20. package/artifacts/schema/app-user-list-response.schema.json +111 -0
  21. package/artifacts/schema/app-user.schema.json +98 -0
  22. package/artifacts/schema/app.schema.json +69 -0
  23. package/artifacts/schema/apply-error.schema.json +41 -0
  24. package/artifacts/schema/asset-list-response.schema.json +288 -0
  25. package/artifacts/schema/asset-sync-request.schema.json +17 -0
  26. package/artifacts/schema/asset-sync-response.schema.json +16 -0
  27. package/artifacts/schema/asset-sync-status.schema.json +136 -0
  28. package/artifacts/schema/asset.schema.json +68 -0
  29. package/artifacts/schema/audit-actor.schema.json +32 -0
  30. package/artifacts/schema/audit-event.schema.json +119 -0
  31. package/artifacts/schema/audit-list-response.schema.json +144 -0
  32. package/artifacts/schema/audit-query.schema.json +79 -0
  33. package/artifacts/schema/auth-error.schema.json +23 -0
  34. package/artifacts/schema/auth-me-response.schema.json +99 -0
  35. package/artifacts/schema/auth-ok.schema.json +115 -0
  36. package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
  37. package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
  38. package/artifacts/schema/bridge-assets-available.schema.json +25 -0
  39. package/artifacts/schema/bridge-camera-state.schema.json +78 -0
  40. package/artifacts/schema/bridge-config-applied.schema.json +67 -0
  41. package/artifacts/schema/bridge-hello.schema.json +65 -0
  42. package/artifacts/schema/bridge-introspect.schema.json +114 -0
  43. package/artifacts/schema/bridge-job-lost.schema.json +22 -0
  44. package/artifacts/schema/bridge-job-update.schema.json +100 -0
  45. package/artifacts/schema/bridge-pong.schema.json +19 -0
  46. package/artifacts/schema/bridge-pressure.schema.json +292 -0
  47. package/artifacts/schema/bridge-state.schema.json +24 -0
  48. package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
  49. package/artifacts/schema/busy-details.schema.json +115 -0
  50. package/artifacts/schema/camera-descriptor.schema.json +45 -0
  51. package/artifacts/schema/camera-list-response.schema.json +58 -0
  52. package/artifacts/schema/camera-source.schema.json +240 -0
  53. package/artifacts/schema/cancel-request.schema.json +20 -0
  54. package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
  55. package/artifacts/schema/client-auth.schema.json +18 -0
  56. package/artifacts/schema/client-cancel.schema.json +45 -0
  57. package/artifacts/schema/client-identity.schema.json +103 -0
  58. package/artifacts/schema/client-invoke.schema.json +45 -0
  59. package/artifacts/schema/client-login-request.schema.json +29 -0
  60. package/artifacts/schema/client-logout-request.schema.json +14 -0
  61. package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
  62. package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
  63. package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
  64. package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
  65. package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
  66. package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
  67. package/artifacts/schema/client-password-reset-request.schema.json +24 -0
  68. package/artifacts/schema/client-provider-list-query.schema.json +17 -0
  69. package/artifacts/schema/client-provider-list-response.schema.json +34 -0
  70. package/artifacts/schema/client-publish.schema.json +40 -0
  71. package/artifacts/schema/client-refresh-request.schema.json +14 -0
  72. package/artifacts/schema/client-register-request.schema.json +44 -0
  73. package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
  74. package/artifacts/schema/client-subscribe.schema.json +43 -0
  75. package/artifacts/schema/client-unsubscribe.schema.json +26 -0
  76. package/artifacts/schema/client-verify-email-request.schema.json +15 -0
  77. package/artifacts/schema/cloud-asset-request.schema.json +37 -0
  78. package/artifacts/schema/cloud-camera-start.schema.json +41 -0
  79. package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
  80. package/artifacts/schema/cloud-cancel.schema.json +33 -0
  81. package/artifacts/schema/cloud-config.schema.json +1635 -0
  82. package/artifacts/schema/cloud-hello-error.schema.json +23 -0
  83. package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
  84. package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
  85. package/artifacts/schema/cloud-invoke.schema.json +40 -0
  86. package/artifacts/schema/cloud-ping.schema.json +19 -0
  87. package/artifacts/schema/cloud-publish.schema.json +28 -0
  88. package/artifacts/schema/cloud-type-request.schema.json +30 -0
  89. package/artifacts/schema/command-result.schema.json +175 -0
  90. package/artifacts/schema/config-draft-response.schema.json +1695 -0
  91. package/artifacts/schema/config-state.schema.json +124 -0
  92. package/artifacts/schema/config-version-response.schema.json +1641 -0
  93. package/artifacts/schema/config-versions-response.schema.json +33 -0
  94. package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
  95. package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
  96. package/artifacts/schema/create-app-request.schema.json +30 -0
  97. package/artifacts/schema/create-app-user-request.schema.json +42 -0
  98. package/artifacts/schema/create-robot-request.schema.json +14 -0
  99. package/artifacts/schema/create-robot-response.schema.json +44 -0
  100. package/artifacts/schema/create-server-key-response.schema.json +65 -0
  101. package/artifacts/schema/create-team-invite-request.schema.json +43 -0
  102. package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
  103. package/artifacts/schema/datapoint-config.schema.json +366 -0
  104. package/artifacts/schema/datapoint-display.schema.json +31 -0
  105. package/artifacts/schema/datapoint-event.schema.json +34 -0
  106. package/artifacts/schema/datapoint-frame.schema.json +28 -0
  107. package/artifacts/schema/datapoint-list-response.schema.json +61 -0
  108. package/artifacts/schema/datapoint-value.schema.json +28 -0
  109. package/artifacts/schema/developer-login-request.schema.json +19 -0
  110. package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
  111. package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
  112. package/artifacts/schema/error-frame.schema.json +23 -0
  113. package/artifacts/schema/exposure-counts.schema.json +39 -0
  114. package/artifacts/schema/exposure-list-response.schema.json +43 -0
  115. package/artifacts/schema/fetch-types-request.schema.json +19 -0
  116. package/artifacts/schema/fetch-types-response.schema.json +163 -0
  117. package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
  118. package/artifacts/schema/fleetless-user.schema.json +60 -0
  119. package/artifacts/schema/history-buckets-response.schema.json +79 -0
  120. package/artifacts/schema/history-query.schema.json +58 -0
  121. package/artifacts/schema/history-response.schema.json +150 -0
  122. package/artifacts/schema/history-samples-response.schema.json +68 -0
  123. package/artifacts/schema/introspection-response.schema.json +118 -0
  124. package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
  125. package/artifacts/schema/invoke-request.schema.json +23 -0
  126. package/artifacts/schema/invoke-response.schema.json +125 -0
  127. package/artifacts/schema/job-actor.schema.json +34 -0
  128. package/artifacts/schema/job-event.schema.json +158 -0
  129. package/artifacts/schema/job-response.schema.json +123 -0
  130. package/artifacts/schema/job-run-list-response.schema.json +222 -0
  131. package/artifacts/schema/job-run-query.schema.json +95 -0
  132. package/artifacts/schema/job-run-summary-query.schema.json +23 -0
  133. package/artifacts/schema/job-run-summary.schema.json +33 -0
  134. package/artifacts/schema/job-run.schema.json +195 -0
  135. package/artifacts/schema/job-state.schema.json +11 -0
  136. package/artifacts/schema/job.schema.json +106 -0
  137. package/artifacts/schema/latency-bucket.schema.json +63 -0
  138. package/artifacts/schema/live-session-response.schema.json +41 -0
  139. package/artifacts/schema/mail-outcome.schema.json +20 -0
  140. package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
  141. package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
  142. package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
  143. package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
  144. package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
  145. package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
  146. package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
  147. package/artifacts/schema/missing-asset-query.schema.json +11 -0
  148. package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
  149. package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
  150. package/artifacts/schema/oauth-token-request.schema.json +47 -0
  151. package/artifacts/schema/oauth-token-response.schema.json +38 -0
  152. package/artifacts/schema/org-alerts-query.schema.json +15 -0
  153. package/artifacts/schema/org-event-dropped.schema.json +26 -0
  154. package/artifacts/schema/org-event-replay.schema.json +97 -0
  155. package/artifacts/schema/org-event-subscribe.schema.json +14 -0
  156. package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
  157. package/artifacts/schema/org-event.schema.json +75 -0
  158. package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
  159. package/artifacts/schema/org-health-query.schema.json +13 -0
  160. package/artifacts/schema/org-latency-query.schema.json +42 -0
  161. package/artifacts/schema/org-latency-response.schema.json +124 -0
  162. package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
  163. package/artifacts/schema/org-quota-usage.schema.json +102 -0
  164. package/artifacts/schema/org-quotas.schema.json +51 -0
  165. package/artifacts/schema/org-usage-query.schema.json +19 -0
  166. package/artifacts/schema/org-usage-response.schema.json +77 -0
  167. package/artifacts/schema/org.schema.json +30 -0
  168. package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
  169. package/artifacts/schema/parameter-spec.schema.json +120 -0
  170. package/artifacts/schema/parameter-violation.schema.json +24 -0
  171. package/artifacts/schema/password-change-request.schema.json +21 -0
  172. package/artifacts/schema/password-reset-confirm.schema.json +19 -0
  173. package/artifacts/schema/password-reset-request.schema.json +14 -0
  174. package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
  175. package/artifacts/schema/patch-app-user-request.schema.json +34 -0
  176. package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
  177. package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
  178. package/artifacts/schema/patch-org-request.schema.json +15 -0
  179. package/artifacts/schema/patch-org-response.schema.json +40 -0
  180. package/artifacts/schema/patch-robot-request.schema.json +15 -0
  181. package/artifacts/schema/patch-robot-response.schema.json +40 -0
  182. package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
  183. package/artifacts/schema/pending-team-invite.schema.json +39 -0
  184. package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
  185. package/artifacts/schema/publish-config-response.schema.json +21 -0
  186. package/artifacts/schema/publish-request.schema.json +17 -0
  187. package/artifacts/schema/publisher-config.schema.json +285 -0
  188. package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
  189. package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
  190. package/artifacts/schema/put-config-draft-request.schema.json +13 -0
  191. package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
  192. package/artifacts/schema/put-robot-details-request.schema.json +41 -0
  193. package/artifacts/schema/put-robot-details-response.schema.json +43 -0
  194. package/artifacts/schema/rate-limit-details.schema.json +15 -0
  195. package/artifacts/schema/refresh-request.schema.json +13 -0
  196. package/artifacts/schema/release-live-query.schema.json +13 -0
  197. package/artifacts/schema/rename-slug-request.schema.json +23 -0
  198. package/artifacts/schema/rename-slug-response.schema.json +24 -0
  199. package/artifacts/schema/resource-health-event.schema.json +72 -0
  200. package/artifacts/schema/resource-health-list-response.schema.json +80 -0
  201. package/artifacts/schema/resource-health-state.schema.json +68 -0
  202. package/artifacts/schema/robot-config-doc.schema.json +1616 -0
  203. package/artifacts/schema/robot-delete-query.schema.json +12 -0
  204. package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
  205. package/artifacts/schema/robot-detail-response.schema.json +262 -0
  206. package/artifacts/schema/robot-details-doc.schema.json +33 -0
  207. package/artifacts/schema/robot-jobs-response.schema.json +119 -0
  208. package/artifacts/schema/robot-latency-series.schema.json +81 -0
  209. package/artifacts/schema/robot-list-item.schema.json +94 -0
  210. package/artifacts/schema/robot-list-response.schema.json +106 -0
  211. package/artifacts/schema/robot.schema.json +30 -0
  212. package/artifacts/schema/role-list-response.schema.json +48 -0
  213. package/artifacts/schema/role-permissions.schema.json +61 -0
  214. package/artifacts/schema/role.schema.json +35 -0
  215. package/artifacts/schema/ros-graph.schema.json +99 -0
  216. package/artifacts/schema/server-key-list-response.schema.json +64 -0
  217. package/artifacts/schema/server-key.schema.json +51 -0
  218. package/artifacts/schema/service-call-response.schema.json +13 -0
  219. package/artifacts/schema/service-config.schema.json +198 -0
  220. package/artifacts/schema/session-tokens.schema.json +28 -0
  221. package/artifacts/schema/sign-up-request.schema.json +26 -0
  222. package/artifacts/schema/sign-up-response.schema.json +127 -0
  223. package/artifacts/schema/slug-usage-response.schema.json +32 -0
  224. package/artifacts/schema/snapshot-header.schema.json +44 -0
  225. package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
  226. package/artifacts/schema/subscribe-error.schema.json +31 -0
  227. package/artifacts/schema/team-invite.schema.json +57 -0
  228. package/artifacts/schema/tier-change-request.schema.json +17 -0
  229. package/artifacts/schema/type-definition.schema.json +144 -0
  230. package/artifacts/schema/types-response.schema.json +156 -0
  231. package/artifacts/schema/update-app-request.schema.json +32 -0
  232. package/artifacts/schema/urdf-completeness.schema.json +50 -0
  233. package/artifacts/schema/validation-issue.schema.json +43 -0
  234. package/artifacts/schema/waitlist-request.schema.json +15 -0
  235. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
  236. package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
  237. package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
  238. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
  239. package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
  240. package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
  241. package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
  242. package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
  243. package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
  244. package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
  245. package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
  246. package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
  247. package/dist/alerts.d.ts +255 -0
  248. package/dist/alerts.js +193 -0
  249. package/dist/app-users.d.ts +606 -0
  250. package/dist/app-users.js +696 -0
  251. package/dist/apps.d.ts +175 -0
  252. package/dist/apps.js +267 -0
  253. package/dist/assets.d.ts +434 -0
  254. package/dist/assets.js +546 -0
  255. package/dist/audit.d.ts +129 -0
  256. package/dist/audit.js +238 -0
  257. package/dist/client-auth.d.ts +409 -0
  258. package/dist/client-auth.js +487 -0
  259. package/dist/common.d.ts +186 -0
  260. package/dist/common.js +199 -0
  261. package/dist/config-issues.d.ts +175 -0
  262. package/dist/config-issues.js +339 -0
  263. package/dist/config.d.ts +862 -0
  264. package/dist/config.js +1988 -0
  265. package/dist/errors.d.ts +52 -0
  266. package/dist/errors.js +786 -0
  267. package/dist/identity.d.ts +549 -0
  268. package/dist/identity.js +503 -0
  269. package/dist/index.d.ts +51 -0
  270. package/dist/index.js +51 -0
  271. package/dist/introspection.d.ts +99 -0
  272. package/dist/introspection.js +97 -0
  273. package/dist/jobs.d.ts +334 -0
  274. package/dist/jobs.js +345 -0
  275. package/dist/mcp.d.ts +239 -0
  276. package/dist/mcp.js +153 -0
  277. package/dist/oauth.d.ts +344 -0
  278. package/dist/oauth.js +488 -0
  279. package/dist/protocol.d.ts +781 -0
  280. package/dist/protocol.js +715 -0
  281. package/dist/realtime.d.ts +494 -0
  282. package/dist/realtime.js +512 -0
  283. package/dist/rest.d.ts +1989 -0
  284. package/dist/rest.js +1963 -0
  285. package/dist/routes.d.ts +94 -0
  286. package/dist/routes.js +2298 -0
  287. package/package.json +61 -0
package/dist/oauth.js ADDED
@@ -0,0 +1,488 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ /**
4
+ * **OAuth 2.1, and it remains only for MCP** (2026-09-05 app-user-auth, D8).
5
+ *
6
+ * This file used to describe two front doors: an app's end users signing in
7
+ * through a Fleetless-hosted, app-branded login page, and MCP clients signing
8
+ * in for the central endpoint. The first is deleted. An app now has its own UI
9
+ * and calls the JSON client-auth API (`client-auth.ts`); Fleetless renders an
10
+ * app user no page, so there is no hosted login, no consent screen, no
11
+ * developer-registered client and no app-level dynamic registration.
12
+ *
13
+ * What remains is the MCP authorization server — central, for Fleetless users,
14
+ * and per app for an app's users — and the console's own OAuth portal, which
15
+ * answers `oauthRedirectResponse` at its login and sign-up steps.
16
+ *
17
+ * The client model this file was written to get right is still the important
18
+ * part, and it survived the cut intact: an MCP client **registers itself**
19
+ * (RFC 7591) because the person only ever pastes a URL into an AI tool.
20
+ * Nobody vetted it, its redirect URIs arrive from the client itself, and the
21
+ * tools it will call move a physical robot. That is why consent names the
22
+ * client with an explicit *unverified* marker — see `clientMcpInteraction` in
23
+ * `client-auth.ts`, which is where the per-app half of that screen is now
24
+ * described, because the app renders it and Fleetless does not.
25
+ */
26
+ /**
27
+ * **This file speaks two error dialects on purpose, and unifying them would
28
+ * break conformance.**
29
+ *
30
+ * The OAuth endpoints (`/mcp/oauth/authorize`, `/mcp/oauth/token`,
31
+ * registration) answer in RFC 6749 §5.2's shape — a flat `error` string from a fixed set,
32
+ * with an optional `error_description`. That is what an RFC-compliant client
33
+ * parses, and `mcp-inspector` is such a client. A Fleetless `apiError` there
34
+ * would be well-formed JSON that no standard client can read.
35
+ *
36
+ * The *management* endpoints beside them — configuring a provider, editing an
37
+ * app's auth settings — are ordinary console API and use `apiError` with
38
+ * `ERROR_CODES` like everything else.
39
+ *
40
+ * So: **two shapes, split by audience, not by accident.** Written down here
41
+ * because the natural instinct on finding two error formats in one server is
42
+ * to unify them, and doing so silently removes the reason the standard one is
43
+ * there.
44
+ *
45
+ * **The split is by audience and the path prefix will mislead you.**
46
+ * `/mcp/oauth/consent` sits under an `/oauth/` segment and is nevertheless an
47
+ * `apiError` endpoint: it is not in RFC 6749's or RFC 7591's endpoint set, and
48
+ * its only caller is a page this server rendered. Whoever later sorts these by
49
+ * prefix will move it, and be wrong. Ask who parses the response, not where it
50
+ * lives.
51
+ */
52
+ export const oauthErrorCode = z.enum([
53
+ 'invalid_request',
54
+ 'invalid_client',
55
+ 'invalid_grant',
56
+ 'unauthorized_client',
57
+ 'unsupported_grant_type',
58
+ 'invalid_scope',
59
+ 'access_denied',
60
+ 'server_error',
61
+ 'temporarily_unavailable',
62
+ /** RFC 8707: the `resource` named is not one this server issues tokens for. */
63
+ 'invalid_target',
64
+ ]);
65
+ export const oauthError = z.object({
66
+ error: oauthErrorCode,
67
+ error_description: z.string().min(1).max(500).optional(),
68
+ /** Echoed back per RFC 6749 §4.1.2.1 so a client can match the response. */
69
+ state: z.string().min(1).max(500).optional(),
70
+ /**
71
+ * **A Fleetless reason carried inside a standard envelope, and it exists
72
+ * because the alternative lost a distinction.**
73
+ *
74
+ * Two policy refusals at the registration endpoint — MCP is off for this
75
+ * app, and the app's client ceiling is full — both map to RFC 6749's
76
+ * `access_denied`, which is the honest standard code for either. Answering with only that
77
+ * makes the two indistinguishable to the caller, and *a field that cannot
78
+ * express a distinction produces a workaround somewhere else*. Answering in
79
+ * `apiError` instead would keep the distinction and hand an RFC-compliant
80
+ * client a body it cannot parse — which is the conformance this wave exists
81
+ * to provide.
82
+ *
83
+ * So both: `error` is what a standard client reads, `fleetless_code` is what
84
+ * our own tooling switches on. RFC 6749 §5.2 permits additional members, and
85
+ * a client that ignores this one still behaves correctly.
86
+ */
87
+ fleetless_code: z.string().min(1).max(60).optional(),
88
+ });
89
+ /**
90
+ * A redirect URI, and the rule is stricter than "a URL".
91
+ *
92
+ * **The defence for this was already written down in this codebase, twice.**
93
+ * `config.ts` validates a V4L2 device path from the wire with a prefix rule
94
+ * *and* an explicit refusal of `..` segments, tested, with the reasoning
95
+ * recorded; W7's review then found a `package://` traversal in the bridge
96
+ * that the same rule would have prevented, and the finding that mattered was
97
+ * not the traversal but that **the rule existed one file over and was never
98
+ * carried across.** A redirect URI is the same shape of problem from a less
99
+ * trusted source: an attacker-supplied string that decides where a credential
100
+ * is sent.
101
+ *
102
+ * Matching at the server is **exact string comparison against a registered
103
+ * value** — never a prefix, never a wildcard host, never "starts with". A
104
+ * prefix match on `https://app.example.com/cb` accepts
105
+ * `https://app.example.com/cb.evil.test`.
106
+ */
107
+ export const redirectUri = z
108
+ .string()
109
+ .min(1)
110
+ .max(2000)
111
+ .refine((v) => {
112
+ // Parsed, not prefix-matched. `startsWith('https://')` alone accepts the
113
+ // literal string `https://` and anything else that merely opens with
114
+ // those characters — a shape check standing in for a value check, which
115
+ // is the failure this project keeps meeting under other names.
116
+ let url;
117
+ try {
118
+ url = new URL(v);
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ if (url.hash !== '')
124
+ return false; // RFC 6749 §3.1.2
125
+ if (url.protocol === 'https:')
126
+ return url.hostname.length > 0;
127
+ // Loopback http is allowed because a native app cannot hold a
128
+ // certificate; `localhost` and the literal addresses only, never an
129
+ // arbitrary host that merely resolves there.
130
+ //
131
+ // **`url.hostname`, not `url.host.split(':')[0]`.** The first version
132
+ // split on `:` to drop the port — which works for `127.0.0.1:8080` and
133
+ // yields `"["` for `[::1]:8080`, because an IPv6 literal is *made of*
134
+ // colons. So `[::1]` never matched the allow-list it is named in, in any
135
+ // spelling, while two developer-facing messages went on saying it was
136
+ // permitted. Found by Momus-W7b, reproduced against the live server.
137
+ // `hostname` already strips the port and keeps the brackets.
138
+ if (url.protocol === 'http:')
139
+ return ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
140
+ return false;
141
+ }, { message: 'redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment' });
142
+ /**
143
+ * OAuth 2.1 removes the implicit and password grants and **makes PKCE
144
+ * mandatory for every client**, public or confidential. `plain` is not
145
+ * offered: a challenge equal to its verifier defends against nothing, and
146
+ * offering it means a downgrade is negotiable.
147
+ */
148
+ export const codeChallengeMethod = z.enum(['S256']);
149
+ /**
150
+ * **How many callbacks one dynamic registration may name.**
151
+ *
152
+ * RFC 7591 lets a client register several; five is above every real MCP client
153
+ * observed and far below "a place to store data" on an endpoint that takes no
154
+ * credential. It lives here rather than in the cloud because this schema now
155
+ * *publishes* the bound: a number the reference states and a different number
156
+ * the server enforces is two policies for one decision, and the endpoint spent
157
+ * a release documenting `20` while refusing the sixth URI.
158
+ */
159
+ export const MCP_DCR_MAX_REDIRECT_URIS = 5;
160
+ /**
161
+ * RFC 7591 dynamic client registration — **the metadata both MCP
162
+ * authorization servers understand**, central and per-app.
163
+ *
164
+ * **Not `.strict()`, and that is the schema agreeing with the server rather
165
+ * than a gap in it.** §3.1 obliges a registration endpoint to ignore metadata
166
+ * it does not understand, and real MCP clients send `client_uri`, `logo_uri`,
167
+ * `software_id` and `contacts`. A strict shape here would describe a `400`
168
+ * that no conforming client ever earns, and would take the whole
169
+ * paste-the-URL flow down if anything ever parsed against it. Unknown keys
170
+ * are therefore stripped by this schema and ignored by the server, which is
171
+ * the same answer said twice.
172
+ *
173
+ * **The server still reads the body field by field** (`registerMcpDynamicClient`
174
+ * in `cloud/src/mcp-oauth-core.ts`), and the reason is the error vocabulary,
175
+ * not the shape: §3.2.2 distinguishes `invalid_redirect_uri` from
176
+ * `invalid_client_metadata`, and one `safeParse` failure cannot say which of
177
+ * the two a caller earned. So this schema is what the endpoint *accepts*, and
178
+ * the handler is what turns a miss into the right RFC code.
179
+ *
180
+ * **`client_name` is optional because the server treats it as optional**: RFC
181
+ * 7591 makes every metadata field optional, and a registration that omits it
182
+ * is recorded under a default name rather than refused. `redirect_uris` is the
183
+ * one field a registration cannot do without — there is nowhere to return a
184
+ * code otherwise.
185
+ */
186
+ export const dynamicClientRegistrationRequest = z
187
+ .object({
188
+ redirect_uris: z.array(redirectUri).min(1).max(MCP_DCR_MAX_REDIRECT_URIS).meta({
189
+ description: `Where the authorization code may be returned, and the one field a registration cannot omit. Each must be an \`https\` URL, or \`http\` on an explicit loopback address for a native app that cannot hold a certificate, and none may carry a fragment. There must be between \`1\` and \`${MCP_DCR_MAX_REDIRECT_URIS}\` of them; duplicates are collapsed rather than counted twice. Matched **exactly** at the authorize step against what was registered here.`,
190
+ }),
191
+ client_name: z.string().min(1).max(200).optional().meta({
192
+ description: 'The name the client calls itself. Optional — a registration without one is recorded under a default name, per RFC 7591\'s making every metadata field optional. It is **not** vouched for by Fleetless and must never be rendered as if it were: a self-registered client chooses this string, and one has called itself *"Fleetless Official Helper"*.',
193
+ }),
194
+ token_endpoint_auth_method: z.enum(['none']).optional().meta({
195
+ 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.',
196
+ }),
197
+ grant_types: z.array(z.enum(['authorization_code', 'refresh_token'])).optional().meta({
198
+ 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.',
199
+ }),
200
+ response_types: z.array(z.enum(['code'])).optional().meta({
201
+ 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.',
202
+ }),
203
+ scope: z.string().max(500).optional().meta({
204
+ description: 'Accepted for conformance and then **ignored**. This authorization server issues no scopes at all, which is why the registration answer carries no `scope` field to echo one back in.',
205
+ }),
206
+ })
207
+ .meta({
208
+ description: 'What an MCP client sends to register itself, per RFC 7591. Unknown metadata is ignored rather than refused (§3.1), and the answer states what was actually granted rather than what was asked for (§3.2.1).',
209
+ });
210
+ export const dynamicClientRegistrationResponse = z.object({
211
+ client_id: z.string().min(1).max(200).meta({
212
+ description: 'The identifier this client sends at the authorize and token endpoints. Opaque, and not the app identifier.',
213
+ }),
214
+ client_name: z.string().min(1).max(200).meta({
215
+ description: 'The name the client registered under, echoed back. Chosen by the client and not vouched for by Fleetless.',
216
+ }),
217
+ redirect_uris: z.array(redirectUri).meta({
218
+ description: 'The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else.',
219
+ }),
220
+ grant_types: z.array(z.string()).meta({
221
+ 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.',
222
+ }),
223
+ response_types: z.array(z.string()).meta({
224
+ description: 'The response types this client may ask for: `code`.',
225
+ }),
226
+ token_endpoint_auth_method: z.literal('none').meta({
227
+ description: '`none` — this server registers public clients only, and PKCE rather than a secret is what protects the exchange.',
228
+ }),
229
+ client_id_issued_at: z.number().int().nonnegative().meta({
230
+ description: 'When the registration was created, in seconds since the epoch, per RFC 7591.',
231
+ }),
232
+ client_secret_expires_at: z.literal(0).meta({
233
+ description: 'Always `0`, which is RFC 7591\'s way of saying the client secret never expires — there is none. The **registration** itself does expire: a self-registered client that never completes a flow is an unauthenticated write somebody left behind.',
234
+ }),
235
+ });
236
+ /**
237
+ * **The MCP token endpoint's request — one grant, because the servers serve
238
+ * one.**
239
+ *
240
+ * Both authorization servers, central and per-app, exchange through
241
+ * `exchangeMcpAuthorizationCode` (`cloud/src/mcp-oauth-core.ts`), whose first
242
+ * act is to refuse anything but `authorization_code` before a single lookup
243
+ * happens. There is no refresh grant here: a session ends when its token
244
+ * expires and the client signs in again.
245
+ *
246
+ * **This was a `discriminatedUnion` with a `refresh_token` branch, and that
247
+ * branch had no producer left.** It described the app-level OAuth surface,
248
+ * which is deleted; an app user's refresh runs through `POST
249
+ * /api/client/refresh` and `refreshRequest`, a different wire on a different
250
+ * route. Keeping it would have published, to every MCP client author reading
251
+ * `/openapi.json`, a grant the endpoint answers `unsupported_grant_type` to.
252
+ * The argument the branch carried is worth keeping even though the branch is
253
+ * not: **RFC 8707's `resource` has to survive rotation**, because a refresh
254
+ * that drops the audience mints a successor with no `aud`, and the validating
255
+ * resource then refuses a token the caller obtained legitimately — one token
256
+ * lifetime after a login that worked, to somebody who did nothing wrong. If a
257
+ * refresh grant is ever added here, it carries `resource`.
258
+ *
259
+ * `code_verifier`'s bounds are RFC 7636 §4.1's, charset included. A verifier
260
+ * is compared, not parsed, so a length nobody checks is a length an attacker
261
+ * chooses.
262
+ *
263
+ * **Deliberately not `.strict()`**, unlike a registration request: that comes
264
+ * from a client we are about to trust, where an unknown key is a caller
265
+ * assuming a feature into existence, while a token request comes from any
266
+ * RFC-compliant client, which may legitimately send parameters this server
267
+ * does not read. Refusing those would be a conformance bug. The consequence
268
+ * is worth stating because it bit the test for this very schema: unknown keys
269
+ * are **stripped**, so `safeParse().success` cannot tell a present field from
270
+ * an absent one. Assert on the parsed value.
271
+ */
272
+ export const oauthTokenRequest = z
273
+ .object({
274
+ grant_type: z.literal('authorization_code').meta({
275
+ 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.',
276
+ }),
277
+ code: z.string().min(1).max(500).meta({
278
+ 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.',
279
+ }),
280
+ redirect_uri: redirectUri.meta({
281
+ description: 'The same redirect URI the authorize request used. It is compared, not merely recorded.',
282
+ }),
283
+ client_id: z.string().min(1).max(200).meta({
284
+ description: 'The client making the exchange, as registered.',
285
+ }),
286
+ code_verifier: z.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, 'code_verifier must be 43-128 unreserved characters (RFC 7636 §4.1)').meta({
287
+ 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.',
288
+ }),
289
+ resource: z.url().optional().meta({
290
+ description: 'The resource the token is being 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.',
291
+ }),
292
+ })
293
+ .meta({
294
+ 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.",
295
+ });
296
+ /**
297
+ * RFC 6749 §5.1's success envelope — **the second deliberate dialect, and this
298
+ * one is a success shape rather than an error shape.**
299
+ *
300
+ * The values inside are the same tokens `/api/client/login` mints; only the
301
+ * envelope differs, because an RFC-compliant client parses this one and knows
302
+ * nothing about Fleetless. So a consumer holding this **normalises it into
303
+ * `sessionTokens` and stores that** — it does not carry the envelope around.
304
+ * Written here rather than invented once in the cloud and once in the SDK,
305
+ * which is how two implementations of one wire shape start disagreeing.
306
+ *
307
+ * `token_type` is `Bearer` as a literal because it is what this server emits.
308
+ * RFC 6749 §5.1 makes the value case-insensitive **for a client reading it**;
309
+ * that leniency belongs in a parser we do not own, not in the shape we
310
+ * produce.
311
+ */
312
+ export const oauthTokenResponse = z.object({
313
+ access_token: z.string().min(1).meta({
314
+ description: 'The bearer token. It is the same token the client login mints — only the envelope differs, because an RFC-compliant client parses this one and knows nothing about Fleetless.',
315
+ }),
316
+ token_type: z.literal('Bearer').meta({
317
+ description: '`Bearer`. RFC 6749 §5.1 makes the value case-insensitive for a client reading it; this is the spelling this server emits.',
318
+ }),
319
+ expires_in: z.number().int().positive().meta({
320
+ description: 'How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds.',
321
+ }),
322
+ refresh_token: z.string().min(1).optional().meta({
323
+ description: 'The refresh token, when one was issued. It rotates on every use.',
324
+ }),
325
+ scope: z.string().max(500).optional().meta({
326
+ description: 'The scopes the issued token actually carries, space-separated.',
327
+ }),
328
+ });
329
+ /** RFC 8414 §2 — the document a client reads *instead of* being told anything. */
330
+ export const authorizationServerMetadata = z.object({
331
+ issuer: z.url().meta({
332
+ description: 'The issuer identifier of this authorization server, per RFC 8414 §2. It is what a client checks a token\'s `iss` against.',
333
+ }),
334
+ authorization_endpoint: z.url().meta({
335
+ description: 'The URL a client sends the user to in order to authorize.',
336
+ }),
337
+ token_endpoint: z.url().meta({
338
+ description: 'The URL where a client exchanges an authorization code, or a refresh token, for tokens.',
339
+ }),
340
+ registration_endpoint: z.url().optional().meta({
341
+ description: 'The URL where a client may register itself, per RFC 7591. Absent when the app does not accept dynamic clients.',
342
+ }),
343
+ response_types_supported: z.array(z.literal('code')).meta({
344
+ description: 'The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1.',
345
+ }),
346
+ grant_types_supported: z.array(z.enum(['authorization_code', 'refresh_token'])).meta({
347
+ description: 'The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here.',
348
+ }),
349
+ code_challenge_methods_supported: z.array(codeChallengeMethod).meta({
350
+ 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.',
351
+ }),
352
+ token_endpoint_auth_methods_supported: z.array(z.literal('none')).meta({
353
+ description: 'How a client authenticates at the token endpoint: `none`, the public-client method, with PKCE protecting the exchange.',
354
+ }),
355
+ scopes_supported: z.array(z.string()).optional().meta({
356
+ description: 'The scopes this server knows about, where it publishes a list.',
357
+ }),
358
+ });
359
+ /**
360
+ * RFC 9728 — what a *resource* publishes about who may authorize for it.
361
+ *
362
+ * W7b mints tokens bound to a resource that W7c builds. **A minting mechanism
363
+ * with no validator is the failure mode this project has now met twelve times
364
+ * in one wave: a check that cannot fail.** So W7b also ships a resource that
365
+ * *rejects* a token whose audience names something else, and the gate measures
366
+ * the rejection rather than the presence of the claim.
367
+ */
368
+ export const protectedResourceMetadata = z.object({
369
+ resource: z.url().meta({
370
+ description: 'The resource identifier this document describes, per RFC 9728. A token whose audience names something else is rejected here rather than merely noted.',
371
+ }),
372
+ authorization_servers: z.array(z.url()).min(1).meta({
373
+ description: 'The authorization servers that may issue tokens for this resource. There is always at least one.',
374
+ }),
375
+ bearer_methods_supported: z.array(z.literal('header')).meta({
376
+ description: 'How a token may be presented: in the `Authorization` header only, never in a query parameter or a form field.',
377
+ }),
378
+ scopes_supported: z.array(z.string()).optional().meta({
379
+ description: 'The scopes this resource understands, where it publishes a list.',
380
+ }),
381
+ });
382
+ /**
383
+ * Where the page goes next, and this shape is a **redirect the server chose**,
384
+ * never one the page may be talked into.
385
+ *
386
+ * The server emits exactly two kinds of value here: its own consent path, or a
387
+ * redirect URI already registered for this client with the code appended.
388
+ * A page must navigate to it and nothing else — in particular it must not fall
389
+ * back to any URL that arrived in its own query string if this field is
390
+ * missing, which is how an open redirect gets built by accident on the way to
391
+ * handling an error.
392
+ *
393
+ * JSON rather than a `302` because the page is an application: a redirect
394
+ * cannot carry a field-level credential error back to a form, and a flow that
395
+ * answers errors by navigating loses the state the user typed.
396
+ */
397
+ export const oauthRedirectResponse = z.object({
398
+ redirect_to: z.string().min(1).max(2000),
399
+ });
400
+ /* `OAUTH_PATHS` was deleted on 2026-09-05, and every one of its nine entries
401
+ * went with the routes it named.
402
+ *
403
+ * It existed so two repositories could not spell a discovered path
404
+ * differently, and it worked — but every path it held belonged to the app-level
405
+ * OAuth flow (`authorize`, `token`, `register`, `consent`, `login`,
406
+ * `impersonate`, `idpCallback`) or to the stub resource's metadata documents,
407
+ * and OAuth 2.1 now remains only for MCP (D8). The MCP authorization server
408
+ * builds its own paths in `cloud/src/routes/mcp-oauth.ts`, where they are read
409
+ * by one file rather than by two repositories.
410
+ *
411
+ * Deleted rather than left with the four entries whose routes this train also
412
+ * removes, because that is precisely the defect this constant was created after
413
+ * and then reproduced: its `idpStart` entry named a route the cloud had deleted
414
+ * and stood for months with nothing noticing. A constant whose every value
415
+ * names a deleted route is that failure at full size.
416
+ */
417
+ /**
418
+ * The authorization request of RFC 6749 §4.1.1 with PKCE (RFC 7636) — the
419
+ * shape `/mcp/oauth/authorize` reads.
420
+ *
421
+ * **The cloud reads every parameter by hand, and that is not an omission** —
422
+ * each failure has its own answer. `client_id` and `redirect_uri` are refused
423
+ * flat, with no redirect, because until both are confirmed there is no trusted
424
+ * target to bounce a browser to; everything after them is reported to the
425
+ * client's own callback as query parameters. A single `safeParse` would
426
+ * collapse those two answers into one. So this schema pins the successful
427
+ * shape and the documentation, not the error path.
428
+ *
429
+ * **Four route entries point at it**: `GET /mcp/oauth/authorize` and
430
+ * `GET /mcp/:appIdentifier/oauth/authorize` (D7), which read the same wire.
431
+ * They spent a release naming nothing — the app-level `/oauth/authorize` this
432
+ * was written for was deleted, and `query: null` was read as "there is no
433
+ * query here" rather than as "the handler reads it by hand" — and the eight
434
+ * documented parameters left `/openapi.json` with nothing able to notice,
435
+ * because the undocumented-field ratchet counts gaps and a fully documented
436
+ * schema leaving makes that number improve.
437
+ */
438
+ export const oauthAuthorizeQuery = z
439
+ .object({
440
+ response_type: z.literal('code').meta({
441
+ description: 'Always `code`. RFC 6749 §4.1.2.1 names `unsupported_response_type` for any other value, but `oauthErrorCode` has no such member — this server issues no other grant from this endpoint — so an unsupported value comes back on the callback as `invalid_request`.',
442
+ }),
443
+ client_id: z.string().min(1).meta({
444
+ description: 'The OAuth client, self-registered or the one well-known central client — **not** the app identifier. Unknown, expired-dynamic and mismatched clients all collapse into the same `400 invalid_client`, answered without a redirect.',
445
+ }),
446
+ redirect_uri: z.string().min(1).meta({
447
+ description: 'One of the client\'s registered redirect URIs, compared **exactly** — string equality against the registered list, never a prefix or a host match. Both the shape (`redirectUri`) and the registration are checked, and a failure of either is a `400 invalid_request` with no redirect.',
448
+ }),
449
+ code_challenge: z.string().min(1).meta({
450
+ description: 'The PKCE challenge; the verifier is presented at the token endpoint. Only non-emptiness is checked here — length and alphabet are not — since the verifier is what actually has to match.',
451
+ }),
452
+ code_challenge_method: z.literal('S256').meta({
453
+ description: 'Only `S256`. `plain` is refused: a challenge equal to its verifier defends against nothing.',
454
+ }),
455
+ state: z.string().optional().meta({
456
+ description: 'Returned unchanged on the callback, and on the error redirect too, so a client can bind either answer to its own request.',
457
+ }),
458
+ resource: z.string().optional().meta({
459
+ description: 'RFC 8707 resource indicator: the API origin or the MCP endpoint the token is for. Checked against the resources this server issues tokens for **on behalf of this client\'s app**; a mismatch is `invalid_target` on the callback.',
460
+ }),
461
+ // **No `scope`, because this authorization server issues none.** The field
462
+ // was here describing itself as "carried onto the interaction and read
463
+ // again at consent"; neither authorize handler reads it, the interaction
464
+ // row has no column for it, and the consent screen answers `scopes: []`
465
+ // from a comment that says so in as many words
466
+ // (`cloud/src/routes/client-mcp-interactions.ts`). A parameter documented
467
+ // as carried and in fact dropped is worse than one that is absent.
468
+ })
469
+ .meta({
470
+ description: 'The authorization request an MCP client sends, per RFC 6749 §4.1.1 with mandatory PKCE. The handler reads it parameter by parameter rather than through one parse, because the answers differ: `client_id` and `redirect_uri` are refused flat, with no redirect, since until both are confirmed there is no trusted target to bounce a browser to, and everything after them is reported to the client\'s own callback as query parameters.',
471
+ });
472
+ /**
473
+ * **`oauthRegisterQuery` is deleted, and this note is what it leaves behind.**
474
+ *
475
+ * It carried one parameter, `app_identifier`, on the argument that RFC 7591's
476
+ * registration body has no field for it and one endpoint could serve every
477
+ * app. It was kept — explicitly, in its own doc comment — "for the per-app MCP
478
+ * registration the MCP train adds (D7), which needs exactly this parameter".
479
+ *
480
+ * **That train shipped and needed no such parameter.** `POST
481
+ * /mcp/:appIdentifier/oauth/register` puts the app in the **path**, built by
482
+ * `MCP_APP_PATHS`, and `resolveAppMcpTarget` reads it from `request.params`;
483
+ * `registerMcpDynamicClient` never looks at a query at all. So the one reason
484
+ * the schema was kept became false the moment its successor arrived, and
485
+ * nothing was watching the reason — which is the failure this comment is here
486
+ * to make expensive to repeat. A schema reserved for a future route is a claim
487
+ * with an expiry date on it, and the expiry has to be checked by somebody.
488
+ */