@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +202 -0
- package/NOTICE +17 -0
- package/README.md +88 -0
- package/artifacts/constants.json +24 -0
- package/artifacts/openapi.json +17219 -0
- package/artifacts/routes.json +4605 -0
- package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
- package/artifacts/schema/action-config.schema.json +198 -0
- package/artifacts/schema/alert-list-response.schema.json +172 -0
- package/artifacts/schema/api-error.schema.json +20 -0
- package/artifacts/schema/app-auth-config.schema.json +106 -0
- package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
- package/artifacts/schema/app-invitation.schema.json +69 -0
- package/artifacts/schema/app-list-response.schema.json +82 -0
- package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
- package/artifacts/schema/app-mail-template.schema.json +54 -0
- package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
- package/artifacts/schema/app-oidc-provider.schema.json +80 -0
- package/artifacts/schema/app-user-list-response.schema.json +111 -0
- package/artifacts/schema/app-user.schema.json +98 -0
- package/artifacts/schema/app.schema.json +69 -0
- package/artifacts/schema/apply-error.schema.json +41 -0
- package/artifacts/schema/asset-list-response.schema.json +288 -0
- package/artifacts/schema/asset-sync-request.schema.json +17 -0
- package/artifacts/schema/asset-sync-response.schema.json +16 -0
- package/artifacts/schema/asset-sync-status.schema.json +136 -0
- package/artifacts/schema/asset.schema.json +68 -0
- package/artifacts/schema/audit-actor.schema.json +32 -0
- package/artifacts/schema/audit-event.schema.json +119 -0
- package/artifacts/schema/audit-list-response.schema.json +144 -0
- package/artifacts/schema/audit-query.schema.json +79 -0
- package/artifacts/schema/auth-error.schema.json +23 -0
- package/artifacts/schema/auth-me-response.schema.json +99 -0
- package/artifacts/schema/auth-ok.schema.json +115 -0
- package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
- package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
- package/artifacts/schema/bridge-assets-available.schema.json +25 -0
- package/artifacts/schema/bridge-camera-state.schema.json +78 -0
- package/artifacts/schema/bridge-config-applied.schema.json +67 -0
- package/artifacts/schema/bridge-hello.schema.json +65 -0
- package/artifacts/schema/bridge-introspect.schema.json +114 -0
- package/artifacts/schema/bridge-job-lost.schema.json +22 -0
- package/artifacts/schema/bridge-job-update.schema.json +100 -0
- package/artifacts/schema/bridge-pong.schema.json +19 -0
- package/artifacts/schema/bridge-pressure.schema.json +292 -0
- package/artifacts/schema/bridge-state.schema.json +24 -0
- package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
- package/artifacts/schema/busy-details.schema.json +115 -0
- package/artifacts/schema/camera-descriptor.schema.json +45 -0
- package/artifacts/schema/camera-list-response.schema.json +58 -0
- package/artifacts/schema/camera-source.schema.json +240 -0
- package/artifacts/schema/cancel-request.schema.json +20 -0
- package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
- package/artifacts/schema/client-auth.schema.json +18 -0
- package/artifacts/schema/client-cancel.schema.json +45 -0
- package/artifacts/schema/client-identity.schema.json +103 -0
- package/artifacts/schema/client-invoke.schema.json +45 -0
- package/artifacts/schema/client-login-request.schema.json +29 -0
- package/artifacts/schema/client-logout-request.schema.json +14 -0
- package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
- package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
- package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
- package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
- package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
- package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
- package/artifacts/schema/client-password-reset-request.schema.json +24 -0
- package/artifacts/schema/client-provider-list-query.schema.json +17 -0
- package/artifacts/schema/client-provider-list-response.schema.json +34 -0
- package/artifacts/schema/client-publish.schema.json +40 -0
- package/artifacts/schema/client-refresh-request.schema.json +14 -0
- package/artifacts/schema/client-register-request.schema.json +44 -0
- package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
- package/artifacts/schema/client-subscribe.schema.json +43 -0
- package/artifacts/schema/client-unsubscribe.schema.json +26 -0
- package/artifacts/schema/client-verify-email-request.schema.json +15 -0
- package/artifacts/schema/cloud-asset-request.schema.json +37 -0
- package/artifacts/schema/cloud-camera-start.schema.json +41 -0
- package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
- package/artifacts/schema/cloud-cancel.schema.json +33 -0
- package/artifacts/schema/cloud-config.schema.json +1635 -0
- package/artifacts/schema/cloud-hello-error.schema.json +23 -0
- package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
- package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
- package/artifacts/schema/cloud-invoke.schema.json +40 -0
- package/artifacts/schema/cloud-ping.schema.json +19 -0
- package/artifacts/schema/cloud-publish.schema.json +28 -0
- package/artifacts/schema/cloud-type-request.schema.json +30 -0
- package/artifacts/schema/command-result.schema.json +175 -0
- package/artifacts/schema/config-draft-response.schema.json +1695 -0
- package/artifacts/schema/config-state.schema.json +124 -0
- package/artifacts/schema/config-version-response.schema.json +1641 -0
- package/artifacts/schema/config-versions-response.schema.json +33 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
- package/artifacts/schema/create-app-request.schema.json +30 -0
- package/artifacts/schema/create-app-user-request.schema.json +42 -0
- package/artifacts/schema/create-robot-request.schema.json +14 -0
- package/artifacts/schema/create-robot-response.schema.json +44 -0
- package/artifacts/schema/create-server-key-response.schema.json +65 -0
- package/artifacts/schema/create-team-invite-request.schema.json +43 -0
- package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
- package/artifacts/schema/datapoint-config.schema.json +366 -0
- package/artifacts/schema/datapoint-display.schema.json +31 -0
- package/artifacts/schema/datapoint-event.schema.json +34 -0
- package/artifacts/schema/datapoint-frame.schema.json +28 -0
- package/artifacts/schema/datapoint-list-response.schema.json +61 -0
- package/artifacts/schema/datapoint-value.schema.json +28 -0
- package/artifacts/schema/developer-login-request.schema.json +19 -0
- package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
- package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
- package/artifacts/schema/error-frame.schema.json +23 -0
- package/artifacts/schema/exposure-counts.schema.json +39 -0
- package/artifacts/schema/exposure-list-response.schema.json +43 -0
- package/artifacts/schema/fetch-types-request.schema.json +19 -0
- package/artifacts/schema/fetch-types-response.schema.json +163 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
- package/artifacts/schema/fleetless-user.schema.json +60 -0
- package/artifacts/schema/history-buckets-response.schema.json +79 -0
- package/artifacts/schema/history-query.schema.json +58 -0
- package/artifacts/schema/history-response.schema.json +150 -0
- package/artifacts/schema/history-samples-response.schema.json +68 -0
- package/artifacts/schema/introspection-response.schema.json +118 -0
- package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
- package/artifacts/schema/invoke-request.schema.json +23 -0
- package/artifacts/schema/invoke-response.schema.json +125 -0
- package/artifacts/schema/job-actor.schema.json +34 -0
- package/artifacts/schema/job-event.schema.json +158 -0
- package/artifacts/schema/job-response.schema.json +123 -0
- package/artifacts/schema/job-run-list-response.schema.json +222 -0
- package/artifacts/schema/job-run-query.schema.json +95 -0
- package/artifacts/schema/job-run-summary-query.schema.json +23 -0
- package/artifacts/schema/job-run-summary.schema.json +33 -0
- package/artifacts/schema/job-run.schema.json +195 -0
- package/artifacts/schema/job-state.schema.json +11 -0
- package/artifacts/schema/job.schema.json +106 -0
- package/artifacts/schema/latency-bucket.schema.json +63 -0
- package/artifacts/schema/live-session-response.schema.json +41 -0
- package/artifacts/schema/mail-outcome.schema.json +20 -0
- package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
- package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
- package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
- package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
- package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
- package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
- package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
- package/artifacts/schema/missing-asset-query.schema.json +11 -0
- package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
- package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
- package/artifacts/schema/oauth-token-request.schema.json +47 -0
- package/artifacts/schema/oauth-token-response.schema.json +38 -0
- package/artifacts/schema/org-alerts-query.schema.json +15 -0
- package/artifacts/schema/org-event-dropped.schema.json +26 -0
- package/artifacts/schema/org-event-replay.schema.json +97 -0
- package/artifacts/schema/org-event-subscribe.schema.json +14 -0
- package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
- package/artifacts/schema/org-event.schema.json +75 -0
- package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
- package/artifacts/schema/org-health-query.schema.json +13 -0
- package/artifacts/schema/org-latency-query.schema.json +42 -0
- package/artifacts/schema/org-latency-response.schema.json +124 -0
- package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
- package/artifacts/schema/org-quota-usage.schema.json +102 -0
- package/artifacts/schema/org-quotas.schema.json +51 -0
- package/artifacts/schema/org-usage-query.schema.json +19 -0
- package/artifacts/schema/org-usage-response.schema.json +77 -0
- package/artifacts/schema/org.schema.json +30 -0
- package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
- package/artifacts/schema/parameter-spec.schema.json +120 -0
- package/artifacts/schema/parameter-violation.schema.json +24 -0
- package/artifacts/schema/password-change-request.schema.json +21 -0
- package/artifacts/schema/password-reset-confirm.schema.json +19 -0
- package/artifacts/schema/password-reset-request.schema.json +14 -0
- package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
- package/artifacts/schema/patch-app-user-request.schema.json +34 -0
- package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
- package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
- package/artifacts/schema/patch-org-request.schema.json +15 -0
- package/artifacts/schema/patch-org-response.schema.json +40 -0
- package/artifacts/schema/patch-robot-request.schema.json +15 -0
- package/artifacts/schema/patch-robot-response.schema.json +40 -0
- package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
- package/artifacts/schema/pending-team-invite.schema.json +39 -0
- package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
- package/artifacts/schema/publish-config-response.schema.json +21 -0
- package/artifacts/schema/publish-request.schema.json +17 -0
- package/artifacts/schema/publisher-config.schema.json +285 -0
- package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
- package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
- package/artifacts/schema/put-config-draft-request.schema.json +13 -0
- package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
- package/artifacts/schema/put-robot-details-request.schema.json +41 -0
- package/artifacts/schema/put-robot-details-response.schema.json +43 -0
- package/artifacts/schema/rate-limit-details.schema.json +15 -0
- package/artifacts/schema/refresh-request.schema.json +13 -0
- package/artifacts/schema/release-live-query.schema.json +13 -0
- package/artifacts/schema/rename-slug-request.schema.json +23 -0
- package/artifacts/schema/rename-slug-response.schema.json +24 -0
- package/artifacts/schema/resource-health-event.schema.json +72 -0
- package/artifacts/schema/resource-health-list-response.schema.json +80 -0
- package/artifacts/schema/resource-health-state.schema.json +68 -0
- package/artifacts/schema/robot-config-doc.schema.json +1616 -0
- package/artifacts/schema/robot-delete-query.schema.json +12 -0
- package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
- package/artifacts/schema/robot-detail-response.schema.json +262 -0
- package/artifacts/schema/robot-details-doc.schema.json +33 -0
- package/artifacts/schema/robot-jobs-response.schema.json +119 -0
- package/artifacts/schema/robot-latency-series.schema.json +81 -0
- package/artifacts/schema/robot-list-item.schema.json +94 -0
- package/artifacts/schema/robot-list-response.schema.json +106 -0
- package/artifacts/schema/robot.schema.json +30 -0
- package/artifacts/schema/role-list-response.schema.json +48 -0
- package/artifacts/schema/role-permissions.schema.json +61 -0
- package/artifacts/schema/role.schema.json +35 -0
- package/artifacts/schema/ros-graph.schema.json +99 -0
- package/artifacts/schema/server-key-list-response.schema.json +64 -0
- package/artifacts/schema/server-key.schema.json +51 -0
- package/artifacts/schema/service-call-response.schema.json +13 -0
- package/artifacts/schema/service-config.schema.json +198 -0
- package/artifacts/schema/session-tokens.schema.json +28 -0
- package/artifacts/schema/sign-up-request.schema.json +26 -0
- package/artifacts/schema/sign-up-response.schema.json +127 -0
- package/artifacts/schema/slug-usage-response.schema.json +32 -0
- package/artifacts/schema/snapshot-header.schema.json +44 -0
- package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
- package/artifacts/schema/subscribe-error.schema.json +31 -0
- package/artifacts/schema/team-invite.schema.json +57 -0
- package/artifacts/schema/tier-change-request.schema.json +17 -0
- package/artifacts/schema/type-definition.schema.json +144 -0
- package/artifacts/schema/types-response.schema.json +156 -0
- package/artifacts/schema/update-app-request.schema.json +32 -0
- package/artifacts/schema/urdf-completeness.schema.json +50 -0
- package/artifacts/schema/validation-issue.schema.json +43 -0
- package/artifacts/schema/waitlist-request.schema.json +15 -0
- package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
- package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
- package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
- package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
- package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
- package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
- package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
- package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
- package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
- package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
- package/dist/alerts.d.ts +255 -0
- package/dist/alerts.js +193 -0
- package/dist/app-users.d.ts +606 -0
- package/dist/app-users.js +696 -0
- package/dist/apps.d.ts +175 -0
- package/dist/apps.js +267 -0
- package/dist/assets.d.ts +434 -0
- package/dist/assets.js +546 -0
- package/dist/audit.d.ts +129 -0
- package/dist/audit.js +238 -0
- package/dist/client-auth.d.ts +409 -0
- package/dist/client-auth.js +487 -0
- package/dist/common.d.ts +186 -0
- package/dist/common.js +199 -0
- package/dist/config-issues.d.ts +175 -0
- package/dist/config-issues.js +339 -0
- package/dist/config.d.ts +862 -0
- package/dist/config.js +1988 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +786 -0
- package/dist/identity.d.ts +549 -0
- package/dist/identity.js +503 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/introspection.d.ts +99 -0
- package/dist/introspection.js +97 -0
- package/dist/jobs.d.ts +334 -0
- package/dist/jobs.js +345 -0
- package/dist/mcp.d.ts +239 -0
- package/dist/mcp.js +153 -0
- package/dist/oauth.d.ts +344 -0
- package/dist/oauth.js +488 -0
- package/dist/protocol.d.ts +781 -0
- package/dist/protocol.js +715 -0
- package/dist/realtime.d.ts +494 -0
- package/dist/realtime.js +512 -0
- package/dist/rest.d.ts +1989 -0
- package/dist/rest.js +1963 -0
- package/dist/routes.d.ts +94 -0
- package/dist/routes.js +2298 -0
- 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
|
+
*/
|