@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +202 -0
- package/NOTICE +17 -0
- package/README.md +88 -0
- package/artifacts/constants.json +24 -0
- package/artifacts/openapi.json +17219 -0
- package/artifacts/routes.json +4605 -0
- package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
- package/artifacts/schema/action-config.schema.json +198 -0
- package/artifacts/schema/alert-list-response.schema.json +172 -0
- package/artifacts/schema/api-error.schema.json +20 -0
- package/artifacts/schema/app-auth-config.schema.json +106 -0
- package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
- package/artifacts/schema/app-invitation.schema.json +69 -0
- package/artifacts/schema/app-list-response.schema.json +82 -0
- package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
- package/artifacts/schema/app-mail-template.schema.json +54 -0
- package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
- package/artifacts/schema/app-oidc-provider.schema.json +80 -0
- package/artifacts/schema/app-user-list-response.schema.json +111 -0
- package/artifacts/schema/app-user.schema.json +98 -0
- package/artifacts/schema/app.schema.json +69 -0
- package/artifacts/schema/apply-error.schema.json +41 -0
- package/artifacts/schema/asset-list-response.schema.json +288 -0
- package/artifacts/schema/asset-sync-request.schema.json +17 -0
- package/artifacts/schema/asset-sync-response.schema.json +16 -0
- package/artifacts/schema/asset-sync-status.schema.json +136 -0
- package/artifacts/schema/asset.schema.json +68 -0
- package/artifacts/schema/audit-actor.schema.json +32 -0
- package/artifacts/schema/audit-event.schema.json +119 -0
- package/artifacts/schema/audit-list-response.schema.json +144 -0
- package/artifacts/schema/audit-query.schema.json +79 -0
- package/artifacts/schema/auth-error.schema.json +23 -0
- package/artifacts/schema/auth-me-response.schema.json +99 -0
- package/artifacts/schema/auth-ok.schema.json +115 -0
- package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
- package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
- package/artifacts/schema/bridge-assets-available.schema.json +25 -0
- package/artifacts/schema/bridge-camera-state.schema.json +78 -0
- package/artifacts/schema/bridge-config-applied.schema.json +67 -0
- package/artifacts/schema/bridge-hello.schema.json +65 -0
- package/artifacts/schema/bridge-introspect.schema.json +114 -0
- package/artifacts/schema/bridge-job-lost.schema.json +22 -0
- package/artifacts/schema/bridge-job-update.schema.json +100 -0
- package/artifacts/schema/bridge-pong.schema.json +19 -0
- package/artifacts/schema/bridge-pressure.schema.json +292 -0
- package/artifacts/schema/bridge-state.schema.json +24 -0
- package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
- package/artifacts/schema/busy-details.schema.json +115 -0
- package/artifacts/schema/camera-descriptor.schema.json +45 -0
- package/artifacts/schema/camera-list-response.schema.json +58 -0
- package/artifacts/schema/camera-source.schema.json +240 -0
- package/artifacts/schema/cancel-request.schema.json +20 -0
- package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
- package/artifacts/schema/client-auth.schema.json +18 -0
- package/artifacts/schema/client-cancel.schema.json +45 -0
- package/artifacts/schema/client-identity.schema.json +103 -0
- package/artifacts/schema/client-invoke.schema.json +45 -0
- package/artifacts/schema/client-login-request.schema.json +29 -0
- package/artifacts/schema/client-logout-request.schema.json +14 -0
- package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
- package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
- package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
- package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
- package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
- package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
- package/artifacts/schema/client-password-reset-request.schema.json +24 -0
- package/artifacts/schema/client-provider-list-query.schema.json +17 -0
- package/artifacts/schema/client-provider-list-response.schema.json +34 -0
- package/artifacts/schema/client-publish.schema.json +40 -0
- package/artifacts/schema/client-refresh-request.schema.json +14 -0
- package/artifacts/schema/client-register-request.schema.json +44 -0
- package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
- package/artifacts/schema/client-subscribe.schema.json +43 -0
- package/artifacts/schema/client-unsubscribe.schema.json +26 -0
- package/artifacts/schema/client-verify-email-request.schema.json +15 -0
- package/artifacts/schema/cloud-asset-request.schema.json +37 -0
- package/artifacts/schema/cloud-camera-start.schema.json +41 -0
- package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
- package/artifacts/schema/cloud-cancel.schema.json +33 -0
- package/artifacts/schema/cloud-config.schema.json +1635 -0
- package/artifacts/schema/cloud-hello-error.schema.json +23 -0
- package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
- package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
- package/artifacts/schema/cloud-invoke.schema.json +40 -0
- package/artifacts/schema/cloud-ping.schema.json +19 -0
- package/artifacts/schema/cloud-publish.schema.json +28 -0
- package/artifacts/schema/cloud-type-request.schema.json +30 -0
- package/artifacts/schema/command-result.schema.json +175 -0
- package/artifacts/schema/config-draft-response.schema.json +1695 -0
- package/artifacts/schema/config-state.schema.json +124 -0
- package/artifacts/schema/config-version-response.schema.json +1641 -0
- package/artifacts/schema/config-versions-response.schema.json +33 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
- package/artifacts/schema/create-app-request.schema.json +30 -0
- package/artifacts/schema/create-app-user-request.schema.json +42 -0
- package/artifacts/schema/create-robot-request.schema.json +14 -0
- package/artifacts/schema/create-robot-response.schema.json +44 -0
- package/artifacts/schema/create-server-key-response.schema.json +65 -0
- package/artifacts/schema/create-team-invite-request.schema.json +43 -0
- package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
- package/artifacts/schema/datapoint-config.schema.json +366 -0
- package/artifacts/schema/datapoint-display.schema.json +31 -0
- package/artifacts/schema/datapoint-event.schema.json +34 -0
- package/artifacts/schema/datapoint-frame.schema.json +28 -0
- package/artifacts/schema/datapoint-list-response.schema.json +61 -0
- package/artifacts/schema/datapoint-value.schema.json +28 -0
- package/artifacts/schema/developer-login-request.schema.json +19 -0
- package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
- package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
- package/artifacts/schema/error-frame.schema.json +23 -0
- package/artifacts/schema/exposure-counts.schema.json +39 -0
- package/artifacts/schema/exposure-list-response.schema.json +43 -0
- package/artifacts/schema/fetch-types-request.schema.json +19 -0
- package/artifacts/schema/fetch-types-response.schema.json +163 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
- package/artifacts/schema/fleetless-user.schema.json +60 -0
- package/artifacts/schema/history-buckets-response.schema.json +79 -0
- package/artifacts/schema/history-query.schema.json +58 -0
- package/artifacts/schema/history-response.schema.json +150 -0
- package/artifacts/schema/history-samples-response.schema.json +68 -0
- package/artifacts/schema/introspection-response.schema.json +118 -0
- package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
- package/artifacts/schema/invoke-request.schema.json +23 -0
- package/artifacts/schema/invoke-response.schema.json +125 -0
- package/artifacts/schema/job-actor.schema.json +34 -0
- package/artifacts/schema/job-event.schema.json +158 -0
- package/artifacts/schema/job-response.schema.json +123 -0
- package/artifacts/schema/job-run-list-response.schema.json +222 -0
- package/artifacts/schema/job-run-query.schema.json +95 -0
- package/artifacts/schema/job-run-summary-query.schema.json +23 -0
- package/artifacts/schema/job-run-summary.schema.json +33 -0
- package/artifacts/schema/job-run.schema.json +195 -0
- package/artifacts/schema/job-state.schema.json +11 -0
- package/artifacts/schema/job.schema.json +106 -0
- package/artifacts/schema/latency-bucket.schema.json +63 -0
- package/artifacts/schema/live-session-response.schema.json +41 -0
- package/artifacts/schema/mail-outcome.schema.json +20 -0
- package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
- package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
- package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
- package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
- package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
- package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
- package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
- package/artifacts/schema/missing-asset-query.schema.json +11 -0
- package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
- package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
- package/artifacts/schema/oauth-token-request.schema.json +47 -0
- package/artifacts/schema/oauth-token-response.schema.json +38 -0
- package/artifacts/schema/org-alerts-query.schema.json +15 -0
- package/artifacts/schema/org-event-dropped.schema.json +26 -0
- package/artifacts/schema/org-event-replay.schema.json +97 -0
- package/artifacts/schema/org-event-subscribe.schema.json +14 -0
- package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
- package/artifacts/schema/org-event.schema.json +75 -0
- package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
- package/artifacts/schema/org-health-query.schema.json +13 -0
- package/artifacts/schema/org-latency-query.schema.json +42 -0
- package/artifacts/schema/org-latency-response.schema.json +124 -0
- package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
- package/artifacts/schema/org-quota-usage.schema.json +102 -0
- package/artifacts/schema/org-quotas.schema.json +51 -0
- package/artifacts/schema/org-usage-query.schema.json +19 -0
- package/artifacts/schema/org-usage-response.schema.json +77 -0
- package/artifacts/schema/org.schema.json +30 -0
- package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
- package/artifacts/schema/parameter-spec.schema.json +120 -0
- package/artifacts/schema/parameter-violation.schema.json +24 -0
- package/artifacts/schema/password-change-request.schema.json +21 -0
- package/artifacts/schema/password-reset-confirm.schema.json +19 -0
- package/artifacts/schema/password-reset-request.schema.json +14 -0
- package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
- package/artifacts/schema/patch-app-user-request.schema.json +34 -0
- package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
- package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
- package/artifacts/schema/patch-org-request.schema.json +15 -0
- package/artifacts/schema/patch-org-response.schema.json +40 -0
- package/artifacts/schema/patch-robot-request.schema.json +15 -0
- package/artifacts/schema/patch-robot-response.schema.json +40 -0
- package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
- package/artifacts/schema/pending-team-invite.schema.json +39 -0
- package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
- package/artifacts/schema/publish-config-response.schema.json +21 -0
- package/artifacts/schema/publish-request.schema.json +17 -0
- package/artifacts/schema/publisher-config.schema.json +285 -0
- package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
- package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
- package/artifacts/schema/put-config-draft-request.schema.json +13 -0
- package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
- package/artifacts/schema/put-robot-details-request.schema.json +41 -0
- package/artifacts/schema/put-robot-details-response.schema.json +43 -0
- package/artifacts/schema/rate-limit-details.schema.json +15 -0
- package/artifacts/schema/refresh-request.schema.json +13 -0
- package/artifacts/schema/release-live-query.schema.json +13 -0
- package/artifacts/schema/rename-slug-request.schema.json +23 -0
- package/artifacts/schema/rename-slug-response.schema.json +24 -0
- package/artifacts/schema/resource-health-event.schema.json +72 -0
- package/artifacts/schema/resource-health-list-response.schema.json +80 -0
- package/artifacts/schema/resource-health-state.schema.json +68 -0
- package/artifacts/schema/robot-config-doc.schema.json +1616 -0
- package/artifacts/schema/robot-delete-query.schema.json +12 -0
- package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
- package/artifacts/schema/robot-detail-response.schema.json +262 -0
- package/artifacts/schema/robot-details-doc.schema.json +33 -0
- package/artifacts/schema/robot-jobs-response.schema.json +119 -0
- package/artifacts/schema/robot-latency-series.schema.json +81 -0
- package/artifacts/schema/robot-list-item.schema.json +94 -0
- package/artifacts/schema/robot-list-response.schema.json +106 -0
- package/artifacts/schema/robot.schema.json +30 -0
- package/artifacts/schema/role-list-response.schema.json +48 -0
- package/artifacts/schema/role-permissions.schema.json +61 -0
- package/artifacts/schema/role.schema.json +35 -0
- package/artifacts/schema/ros-graph.schema.json +99 -0
- package/artifacts/schema/server-key-list-response.schema.json +64 -0
- package/artifacts/schema/server-key.schema.json +51 -0
- package/artifacts/schema/service-call-response.schema.json +13 -0
- package/artifacts/schema/service-config.schema.json +198 -0
- package/artifacts/schema/session-tokens.schema.json +28 -0
- package/artifacts/schema/sign-up-request.schema.json +26 -0
- package/artifacts/schema/sign-up-response.schema.json +127 -0
- package/artifacts/schema/slug-usage-response.schema.json +32 -0
- package/artifacts/schema/snapshot-header.schema.json +44 -0
- package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
- package/artifacts/schema/subscribe-error.schema.json +31 -0
- package/artifacts/schema/team-invite.schema.json +57 -0
- package/artifacts/schema/tier-change-request.schema.json +17 -0
- package/artifacts/schema/type-definition.schema.json +144 -0
- package/artifacts/schema/types-response.schema.json +156 -0
- package/artifacts/schema/update-app-request.schema.json +32 -0
- package/artifacts/schema/urdf-completeness.schema.json +50 -0
- package/artifacts/schema/validation-issue.schema.json +43 -0
- package/artifacts/schema/waitlist-request.schema.json +15 -0
- package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
- package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
- package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
- package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
- package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
- package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
- package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
- package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
- package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
- package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
- package/dist/alerts.d.ts +255 -0
- package/dist/alerts.js +193 -0
- package/dist/app-users.d.ts +606 -0
- package/dist/app-users.js +696 -0
- package/dist/apps.d.ts +175 -0
- package/dist/apps.js +267 -0
- package/dist/assets.d.ts +434 -0
- package/dist/assets.js +546 -0
- package/dist/audit.d.ts +129 -0
- package/dist/audit.js +238 -0
- package/dist/client-auth.d.ts +409 -0
- package/dist/client-auth.js +487 -0
- package/dist/common.d.ts +186 -0
- package/dist/common.js +199 -0
- package/dist/config-issues.d.ts +175 -0
- package/dist/config-issues.js +339 -0
- package/dist/config.d.ts +862 -0
- package/dist/config.js +1988 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +786 -0
- package/dist/identity.d.ts +549 -0
- package/dist/identity.js +503 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/introspection.d.ts +99 -0
- package/dist/introspection.js +97 -0
- package/dist/jobs.d.ts +334 -0
- package/dist/jobs.js +345 -0
- package/dist/mcp.d.ts +239 -0
- package/dist/mcp.js +153 -0
- package/dist/oauth.d.ts +344 -0
- package/dist/oauth.js +488 -0
- package/dist/protocol.d.ts +781 -0
- package/dist/protocol.js +715 -0
- package/dist/realtime.d.ts +494 -0
- package/dist/realtime.js +512 -0
- package/dist/rest.d.ts +1989 -0
- package/dist/rest.js +1963 -0
- package/dist/routes.d.ts +94 -0
- package/dist/routes.js +2298 -0
- 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
|
+
];
|