@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/errors.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { z } from 'zod';
2
3
  /**
3
- * The one error shape of the REST and realtime APIs (spec §11.5): a stable
4
+ * The one error shape of the REST and realtime APIs: a stable
4
5
  * machine-readable code plus a human message; validation errors name the
5
6
  * field and the violated rule in `details`.
6
7
  */
@@ -11,7 +12,7 @@ export declare const apiError: z.ZodObject<{
11
12
  }, z.core.$strip>;
12
13
  export type ApiError = z.infer<typeof apiError>;
13
14
  /**
14
- * One violated §4.4 rule. `details` on the envelope stays `unknown` — codes
15
+ * One violated parameter rule. `details` on the envelope stays `unknown` — codes
15
16
  * are an open set, so their payloads cannot all be enumerated — but the
16
17
  * payload of `parameter_invalid` **is** pinned here, because otherwise every
17
18
  * consumer guesses: the cloud emits one shape, the SDK sniffs for two, the
@@ -44,7 +45,7 @@ export declare const parameterInvalidDetails: z.ZodObject<{
44
45
  }, z.core.$strip>;
45
46
  export type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
46
47
  /**
47
- * The codes in use as of W2. The wire deliberately allows any string — this
48
+ * The codes in use today. The wire deliberately allows any string — this
48
49
  * list is the shared vocabulary, not a closed set, so a new refusal never
49
50
  * needs a contracts release before it can be reported honestly.
50
51
  */
package/dist/errors.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
3
  /**
4
- * The one error shape of the REST and realtime APIs (spec §11.5): a stable
4
+ * The one error shape of the REST and realtime APIs: a stable
5
5
  * machine-readable code plus a human message; validation errors name the
6
6
  * field and the violated rule in `details`.
7
7
  */
@@ -11,7 +11,7 @@ export const apiError = z.object({
11
11
  details: z.unknown().optional(),
12
12
  });
13
13
  /**
14
- * One violated §4.4 rule. `details` on the envelope stays `unknown` — codes
14
+ * One violated parameter rule. `details` on the envelope stays `unknown` — codes
15
15
  * are an open set, so their payloads cannot all be enumerated — but the
16
16
  * payload of `parameter_invalid` **is** pinned here, because otherwise every
17
17
  * consumer guesses: the cloud emits one shape, the SDK sniffs for two, the
@@ -39,12 +39,12 @@ export const parameterInvalidDetails = z.object({
39
39
  violations: z.array(parameterViolation).min(1),
40
40
  });
41
41
  /**
42
- * The codes in use as of W2. The wire deliberately allows any string — this
42
+ * The codes in use today. The wire deliberately allows any string — this
43
43
  * list is the shared vocabulary, not a closed set, so a new refusal never
44
44
  * needs a contracts release before it can be reported honestly.
45
45
  */
