@fleetless/contracts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +17 -0
  4. package/README.md +88 -0
  5. package/artifacts/constants.json +24 -0
  6. package/artifacts/openapi.json +17219 -0
  7. package/artifacts/routes.json +4605 -0
  8. package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
  9. package/artifacts/schema/action-config.schema.json +198 -0
  10. package/artifacts/schema/alert-list-response.schema.json +172 -0
  11. package/artifacts/schema/api-error.schema.json +20 -0
  12. package/artifacts/schema/app-auth-config.schema.json +106 -0
  13. package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
  14. package/artifacts/schema/app-invitation.schema.json +69 -0
  15. package/artifacts/schema/app-list-response.schema.json +82 -0
  16. package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
  17. package/artifacts/schema/app-mail-template.schema.json +54 -0
  18. package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
  19. package/artifacts/schema/app-oidc-provider.schema.json +80 -0
  20. package/artifacts/schema/app-user-list-response.schema.json +111 -0
  21. package/artifacts/schema/app-user.schema.json +98 -0
  22. package/artifacts/schema/app.schema.json +69 -0
  23. package/artifacts/schema/apply-error.schema.json +41 -0
  24. package/artifacts/schema/asset-list-response.schema.json +288 -0
  25. package/artifacts/schema/asset-sync-request.schema.json +17 -0
  26. package/artifacts/schema/asset-sync-response.schema.json +16 -0
  27. package/artifacts/schema/asset-sync-status.schema.json +136 -0
  28. package/artifacts/schema/asset.schema.json +68 -0
  29. package/artifacts/schema/audit-actor.schema.json +32 -0
  30. package/artifacts/schema/audit-event.schema.json +119 -0
  31. package/artifacts/schema/audit-list-response.schema.json +144 -0
  32. package/artifacts/schema/audit-query.schema.json +79 -0
  33. package/artifacts/schema/auth-error.schema.json +23 -0
  34. package/artifacts/schema/auth-me-response.schema.json +99 -0
  35. package/artifacts/schema/auth-ok.schema.json +115 -0
  36. package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
  37. package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
  38. package/artifacts/schema/bridge-assets-available.schema.json +25 -0
  39. package/artifacts/schema/bridge-camera-state.schema.json +78 -0
  40. package/artifacts/schema/bridge-config-applied.schema.json +67 -0
  41. package/artifacts/schema/bridge-hello.schema.json +65 -0
  42. package/artifacts/schema/bridge-introspect.schema.json +114 -0
  43. package/artifacts/schema/bridge-job-lost.schema.json +22 -0
  44. package/artifacts/schema/bridge-job-update.schema.json +100 -0
  45. package/artifacts/schema/bridge-pong.schema.json +19 -0
  46. package/artifacts/schema/bridge-pressure.schema.json +292 -0
  47. package/artifacts/schema/bridge-state.schema.json +24 -0
  48. package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
  49. package/artifacts/schema/busy-details.schema.json +115 -0
  50. package/artifacts/schema/camera-descriptor.schema.json +45 -0
  51. package/artifacts/schema/camera-list-response.schema.json +58 -0
  52. package/artifacts/schema/camera-source.schema.json +240 -0
  53. package/artifacts/schema/cancel-request.schema.json +20 -0
  54. package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
  55. package/artifacts/schema/client-auth.schema.json +18 -0
  56. package/artifacts/schema/client-cancel.schema.json +45 -0
  57. package/artifacts/schema/client-identity.schema.json +103 -0
  58. package/artifacts/schema/client-invoke.schema.json +45 -0
  59. package/artifacts/schema/client-login-request.schema.json +29 -0
  60. package/artifacts/schema/client-logout-request.schema.json +14 -0
  61. package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
  62. package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
  63. package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
  64. package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
  65. package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
  66. package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
  67. package/artifacts/schema/client-password-reset-request.schema.json +24 -0
  68. package/artifacts/schema/client-provider-list-query.schema.json +17 -0
  69. package/artifacts/schema/client-provider-list-response.schema.json +34 -0
  70. package/artifacts/schema/client-publish.schema.json +40 -0
  71. package/artifacts/schema/client-refresh-request.schema.json +14 -0
  72. package/artifacts/schema/client-register-request.schema.json +44 -0
  73. package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
  74. package/artifacts/schema/client-subscribe.schema.json +43 -0
  75. package/artifacts/schema/client-unsubscribe.schema.json +26 -0
  76. package/artifacts/schema/client-verify-email-request.schema.json +15 -0
  77. package/artifacts/schema/cloud-asset-request.schema.json +37 -0
  78. package/artifacts/schema/cloud-camera-start.schema.json +41 -0
  79. package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
  80. package/artifacts/schema/cloud-cancel.schema.json +33 -0
  81. package/artifacts/schema/cloud-config.schema.json +1635 -0
  82. package/artifacts/schema/cloud-hello-error.schema.json +23 -0
  83. package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
  84. package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
  85. package/artifacts/schema/cloud-invoke.schema.json +40 -0
  86. package/artifacts/schema/cloud-ping.schema.json +19 -0
  87. package/artifacts/schema/cloud-publish.schema.json +28 -0
  88. package/artifacts/schema/cloud-type-request.schema.json +30 -0
  89. package/artifacts/schema/command-result.schema.json +175 -0
  90. package/artifacts/schema/config-draft-response.schema.json +1695 -0
  91. package/artifacts/schema/config-state.schema.json +124 -0
  92. package/artifacts/schema/config-version-response.schema.json +1641 -0
  93. package/artifacts/schema/config-versions-response.schema.json +33 -0
  94. package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
  95. package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
  96. package/artifacts/schema/create-app-request.schema.json +30 -0
  97. package/artifacts/schema/create-app-user-request.schema.json +42 -0
  98. package/artifacts/schema/create-robot-request.schema.json +14 -0
  99. package/artifacts/schema/create-robot-response.schema.json +44 -0
  100. package/artifacts/schema/create-server-key-response.schema.json +65 -0
  101. package/artifacts/schema/create-team-invite-request.schema.json +43 -0
  102. package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
  103. package/artifacts/schema/datapoint-config.schema.json +366 -0
  104. package/artifacts/schema/datapoint-display.schema.json +31 -0
  105. package/artifacts/schema/datapoint-event.schema.json +34 -0
  106. package/artifacts/schema/datapoint-frame.schema.json +28 -0
  107. package/artifacts/schema/datapoint-list-response.schema.json +61 -0
  108. package/artifacts/schema/datapoint-value.schema.json +28 -0
  109. package/artifacts/schema/developer-login-request.schema.json +19 -0
  110. package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
  111. package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
  112. package/artifacts/schema/error-frame.schema.json +23 -0
  113. package/artifacts/schema/exposure-counts.schema.json +39 -0
  114. package/artifacts/schema/exposure-list-response.schema.json +43 -0
  115. package/artifacts/schema/fetch-types-request.schema.json +19 -0
  116. package/artifacts/schema/fetch-types-response.schema.json +163 -0
  117. package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
  118. package/artifacts/schema/fleetless-user.schema.json +60 -0
  119. package/artifacts/schema/history-buckets-response.schema.json +79 -0
  120. package/artifacts/schema/history-query.schema.json +58 -0
  121. package/artifacts/schema/history-response.schema.json +150 -0
  122. package/artifacts/schema/history-samples-response.schema.json +68 -0
  123. package/artifacts/schema/introspection-response.schema.json +118 -0
  124. package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
  125. package/artifacts/schema/invoke-request.schema.json +23 -0
  126. package/artifacts/schema/invoke-response.schema.json +125 -0
  127. package/artifacts/schema/job-actor.schema.json +34 -0
  128. package/artifacts/schema/job-event.schema.json +158 -0
  129. package/artifacts/schema/job-response.schema.json +123 -0
  130. package/artifacts/schema/job-run-list-response.schema.json +222 -0
  131. package/artifacts/schema/job-run-query.schema.json +95 -0
  132. package/artifacts/schema/job-run-summary-query.schema.json +23 -0
  133. package/artifacts/schema/job-run-summary.schema.json +33 -0
  134. package/artifacts/schema/job-run.schema.json +195 -0
  135. package/artifacts/schema/job-state.schema.json +11 -0
  136. package/artifacts/schema/job.schema.json +106 -0
  137. package/artifacts/schema/latency-bucket.schema.json +63 -0
  138. package/artifacts/schema/live-session-response.schema.json +41 -0
  139. package/artifacts/schema/mail-outcome.schema.json +20 -0
  140. package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
  141. package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
  142. package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
  143. package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
  144. package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
  145. package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
  146. package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
  147. package/artifacts/schema/missing-asset-query.schema.json +11 -0
  148. package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
  149. package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
  150. package/artifacts/schema/oauth-token-request.schema.json +47 -0
  151. package/artifacts/schema/oauth-token-response.schema.json +38 -0
  152. package/artifacts/schema/org-alerts-query.schema.json +15 -0
  153. package/artifacts/schema/org-event-dropped.schema.json +26 -0
  154. package/artifacts/schema/org-event-replay.schema.json +97 -0
  155. package/artifacts/schema/org-event-subscribe.schema.json +14 -0
  156. package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
  157. package/artifacts/schema/org-event.schema.json +75 -0
  158. package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
  159. package/artifacts/schema/org-health-query.schema.json +13 -0
  160. package/artifacts/schema/org-latency-query.schema.json +42 -0
  161. package/artifacts/schema/org-latency-response.schema.json +124 -0
  162. package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
  163. package/artifacts/schema/org-quota-usage.schema.json +102 -0
  164. package/artifacts/schema/org-quotas.schema.json +51 -0
  165. package/artifacts/schema/org-usage-query.schema.json +19 -0
  166. package/artifacts/schema/org-usage-response.schema.json +77 -0
  167. package/artifacts/schema/org.schema.json +30 -0
  168. package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
  169. package/artifacts/schema/parameter-spec.schema.json +120 -0
  170. package/artifacts/schema/parameter-violation.schema.json +24 -0
  171. package/artifacts/schema/password-change-request.schema.json +21 -0
  172. package/artifacts/schema/password-reset-confirm.schema.json +19 -0
  173. package/artifacts/schema/password-reset-request.schema.json +14 -0
  174. package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
  175. package/artifacts/schema/patch-app-user-request.schema.json +34 -0
  176. package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
  177. package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
  178. package/artifacts/schema/patch-org-request.schema.json +15 -0
  179. package/artifacts/schema/patch-org-response.schema.json +40 -0
  180. package/artifacts/schema/patch-robot-request.schema.json +15 -0
  181. package/artifacts/schema/patch-robot-response.schema.json +40 -0
  182. package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
  183. package/artifacts/schema/pending-team-invite.schema.json +39 -0
  184. package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
  185. package/artifacts/schema/publish-config-response.schema.json +21 -0
  186. package/artifacts/schema/publish-request.schema.json +17 -0
  187. package/artifacts/schema/publisher-config.schema.json +285 -0
  188. package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
  189. package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
  190. package/artifacts/schema/put-config-draft-request.schema.json +13 -0
  191. package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
  192. package/artifacts/schema/put-robot-details-request.schema.json +41 -0
  193. package/artifacts/schema/put-robot-details-response.schema.json +43 -0
  194. package/artifacts/schema/rate-limit-details.schema.json +15 -0
  195. package/artifacts/schema/refresh-request.schema.json +13 -0
  196. package/artifacts/schema/release-live-query.schema.json +13 -0
  197. package/artifacts/schema/rename-slug-request.schema.json +23 -0
  198. package/artifacts/schema/rename-slug-response.schema.json +24 -0
  199. package/artifacts/schema/resource-health-event.schema.json +72 -0
  200. package/artifacts/schema/resource-health-list-response.schema.json +80 -0
  201. package/artifacts/schema/resource-health-state.schema.json +68 -0
  202. package/artifacts/schema/robot-config-doc.schema.json +1616 -0
  203. package/artifacts/schema/robot-delete-query.schema.json +12 -0
  204. package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
  205. package/artifacts/schema/robot-detail-response.schema.json +262 -0
  206. package/artifacts/schema/robot-details-doc.schema.json +33 -0
  207. package/artifacts/schema/robot-jobs-response.schema.json +119 -0
  208. package/artifacts/schema/robot-latency-series.schema.json +81 -0
  209. package/artifacts/schema/robot-list-item.schema.json +94 -0
  210. package/artifacts/schema/robot-list-response.schema.json +106 -0
  211. package/artifacts/schema/robot.schema.json +30 -0
  212. package/artifacts/schema/role-list-response.schema.json +48 -0
  213. package/artifacts/schema/role-permissions.schema.json +61 -0
  214. package/artifacts/schema/role.schema.json +35 -0
  215. package/artifacts/schema/ros-graph.schema.json +99 -0
  216. package/artifacts/schema/server-key-list-response.schema.json +64 -0
  217. package/artifacts/schema/server-key.schema.json +51 -0
  218. package/artifacts/schema/service-call-response.schema.json +13 -0
  219. package/artifacts/schema/service-config.schema.json +198 -0
  220. package/artifacts/schema/session-tokens.schema.json +28 -0
  221. package/artifacts/schema/sign-up-request.schema.json +26 -0
  222. package/artifacts/schema/sign-up-response.schema.json +127 -0
  223. package/artifacts/schema/slug-usage-response.schema.json +32 -0
  224. package/artifacts/schema/snapshot-header.schema.json +44 -0
  225. package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
  226. package/artifacts/schema/subscribe-error.schema.json +31 -0
  227. package/artifacts/schema/team-invite.schema.json +57 -0
  228. package/artifacts/schema/tier-change-request.schema.json +17 -0
  229. package/artifacts/schema/type-definition.schema.json +144 -0
  230. package/artifacts/schema/types-response.schema.json +156 -0
  231. package/artifacts/schema/update-app-request.schema.json +32 -0
  232. package/artifacts/schema/urdf-completeness.schema.json +50 -0
  233. package/artifacts/schema/validation-issue.schema.json +43 -0
  234. package/artifacts/schema/waitlist-request.schema.json +15 -0
  235. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
  236. package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
  237. package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
  238. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
  239. package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
  240. package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
  241. package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
  242. package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
  243. package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
  244. package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
  245. package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
  246. package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
  247. package/dist/alerts.d.ts +255 -0
  248. package/dist/alerts.js +193 -0
  249. package/dist/app-users.d.ts +606 -0
  250. package/dist/app-users.js +696 -0
  251. package/dist/apps.d.ts +175 -0
  252. package/dist/apps.js +267 -0
  253. package/dist/assets.d.ts +434 -0
  254. package/dist/assets.js +546 -0
  255. package/dist/audit.d.ts +129 -0
  256. package/dist/audit.js +238 -0
  257. package/dist/client-auth.d.ts +409 -0
  258. package/dist/client-auth.js +487 -0
  259. package/dist/common.d.ts +186 -0
  260. package/dist/common.js +199 -0
  261. package/dist/config-issues.d.ts +175 -0
  262. package/dist/config-issues.js +339 -0
  263. package/dist/config.d.ts +862 -0
  264. package/dist/config.js +1988 -0
  265. package/dist/errors.d.ts +52 -0
  266. package/dist/errors.js +786 -0
  267. package/dist/identity.d.ts +549 -0
  268. package/dist/identity.js +503 -0
  269. package/dist/index.d.ts +51 -0
  270. package/dist/index.js +51 -0
  271. package/dist/introspection.d.ts +99 -0
  272. package/dist/introspection.js +97 -0
  273. package/dist/jobs.d.ts +334 -0
  274. package/dist/jobs.js +345 -0
  275. package/dist/mcp.d.ts +239 -0
  276. package/dist/mcp.js +153 -0
  277. package/dist/oauth.d.ts +344 -0
  278. package/dist/oauth.js +488 -0
  279. package/dist/protocol.d.ts +781 -0
  280. package/dist/protocol.js +715 -0
  281. package/dist/realtime.d.ts +494 -0
  282. package/dist/realtime.js +512 -0
  283. package/dist/rest.d.ts +1989 -0
  284. package/dist/rest.js +1963 -0
  285. package/dist/routes.d.ts +94 -0
  286. package/dist/routes.js +2298 -0
  287. package/package.json +61 -0
