@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +97 -2
  2. package/CODE_OF_CONDUCT.md +83 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/README.md +49 -13
  5. package/SECURITY.md +55 -0
  6. package/artifacts/openapi.json +11 -11
  7. package/artifacts/routes.json +12 -12
  8. package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
  9. package/dist/alerts.d.ts +23 -28
  10. package/dist/alerts.js +23 -29
  11. package/dist/app-users.d.ts +18 -19
  12. package/dist/app-users.js +18 -20
  13. package/dist/apps.d.ts +21 -25
  14. package/dist/apps.js +42 -52
  15. package/dist/assets.d.ts +70 -132
  16. package/dist/assets.js +130 -223
  17. package/dist/audit.d.ts +14 -15
  18. package/dist/audit.js +28 -55
  19. package/dist/client-auth.d.ts +9 -9
  20. package/dist/client-auth.js +8 -9
  21. package/dist/common.d.ts +29 -37
  22. package/dist/common.js +28 -37
  23. package/dist/config-issues.d.ts +23 -25
  24. package/dist/config-issues.js +17 -17
  25. package/dist/config.d.ts +37 -44
  26. package/dist/config.js +145 -187
  27. package/dist/errors.d.ts +4 -3
  28. package/dist/errors.js +83 -116
  29. package/dist/identity.d.ts +24 -27
  30. package/dist/identity.js +23 -27
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +14 -15
  33. package/dist/introspection.d.ts +7 -6
  34. package/dist/introspection.js +6 -6
  35. package/dist/jobs.d.ts +16 -16
  36. package/dist/jobs.js +24 -29
  37. package/dist/mcp.d.ts +14 -15
  38. package/dist/mcp.js +12 -14
  39. package/dist/oauth.d.ts +21 -27
  40. package/dist/oauth.js +33 -43
  41. package/dist/protocol.d.ts +51 -62
  42. package/dist/protocol.js +107 -139
  43. package/dist/realtime.d.ts +53 -68
  44. package/dist/realtime.js +78 -104
  45. package/dist/rest.d.ts +183 -244
  46. package/dist/rest.js +305 -399
  47. package/dist/routes.d.ts +4 -3
  48. package/dist/routes.js +33 -32
  49. 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 (spec §6.1, §11.3): one running unit of work on a robot — an action
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 (§11.3);
12
- * a client watches `robot × slug` and sees whatever job is running there,
13
- * which is also why every observer of a slug sees the same job.
14
- * 2. **`lost` is a real outcome and must be said out loud** (§6.1). Job state
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 (W7), and the **named**
41
- * tiebreaker for any listing that claims an order.
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. Exactly the defect
47
- * `auditEvent.seq` was added for in W6b, in a route the same wave shipped.
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
- * (§6.1 — that is why `lost` exists at all), so this counter restarts when
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. W6b shipped `job_queue_full` with a documented
66
- * `{limit, queued}` payload and **nowhere to put it**: the bridge reports a
67
- * full queue as a job error, this shape had no `details`, and so the numbers
68
- * were formatted into the message and lost. The console then rendered a
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
- * (§6.3 says action feedback carries it too) — so a client computes the age
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 (spec §11.3: "inkl. Information, was
117
- * läuft"). A refusal that only says "busy" forces the caller to guess whether
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 (spec §6.4).
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 (W6b).
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
- * (app-user auth, D1). The seam this comment used to describe — two names for
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 (spec `2026-08-20-timeseries-and-run-history`,
201
- * D2). One row per run, never one per event: the per-event timeline's write rate
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
- * the instrument this codebase refuses everywhere else. The live timeline is
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 of §17: **one** remote MCP endpoint for the whole platform,
4
- * whose tools are the exposed services and datapoints the signed-in user's
5
- * roles permit. The per-app `/mcp/<identifier>` servers this file once
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 W7c speaks. Chosen with André on 2026-08-18 over the
19
- * newer `2026-07-28`.
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 we would
28
- * have to delete again is work in the wrong direction, and a per-process
29
- * session map is the assumption that breaks at the second cloud instance —
30
- * the register already carries one row of exactly that shape
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 (2026-09-05, D7); an app's users reach a different endpoint, whose
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 (D7) — what an app user pastes into
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** (D7).
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 (FL-006). Every exposure the role grants is listed —
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 of §17: **one** remote MCP endpoint for the whole platform,
6
- * whose tools are the exposed services and datapoints the signed-in user's
7
- * roles permit. The per-app `/mcp/<identifier>` servers this file once
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 W7c speaks. Chosen with André on 2026-08-18 over the
21
- * newer `2026-07-28`.
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 we would
30
- * have to delete again is work in the wrong direction, and a per-process
31
- * session map is the assumption that breaks at the second cloud instance —
32
- * the register already carries one row of exactly that shape
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 (2026-09-05, D7); an app's users reach a different endpoint, whose
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 (D7) — what an app user pastes into
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 (FL-006). Every exposure the role grants is listed —
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** (2026-09-05 app-user-auth, D8).
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 for this was already written down in this codebase, twice.**
86
- * `config.ts` validates a V4L2 device path from the wire with a prefix rule
87
- * *and* an explicit refusal of `..` segments, tested, with the reasoning
88
- * recorded; W7's review then found a `package://` traversal in the bridge
89
- * that the same rule would have prevented, and the finding that mattered was
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** (`registerMcpDynamicClient`
136
- * in `cloud/src/mcp-oauth-core.ts`), and the reason is the error vocabulary,
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
- * `exchangeMcpAuthorizationCode` (`cloud/src/mcp-oauth-core.ts`), whose first
181
- * act is to refuse anything but `authorization_code` before a single lookup
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
- * W7b mints tokens bound to a resource that W7c builds. **A minting mechanism
266
- * with no validator is the failure mode this project has now met twelve times
267
- * in one wave: a check that cannot fail.** So W7b also ships a resource that
268
- * *rejects* a token whose audience names something else, and the gate measures
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` (D7), which read the same wire.
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 — "for the per-app MCP
334
- * registration the MCP train adds (D7), which needs exactly this parameter".
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 train shipped and needed no such parameter.** `POST
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** (2026-09-05 app-user-auth, D8).
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 — which is the conformance this wave exists
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 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.
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, which
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. Found by Momus-W7b, reproduced against the live server.
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** (`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
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
- * `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
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
- * 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.
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 (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.
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 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
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` (D7), which read the same wire.
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
- // 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.
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 — "for the per-app MCP
478
- * registration the MCP train adds (D7), which needs exactly this parameter".
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 train shipped and needed no such parameter.** `POST
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