@fleetless/contracts 1.0.0 → 1.0.2

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 (48) hide show
  1. package/CHANGELOG.md +68 -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 +1 -1
  7. package/artifacts/routes.json +1 -1
  8. package/dist/alerts.d.ts +19 -24
  9. package/dist/alerts.js +18 -24
  10. package/dist/app-users.d.ts +7 -6
  11. package/dist/app-users.js +6 -6
  12. package/dist/apps.d.ts +21 -25
  13. package/dist/apps.js +40 -51
  14. package/dist/assets.d.ts +70 -132
  15. package/dist/assets.js +130 -223
  16. package/dist/audit.d.ts +11 -11
  17. package/dist/audit.js +25 -51
  18. package/dist/client-auth.d.ts +4 -4
  19. package/dist/client-auth.js +3 -4
  20. package/dist/common.d.ts +27 -35
  21. package/dist/common.js +26 -35
  22. package/dist/config-issues.d.ts +4 -3
  23. package/dist/config-issues.js +7 -6
  24. package/dist/config.d.ts +31 -37
  25. package/dist/config.js +81 -110
  26. package/dist/errors.d.ts +4 -3
  27. package/dist/errors.js +61 -87
  28. package/dist/identity.d.ts +18 -21
  29. package/dist/identity.js +17 -21
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.js +12 -13
  32. package/dist/introspection.d.ts +7 -6
  33. package/dist/introspection.js +6 -6
  34. package/dist/jobs.d.ts +12 -12
  35. package/dist/jobs.js +20 -25
  36. package/dist/mcp.d.ts +11 -12
  37. package/dist/mcp.js +10 -12
  38. package/dist/oauth.d.ts +13 -18
  39. package/dist/oauth.js +13 -19
  40. package/dist/protocol.d.ts +51 -62
  41. package/dist/protocol.js +107 -139
  42. package/dist/realtime.d.ts +53 -68
  43. package/dist/realtime.js +77 -103
  44. package/dist/rest.d.ts +182 -243
  45. package/dist/rest.js +301 -395
  46. package/dist/routes.d.ts +4 -3
  47. package/dist/routes.js +3 -2
  48. package/package.json +12 -7
@@ -1,6 +1,7 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { z } from 'zod';
2
3
  /**
3
- * Introspection (spec §4.1, §4.5): what the connected bridge can tell the
4
+ * Introspection: what the connected bridge can tell the
4
5
  * cloud about the robot's ROS graph, so the console can offer a quick pick
5
6
  * instead of a blank text field.
6
7
  *
@@ -45,10 +46,10 @@ export interface TypeField {
45
46
  }
46
47
  export declare const typeField: z.ZodType<TypeField>;
47
48
  /**
48
- * A resolved type of one robot. Custom types are per robot (spec §4.5): two
49
- * robots may define `custom_msgs/msg/Speed` differently and both are right.
49
+ * A resolved type of one robot. Custom types are per robot: two robots may
50
+ * define `custom_msgs/msg/Speed` differently and both are right.
50
51
  *
51
- * W2 resolved messages only; W4 adds services and actions, because a
52
+ * Messages, services and actions all appear here, because a
52
53
  * `parameterSpec.name` has to resolve against *something*, and an action has
53
54
  * no flat field list — it has a goal, a result and a feedback tree.
54
55
  *
@@ -65,8 +66,8 @@ export declare const typeField: z.ZodType<TypeField>;
65
66
  * a result in. They are carried so the console can show what an action will
66
67
  * report back, and so a client knows the shape of `job.result` in advance.
67
68
  *
68
- * The `msg` member keeps W2's exact shape, so every type already stored stays
69
- * valid without migration.
69
+ * The `msg` member is the flat field list a message resolves to; the other
70
+ * two carry one tree per part.
70
71
  */
