@fleetless/contracts 1.0.0 → 1.0.3
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 +97 -2
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +136 -0
- package/README.md +49 -13
- package/SECURITY.md +55 -0
- package/artifacts/openapi.json +11 -11
- package/artifacts/routes.json +12 -12
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
- package/dist/alerts.d.ts +23 -28
- package/dist/alerts.js +23 -29
- package/dist/app-users.d.ts +18 -19
- package/dist/app-users.js +18 -20
- package/dist/apps.d.ts +21 -25
- package/dist/apps.js +42 -52
- package/dist/assets.d.ts +70 -132
- package/dist/assets.js +130 -223
- package/dist/audit.d.ts +14 -15
- package/dist/audit.js +28 -55
- package/dist/client-auth.d.ts +9 -9
- package/dist/client-auth.js +8 -9
- package/dist/common.d.ts +29 -37
- package/dist/common.js +28 -37
- package/dist/config-issues.d.ts +23 -25
- package/dist/config-issues.js +17 -17
- package/dist/config.d.ts +37 -44
- package/dist/config.js +145 -187
- package/dist/errors.d.ts +4 -3
- package/dist/errors.js +83 -116
- package/dist/identity.d.ts +24 -27
- package/dist/identity.js +23 -27
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -15
- package/dist/introspection.d.ts +7 -6
- package/dist/introspection.js +6 -6
- package/dist/jobs.d.ts +16 -16
- package/dist/jobs.js +24 -29
- package/dist/mcp.d.ts +14 -15
- package/dist/mcp.js +12 -14
- package/dist/oauth.d.ts +21 -27
- package/dist/oauth.js +33 -43
- package/dist/protocol.d.ts +51 -62
- package/dist/protocol.js +107 -139
- package/dist/realtime.d.ts +53 -68
- package/dist/realtime.js +78 -104
- package/dist/rest.d.ts +183 -244
- package/dist/rest.js +305 -399
- package/dist/routes.d.ts +4 -3
- package/dist/routes.js +33 -32
- package/package.json +12 -7
package/dist/jobs.js
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { slug, wireSeqCursor, wireTimestampMs } from './common.js';
|
|
4
4
|
/**
|
|
5
|
-
* Jobs
|
|
5
|
+
* Jobs: one running unit of work on a robot — an action
|
|
6
6
|
* goal or a service call — with an id both sides know, so bridge and cloud
|
|
7
7
|
* stay in sync across a disconnect.
|
|
8
8
|
*
|
|
9
9
|
* Two rules shape everything here:
|
|
10
10
|
*
|
|
11
|
-
* 1. **State is observed by slug, not by id.** The id is informative
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 2. **`lost` is a real outcome and must be said out loud
|
|
11
|
+
* 1. **State is observed by slug, not by id.** The id is informative; a client
|
|
12
|
+
* watches `robot × slug` and sees whatever job is running there, which is
|
|
13
|
+
* also why every observer of a slug sees the same job.
|
|
14
|
+
* 2. **`lost` is a real outcome and must be said out loud.** Job state
|
|
15
15
|
* lives only in the bridge's memory; if it restarts mid-job, the results
|
|
16
16
|
* are gone. The cloud then marks the job `lost` — never leaves it reading
|
|
17
17
|
* "running" because nobody contradicted it. A system that reports a
|
|
@@ -37,17 +37,17 @@ export const job = z.object({
|
|
|
37
37
|
description: 'When this job last changed, as an ISO 8601 timestamp.',
|
|
38
38
|
}),
|
|
39
39
|
/**
|
|
40
|
-
* A monotonic counter, ascending in mint order
|
|
41
|
-
*
|
|
40
|
+
* A monotonic counter, ascending in mint order, and the **named** tiebreaker
|
|
41
|
+
* for any listing that claims an order.
|
|
42
42
|
*
|
|
43
43
|
* `started_at` is not a total order: two jobs minted in the same millisecond
|
|
44
44
|
* sort against each other arbitrarily, and arbitrarily means *differently on
|
|
45
45
|
* each query* — so `GET /api/robots/:id/jobs`, which documents "newest
|
|
46
|
-
* first", can show one twice and the other not at all.
|
|
47
|
-
*
|
|
46
|
+
* first", can show one twice and the other not at all. `auditEvent.seq`
|
|
47
|
+
* exists for the same reason on the audit log.
|
|
48
48
|
*
|
|
49
49
|
* **Scoped honestly: per cloud process, per run.** Job state lives in memory
|
|
50
|
-
*
|
|
50
|
+
* — that is why `lost` exists at all — so this counter restarts when
|
|
51
51
|
* the cloud does, alongside the jobs it orders. Sound, because it only ever
|
|
52
52
|
* orders jobs that coexist in one registry — and stated, because a reader
|
|
53
53
|
* who assumed `auditEvent.seq`'s durable semantics would be wrong.
|
|
@@ -62,14 +62,10 @@ export const job = z.object({
|
|
|
62
62
|
* Present on `failed`; a human message, plus a code where one exists.
|
|
63
63
|
*
|
|
64
64
|
* `details` exists because a refusal that carries only prose forces every
|
|
65
|
-
* consumer to parse it.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* "wait for one of N to finish" alert from a shape nothing in the system
|
|
70
|
-
* produced, and its test built that shape by hand — three repos agreeing
|
|
71
|
-
* with each other about a payload none of them exchanged (Momus, W6b
|
|
72
|
-
* review).
|
|
65
|
+
* consumer to parse it. A documented payload with nowhere to put it — the
|
|
66
|
+
* bridge reports a full queue as a job error — ends up formatted into the
|
|
67
|
+
* message and lost, and every consumer then builds the structured shape by
|
|
68
|
+
* hand from its own assumption.
|
|
73
69
|
*
|
|
74
70
|
* Optional, because most job errors have nothing structured to add. Where a
|
|
75
71
|
* code has a documented payload — `job_queue_full` has
|
|
@@ -95,8 +91,8 @@ export const job = z.object({
|
|
|
95
91
|
/**
|
|
96
92
|
* One update about a job, pushed to subscribers of its slug.
|
|
97
93
|
*
|
|
98
|
-
* `timestamp_ms` is the bridge's capture time, exactly as for a datapoint
|
|
99
|
-
*
|
|
94
|
+
* `timestamp_ms` is the bridge's capture time, exactly as for a datapoint —
|
|
95
|
+
* action feedback carries it too — so a client computes the age
|
|
100
96
|
* of a progress report the same way it computes the age of a sensor value,
|
|
101
97
|
* and a burst of late-delivered feedback after a reconnect is visibly late
|
|
102
98
|
* rather than looking current.
|
|
@@ -113,15 +109,14 @@ export const jobEvent = z.object({
|
|
|
113
109
|
timestamp_ms: z.number().int().nonnegative(),
|
|
114
110
|
});
|
|
115
111
|
/**
|
|
116
|
-
* What a busy refusal tells the caller
|
|
117
|
-
*
|
|
118
|
-
* to wait or to give up.
|
|
112
|
+
* What a busy refusal tells the caller: what is already running. A refusal that
|
|
113
|
+
* only says "busy" forces the caller to guess whether to wait or to give up.
|
|
119
114
|
*/
|
|
120
115
|
export const busyDetails = z.object({
|
|
121
116
|
running: job,
|
|
122
117
|
});
|
|
123
118
|
/**
|
|
124
|
-
* What a `publisher_busy` refusal tells the caller
|
|
119
|
+
* What a `publisher_busy` refusal tells the caller.
|
|
125
120
|
*
|
|
126
121
|
* "Another caller is publishing and has not been quiet long enough" names a
|
|
127
122
|
* state and no action: the caller does not know how much longer, because
|
|
@@ -140,7 +135,7 @@ export const publisherBusyDetails = z.object({
|
|
|
140
135
|
retry_after_ms: z.number().int().nonnegative(),
|
|
141
136
|
});
|
|
142
137
|
/**
|
|
143
|
-
* What a `job_queue_full` refusal tells the caller
|
|
138
|
+
* What a `job_queue_full` refusal tells the caller.
|
|
144
139
|
*
|
|
145
140
|
* Both numbers, not just the limit: `limit` alone says how big the queue is
|
|
146
141
|
* and nothing about whether waiting will help, and `queued` alone cannot be
|
|
@@ -171,7 +166,7 @@ export const JOB_RUN_RETENTION_DAYS = 90;
|
|
|
171
166
|
* invites every reader to handle it.
|
|
172
167
|
*
|
|
173
168
|
* **`app_user` is what a client-app caller writes now, and `end_user` stays**
|
|
174
|
-
*
|
|
169
|
+
* identity spaces. The seam this comment used to describe — two names for
|
|
175
170
|
* two ways into one merged pool — is settled: the two identity spaces are
|
|
176
171
|
* separate tables again, and `app_user` is a row in `app_users`, belonging to
|
|
177
172
|
* exactly one app. `end_user` is kept for the same reason `auditActor.kind`
|
|
@@ -197,10 +192,10 @@ export const jobActor = z.object({
|
|
|
197
192
|
});
|
|
198
193
|
export const jobRunKind = z.enum(['action', 'service']);
|
|
199
194
|
/**
|
|
200
|
-
* One durable record of one invocation
|
|
201
|
-
*
|
|
195
|
+
* One durable record of one invocation. One row per run, never one per event:
|
|
196
|
+
* the per-event timeline's write rate
|
|
202
197
|
* is set by the bridge, and a throttled log that cannot say it was throttled is
|
|
203
|
-
*
|
|
198
|
+
* an instrument that cannot say what it does not know. The live timeline is
|
|
204
199
|
* delivered in full by realtime, for as long as somebody is watching.
|
|
205
200
|
*/
|
|
206
201
|
export const jobRun = z.object({
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
|
-
* The MCP server
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* described were deleted by the org-central identity redesign (D5/D6).
|
|
4
|
+
* The MCP server: **one** remote MCP endpoint for the whole platform, whose
|
|
5
|
+
* tools are the exposed services and datapoints the signed-in user's roles
|
|
6
|
+
* permit. There is no per-app endpoint.
|
|
7
7
|
*
|
|
8
8
|
* **This file describes the seam, not the protocol.** The MCP messages
|
|
9
9
|
* themselves (`initialize`, `tools/list`, `tools/call`) are defined by the
|
|
@@ -15,8 +15,8 @@ import { z } from 'zod';
|
|
|
15
15
|
* an end user ever connects.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
* The protocol revision
|
|
19
|
-
*
|
|
18
|
+
* The protocol revision this server speaks, chosen over the newer
|
|
19
|
+
* `2026-07-28`.
|
|
20
20
|
*
|
|
21
21
|
* This is the latest revision the **stable** MCP TypeScript SDK ships, and it
|
|
22
22
|
* negotiates down to `2024-11-05`, so it covers the AI tools that exist today.
|
|
@@ -24,11 +24,10 @@ import { z } from 'zod';
|
|
|
24
24
|
* Streamable HTTP's session ids, the standalone SSE channel and resumability,
|
|
25
25
|
* and servers speaking only it answer `405` to GET and DELETE.
|
|
26
26
|
*
|
|
27
|
-
* **Which is why this server is stateless anyway.** Building sessions
|
|
28
|
-
*
|
|
29
|
-
* session map is
|
|
30
|
-
*
|
|
31
|
-
* (`max_realtime_connections`), and W8 is where a second instance appears.
|
|
27
|
+
* **Which is why this server is stateless anyway.** Building sessions that a
|
|
28
|
+
* later revision removes is work in the wrong direction, and a per-process
|
|
29
|
+
* session map is an assumption that breaks the moment a second cloud instance
|
|
30
|
+
* exists.
|
|
32
31
|
*/
|
|
33
32
|
export declare const MCP_PROTOCOL_VERSION: "2025-11-25";
|
|
34
33
|
/**
|
|
@@ -37,7 +36,7 @@ export declare const MCP_PROTOCOL_VERSION: "2025-11-25";
|
|
|
37
36
|
*
|
|
38
37
|
* **Not parameterised, and that is now a statement rather than the absence of
|
|
39
38
|
* one.** The central endpoint serves the org's team with the console tool
|
|
40
|
-
* family
|
|
39
|
+
* family; an app's users reach a different endpoint, whose
|
|
41
40
|
* path `mcpAppEndpointPath` builds. Two constants for two audiences, so a call
|
|
42
41
|
* site says which it means instead of an argument deciding it.
|
|
43
42
|
*
|
|
@@ -49,7 +48,7 @@ export declare const MCP_PROTOCOL_VERSION: "2025-11-25";
|
|
|
49
48
|
*/
|
|
50
49
|
export declare const MCP_ENDPOINT_PATH: "/mcp";
|
|
51
50
|
/**
|
|
52
|
-
* The path of **one app's** MCP server
|
|
51
|
+
* The path of **one app's** MCP server — what an app user pastes into
|
|
53
52
|
* their AI tool, served only while the app's `appAuthConfig.mcp_enabled` is on.
|
|
54
53
|
*
|
|
55
54
|
* A helper rather than a template literal at four call sites, for
|
|
@@ -73,7 +72,7 @@ export declare const MCP_ENDPOINT_PATH: "/mcp";
|
|
|
73
72
|
export declare function mcpAppEndpointPath(appIdentifier: string): string;
|
|
74
73
|
/**
|
|
75
74
|
* **Every path one app's MCP server answers on, built from its identifier
|
|
76
|
-
* once
|
|
75
|
+
* once**.
|
|
77
76
|
*
|
|
78
77
|
* Six strings, and each of them is spelled in at least three places that
|
|
79
78
|
* cannot see one another: the cloud registers the route, the console renders a
|
|
@@ -144,7 +143,7 @@ export declare const MCP_TOOL_NAME_MAX = 128;
|
|
|
144
143
|
export declare const mcpToolNamePattern: RegExp;
|
|
145
144
|
/**
|
|
146
145
|
* One exposure of a robot, as `robot_describe` and the console's per-role
|
|
147
|
-
* preview list it
|
|
146
|
+
* preview list it. Every exposure the role grants is listed —
|
|
148
147
|
* a missing `description` is shown as `null`, never used to hide the entry.
|
|
149
148
|
*
|
|
150
149
|
* `input_schema` is a JSON Schema document generated from an action's,
|
package/dist/mcp.js
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { slug } from './common.js';
|
|
4
4
|
/**
|
|
5
|
-
* The MCP server
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* described were deleted by the org-central identity redesign (D5/D6).
|
|
5
|
+
* The MCP server: **one** remote MCP endpoint for the whole platform, whose
|
|
6
|
+
* tools are the exposed services and datapoints the signed-in user's roles
|
|
7
|
+
* permit. There is no per-app endpoint.
|
|
9
8
|
*
|
|
10
9
|
* **This file describes the seam, not the protocol.** The MCP messages
|
|
11
10
|
* themselves (`initialize`, `tools/list`, `tools/call`) are defined by the
|
|
@@ -17,8 +16,8 @@ import { slug } from './common.js';
|
|
|
17
16
|
* an end user ever connects.
|
|
18
17
|
*/
|
|
19
18
|
/**
|
|
20
|
-
* The protocol revision
|
|
21
|
-
*
|
|
19
|
+
* The protocol revision this server speaks, chosen over the newer
|
|
20
|
+
* `2026-07-28`.
|
|
22
21
|
*
|
|
23
22
|
* This is the latest revision the **stable** MCP TypeScript SDK ships, and it
|
|
24
23
|
* negotiates down to `2024-11-05`, so it covers the AI tools that exist today.
|
|
@@ -26,11 +25,10 @@ import { slug } from './common.js';
|
|
|
26
25
|
* Streamable HTTP's session ids, the standalone SSE channel and resumability,
|
|
27
26
|
* and servers speaking only it answer `405` to GET and DELETE.
|
|
28
27
|
*
|
|
29
|
-
* **Which is why this server is stateless anyway.** Building sessions
|
|
30
|
-
*
|
|
31
|
-
* session map is
|
|
32
|
-
*
|
|
33
|
-
* (`max_realtime_connections`), and W8 is where a second instance appears.
|
|
28
|
+
* **Which is why this server is stateless anyway.** Building sessions that a
|
|
29
|
+
* later revision removes is work in the wrong direction, and a per-process
|
|
30
|
+
* session map is an assumption that breaks the moment a second cloud instance
|
|
31
|
+
* exists.
|
|
34
32
|
*/
|
|
35
33
|
export const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
36
34
|
/**
|
|
@@ -39,7 +37,7 @@ export const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
|
39
37
|
*
|
|
40
38
|
* **Not parameterised, and that is now a statement rather than the absence of
|
|
41
39
|
* one.** The central endpoint serves the org's team with the console tool
|
|
42
|
-
* family
|
|
40
|
+
* family; an app's users reach a different endpoint, whose
|
|
43
41
|
* path `mcpAppEndpointPath` builds. Two constants for two audiences, so a call
|
|
44
42
|
* site says which it means instead of an argument deciding it.
|
|
45
43
|
*
|
|
@@ -51,7 +49,7 @@ export const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
|
51
49
|
*/
|
|
52
50
|
export const MCP_ENDPOINT_PATH = '/mcp';
|
|
53
51
|
/**
|
|
54
|
-
* The path of **one app's** MCP server
|
|
52
|
+
* The path of **one app's** MCP server — what an app user pastes into
|
|
55
53
|
* their AI tool, served only while the app's `appAuthConfig.mcp_enabled` is on.
|
|
56
54
|
*
|
|
57
55
|
* A helper rather than a template literal at four call sites, for
|
|
@@ -97,7 +95,7 @@ export const MCP_TOOL_NAME_MAX = 128;
|
|
|
97
95
|
export const mcpToolNamePattern = /^[a-z0-9][a-z0-9_-]*$/;
|
|
98
96
|
/**
|
|
99
97
|
* One exposure of a robot, as `robot_describe` and the console's per-role
|
|
100
|
-
* preview list it
|
|
98
|
+
* preview list it. Every exposure the role grants is listed —
|
|
101
99
|
* a missing `description` is shown as `null`, never used to hide the entry.
|
|
102
100
|
*
|
|
103
101
|
* `input_schema` is a JSON Schema document generated from an action's,
|
package/dist/oauth.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
|
-
* **OAuth 2.1, and it remains only for MCP
|
|
4
|
+
* **OAuth 2.1, and it remains only for MCP.**
|
|
4
5
|
*
|
|
5
6
|
* This file used to describe two front doors: an app's end users signing in
|
|
6
7
|
* through a Fleetless-hosted, app-branded login page, and MCP clients signing
|
|
@@ -82,15 +83,11 @@ export type OauthError = z.infer<typeof oauthError>;
|
|
|
82
83
|
/**
|
|
83
84
|
* A redirect URI, and the rule is stricter than "a URL".
|
|
84
85
|
*
|
|
85
|
-
* **The defence
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* not the traversal but that **the rule existed one file over and was never
|
|
91
|
-
* carried across.** A redirect URI is the same shape of problem from a less
|
|
92
|
-
* trusted source: an attacker-supplied string that decides where a credential
|
|
93
|
-
* is sent.
|
|
86
|
+
* **The same defence is written down elsewhere in this package.** `config.ts`
|
|
87
|
+
* validates a capture-device path from the wire with a prefix rule *and* an
|
|
88
|
+
* explicit refusal of `..` segments. A redirect URI is the same shape of
|
|
89
|
+
* problem from a less trusted source: an attacker-supplied string that decides
|
|
90
|
+
* where a credential is sent.
|
|
94
91
|
*
|
|
95
92
|
* Matching at the server is **exact string comparison against a registered
|
|
96
93
|
* value** — never a prefix, never a wildcard host, never "starts with". A
|
|
@@ -124,7 +121,7 @@ export declare const MCP_DCR_MAX_REDIRECT_URIS = 5;
|
|
|
124
121
|
* authorization servers understand**, central and per-app.
|
|
125
122
|
*
|
|
126
123
|
* **Not `.strict()`, and that is the schema agreeing with the server rather
|
|
127
|
-
* than a gap in it.** §3.1 obliges a registration endpoint to ignore metadata
|
|
124
|
+
* than a gap in it.** RFC 7591 §3.1 obliges a registration endpoint to ignore metadata
|
|
128
125
|
* it does not understand, and real MCP clients send `client_uri`, `logo_uri`,
|
|
129
126
|
* `software_id` and `contacts`. A strict shape here would describe a `400`
|
|
130
127
|
* that no conforming client ever earns, and would take the whole
|
|
@@ -132,9 +129,8 @@ export declare const MCP_DCR_MAX_REDIRECT_URIS = 5;
|
|
|
132
129
|
* are therefore stripped by this schema and ignored by the server, which is
|
|
133
130
|
* the same answer said twice.
|
|
134
131
|
*
|
|
135
|
-
* **The server still reads the body field by field
|
|
136
|
-
*
|
|
137
|
-
* not the shape: §3.2.2 distinguishes `invalid_redirect_uri` from
|
|
132
|
+
* **The server still reads the body field by field**, and the reason is the
|
|
133
|
+
* error vocabulary, not the shape: RFC 7591 §3.2.2 distinguishes `invalid_redirect_uri` from
|
|
138
134
|
* `invalid_client_metadata`, and one `safeParse` failure cannot say which of
|
|
139
135
|
* the two a caller earned. So this schema is what the endpoint *accepts*, and
|
|
140
136
|
* the handler is what turns a miss into the right RFC code.
|
|
@@ -176,10 +172,9 @@ export type DynamicClientRegistrationResponse = z.infer<typeof dynamicClientRegi
|
|
|
176
172
|
* **The MCP token endpoint's request — one grant, because the servers serve
|
|
177
173
|
* one.**
|
|
178
174
|
*
|
|
179
|
-
* Both authorization servers, central and per-app, exchange through
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* happens. There is no refresh grant here: a session ends when its token
|
|
175
|
+
* Both authorization servers, central and per-app, exchange through one
|
|
176
|
+
* implementation, whose first act is to refuse anything but
|
|
177
|
+
* `authorization_code` before a single lookup happens. There is no refresh grant here: a session ends when its token
|
|
183
178
|
* expires and the client signs in again.
|
|
184
179
|
*
|
|
185
180
|
* **This was a `discriminatedUnion` with a `refresh_token` branch, and that
|
|
@@ -262,11 +257,10 @@ export type AuthorizationServerMetadata = z.infer<typeof authorizationServerMeta
|
|
|
262
257
|
/**
|
|
263
258
|
* RFC 9728 — what a *resource* publishes about who may authorize for it.
|
|
264
259
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* the rejection rather than the presence of the claim.
|
|
260
|
+
* Tokens are minted bound to a resource. **A minting mechanism with no
|
|
261
|
+
* validator is a check that cannot fail**, so the resource itself *rejects* a
|
|
262
|
+
* token whose audience names something else, and that rejection is what a check
|
|
263
|
+
* measures rather than the presence of the claim.
|
|
270
264
|
*/
|
|
271
265
|
export declare const protectedResourceMetadata: z.ZodObject<{
|
|
272
266
|
resource: z.ZodURL;
|
|
@@ -307,7 +301,7 @@ export type OauthRedirectResponse = z.infer<typeof oauthRedirectResponse>;
|
|
|
307
301
|
* shape and the documentation, not the error path.
|
|
308
302
|
*
|
|
309
303
|
* **Four route entries point at it**: `GET /mcp/oauth/authorize` and
|
|
310
|
-
* `GET /mcp/:appIdentifier/oauth/authorize
|
|
304
|
+
* `GET /mcp/:appIdentifier/oauth/authorize`, which read the same wire.
|
|
311
305
|
* They spent a release naming nothing — the app-level `/oauth/authorize` this
|
|
312
306
|
* was written for was deleted, and `query: null` was read as "there is no
|
|
313
307
|
* query here" rather than as "the handler reads it by hand" — and the eight
|
|
@@ -330,10 +324,10 @@ export type OauthAuthorizeQuery = z.infer<typeof oauthAuthorizeQuery>;
|
|
|
330
324
|
*
|
|
331
325
|
* It carried one parameter, `app_identifier`, on the argument that RFC 7591's
|
|
332
326
|
* registration body has no field for it and one endpoint could serve every
|
|
333
|
-
* app. It was kept — explicitly, in its own doc comment —
|
|
334
|
-
* registration
|
|
327
|
+
* app. It was kept — explicitly, in its own doc comment — for a per-app MCP
|
|
328
|
+
* registration that was said to need exactly this parameter.
|
|
335
329
|
*
|
|
336
|
-
* **That
|
|
330
|
+
* **That registration shipped and needed no such parameter.** `POST
|
|
337
331
|
* /mcp/:appIdentifier/oauth/register` puts the app in the **path**, built by
|
|
338
332
|
* `MCP_APP_PATHS`, and `resolveAppMcpTarget` reads it from `request.params`;
|
|
339
333
|
* `registerMcpDynamicClient` never looks at a query at all. So the one reason
|
package/dist/oauth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
* **OAuth 2.1, and it remains only for MCP
|
|
4
|
+
* **OAuth 2.1, and it remains only for MCP.**
|
|
5
5
|
*
|
|
6
6
|
* This file used to describe two front doors: an app's end users signing in
|
|
7
7
|
* through a Fleetless-hosted, app-branded login page, and MCP clients signing
|
|
@@ -77,8 +77,8 @@ export const oauthError = z.object({
|
|
|
77
77
|
* makes the two indistinguishable to the caller, and *a field that cannot
|
|
78
78
|
* express a distinction produces a workaround somewhere else*. Answering in
|
|
79
79
|
* `apiError` instead would keep the distinction and hand an RFC-compliant
|
|
80
|
-
* client a body it cannot parse
|
|
81
|
-
* to provide.
|
|
80
|
+
* client a body it cannot parse, which is the conformance this dialect
|
|
81
|
+
* exists to provide.
|
|
82
82
|
*
|
|
83
83
|
* So both: `error` is what a standard client reads, `fleetless_code` is what
|
|
84
84
|
* our own tooling switches on. RFC 6749 §5.2 permits additional members, and
|
|
@@ -89,15 +89,11 @@ export const oauthError = z.object({
|
|
|
89
89
|
/**
|
|
90
90
|
* A redirect URI, and the rule is stricter than "a URL".
|
|
91
91
|
*
|
|
92
|
-
* **The defence
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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.
|
|
92
|
+
* **The same defence is written down elsewhere in this package.** `config.ts`
|
|
93
|
+
* validates a capture-device path from the wire with a prefix rule *and* an
|
|
94
|
+
* explicit refusal of `..` segments. A redirect URI is the same shape of
|
|
95
|
+
* problem from a less trusted source: an attacker-supplied string that decides
|
|
96
|
+
* where a credential is sent.
|
|
101
97
|
*
|
|
102
98
|
* Matching at the server is **exact string comparison against a registered
|
|
103
99
|
* value** — never a prefix, never a wildcard host, never "starts with". A
|
|
@@ -111,8 +107,7 @@ export const redirectUri = z
|
|
|
111
107
|
.refine((v) => {
|
|
112
108
|
// Parsed, not prefix-matched. `startsWith('https://')` alone accepts the
|
|
113
109
|
// literal string `https://` and anything else that merely opens with
|
|
114
|
-
// those characters — a shape check standing in for a value check
|
|
115
|
-
// is the failure this project keeps meeting under other names.
|
|
110
|
+
// those characters — a shape check standing in for a value check.
|
|
116
111
|
let url;
|
|
117
112
|
try {
|
|
118
113
|
url = new URL(v);
|
|
@@ -133,7 +128,7 @@ export const redirectUri = z
|
|
|
133
128
|
// yields `"["` for `[::1]:8080`, because an IPv6 literal is *made of*
|
|
134
129
|
// colons. So `[::1]` never matched the allow-list it is named in, in any
|
|
135
130
|
// spelling, while two developer-facing messages went on saying it was
|
|
136
|
-
// permitted.
|
|
131
|
+
// permitted.
|
|
137
132
|
// `hostname` already strips the port and keeps the brackets.
|
|
138
133
|
if (url.protocol === 'http:')
|
|
139
134
|
return ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
@@ -162,7 +157,7 @@ export const MCP_DCR_MAX_REDIRECT_URIS = 5;
|
|
|
162
157
|
* authorization servers understand**, central and per-app.
|
|
163
158
|
*
|
|
164
159
|
* **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
|
|
160
|
+
* than a gap in it.** RFC 7591 §3.1 obliges a registration endpoint to ignore metadata
|
|
166
161
|
* it does not understand, and real MCP clients send `client_uri`, `logo_uri`,
|
|
167
162
|
* `software_id` and `contacts`. A strict shape here would describe a `400`
|
|
168
163
|
* that no conforming client ever earns, and would take the whole
|
|
@@ -170,9 +165,8 @@ export const MCP_DCR_MAX_REDIRECT_URIS = 5;
|
|
|
170
165
|
* are therefore stripped by this schema and ignored by the server, which is
|
|
171
166
|
* the same answer said twice.
|
|
172
167
|
*
|
|
173
|
-
* **The server still reads the body field by field
|
|
174
|
-
*
|
|
175
|
-
* not the shape: §3.2.2 distinguishes `invalid_redirect_uri` from
|
|
168
|
+
* **The server still reads the body field by field**, and the reason is the
|
|
169
|
+
* error vocabulary, not the shape: RFC 7591 §3.2.2 distinguishes `invalid_redirect_uri` from
|
|
176
170
|
* `invalid_client_metadata`, and one `safeParse` failure cannot say which of
|
|
177
171
|
* the two a caller earned. So this schema is what the endpoint *accepts*, and
|
|
178
172
|
* the handler is what turns a miss into the right RFC code.
|
|
@@ -237,10 +231,9 @@ export const dynamicClientRegistrationResponse = z.object({
|
|
|
237
231
|
* **The MCP token endpoint's request — one grant, because the servers serve
|
|
238
232
|
* one.**
|
|
239
233
|
*
|
|
240
|
-
* Both authorization servers, central and per-app, exchange through
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* happens. There is no refresh grant here: a session ends when its token
|
|
234
|
+
* Both authorization servers, central and per-app, exchange through one
|
|
235
|
+
* implementation, whose first act is to refuse anything but
|
|
236
|
+
* `authorization_code` before a single lookup happens. There is no refresh grant here: a session ends when its token
|
|
244
237
|
* expires and the client signs in again.
|
|
245
238
|
*
|
|
246
239
|
* **This was a `discriminatedUnion` with a `refresh_token` branch, and that
|
|
@@ -359,11 +352,10 @@ export const authorizationServerMetadata = z.object({
|
|
|
359
352
|
/**
|
|
360
353
|
* RFC 9728 — what a *resource* publishes about who may authorize for it.
|
|
361
354
|
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* the rejection rather than the presence of the claim.
|
|
355
|
+
* Tokens are minted bound to a resource. **A minting mechanism with no
|
|
356
|
+
* validator is a check that cannot fail**, so the resource itself *rejects* a
|
|
357
|
+
* token whose audience names something else, and that rejection is what a check
|
|
358
|
+
* measures rather than the presence of the claim.
|
|
367
359
|
*/
|
|
368
360
|
export const protectedResourceMetadata = z.object({
|
|
369
361
|
resource: z.url().meta({
|
|
@@ -404,14 +396,13 @@ export const oauthRedirectResponse = z.object({
|
|
|
404
396
|
* differently, and it worked — but every path it held belonged to the app-level
|
|
405
397
|
* OAuth flow (`authorize`, `token`, `register`, `consent`, `login`,
|
|
406
398
|
* `impersonate`, `idpCallback`) or to the stub resource's metadata documents,
|
|
407
|
-
* and OAuth 2.1 now remains only for MCP
|
|
408
|
-
*
|
|
409
|
-
* by one file rather than by two repositories.
|
|
399
|
+
* and OAuth 2.1 now remains only for MCP. The MCP authorization server builds
|
|
400
|
+
* its own paths, where they are read by one file rather than by two.
|
|
410
401
|
*
|
|
411
|
-
* Deleted rather than left
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
402
|
+
* Deleted rather than left holding entries whose routes are gone, which is the
|
|
403
|
+
* defect it was created to prevent and then reproduced: an entry naming a route
|
|
404
|
+
* the server had deleted stood for months with nothing noticing. A constant
|
|
405
|
+
* whose every value
|
|
415
406
|
* names a deleted route is that failure at full size.
|
|
416
407
|
*/
|
|
417
408
|
/**
|
|
@@ -427,7 +418,7 @@ export const oauthRedirectResponse = z.object({
|
|
|
427
418
|
* shape and the documentation, not the error path.
|
|
428
419
|
*
|
|
429
420
|
* **Four route entries point at it**: `GET /mcp/oauth/authorize` and
|
|
430
|
-
* `GET /mcp/:appIdentifier/oauth/authorize
|
|
421
|
+
* `GET /mcp/:appIdentifier/oauth/authorize`, which read the same wire.
|
|
431
422
|
* They spent a release naming nothing — the app-level `/oauth/authorize` this
|
|
432
423
|
* was written for was deleted, and `query: null` was read as "there is no
|
|
433
424
|
* query here" rather than as "the handler reads it by hand" — and the eight
|
|
@@ -461,10 +452,9 @@ export const oauthAuthorizeQuery = z
|
|
|
461
452
|
// **No `scope`, because this authorization server issues none.** The field
|
|
462
453
|
// was here describing itself as "carried onto the interaction and read
|
|
463
454
|
// again at consent"; neither authorize handler reads it, the interaction
|
|
464
|
-
// row has no column for it, and the consent screen answers `scopes: []
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
// as carried and in fact dropped is worse than one that is absent.
|
|
455
|
+
// row has no column for it, and the consent screen answers `scopes: []`.
|
|
456
|
+
// A parameter documented as carried and in fact dropped is worse than one
|
|
457
|
+
// that is absent.
|
|
468
458
|
})
|
|
469
459
|
.meta({
|
|
470
460
|
description: 'The authorization request an MCP client sends, per RFC 6749 §4.1.1 with mandatory PKCE. The handler reads it parameter by parameter rather than through one parse, because the answers differ: `client_id` and `redirect_uri` are refused flat, with no redirect, since until both are confirmed there is no trusted target to bounce a browser to, and everything after them is reported to the client\'s own callback as query parameters.',
|
|
@@ -474,10 +464,10 @@ export const oauthAuthorizeQuery = z
|
|
|
474
464
|
*
|
|
475
465
|
* It carried one parameter, `app_identifier`, on the argument that RFC 7591's
|
|
476
466
|
* registration body has no field for it and one endpoint could serve every
|
|
477
|
-
* app. It was kept — explicitly, in its own doc comment —
|
|
478
|
-
* registration
|
|
467
|
+
* app. It was kept — explicitly, in its own doc comment — for a per-app MCP
|
|
468
|
+
* registration that was said to need exactly this parameter.
|
|
479
469
|
*
|
|
480
|
-
* **That
|
|
470
|
+
* **That registration shipped and needed no such parameter.** `POST
|
|
481
471
|
* /mcp/:appIdentifier/oauth/register` puts the app in the **path**, built by
|
|
482
472
|
* `MCP_APP_PATHS`, and `resolveAppMcpTarget` reads it from `request.params`;
|
|
483
473
|
* `registerMcpDynamicClient` never looks at a query at all. So the one reason
|