46
46
  export const ERROR_CODES = [
47
- // W1
47
+ // Core.
48
48
  'not_found',
49
49
  'validation_error',
50
50
  'bad_request',
@@ -52,7 +52,7 @@ export const ERROR_CODES = [
52
52
  'invalid_token',
53
53
  'protocol_mismatch',
54
54
  'invalid_frame',
55
- // W2 — configuration
55
+ // Configuration.
56
56
  'duplicate_slug',
57
57
  'reserved_slug',
58
58
  /**
@@ -67,14 +67,14 @@ export const ERROR_CODES = [
67
67
  'invalid_rate',
68
68
  'invalid_range',
69
69
  'config_conflict',
70
- // W2 — reading
70
+ // Reading.
71
71
  'no_data',
72
- // W2 — talking to the robot
72
+ // Talking to the robot.
73
73
  'robot_offline',
74
74
  'bridge_timeout',
75
- // W3 — identity and rights. `forbidden` is deliberately the answer both
75
+ // Identity and rights. `forbidden` is deliberately the answer both
76
76
  // for "your role does not grant this" and for "there is no such slug":
77
- // roles are the only filter (§3.3), and a caller must not be able to map
77
+ // roles are the only filter, and a caller must not be able to map
78
78
  // the configuration of an app they have no rights in.
79
79
  'unauthorized',
80
80
  'forbidden',
@@ -95,15 +95,11 @@ export const ERROR_CODES = [
95
95
  * project's list: a documented refusal no caller can receive, which a reader
96
96
  * would reasonably branch on. */
97
97
  /**
98
- * The address is already taken — **globally, across every org** (Andre,
99
- * 2026-08-29).
98
+ * The address is already taken — **globally, across every org**.
100
99
  *
101
- * The 2026-08-29 redesign first made `users.email` unique *per org* (D1), so
102
- * this code briefly meant only *this org already has this address*. That was
103
- * reversed the same day: email is **globally unique** again, one address is
104
- * exactly one account in exactly one org, and this code means *somebody,
105
- * somewhere already has this address* — the pre-redesign meaning the code's
106
- * name always implied. There is no per-org reading of it any more.
100
+ * A Fleetless user's email is globally unique: one address is exactly one
101
+ * account in exactly one org, and this code means *somebody, somewhere
102
+ * already has this address*. There is no per-org reading of it.
107
103
  *
108
104
  * It stays an answer to a *write* an authenticated caller made — signing up,
109
105
  * inviting or creating — never to a login, which may not say whether an
@@ -116,53 +112,36 @@ export const ERROR_CODES = [
116
112
  'email_taken',
117
113
  'identifier_taken',
118
114
  'weak_password',
119
- /* `not_a_member` was removed on 2026-08-29. It had no producer anywhere in
120
- * this repository or in the cloud (`grep` found exactly two hits: its own
121
- * entry here and a test asserting the entry existed), and its vocabulary was
122
- * the deleted model's — "member" of an app's pool, in a platform whose
123
- * membership is now a group and whose access is an assignment. A code that
124
- * nothing emits and whose noun no longer exists is the third failure mode in
125
- * this project's list: a guard written against a state no producer reports.
126
- * The refusals that do the work are `forbidden` (silent about existence) and
115
+ /* `not_a_member` is gone. It had no producer anywhere, and its vocabulary was
116
+ * a deleted model's — "member" of an app's pool, in a platform whose access is
117
+ * an assignment. A code that nothing emits, whose noun no longer exists, is a
118
+ * refusal a consumer must still branch on and can never receive. The refusals
119
+ * that do the work are `forbidden` (silent about existence) and
127
120
  * `tier_required` (about the caller's own tier). */
128
121
  /**
129
122
  * The account itself is blocked — distinct from `forbidden` on purpose: it
130
123
  * tells the account holder something about *their own* account, and reveals
131
124
  * nothing about any other principal or about what exists.
132
125
  *
133
- * **It has now lost its producer, as this comment predicted it would.** The
134
- * paragraph here used to say "it loses its producer when D1's `users`
135
- * replaces `end_users` — it has not lost it yet", and named the five sites
136
- * that still emitted it, all reading `end_users.status === 'blocked'`. D1
137
- * landed. `users` has no `status` column, nothing reinstates one, and
138
- * removing a user's assignments is what withdraws access instead — so those
139
- * five sites went with the old tables.
140
- *
141
- * What is left in the cloud is a *shape* with no input: `TokenRefusalReason`
142
- * still admits `'blocked'` and `sendTokenRefusal` still has an arm for it
143
- * (`auth.ts`), as does `ws/realtime.ts` — but no site anywhere constructs
144
- * `reason: 'blocked'`, so neither arm is reachable. Verified by grep in
145
- * FL-007, after `routes.ts` listed this code on the dual-auth guard and a
146
- * review asked what produces it. Nothing does.
126
+ * **It has no producer today.** No user table carries a `status` column, and
127
+ * removing a user's assignments is what withdraws access instead. The cloud
128
+ * still has code paths shaped to carry this refusal, but nothing constructs
129
+ * one, so no caller can receive it.
147
130
  *
148
131
  * Kept, like `mcp_disabled` and for the same reason: the reserved shape is
149
132
  * the point, and a code removed from the vocabulary is a code the next
150
133
  * producer re-invents differently. But **do not list it as a refusal of any
151
134
  * route** — that would document an answer no caller can receive.
152
- *
153
- * The tense discipline this comment was written under still stands: it now
154
- * says the producer is gone because the producer is gone, not because a plan
155
- * expects it to be.
156
135
  */
157
136
  'account_blocked',
158
- // W4 — the command path.
159
- /** One job per action slug (§11.3); the refusal carries what is running. */
137
+ // The command path.
138
+ /** One job per action slug; the refusal carries what is running. */
160
139
  'busy',
161
- /** A parameter failed its §4.4 rule; details name the field and the rule. */
140
+ /** A parameter failed its declared rule; details name the field and the rule. */
162
141
  'parameter_invalid',
163
- /** The bridge could not account for this job after a restart (§6.1). */
142
+ /** The bridge could not account for this job after a restart. */
164
143
  'job_lost',
165
- /** Another user holds this publisher and has not been quiet long enough (§6.4). */
144
+ /** Another user holds this publisher and has not been quiet long enough. */
166
145
  'publisher_busy',
167
146
  /** A well-formed realtime frame this server does not know — the socket stays open. */
168
147
  'unknown_command',
@@ -173,8 +152,8 @@ export const ERROR_CODES = [
173
152
  * it sends them looking for a configuration mistake that is not there.
174
153
  */
175
154
  'not_subscribable',
176
- // W5 — cameras.
177
- /** The robot is connected but this camera is not publishing (§10). */
155
+ // Cameras.
156
+ /** The robot is connected but this camera is not publishing. */
178
157
  'camera_offline',
179
158
  /**
180
159
  * Nothing has been captured yet. An answer, not a failure: a camera
@@ -192,7 +171,7 @@ export const ERROR_CODES = [
192
171
  * nothing.
193
172
  */
194
173
  'wrong_kind',
195
- // W6 — retention and history.
174
+ // Retention and history.
196
175
  /**
197
176
  * The slug exists and is granted, but is configured live-only, so there is
198
177
  * no history to return. An empty array would be indistinguishable from a
@@ -209,7 +188,7 @@ export const ERROR_CODES = [
209
188
  */
210
189
  'not_aggregatable',
211
190
  /**
212
- * An org quota (§12.4) is exhausted. The message names **which** one —
191
+ * An org quota is exhausted. The message names **which** one —
213
192
  * "quota exceeded" without saying which is a dead end for whoever has to
214
193
  * act on it. Recording stops; live values keep flowing, because a storage
215
194
  * limit is not a reason to take a robot away from its operator.
@@ -226,13 +205,13 @@ export const ERROR_CODES = [
226
205
  'credential_in_use',
227
206
  /**
228
207
  * An action goal was never accepted — no server answered within the
229
- * bridge's patience (W5, from W4's review). Distinct from `failed`, which
208
+ * bridge's patience. Distinct from `failed`, which
230
209
  * means the robot tried: nothing tried here. It exists so a slug whose ROS
231
210
  * server is absent cannot stay wedged forever with the platform reporting
232
211
  * a machine as busy doing something it never started.
233
212
  */
234
213
  'goal_timeout',
235
- // W6a — deletion.
214
+ // Deletion.
236
215
  /**
237
216
  * A robot cannot be deleted while a live session is open. Refusing beats
238
217
  * deleting for the same reason `credential_in_use` does: the session
@@ -246,15 +225,15 @@ export const ERROR_CODES = [
246
225
  * A deletion destroyed some of a robot and then failed. The robot still
247
226
  * exists and is **not intact**; retrying the delete is the way out.
248
227
  *
249
- * It exists because the alternative was a generic `internal_error`, which
228
+ * It exists because the alternative is a generic `internal_error`, which
250
229
  * says "nothing happened" — and a caller who reads that goes looking for a
251
- * transient glitch. W6a's review measured the state it hides: configuration,
252
- * drafts, types and 300 000 rows gone, the robot still listed, and no audit
253
- * event. A failure that cannot be told apart from a no-op is how that state
254
- * stayed invisible.
230
+ * transient glitch. What it can hide is configuration, drafts, types and
231
+ * hundreds of thousands of rows already gone, the robot still listed, and no
232
+ * audit event. A failure that cannot be told apart from a no-op is how that
233
+ * state stays invisible.
255
234
  */
256
235
  'robot_deletion_partial',
257
- // W6b — addressing.
236
+ // Addressing.
258
237
  /**
259
238
  * The bridge will not queue another job: its queue is full.
260
239
  *
@@ -272,7 +251,7 @@ export const ERROR_CODES = [
272
251
  *
273
252
  * The numbers ride with it for the reason `publisher_busy` carries
274
253
  * `retry_after_ms`: a refusal that names a state and no action leaves the
275
- * caller to busy-loop, on a platform with no rate limiting until W8.
254
+ * caller to busy-loop.
276
255
  */
277
256
  'job_queue_full',
278
257
  /**
@@ -281,9 +260,9 @@ export const ERROR_CODES = [
281
260
  * **Path and query only.** A malformed uuid in a *body* is caught by the
282
261
  * body schema first and answers `validation_error` — the same mistake under
283
262
  * two codes, split by where the id sat. Stated here rather than promised
284
- * away: a consumer branching on `invalid_uuid` must not expect it for a
285
- * body field (Momus, W6b review). Unifying them is a W7 question, because
286
- * it means refusing before schema validation on every route that takes one.
263
+ * away: a consumer branching on `invalid_uuid` must not expect it for a body
264
+ * field. Unifying the two would mean refusing before schema validation on
265
+ * every route that takes an id.
287
266
  *
288
267
  * Distinct from `not_found`, which was the answer for both and made a
289
268
  * **typo indistinguishable from a deletion**. A developer whose client
@@ -292,7 +271,7 @@ export const ERROR_CODES = [
292
271
  * refused before any lookup — so it leaks nothing that `not_found` did not.
293
272
  */
294
273
  'invalid_uuid',
295
- // W6c — identity, and the limit that has to exist before it.
274
+ // Identity, and the limit that has to exist before it.
296
275
  /**
297
276
  * Too many attempts. The details carry `retry_after_ms`, for the reason
298
277
  * `publisher_busy` carries it: a refusal that names a state and no action
@@ -308,7 +287,7 @@ export const ERROR_CODES = [
308
287
  /**
309
288
  * The caller's **tier** is insufficient — an org Member reaching for what
310
289
  * only an Owner may do. Distinct from `forbidden`, which stays deliberately
311
- * silent about existence (§3.3): this one says nothing about the target
290
+ * silent about existence: this one says nothing about the target
312
291
  * either, only about the caller's own role, which they can already read.
313
292
  *
314
293
  * Without it, "ask an owner to do this" and "you have the wrong id" are the
@@ -322,7 +301,7 @@ export const ERROR_CODES = [
322
301
  * ask for a new link.
323
302
  */
324
303
  'token_spent',
325
- // W7 — the command path, still.
304
+ // The command path, continued.
326
305
  /**
327
306
  * A **service call** was dispatched and never returned. Distinct from
328
307
  * `goal_timeout`, which means an action goal was never *accepted* — nothing
@@ -331,13 +310,13 @@ export const ERROR_CODES = [
331
310
  * It exists because the bridge previously bounded a hung service with
332
311
  * nothing at all: `_invoke_service` took no patience, so the caller got
333
312
  * `bridge_timeout` from the cloud while the job stayed `running` forever on
334
- * both sides and the slug was busy for good (register row 2n, and 2e for the
335
- * cloud half). Rosie-W7 established that rclpy's
336
- * `Client.remove_pending_request` can abandon the future cheaply, so unlike
337
- * the action path this one can guarantee the callback never fires late.
313
+ * both sides and the slug busy for good. rclpy's
314
+ * `Client.remove_pending_request` abandons the pending future cheaply, so
315
+ * unlike the action path this one can guarantee the callback never fires
316
+ * late.
338
317
  */
339
318
  'service_timeout',
340
- // W7 — the asset store.
319
+ // The asset store.
341
320
  /**
342
321
  * A URDF references a mesh the store does not have. Distinct from
343
322
  * `not_found` on the URDF itself: the URDF is present and readable, and the
@@ -355,7 +334,7 @@ export const ERROR_CODES = [
355
334
  * cannot be read without the limit.
356
335
  */
357
336
  'asset_too_large',
358
- // W7b — the hosted authorization server.
337
+ // The hosted authorization server.
359
338
  //
360
339
  // **This comment was wrong in its first form and a teammate followed it
361
340
  // faithfully into a conformance bug.** It said these were "management-side
@@ -375,9 +354,8 @@ export const ERROR_CODES = [
375
354
  // (`/mcp/oauth/register` today), and as an ordinary `apiError` code at the
376
355
  // developer-facing management routes.
377
356
  //
378
- // Every code below has a producer landing in this same wave. W6b's lesson:
379
- // an enum value with no producer is precisely the defect that wave was
380
- // cataloguing, and a teammate was right to refuse to add one.
357
+ // Every code below has a producer. An enum value with no producer is a
358
+ // refusal a caller can never receive and a consumer must still branch on.
381
359
  /**
382
360
  * The app has not opted in to dynamic client registration. A normal app has
383
361
  * no reason to accept self-registering clients, so the flag is off by
@@ -398,7 +376,7 @@ export const ERROR_CODES = [
398
376
  * fix it.
399
377
  */
400
378
  'idp_unavailable',
401
- // W7c — the MCP server, and a THIRD dialect on the same process.
379
+ // The MCP server, and a THIRD dialect on the same process.
402
380
  //
403
381
  // The correction above is about two dialects; there are now three, and the
404
382
  // MCP endpoint speaks the one that is neither. **Inside the protocol** —
@@ -407,9 +385,7 @@ export const ERROR_CODES = [
407
385
  // general-purpose implementation of somebody else's specification, and a
408
386
  // body it cannot parse is indistinguishable from a broken server.
409
387
  //
410
- // **This claim was wider than the code in W7c's first version, and Momus-W7c
411
- // caught it in the same comment block whose opening sentence is about a
412
- // previous comment here misleading somebody.** The five refusals that happen
388
+ // **The claim is narrower than it first reads.** The five refusals that happen
413
389
  // *before* a bearer token is read — unknown app or MCP off (`404`), no or
414
390
  // bad token (`401`), foreign `Origin` (`403`), `GET`/`DELETE` (`405`),
415
391
  // malformed body (`400`) — are plain HTTP and answer `apiError`, exactly as
@@ -418,7 +394,7 @@ export const ERROR_CODES = [
418
394
  // `WWW-Authenticate` **header**, which is correct and present, not the body.
419
395
  //
420
396
  // So the rule is about the JSON-RPC layer, and the transport layer below it
421
- // is ordinary Fastify. Stating it as "never `apiError` anywhere" was the
397
+ // is ordinary HTTP. Stating it as "never `apiError` anywhere" was the
422
398
  // kind of tidy sentence that is easier to remember than the truth — and
423
399
  // this file has now produced two of those about itself.
424
400
  //
@@ -437,8 +413,8 @@ export const ERROR_CODES = [
437
413
  * flag; the central-MCP cut deleted the per-app `/mcp/<identifier>` endpoint
438
414
  * that flag gated, and 2026-08-29 removed the field itself from `app`, so
439
415
  * the code stood for a year with nothing able to produce it. The
440
- * app-user-auth design brings the per-app endpoint back (D7) with the switch
441
- * on `appAuthConfig` rather than on `app`, and this is its refusal again.
416
+ * per-app endpoint is back, with the switch on `appAuthConfig` rather than on
417
+ * `app`, and this is its refusal again.
442
418
  *
443
419
  * The lesson that survives is about the year in between: an enum member with
444
420
  * no producer is not harmless, because a reader arriving at it takes it for
@@ -461,11 +437,11 @@ export const ERROR_CODES = [
461
437
  * **Deliberately one code for both**: to a developer holding the console,
462
438
  * the role's datasheet (`mcpRobotDatasheet`) already lists every exposure
463
439
  * the role does grant, so a second code would split an outcome nobody acts
464
- * on differently. To anyone else the two must be indistinguishable anyway —
465
- * §3.3.
440
+ * on differently. To anyone else the two must be indistinguishable anyway,
441
+ * because roles are the only filter.
466
442
  */
467
443
  'tool_not_available',
468
- // W9 — capabilities.
444
+ // Capabilities.
469
445
  /**
470
446
  * An app-wide **capability** the caller's role does not grant — today
471
447
  * `assets` (`GET /api/robots/:id/assets` and the URDF/by-id byte routes)
@@ -475,31 +451,26 @@ export const ERROR_CODES = [
475
451
  * **Distinct from `forbidden`, and the distinction is the point.**
476
452
  * `forbidden` is deliberately silent about existence, because roles are the
477
453
  * only filter and a slug the caller cannot use must be indistinguishable
478
- * from a slug that is not there (§3.3). A capability is not a slug: it is a
454
+ * from a slug that is not there. A capability is not a slug: it is a
479
455
  * switch in the console that the developer owns, and the caller reaching
480
456
  * this refusal has already been proven to reach the robot. Answering
481
457
  * `forbidden` there tells a developer only that they may not — not which
482
458
  * toggle to flip — and a promise the console makes is exactly what these
483
459
  * capabilities have historically failed to keep.
484
460
  *
485
- * Both gates answered differently for one wave: `assets` said `forbidden`,
486
- * the newer `action_history` said this. One decision with two codes makes a
487
- * client branch on which route it called, so `assets` was moved here.
461
+ * Both capability gates answer with this code. One decision with two codes
462
+ * would make a client branch on which route it called.
488
463
  */
489
464
  'capability_required',
490
- // 2026-08-29 — org-central identity (D1/D2).
465
+ // Org-central identity.
491
466
  /**
492
467
  * **An org must keep at least one Owner**, so the last one is neither
493
468
  * deletable nor demotable. 409, on both `DELETE /api/org/users/:id` and
494
469
  * `PATCH /api/org/users/:id/tier`.
495
470
  *
496
- * **It was already being emitted before it was registered here** — the cloud
497
- * has answered `last_owner` from `org-members.ts` since W3a, and
498
- * `identity.ts`'s own doc comment named it, but `sendError` takes a bare
499
- * `string` and nothing ever compared the two lists. So a consumer switching
500
- * exhaustively over `ERROR_CODES` could not handle a code the server
501
- * actually sends. Registered as part of carrying the rule onto the new
502
- * tiers, and named as the pre-existing gap it was rather than as a new code.
471
+ * A code the server sends must be in this list, or a consumer switching
472
+ * exhaustively over `ERROR_CODES` cannot handle it. Nothing compares the two
473
+ * automatically, because the cloud's error helper takes a bare string.
503
474
  *
504
475
  * Deliberately not `forbidden` or `tier_required`: an Owner reaching this
505
476
  * has every permission the act needs. The refusal is about the org's
@@ -507,7 +478,7 @@ export const ERROR_CODES = [
507
478
  * caller could infer from a silence about existence.
508
479
  */
509
480
  'last_owner',
510
- // 2026-08-29 — oidc-federation (D3/D4).
481
+ // OIDC federation.
511
482
  /**
512
483
  * **The target is in a state that refuses the operation** — not the caller's
513
484
  * rights, not the target's existence, but *what the target currently is*.
@@ -549,7 +520,7 @@ export const ERROR_CODES = [
549
520
  * `routes/console-oauth.ts` in the same release.
550
521
  */
551
522
  'signup_closed',
552
- // FL-007 (route manifest): emitted by the cloud, catalogued late.
523
+ // Emitted by the cloud, catalogued late.
553
524
  //
554
525
  // Every one of the five below has had a live producer for some time; what
555
526
  // they never had was an entry here. **Each was confirmed by grepping the
@@ -571,7 +542,7 @@ export const ERROR_CODES = [
571
542
  * `409` from the configuration routes: the draft parses as YAML but its root
572
543
  * is not a mapping — a list, a scalar, or an empty document. Distinct from
573
544
  * `validation_error`, which is about a field inside a document that *is* one.
574
- * Produced by `cloud/src/routes/config.ts`.
545
+ * Produced by the configuration draft route.
575
546
  */
576
547
  'draft_not_a_document',
577
548
  /**
@@ -579,23 +550,22 @@ export const ERROR_CODES = [
579
550
  * it has no mapping for, and the code the realtime socket sends for the same
580
551
  * state. It says nothing about the request, deliberately: a caller cannot act
581
552
  * on it beyond retrying, and the detail belongs in the server's log rather
582
- * than in a body a stranger receives. Produced by `cloud/src/server.ts`'s
583
- * error handler and `cloud/src/ws/realtime.ts`.
553
+ * than in a body a stranger receives. Produced by the server's own error
554
+ * handler and by the realtime socket.
584
555
  */
585
556
  'internal_error',
586
557
  /**
587
558
  * `422` from `POST /api/robots/:id/jobs/:slug/cancel`: the job exists and the
588
559
  * caller may address it, but it is in a state that has nothing left to
589
560
  * cancel — already settled, or of a kind that does not support cancellation.
590
- * Produced by `cloud/src/commands.ts` and mapped in
591
- * `cloud/src/routes/commands.ts`.
561
+ * Produced by the command layer and mapped onto the job routes.
592
562
  */
593
563
  'not_cancellable',
594
564
  /**
595
565
  * `415`. The request carried a body in a media type the route does not read.
596
566
  * It is the cloud-wide answer from the content-type parser, not one route's:
597
567
  * a caller reaching it never got as far as validation, which is why this is
598
- * not a `validation_error`. Produced by `cloud/src/server.ts`.
568
+ * not a `validation_error`. Produced by the server itself.
599
569
  */
600
570
  'unsupported_media_type',
601
571
  /**
@@ -605,16 +575,14 @@ export const ERROR_CODES = [
605
575
  * hash recorded on the interaction row.
606
576
  *
607
577
  * **The impersonation interstitial it also named is deleted** with the rest
608
- * of the app OAuth flow (2026-09-05, D1/D2). That page is where this defence
609
- * was found missing on a GET rather than a POST — three times over, on three
610
- * different screens — which is the reason worth carrying forward: the check
611
- * belongs on every verb that *renders* the step, not only on the one that
578
+ * of the app OAuth flow. The rule that outlives it: this defence goes on
579
+ * every verb that *renders* the step, not only on the one that
612
580
  * completes it.
613
581
  *
614
582
  * Deliberately not `invalid_token` or `unauthorized`: nothing about the
615
583
  * caller's credential is being refused, and the remedy is specific and
616
- * actionable — start the flow again in this browser. Produced by
617
- * `cloud/src/routes/console-oauth.ts` and `cloud/src/routes/mcp-oauth.ts`.
584
+ * actionable — start the flow again in this browser. Produced by both
585
+ * hosted authorization flows.
618
586
  */
619
587
  'wrong_browser',
620
588
  /**
@@ -628,9 +596,8 @@ export const ERROR_CODES = [
628
596
  * why nothing noticed. The envelope validates; only the *code* was absent
629
597
  * from the one list a client can match against, so a caller branching on
630
598
  * `ERROR_CODES` fell through to its unknown-error arm for the single most
631
- * common refusal the editor produces. That is this file's own "documented
632
- * absence" failure, on the codes list itself. Produced by
633
- * `cloud/src/routes/config.ts`.
599
+ * common refusal an editor produces — a documented absence on the codes list
600
+ * itself. Produced by the configuration draft route.
634
601
  */
635
602
  'invalid_yaml',
636
603
  /**
@@ -640,7 +607,7 @@ export const ERROR_CODES = [
640
607
  *
641
608
  * Two codes rather than one, because the two say different things to whoever
642
609
  * typed the text: the first means "this is not YAML", the second means "this
643
- * is YAML I cannot keep". Produced by `cloud/src/routes/config.ts`.
610
+ * is YAML I cannot keep". Produced by the configuration draft route.
644
611
  */
645
612
  'unstorable_yaml',
646
613
  // 2026-09-05 — app-user auth (two identity spaces, the JSON client API).