71
72
  export declare const typeDefinition: z.ZodDiscriminatedUnion<[z.ZodObject<{
72
73
  name: z.ZodString;
@@ -2,7 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { rosName, rosTypeName } from './common.js';
4
4
  /**
5
- * Introspection (spec §4.1, §4.5): what the connected bridge can tell the
5
+ * Introspection: what the connected bridge can tell the
6
6
  * cloud about the robot's ROS graph, so the console can offer a quick pick
7
7
  * instead of a blank text field.
8
8
  *
@@ -31,10 +31,10 @@ export const typeField = z.lazy(() => z.object({
31
31
  fields: z.array(typeField).nullable(),
32
32
  }));
33
33
  /**
34
- * A resolved type of one robot. Custom types are per robot (spec §4.5): two
35
- * robots may define `custom_msgs/msg/Speed` differently and both are right.
34
+ * A resolved type of one robot. Custom types are per robot: two robots may
35
+ * define `custom_msgs/msg/Speed` differently and both are right.
36
36
  *
37
- * W2 resolved messages only; W4 adds services and actions, because a
37
+ * Messages, services and actions all appear here, because a
38
38
  * `parameterSpec.name` has to resolve against *something*, and an action has
39
39
  * no flat field list — it has a goal, a result and a feedback tree.
40
40
  *
@@ -51,8 +51,8 @@ export const typeField = z.lazy(() => z.object({
51
51
  * a result in. They are carried so the console can show what an action will
52
52
  * report back, and so a client knows the shape of `job.result` in advance.
53
53
  *
54
- * The `msg` member keeps W2's exact shape, so every type already stored stays
55
- * valid without migration.
54
+ * The `msg` member is the flat field list a message resolves to; the other
55
+ * two carry one tree per part.
56
56
  */
57
57
  export const typeDefinition = z.discriminatedUnion('kind', [
58
58
  z.object({
package/dist/jobs.d.ts CHANGED
@@ -1,15 +1,16 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { z } from 'zod';
2
3
  /**
3
- * Jobs (spec §6.1, §11.3): one running unit of work on a robot — an action
4
+ * Jobs: one running unit of work on a robot — an action
4
5
  * goal or a service call — with an id both sides know, so bridge and cloud
5
6
  * stay in sync across a disconnect.
6
7
  *
7
8
  * Two rules shape everything here:
8
9
  *
9
- * 1. **State is observed by slug, not by id.** The id is informative (§11.3);
10
- * a client watches `robot × slug` and sees whatever job is running there,
11
- * which is also why every observer of a slug sees the same job.
12
- * 2. **`lost` is a real outcome and must be said out loud** (§6.1). Job state
10
+ * 1. **State is observed by slug, not by id.** The id is informative; a client
11
+ * watches `robot × slug` and sees whatever job is running there, which is
12
+ * also why every observer of a slug sees the same job.
13
+ * 2. **`lost` is a real outcome and must be said out loud.** Job state
13
14
  * lives only in the bridge's memory; if it restarts mid-job, the results
14
15
  * are gone. The cloud then marks the job `lost` — never leaves it reading
15
16
  * "running" because nobody contradicted it. A system that reports a
@@ -49,8 +50,8 @@ export type Job = z.infer<typeof job>;
49
50
  /**
50
51
  * One update about a job, pushed to subscribers of its slug.
51
52
  *
52
- * `timestamp_ms` is the bridge's capture time, exactly as for a datapoint
53
- * (§6.3 says action feedback carries it too) — so a client computes the age
53
+ * `timestamp_ms` is the bridge's capture time, exactly as for a datapoint —
54
+ * action feedback carries it too — so a client computes the age
54
55
  * of a progress report the same way it computes the age of a sensor value,
55
56
  * and a burst of late-delivered feedback after a reconnect is visibly late
56
57
  * rather than looking current.
@@ -86,9 +87,8 @@ export declare const jobEvent: z.ZodObject<{
86
87
  }, z.core.$strip>;
87
88
  export type JobEvent = z.infer<typeof jobEvent>;
88
89
  /**
89
- * What a busy refusal tells the caller (spec §11.3: "inkl. Information, was
90
- * läuft"). A refusal that only says "busy" forces the caller to guess whether
91
- * to wait or to give up.
90
+ * What a busy refusal tells the caller: what is already running. A refusal that
91
+ * only says "busy" forces the caller to guess whether to wait or to give up.
92
92
  */
93
93
  export declare const busyDetails: z.ZodObject<{
94
94
  running: z.ZodObject<{
@@ -115,7 +115,7 @@ export declare const busyDetails: z.ZodObject<{
115
115
  }, z.core.$strip>;
116
116
  export type BusyDetails = z.infer<typeof busyDetails>;
117
117
  /**
118
- * What a `publisher_busy` refusal tells the caller (spec §6.4).
118
+ * What a `publisher_busy` refusal tells the caller.
119
119
  *
120
120
  * "Another caller is publishing and has not been quiet long enough" names a
121
121
  * state and no action: the caller does not know how much longer, because
@@ -133,7 +133,7 @@ export declare const publisherBusyDetails: z.ZodObject<{
133
133
  }, z.core.$strip>;
134
134
  export type PublisherBusyDetails = z.infer<typeof publisherBusyDetails>;
135
135
  /**
136
- * What a `job_queue_full` refusal tells the caller (W6b).
136
+ * What a `job_queue_full` refusal tells the caller.
137
137
  *
138
138
  * Both numbers, not just the limit: `limit` alone says how big the queue is
139
139
  * and nothing about whether waiting will help, and `queued` alone cannot be
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
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
  /**
@@ -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
  /**
@@ -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,3 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { z } from 'zod';
2
3
  /**
3
4
  * **OAuth 2.1, and it remains only for MCP** (2026-09-05 app-user-auth, D8).
@@ -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.
@@ -262,11 +258,10 @@ export type AuthorizationServerMetadata = z.infer<typeof authorizationServerMeta
262
258
  /**
263
259
  * RFC 9728 — what a *resource* publishes about who may authorize for it.
264
260
  *
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.
261
+ * Tokens are minted bound to a resource. **A minting mechanism with no
262
+ * validator is a check that cannot fail**, so the resource itself *rejects* a
263
+ * token whose audience names something else, and that rejection is what a check
264
+ * measures rather than the presence of the claim.
270
265
  */
271
266
  export declare const protectedResourceMetadata: z.ZodObject<{
272
267
  resource: z.ZodURL;
package/dist/oauth.js CHANGED
@@ -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
@@ -133,7 +129,7 @@ export const redirectUri = z
133
129
  // yields `"["` for `[::1]:8080`, because an IPv6 literal is *made of*
134
130
  // colons. So `[::1]` never matched the allow-list it is named in, in any
135
131
  // spelling, while two developer-facing messages went on saying it was
136
- // permitted. Found by Momus-W7b, reproduced against the live server.
132
+ // permitted.
137
133
  // `hostname` already strips the port and keeps the brackets.
138
134
  if (url.protocol === 'http:')
139
135
  return ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
@@ -162,7 +158,7 @@ export const MCP_DCR_MAX_REDIRECT_URIS = 5;
162
158
  * authorization servers understand**, central and per-app.
163
159
  *
164
160
  * **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
161
+ * than a gap in it.** RFC 7591 §3.1 obliges a registration endpoint to ignore metadata
166
162
  * it does not understand, and real MCP clients send `client_uri`, `logo_uri`,
167
163
  * `software_id` and `contacts`. A strict shape here would describe a `400`
168
164
  * that no conforming client ever earns, and would take the whole
@@ -170,9 +166,8 @@ export const MCP_DCR_MAX_REDIRECT_URIS = 5;
170
166
  * are therefore stripped by this schema and ignored by the server, which is
171
167
  * the same answer said twice.
172
168
  *
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
169
+ * **The server still reads the body field by field**, and the reason is the
170
+ * error vocabulary, not the shape: RFC 7591 §3.2.2 distinguishes `invalid_redirect_uri` from
176
171
  * `invalid_client_metadata`, and one `safeParse` failure cannot say which of
177
172
  * the two a caller earned. So this schema is what the endpoint *accepts*, and
178
173
  * the handler is what turns a miss into the right RFC code.
@@ -359,11 +354,10 @@ export const authorizationServerMetadata = z.object({
359
354
  /**
360
355
  * RFC 9728 — what a *resource* publishes about who may authorize for it.
361
356
  *
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.
357
+ * Tokens are minted bound to a resource. **A minting mechanism with no
358
+ * validator is a check that cannot fail**, so the resource itself *rejects* a
359
+ * token whose audience names something else, and that rejection is what a check
360
+ * measures rather than the presence of the claim.
367
361
  */
368
362
  export const protectedResourceMetadata = z.object({
369
363
  resource: z.url().meta({