package/dist/errors.js ADDED
@@ -0,0 +1,786 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ /**
4
+ * The one error shape of the REST and realtime APIs (spec §11.5): a stable
5
+ * machine-readable code plus a human message; validation errors name the
6
+ * field and the violated rule in `details`.
7
+ */
8
+ export const apiError = z.object({
9
+ code: z.string().min(1),
10
+ message: z.string().min(1),
11
+ details: z.unknown().optional(),
12
+ });
13
+ /**
14
+ * One violated §4.4 rule. `details` on the envelope stays `unknown` — codes
15
+ * are an open set, so their payloads cannot all be enumerated — but the
16
+ * payload of `parameter_invalid` **is** pinned here, because otherwise every
17
+ * consumer guesses: the cloud emits one shape, the SDK sniffs for two, the
18
+ * console renders a third, and each is right in its own tests.
19
+ *
20
+ * `field` is the **flat key exactly as the caller sent it** — the same string
21
+ * as the `parameterSpec.name` it violated. That is the entire justification
22
+ * for the flat parameter form: a refusal has to name something the caller can
23
+ * find in what they typed, and a console can attach the error to that one
24
+ * input rather than to the form.
25
+ */
26
+ export const parameterViolation = z.object({
27
+ field: z.string().min(1),
28
+ /** Which rule failed — `min`, `max`, `enum`, `pattern`, `required`, `undeclared`. */
29
+ rule: z.string().min(1),
30
+ message: z.string().min(1),
31
+ });
32
+ /**
33
+ * The `details` of a `parameter_invalid` refusal. Always at least one
34
+ * violation: a refusal that names none would leave the caller with nothing to
35
+ * fix. All violations are reported at once, not just the first — a caller
36
+ * fixing parameters one round-trip at a time is a caller who gives up.
37
+ */
38
+ export const parameterInvalidDetails = z.object({
39
+ violations: z.array(parameterViolation).min(1),
40
+ });
41
+ /**
42
+ * The codes in use as of W2. The wire deliberately allows any string — this
43
+ * list is the shared vocabulary, not a closed set, so a new refusal never
44
+ * needs a contracts release before it can be reported honestly.
45
+ */
46
+ export const ERROR_CODES = [
47
+ // W1
48
+ 'not_found',
49
+ 'validation_error',
50
+ 'bad_request',
51
+ 'unknown_datapoint',
52
+ 'invalid_token',
53
+ 'protocol_mismatch',
54
+ 'invalid_frame',
55
+ // W2 — configuration
56
+ 'duplicate_slug',
57
+ 'reserved_slug',
58
+ /**
59
+ * `POST /api/robots/:id/config/rename-slug`'s `from` names nothing in the
60
+ * draft — distinct from `unknown_datapoint`, which is a read against a
61
+ * *published* config; a rename only ever inspects the draft.
62
+ */
63
+ 'unknown_slug',
64
+ 'unknown_field_path',
65
+ 'unknown_type',
66
+ 'unknown_topic',
67
+ 'invalid_rate',
68
+ 'invalid_range',
69
+ 'config_conflict',
70
+ // W2 — reading
71
+ 'no_data',
72
+ // W2 — talking to the robot
73
+ 'robot_offline',
74
+ 'bridge_timeout',
75
+ // W3 — identity and rights. `forbidden` is deliberately the answer both
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
78
+ // the configuration of an app they have no rights in.
79
+ 'unauthorized',
80
+ 'forbidden',
81
+ 'invalid_credentials',
82
+ 'token_expired',
83
+ 'token_revoked',
84
+ /* `invite_expired` and `invite_used` were removed on 2026-09-05, by the same
85
+ * reasoning that removed `not_a_member` and with the same evidence: a `grep`
86
+ * across contracts, cloud, sdk, console and bridge found their own entries
87
+ * here and one test asserting those entries existed. Nothing has ever emitted
88
+ * either.
89
+ *
90
+ * They were written for `POST /api/client/invitations/accept`, to tell an
91
+ * expired invitation from an already-accepted one. That route answers `410
92
+ * token_spent` to both, and to an unknown token and a revoked one as well —
93
+ * see that code's own entry for why. Keeping two codes for a distinction the
94
+ * wire deliberately refuses to make is the third failure mode in this
95
+ * project's list: a documented refusal no caller can receive, which a reader
96
+ * would reasonably branch on. */
97
+ /**
98
+ * The address is already taken — **globally, across every org** (Andre,
99
+ * 2026-08-29).
100
+ *
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.
107
+ *
108
+ * It stays an answer to a *write* an authenticated caller made — signing up,
109
+ * inviting or creating — never to a login, which may not say whether an
110
+ * address exists. The app-user surface has the same split: creating a user
111
+ * through the developer-authenticated route may answer `email_taken`, while
112
+ * `POST /api/client/register` answers `202` either way. On an app user the
113
+ * code means *this app already has this address*, since app-user email is
114
+ * unique per app rather than globally.
115
+ */
116
+ 'email_taken',
117
+ 'identifier_taken',
118
+ '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
127
+ * `tier_required` (about the caller's own tier). */
128
+ /**
129
+ * The account itself is blocked — distinct from `forbidden` on purpose: it
130
+ * tells the account holder something about *their own* account, and reveals
131
+ * nothing about any other principal or about what exists.
132
+ *
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.
147
+ *
148
+ * Kept, like `mcp_disabled` and for the same reason: the reserved shape is
149
+ * the point, and a code removed from the vocabulary is a code the next
150
+ * producer re-invents differently. But **do not list it as a refusal of any
151
+ * 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
+ */
157
+ 'account_blocked',
158
+ // W4 — the command path.
159
+ /** One job per action slug (§11.3); the refusal carries what is running. */
160
+ 'busy',
161
+ /** A parameter failed its §4.4 rule; details name the field and the rule. */
162
+ 'parameter_invalid',
163
+ /** The bridge could not account for this job after a restart (§6.1). */
164
+ 'job_lost',
165
+ /** Another user holds this publisher and has not been quiet long enough (§6.4). */
166
+ 'publisher_busy',
167
+ /** A well-formed realtime frame this server does not know — the socket stays open. */
168
+ 'unknown_command',
169
+ /**
170
+ * The slug exists and is granted, but has nothing to observe — a publisher
171
+ * has no job and no stream. Distinct from `unknown_datapoint` on purpose:
172
+ * answering "no such slug" about one the caller was granted is a lie, and
173
+ * it sends them looking for a configuration mistake that is not there.
174
+ */
175
+ 'not_subscribable',
176
+ // W5 — cameras.
177
+ /** The robot is connected but this camera is not publishing (§10). */
178
+ 'camera_offline',
179
+ /**
180
+ * Nothing has been captured yet. An answer, not a failure: a camera
181
+ * configured a moment ago has no frame, and serving an older one from a
182
+ * different camera — or none, silently — would both be worse.
183
+ */
184
+ 'no_snapshot_yet',
185
+ /** Live cannot start: no media server, no token, or the bridge refused. */
186
+ 'live_unavailable',
187
+ /**
188
+ * The slug exists and is granted, but is not the kind this verb addresses —
189
+ * subscribing to a datapoint with an action helper, calling a service
190
+ * helper on an action. Answering instead of falling silent is the point:
191
+ * silence is also what an idle slug looks like, so it tells the caller
192
+ * nothing.
193
+ */
194
+ 'wrong_kind',
195
+ // W6 — retention and history.
196
+ /**
197
+ * The slug exists and is granted, but is configured live-only, so there is
198
+ * no history to return. An empty array would be indistinguishable from a
199
+ * recorded datapoint that happens to have no samples in the range, and the
200
+ * two need completely different actions from the developer: one is "turn
201
+ * recording on", the other is "look at a different window".
202
+ */
203
+ 'not_recorded',
204
+ /**
205
+ * `min`/`max`/`avg` was asked of a value that is not a number, and no
206
+ * numeric `field` was named. Refusing beats coercing: an average of
207
+ * booleans or strings is a number that means nothing, and it would be
208
+ * charted as confidently as a real one.
209
+ */
210
+ 'not_aggregatable',
211
+ /**
212
+ * An org quota (§12.4) is exhausted. The message names **which** one —
213
+ * "quota exceeded" without saying which is a dead end for whoever has to
214
+ * act on it. Recording stops; live values keep flowing, because a storage
215
+ * limit is not a reason to take a robot away from its operator.
216
+ */
217
+ 'quota_exceeded',
218
+ /**
219
+ * A named credential cannot be deleted because cameras still reference it.
220
+ * Refusing beats deleting: a shared credential typically serves several
221
+ * cameras across several robots, so a blind rotation is exactly how one of
222
+ * them silently stops working — on a robot the developer had forgotten
223
+ * about. The details carry `used_by`, so the answer names what to fix
224
+ * rather than only what went wrong.
225
+ */
226
+ 'credential_in_use',
227
+ /**
228
+ * An action goal was never accepted — no server answered within the
229
+ * bridge's patience (W5, from W4's review). Distinct from `failed`, which
230
+ * means the robot tried: nothing tried here. It exists so a slug whose ROS
231
+ * server is absent cannot stay wedged forever with the platform reporting
232
+ * a machine as busy doing something it never started.
233
+ */
234
+ 'goal_timeout',
235
+ // W6a — deletion.
236
+ /**
237
+ * A robot cannot be deleted while a live session is open. Refusing beats
238
+ * deleting for the same reason `credential_in_use` does: the session
239
+ * belongs to somebody who is watching right now, and taking it away
240
+ * without a word is indistinguishable from a crash. `?force=true` says
241
+ * "yes, I know" — the caller has to say it, rather than the platform
242
+ * deciding for them.
243
+ */
244
+ 'robot_in_use',
245
+ /**
246
+ * A deletion destroyed some of a robot and then failed. The robot still
247
+ * exists and is **not intact**; retrying the delete is the way out.
248
+ *
249
+ * It exists because the alternative was a generic `internal_error`, which
250
+ * 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.
255
+ */
256
+ 'robot_deletion_partial',
257
+ // W6b — addressing.
258
+ /**
259
+ * The bridge will not queue another job: its queue is full.
260
+ *
261
+ * The queue was **unbounded**, which is not the same as generous — it is a
262
+ * robot that accepts a thousand goals it will never reach, reports every
263
+ * one of them as queued, and runs out of memory rather than saying no. A
264
+ * bound turns that into an answer the caller can act on, which is the whole
265
+ * of the difference.
266
+ *
267
+ * It rides on **`job.error.details`** as `jobQueueFullDetails`, not on an
268
+ * `apiError` envelope — and that distinction is load-bearing. A full queue
269
+ * is discovered by the *bridge*, after the cloud has already answered the
270
+ * invoke with a minted job, so it can never be the refusal of the call. It
271
+ * reaches the caller as the job's terminal failure.
272
+ *
273
+ * The numbers ride with it for the reason `publisher_busy` carries
274
+ * `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.
276
+ */
277
+ 'job_queue_full',
278
+ /**
279
+ * An id in the path or body is not a uuid at all.
280
+ *
281
+ * **Path and query only.** A malformed uuid in a *body* is caught by the
282
+ * body schema first and answers `validation_error` — the same mistake under
283
+ * 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.
287
+ *
288
+ * Distinct from `not_found`, which was the answer for both and made a
289
+ * **typo indistinguishable from a deletion**. A developer whose client
290
+ * concatenated a template variable wrong got a clean `404` and went looking
291
+ * for a robot they had never lost. It says nothing about existence — it is
292
+ * refused before any lookup — so it leaks nothing that `not_found` did not.
293
+ */
294
+ 'invalid_uuid',
295
+ // W6c — identity, and the limit that has to exist before it.
296
+ /**
297
+ * Too many attempts. The details carry `retry_after_ms`, for the reason
298
+ * `publisher_busy` carries it: a refusal that names a state and no action
299
+ * leaves the caller to busy-loop, which on *this* code is the attack.
300
+ *
301
+ * It must be answerable **before** any password verification. A limiter that
302
+ * refuses after argon2 has run has not removed the denial of service, it has
303
+ * only added a message to it — and that is invisible to every test that
304
+ * checks the status code, which is why the gate measures the *cost* of a
305
+ * refusal and not merely its shape.
306
+ */
307
+ 'rate_limited',
308
+ /**
309
+ * The caller's **tier** is insufficient — an org Member reaching for what
310
+ * only an Owner may do. Distinct from `forbidden`, which stays deliberately
311
+ * silent about existence (§3.3): this one says nothing about the target
312
+ * either, only about the caller's own role, which they can already read.
313
+ *
314
+ * Without it, "ask an owner to do this" and "you have the wrong id" are the
315
+ * same answer, and only one of them is worth acting on.
316
+ */
317
+ 'tier_required',
318
+ /**
319
+ * A password reset or invitation token has been spent, or has expired.
320
+ * Deliberately one code for both: distinguishing them tells a stranger
321
+ * whether a token ever existed, and the recovery is identical either way —
322
+ * ask for a new link.
323
+ */
324
+ 'token_spent',
325
+ // W7 — the command path, still.
326
+ /**
327
+ * A **service call** was dispatched and never returned. Distinct from
328
+ * `goal_timeout`, which means an action goal was never *accepted* — nothing
329
+ * tried there; here the robot was asked and stopped answering.
330
+ *
331
+ * It exists because the bridge previously bounded a hung service with
332
+ * nothing at all: `_invoke_service` took no patience, so the caller got
333
+ * `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.
338
+ */
339
+ 'service_timeout',
340
+ // W7 — the asset store.
341
+ /**
342
+ * A URDF references a mesh the store does not have. Distinct from
343
+ * `not_found` on the URDF itself: the URDF is present and readable, and the
344
+ * thing to fix is a sync that came back incomplete, not a missing robot.
345
+ *
346
+ * It exists because the alternative is a renderer drawing a robot with
347
+ * missing limbs and no explanation — a failure that surfaces far from its
348
+ * cause, in somebody else's application.
349
+ */
350
+ 'asset_missing',
351
+ /**
352
+ * The asset exceeds the per-file ceiling. Carries `assetTooLargeDetails`
353
+ * with both numbers, for the reason `job_queue_full` carries both: the limit
354
+ * alone does not tell the caller how far over they are, and the size alone
355
+ * cannot be read without the limit.
356
+ */
357
+ 'asset_too_large',
358
+ // W7b — the hosted authorization server.
359
+ //
360
+ // **This comment was wrong in its first form and a teammate followed it
361
+ // faithfully into a conformance bug.** It said these were "management-side
362
+ // codes only" and then listed two whose only producer is the dynamic client
363
+ // registration endpoint,
364
+ // which is an OAuth endpoint. Read literally — correctly — that instructs
365
+ // you to answer a *standard* client with an `apiError` body it cannot parse.
366
+ //
367
+ // The rule is unchanged and the placement of these two was the error: the
368
+ // OAuth endpoints answer in RFC 6749's own error shape, always, including
369
+ // their policy refusals. The Fleetless reason rides along in
370
+ // `oauthError.fleetless_code`, so `error` stays what a standard client reads
371
+ // and the distinction between "not opted in" and "ceiling full" survives.
372
+ //
373
+ // These codes therefore appear in BOTH places by design: as the value of
374
+ // `fleetless_code` inside an RFC envelope at the registration endpoint
375
+ // (`/mcp/oauth/register` today), and as an ordinary `apiError` code at the
376
+ // developer-facing management routes.
377
+ //
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.
381
+ /**
382
+ * The app has not opted in to dynamic client registration. A normal app has
383
+ * no reason to accept self-registering clients, so the flag is off by
384
+ * default and this is the answer — distinct from `forbidden`, because it
385
+ * tells the *developer* something actionable about their own app rather
386
+ * than telling a stranger what exists.
387
+ */
388
+ 'dynamic_registration_disabled',
389
+ /**
390
+ * The per-app ceiling on dynamically-registered clients is reached. Carries
391
+ * both numbers for the same reason `asset_too_large` does.
392
+ */
393
+ 'client_limit_reached',
394
+ /**
395
+ * The developer's IdP could not be reached or its discovery document could
396
+ * not be read. Distinct from `server_error` on purpose — the fault is in a
397
+ * system Fleetless does not run, and the developer is the only one who can
398
+ * fix it.
399
+ */
400
+ 'idp_unavailable',
401
+ // W7c — the MCP server, and a THIRD dialect on the same process.
402
+ //
403
+ // The correction above is about two dialects; there are now three, and the
404
+ // MCP endpoint speaks the one that is neither. **Inside the protocol** —
405
+ // once a request is a JSON-RPC message — `/mcp/<app>` answers **JSON-RPC
406
+ // errors**, never `apiError` and never `oauthError`. An MCP client is a
407
+ // general-purpose implementation of somebody else's specification, and a
408
+ // body it cannot parse is indistinguishable from a broken server.
409
+ //
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
413
+ // *before* a bearer token is read — unknown app or MCP off (`404`), no or
414
+ // bad token (`401`), foreign `Origin` (`403`), `GET`/`DELETE` (`405`),
415
+ // malformed body (`400`) — are plain HTTP and answer `apiError`, exactly as
416
+ // every other route does. That is deliberate and it is safe: what a
417
+ // conforming MCP client reads at that layer is the RFC 6750
418
+ // `WWW-Authenticate` **header**, which is correct and present, not the body.
419
+ //
420
+ // 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
422
+ // kind of tidy sentence that is easier to remember than the truth — and
423
+ // this file has now produced two of those about itself.
424
+ //
425
+ // So the two codes below appear at the **management** routes only — the
426
+ // console asking about an app or a role. Nothing in `/mcp/<app>` produces
427
+ // them, and if one ever seems to belong there, the answer is a JSON-RPC
428
+ // error whose message says the same thing.
429
+ /**
430
+ * **This app does not serve an MCP endpoint** — `appAuthConfig.mcp_enabled`
431
+ * is off. `403` from the app's whole OAuth surface, not merely from its tool
432
+ * calls, and re-read on every request rather than cached off a token, so
433
+ * turning it off bites at the next call.
434
+ *
435
+ * **It has had a switch, lost it, and has one again, which is why the
436
+ * history is worth keeping.** It was reserved for a per-app `mcp_enabled`
437
+ * flag; the central-MCP cut deleted the per-app `/mcp/<identifier>` endpoint
438
+ * that flag gated, and 2026-08-29 removed the field itself from `app`, so
439
+ * 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.
442
+ *
443
+ * The lesson that survives is about the year in between: an enum member with
444
+ * no producer is not harmless, because a reader arriving at it takes it for
445
+ * a live refusal. Say which it is, and say when it changes.
446
+ *
447
+ * **It has one producer already, one train early**: `POST /mcp` answers it to
448
+ * an `mcp_session` token whose subject is an app user, because the central
449
+ * endpoint serves the team only. No client can hold such a token yet — only
450
+ * a test mints one — and the per-app train adds the
451
+ * `appAuthConfig.mcp_enabled` gate this comment describes.
452
+ *
453
+ * `tool_not_available` below still has no producer — `grep` finds it nowhere
454
+ * in `cloud/src`. Named as unproduced, for the same reason.
455
+ */
456
+ 'mcp_disabled',
457
+ /**
458
+ * A tool the caller cannot use on this robot — because the role grants
459
+ * neither the slug it needs nor the capability behind it.
460
+ *
461
+ * **Deliberately one code for both**: to a developer holding the console,
462
+ * the role's datasheet (`mcpRobotDatasheet`) already lists every exposure
463
+ * 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.
466
+ */
467
+ 'tool_not_available',
468
+ // W9 — capabilities.
469
+ /**
470
+ * An app-wide **capability** the caller's role does not grant — today
471
+ * `assets` (`GET /api/robots/:id/assets` and the URDF/by-id byte routes)
472
+ * and `action_history` (`GET /api/robots/:id/jobs/history`). The message
473
+ * names which one.
474
+ *
475
+ * **Distinct from `forbidden`, and the distinction is the point.**
476
+ * `forbidden` is deliberately silent about existence, because roles are the
477
+ * 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
479
+ * switch in the console that the developer owns, and the caller reaching
480
+ * this refusal has already been proven to reach the robot. Answering
481
+ * `forbidden` there tells a developer only that they may not — not which
482
+ * toggle to flip — and a promise the console makes is exactly what these
483
+ * capabilities have historically failed to keep.
484
+ *
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.
488
+ */
489
+ 'capability_required',
490
+ // 2026-08-29 — org-central identity (D1/D2).
491
+ /**
492
+ * **An org must keep at least one Owner**, so the last one is neither
493
+ * deletable nor demotable. 409, on both `DELETE /api/org/users/:id` and
494
+ * `PATCH /api/org/users/:id/tier`.
495
+ *
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.
503
+ *
504
+ * Deliberately not `forbidden` or `tier_required`: an Owner reaching this
505
+ * has every permission the act needs. The refusal is about the org's
506
+ * remaining state, and the remedy — promote somebody first — is nothing the
507
+ * caller could infer from a silence about existence.
508
+ */
509
+ 'last_owner',
510
+ // 2026-08-29 — oidc-federation (D3/D4).
511
+ /**
512
+ * **The target is in a state that refuses the operation** — not the caller's
513
+ * rights, not the target's existence, but *what the target currently is*.
514
+ * 409.
515
+ *
516
+ * It exists because refusals of this shape were riding a `400
517
+ * validation_error` with a `rule` string: the body is well-formed and names a
518
+ * real target whose *state* is the obstacle. A `400` said "you sent something
519
+ * invalid" for a request that was nothing of the kind, and a bare `rule`
520
+ * string on the validation envelope is not a code a consumer can switch on.
521
+ *
522
+ * Its producers in the two-space model are the ones about an app or an
523
+ * account rather than about a caller: `send_mail: true` on an app that has
524
+ * configured no `invite_url` (the `details` name the field), and a password
525
+ * change on an app user who has no password at all — an OIDC-only account,
526
+ * where the session is live and it is the target's state that refuses.
527
+ *
528
+ * **A tier change aimed at somebody who is not a Fleetless user of this org
529
+ * was listed here and stopped being a producer at the cut.** That refusal
530
+ * had one implementation, the Org Admins membership check; without it `PUT
531
+ * /api/org/users/:id/tier` scopes through `scopedUser` and answers `404
532
+ * not_found`. Left in place, the sentence documented a 409 no caller could
533
+ * receive.
534
+ *
535
+ * Deliberately not `forbidden` (which is silent about existence and about
536
+ * the target) and not `tier_required` (which is about the caller's own
537
+ * rank): a caller reaching this has the rights and named a real thing — the
538
+ * obstacle is the target's state, and the remedy is to change that state or
539
+ * pick a different target, neither of which a silence would reveal.
540
+ *
541
+ */
542
+ 'target_state_conflict',
543
+ // 2026-09-04 — the public site (closed beta).
544
+ /**
545
+ * `403` from `POST /api/auth/signup` and the portal's sign-up pages while
546
+ * `SIGNUP_MODE=closed`. Not `forbidden`: nothing about the caller is
547
+ * refused, the door is closed for everyone. The message names the
548
+ * waiting list. Produced by cloud `routes/auth.ts` and
549
+ * `routes/console-oauth.ts` in the same release.
550
+ */
551
+ 'signup_closed',
552
+ // FL-007 (route manifest): emitted by the cloud, catalogued late.
553
+ //
554
+ // Every one of the five below has had a live producer for some time; what
555
+ // they never had was an entry here. **Each was confirmed by grepping the
556
+ // cloud for its own string before being written down** — the producer named
557
+ // in each comment is a file that was read, not one that was assumed.
558
+ //
559
+ // The type checker is *not* what surfaced them, and that is worth saying.
560
+ // `RouteEntry.errors` is typed `ErrorCode[]`, so it refuses a code an entry
561
+ // *names* — but only one of these five (`wrong_browser`) is named by any entry
562
+ // in the commit that added them. The other four would have stayed
563
+ // uncatalogued had nobody gone looking. The difference matters to whoever adds
564
+ // the sixth: neither the type nor a pass over the route entries is a census of
565
+ // what the cloud actually sends.
566
+ //
567
+ // They are listed in one block, with their producer named, rather than filed
568
+ // among the waves that introduced them — the honest record is *when this list
569
+ // learned about them*, not when the cloud started sending them.
570
+ /**
571
+ * `409` from the configuration routes: the draft parses as YAML but its root
572
+ * is not a mapping — a list, a scalar, or an empty document. Distinct from
573
+ * `validation_error`, which is about a field inside a document that *is* one.
574
+ * Produced by `cloud/src/routes/config.ts`.
575
+ */
576
+ 'draft_not_a_document',
577
+ /**
578
+ * `500`. The cloud's own last-resort answer when a handler throws something
579
+ * it has no mapping for, and the code the realtime socket sends for the same
580
+ * state. It says nothing about the request, deliberately: a caller cannot act
581
+ * 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`.
584
+ */
585
+ 'internal_error',
586
+ /**
587
+ * `422` from `POST /api/robots/:id/jobs/:slug/cancel`: the job exists and the
588
+ * caller may address it, but it is in a state that has nothing left to
589
+ * 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`.
592
+ */
593
+ 'not_cancellable',
594
+ /**
595
+ * `415`. The request carried a body in a media type the route does not read.
596
+ * It is the cloud-wide answer from the content-type parser, not one route's:
597
+ * 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`.
599
+ */
600
+ 'unsupported_media_type',
601
+ /**
602
+ * `401` from the multi-step browser flows — console sign-up, and the MCP
603
+ * consent screen. The step being finished was started in a *different*
604
+ * browser: the per-interaction proof cookie is missing or does not match the
605
+ * hash recorded on the interaction row.
606
+ *
607
+ * **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
612
+ * completes it.
613
+ *
614
+ * Deliberately not `invalid_token` or `unauthorized`: nothing about the
615
+ * 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`.
618
+ */
619
+ 'wrong_browser',
620
+ /**
621
+ * `422` from `PUT /api/robots/:id/config/draft`: the text the author sent is
622
+ * not YAML at all. The parser's own message travels in `details`.
623
+ *
624
+ * **Catalogued in the same round as the two below it were found, and by the
625
+ * same means: reading the producer.** Both this and `unstorable_yaml` have
626
+ * been on the wire since the config editor shipped, as route-local string
627
+ * literals inside a perfectly ordinary `apiError` envelope — which is exactly
628
+ * why nothing noticed. The envelope validates; only the *code* was absent
629
+ * from the one list a client can match against, so a caller branching on
630
+ * `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`.
634
+ */
635
+ 'invalid_yaml',
636
+ /**
637
+ * `422` from the same route, for the other half: the text *is* YAML and
638
+ * cannot be stored — an anchor cycle, or anything else that parses into a
639
+ * value with no JSON representation.
640
+ *
641
+ * Two codes rather than one, because the two say different things to whoever
642
+ * 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`.
644
+ */
645
+ 'unstorable_yaml',
646
+ // 2026-09-05 — app-user auth (two identity spaces, the JSON client API).
647
+ //
648
+ // **What is honest here and what is not, in one place.** The client auth
649
+ // family answers `202` for `register`, `resend-verification` and
650
+ // `password/reset` whether or not the address exists, and answers one
651
+ // `invalid_credentials` for a wrong password, a `blocked` account and an
652
+ // unverified one. The codes below are the exceptions, and each is an
653
+ // exception for the same reason: it describes the **app's policy or
654
+ // configuration**, which the developer set and which reveals nothing about
655
+ // whether a particular person has an account.
656
+ /**
657
+ * `403` from `POST /api/client/register`: this app has `self_registration`
658
+ * off, so nobody may create an account without an invitation. Not
659
+ * `forbidden` — nothing about the caller is refused, the door is closed for
660
+ * everyone — and honest for the reason above: a stranger learns the app's
661
+ * policy, not who is in it. Also the reason an unknown federated identity is
662
+ * turned away at an OIDC callback, where it travels as
663
+ * `clientOidcErrorCode` rather than as an `apiError`: one switch, one
664
+ * decision, whichever door somebody arrives at.
665
+ */
666
+ 'registration_closed',
667
+ /**
668
+ * `403` from `POST /api/client/register`: the address is outside the app's
669
+ * `allowed_domains`. Same standing as `registration_closed` — it is about
670
+ * the domain the caller typed, which they already know, and about a list the
671
+ * developer configured. **An invitation always bypasses it**, so this is
672
+ * never the answer to accepting one.
673
+ */
674
+ 'domain_not_allowed',
675
+ /**
676
+ * The address has not been confirmed, and something that is not a login
677
+ * needs it to have been.
678
+ *
679
+ * **Never the answer to `POST /api/client/login`**, which refuses a
680
+ * `pending_verification` account with the same `invalid_credentials` a wrong
681
+ * password gets — that is the whole of the enumeration discipline, and a
682
+ * code that leaked the distinction there would undo it. Its producer is the
683
+ * federated path: an identity provider that asserts an address without
684
+ * `email_verified` never produces or links an account, and the app is told
685
+ * why so it can say "confirm your address with your provider first".
686
+ */
687
+ 'email_unverified',
688
+ /**
689
+ * `403`: the request's `Origin` is not one of the app's `allowed_origins`.
690
+ * The same list is the CORS allow-list and the OIDC `redirect_uri` check, so
691
+ * this is the refusal for both — a browser sees a failed preflight, and a
692
+ * start request naming an unlisted redirect target sees this code with no
693
+ * redirect, because until the target is confirmed there is nowhere trusted
694
+ * to bounce a browser to.
695
+ */
696
+ 'origin_not_allowed',
697
+ /**
698
+ * `422` from the mail-template PUT and preview: the Liquid template does not
699
+ * render. `details` is a `mailTemplateProblemDetails` naming **which of the
700
+ * three parts** failed and the renderer's own message — an error that did not
701
+ * say which part leaves the developer re-reading all three.
702
+ *
703
+ * Liquid runs in strict mode, so an unknown variable is one of these rather
704
+ * than an empty string in a mail somebody already received. A template that
705
+ * renders at save time and fails at send time falls back to the Fleetless
706
+ * default and writes an audit event; nothing on the wire can promise that a
707
+ * template which rendered once will render for every recipient.
708
+ */
709
+ 'template_invalid',
710
+ /**
711
+ * The named OIDC provider exists on this app and is turned off. Distinct
712
+ * from `not_found`, which is what an unknown slug gets: `enabled` is a
713
+ * switch the developer flipped, and a disabled provider keeps its row and
714
+ * its linked identities, so telling the two apart is what lets a developer's
715
+ * page say "that button is temporarily off" rather than "that provider was
716
+ * deleted". It reaches an app user as a `clientOidcErrorCode` of the same
717
+ * name, redirected to the app rather than rendered here.
718
+ */
719
+ 'provider_disabled',
720
+ /**
721
+ * `422`: the provider's own configuration cannot complete a sign-in, and
722
+ * only the **developer** can fix it. Discovery answered something that is
723
+ * not an OIDC discovery document, the issuer in it disagrees with the
724
+ * configured one, or one of the three endpoints it publishes
725
+ * (`authorization_endpoint`, `token_endpoint`, `jwks_uri`) is not an http(s)
726
+ * URL. Every one of those is decided by the discovery step, which is what
727
+ * lets `POST`/`PATCH` refuse the provider at the form.
728
+ *
729
+ * **Two failures that sound like this one and are not**, listed because an
730
+ * earlier draft of this text claimed them: a JWKS carrying no key that can
731
+ * verify the token reaches the app as `claims_incomplete`, and a client
732
+ * secret the token endpoint rejects reaches it as `exchange_failed`. Both are
733
+ * decided in the middle of a sign-in, against a document that was fine when
734
+ * the provider was stored, so neither can be a create-time refusal — and
735
+ * naming them here sent a developer looking up a code their logs would never
736
+ * show. The `reason` behind `claims_incomplete` is in the cloud's log.
737
+ *
738
+ * Distinct from `idp_unavailable`, which is the same fault line drawn one
739
+ * step earlier: there the provider could not be **reached**, and retrying may
740
+ * work; here it answered and the answer was unusable, so retrying will do
741
+ * the same thing until somebody changes the configuration. Collapsing the
742
+ * two would tell a developer to wait when the fix is theirs to make.
743
+ *
744
+ * Distinct from `validation_error` for the same reason `invalid_redirect_uri`
745
+ * is: the shape of what the developer typed was fine, and what failed is a
746
+ * fact about a remote system that no request-body check could have caught.
747
+ * `POST` and `PATCH` on `/api/apps/:id/oidc-providers` run discovery before
748
+ * storing a row, so the refusal arrives while the developer is looking at
749
+ * the form rather than at an app user's failed sign-in a week later.
750
+ *
751
+ * It reaches an app user as a `clientOidcErrorCode` of the same name,
752
+ * redirected to the app rather than rendered here.
753
+ */
754
+ 'provider_misconfigured',
755
+ /**
756
+ * A redirect URI that is not usable: malformed, or an origin the app has not
757
+ * listed. Refused **flat, with no redirect** — sending a browser to an
758
+ * unconfirmed target is the attack this check exists to prevent, so an
759
+ * open-redirect attempt cannot be reported by redirecting.
760
+ *
761
+ * It was a `validation_error` with rule `invalid_redirect_uri` on the deleted
762
+ * app-level OAuth client routes. Promoted to a code of its own because a
763
+ * bare `rule` string on the validation envelope is not something a consumer
764
+ * can switch on, and this is a refusal a developer's own login page has to
765
+ * branch on.
766
+ */
767
+ 'invalid_redirect_uri',
768
+ /**
769
+ * `410`: an interaction is past its window. OIDC interactions live ten
770
+ * minutes, the one-time code sixty seconds, and an MCP interaction ten
771
+ * minutes.
772
+ *
773
+ * Deliberately **not** `token_spent`, and the difference is what the value
774
+ * IS rather than how many states the answer covers. `token_spent` is the one
775
+ * answer to a mailed credential that does not work; this is the one answer to
776
+ * an interaction that is no longer live. **The MCP interaction routes collapse
777
+ * unknown, expired, already-decided and not-this-surface into this single
778
+ * code**, exactly as `token_spent` collapses its four — an id nobody holds
779
+ * must not be distinguishable from one that ran out, or a caller who did not
780
+ * start the flow learns whether somebody else's sign-in is in progress. What
781
+ * survives the collapse is the word: an app's page can say "that took too
782
+ * long, start again" rather than "that link is invalid", which is the right
783
+ * advice for the state a person is actually in.
784
+ */
785
+ 'interaction_expired',
786
+ ];