@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
@@ -0,0 +1,512 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ import { RESOURCE_HEALTH_STATES } from './rest.js';
4
+ import { MAX_PATIENCE_MS, MIN_PATIENCE_MS } from './protocol.js';
5
+ import { slug } from './common.js';
6
+ import { clientIdentity } from './client-auth.js';
7
+ import { job } from './jobs.js';
8
+ /**
9
+ * Client realtime protocol (spec §11.1): WebSocket subscriptions on
10
+ * datapoints. W1 scope: subscribe/unsubscribe plus the datapoint event
11
+ * stream; command parity arrives in W4.
12
+ */
13
+ /**
14
+ * The first frame a client sends after the socket opens (W3, spec §3.4).
15
+ *
16
+ * A browser cannot set an `Authorization` header on a WebSocket handshake,
17
+ * and a token in the query string would outlive the request in server,
18
+ * proxy and browser-history logs. So `/realtime` authenticates the way
19
+ * `/bridge` already does: with a frame. `token` is the same bearer value
20
+ * REST takes — a user's JWT or a server key (`flk_…`), told apart by prefix.
21
+ *
22
+ * Any `subscribe` before `auth_ok` is refused, and a socket that sends no
23
+ * auth frame in time is closed with **4002**, the code the bridge socket
24
+ * already uses for a missing hello.
25
+ */
26
+ export const clientAuth = z.object({
27
+ type: z.literal('auth'),
28
+ token: z.string().min(1),
29
+ });
30
+ /**
31
+ * Who the socket turned out to belong to. Carrying the identity here means a
32
+ * client never has to decode a JWT to render its own session — decoding a
33
+ * token client-side is how apps end up trusting claims nobody verified.
34
+ */
35
+ export const authOk = z.object({
36
+ type: z.literal('auth_ok'),
37
+ identity: clientIdentity,
38
+ });
39
+ /** Refusal, followed by close 1008 — same shape as the bridge's hello_error. */
40
+ export const authError = z.object({
41
+ type: z.literal('auth_error'),
42
+ code: z.string().min(1),
43
+ message: z.string().min(1),
44
+ });
45
+ /**
46
+ * Command parity (spec §11.1): everything REST can do — invoke an action,
47
+ * call a service, publish, cancel — also travels over this socket.
48
+ *
49
+ * **Every command carries a `request_id` and every reply echoes it.** A
50
+ * subscribe that gets dropped is self-healing: the client resubscribes on
51
+ * reconnect and nothing was promised. A *command* that gets dropped is an
52
+ * instruction someone believes they issued and no one will ever run — on a
53
+ * machine that may be moving. Correlation is what makes the difference
54
+ * observable instead of silent.
55
+ */
56
+ export const clientInvoke = z.object({
57
+ type: z.literal('invoke'),
58
+ request_id: z.string().min(1).max(64),
59
+ robot_id: z.uuid(),
60
+ slug,
61
+ /** Parameters by field path, validated against the config's rules (§4.4). */
62
+ params: z.record(z.string(), z.unknown()),
63
+ /**
64
+ * How long this one call is worth waiting for (W6b) — the same field,
65
+ * meaning and cap as `invokeRequest.patience_ms`; absent means
66
+ * `DEFAULT_PATIENCE_MS`.
67
+ *
68
+ * It is here because **§11.1 parity is a rule, not a preference**: what REST
69
+ * can do travels over this socket. The first version of this delta gave
70
+ * `patience_ms` to the REST body only — and the SDK invokes exclusively over
71
+ * the realtime channel, so the field would have been unreachable for every
72
+ * SDK caller while appearing in the documentation. W6a shipped four SDK
73
+ * methods no SDK caller could invoke; this is the same defect caught before
74
+ * it shipped, by the SDK owner rather than by a reviewer.
75
+ */
76
+ patience_ms: z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional(),
77
+ });
78
+ export const clientCancel = z.object({
79
+ type: z.literal('cancel'),
80
+ request_id: z.string().min(1).max(64),
81
+ robot_id: z.uuid(),
82
+ /** Which slug — required, and the only address a cancel had until W6b. */
83
+ slug,
84
+ /**
85
+ * Which job on that slug (W6b), or `null` for *whatever is running there*.
86
+ *
87
+ * The two are different requests and both are legitimate. An operator
88
+ * hitting a stop button means the second: stop the machine, whatever it is
89
+ * doing. A client cancelling the job it started means the first — and until
90
+ * this field existed it could not say so, so a cancel that arrived just
91
+ * after its own job ended stopped the next caller's job instead. Same slug,
92
+ * same wire frame, entirely different machine behaviour, and nothing in the
93
+ * protocol able to tell them apart.
94
+ *
95
+ * A named id that is not running answers `not_found` rather than falling
96
+ * back to the slug. Falling back would be the platform deciding that the
97
+ * caller did not really mean the id they typed.
98
+ */
99
+ job_id: z.uuid().nullable(),
100
+ });
101
+ export const clientPublish = z.object({
102
+ type: z.literal('publish'),
103
+ request_id: z.string().min(1).max(64),
104
+ robot_id: z.uuid(),
105
+ slug,
106
+ message: z.record(z.string(), z.unknown()),
107
+ });
108
+ /**
109
+ * The reply to exactly one command. `ok:false` carries the stable code —
110
+ * `busy`, `robot_offline`, `parameter_invalid`, `forbidden` — so a caller
111
+ * branches without parsing prose.
112
+ *
113
+ * **`request_id` is the only correlation, and the order these arrive in is
114
+ * not promised.** Commands sent on one socket reach the robot in the order
115
+ * they were sent — that ordering is guaranteed and is the point of the
116
+ * command chain — but their *replies* may arrive in any order, and a client
117
+ * that pairs them up by arrival order will attribute an outcome to the wrong
118
+ * command.
119
+ *
120
+ * This is not theoretical and not jitter. The ordering chain is released once
121
+ * a command has reached the bridge, deliberately, so that the next command —
122
+ * a stop, say — is never held up behind bookkeeping. Work that follows the
123
+ * send therefore runs unordered: a publish that *acquires* a slug writes an
124
+ * audit record before answering, while an immediately following publish by
125
+ * the now-current holder has nothing to write and answers at once. Its reply
126
+ * overtakes. Measured in W5: exactly one reversal in fifty-six zero-gap
127
+ * bursts, which is the signature of that cause — it can happen only once per
128
+ * identity and slug — and not of a race.
129
+ *
130
+ * Serialising the replies would mean putting that bookkeeping in front of
131
+ * every following command, including the stop. The ordering that matters is
132
+ * the one on the wire to the robot, and it is kept.
133
+ */
134
+ export const commandResult = z.object({
135
+ type: z.literal('command_result'),
136
+ request_id: z.string().min(1).max(64),
137
+ ok: z.boolean(),
138
+ /**
139
+ * The job this reply is *about* — which is not always the caller's own.
140
+ *
141
+ * - `ok:true` on an invoke or a call: the job that was just created.
142
+ * - `ok:true` on a cancel: the job the cancel was sent to.
143
+ * - `ok:false, code:'busy'`: **the job that is already running** — the
144
+ * caller has none. This is the §11.3 "inkl. Information, was läuft", and
145
+ * it is the whole reason a busy refusal is useful: the caller learns
146
+ * whether to wait or to give up (see `busyDetails`).
147
+ * - any other refusal: `null`.
148
+ */
149
+ job: job.nullable(),
150
+ /**
151
+ * The **kind** of the slug this command addressed, when the slug resolved.
152
+ *
153
+ * Invoke and call share a route on purpose — both create the job on the
154
+ * slug — but a *client* library distinguishes them: `services.call` waits
155
+ * for a terminal state, `actions.invoke` does not. Without the kind coming
156
+ * back, calling a service helper on an action slug **starts the real action
157
+ * on the robot** and then blames the robot for not finishing, half a minute
158
+ * later. Returning the kind lets a client refuse its own mistake at once,
159
+ * instead of reporting it as the machine's.
160
+ */
161
+ kind: z.enum(['datapoint', 'action', 'service', 'publisher', 'camera']).nullable(),
162
+ code: z.string().nullable(),
163
+ message: z.string().nullable(),
164
+ /**
165
+ * The same payload the REST envelope carries in `apiError.details` — for
166
+ * `parameter_invalid`, a `parameterInvalidDetails`.
167
+ *
168
+ * Added because it was missing, and its absence quietly broke §11.1: this
169
+ * socket is supposed to do *everything* REST can do, but a
170
+ * `parameter_invalid` arriving here had nowhere to put its violations, so
171
+ * the same refusal was actionable over HTTP and opaque over the socket.
172
+ * A client cannot bind an error to the input that caused it from a code
173
+ * alone — which is the entire point of the flat parameter shape.
174
+ */
175
+ details: z.unknown().optional(),
176
+ });
177
+ /**
178
+ * A well-formed frame this server does not understand. The socket **stays
179
+ * open** — closing it would mean a newer client against an older cloud
180
+ * reconnects, resends, and takes every unrelated subscription down with it
181
+ * on every attempt. Close 1008 is reserved for frames that are not parseable
182
+ * JSON objects at all.
183
+ */
184
+ export const errorFrame = z.object({
185
+ type: z.literal('error'),
186
+ code: z.string().min(1),
187
+ message: z.string().min(1),
188
+ });
189
+ /**
190
+ * Subscribe to a slug's stream (spec §11.3: **state is observed by slug**).
191
+ *
192
+ * Which kinds are subscribable, and why it is not a matter of taste:
193
+ *
194
+ * - **datapoint** — its values.
195
+ * - **action** — its `jobEvent`s.
196
+ * - **service** — its `jobEvent`s too. A service call mints a job like an
197
+ * action does; the only difference is that the caller usually gets the
198
+ * result inline. But when they do *not* — a socket that died before the
199
+ * reply, answered with `command_outcome_unknown` — the documented recovery
200
+ * is to observe the slug. Refusing that leaves the caller with an error
201
+ * that names a remedy the platform does not offer.
202
+ * - **publisher** — not subscribable: there is no job and no stream. It must
203
+ * still be refused *honestly*, with a code saying so, and never as though
204
+ * the slug did not exist. A caller who was granted a slug is entitled to
205
+ * be told the truth about it.
206
+ */
207
+ export const clientSubscribe = z.object({
208
+ type: z.literal('subscribe'),
209
+ robot_id: z.uuid(),
210
+ slug,
211
+ /**
212
+ * What the subscriber expects, and how it wants it (W5).
213
+ *
214
+ * `kind` lets the server answer **`wrong_kind`** instead of accepting a
215
+ * subscribe the client will then filter to silence — and silence is
216
+ * indistinguishable from an idle slug, so it tells a developer nothing.
217
+ * Optional, so an older client that omits it keeps today's behaviour.
218
+ *
219
+ * `options` is where a camera says what it wants; a datapoint needs none.
220
+ * It exists now rather than later because adding a field to a frame three
221
+ * repos parse is cheap once and expensive twice.
222
+ */
223
+ /**
224
+ * `publisher` is here even though a publisher is not subscribable: a client
225
+ * that models the five grantable kinds and honestly names one gets the
226
+ * informative `not_subscribable` the cloud already computes, instead of a
227
+ * `validation_error` reciting an enum. Refusing the *word* rather than the
228
+ * request was the same mistake as the silent wrong-verb subscribe this
229
+ * field was added to fix.
230
+ */
231
+ kind: z.enum(['datapoint', 'action', 'service', 'publisher', 'camera']).optional(),
232
+ options: z.record(z.string(), z.unknown()).optional(),
233
+ });
234
+ export const clientUnsubscribe = z.object({
235
+ type: z.literal('unsubscribe'),
236
+ robot_id: z.uuid(),
237
+ slug,
238
+ });
239
+ /**
240
+ * Refusal of a subscribe, addressed by the (robot_id, slug) it refers to.
241
+ * Codes follow the §11.5 error culture: stable code + human message.
242
+ * robot_id/slug are plain strings ECHOING what the client sent — the frame
243
+ * must be constructible precisely when those values are malformed, so that
244
+ * a bad robot_id or slug gets a diagnosis instead of a dead socket.
245
+ */
246
+ export const subscribeError = z.object({
247
+ type: z.literal('subscribe_error'),
248
+ robot_id: z.string(),
249
+ slug: z.string(),
250
+ code: z.string().min(1),
251
+ message: z.string().min(1),
252
+ });
253
+ /**
254
+ * One datapoint sample pushed to a subscriber. The current value arrives
255
+ * immediately on subscribe, then every change. `timestamp_ms` semantics as
256
+ * in `datapointValue` (capture time; cloud-observed for `bridge_state`).
257
+ */
258
+ export const datapointEvent = z.object({
259
+ type: z.literal('datapoint'),
260
+ robot_id: z.uuid(),
261
+ slug,
262
+ value: z.unknown(),
263
+ timestamp_ms: z.number().int().nonnegative(),
264
+ });
265
+ /**
266
+ * A change in the health of something the developer configured (W6a).
267
+ *
268
+ * The push half of `resourceHealthState`; the REST list is the snapshot half,
269
+ * and neither is useful alone — a page that loads after the change would see
270
+ * nothing, and a page that never reloads would never learn.
271
+ *
272
+ * Delivered on the **developer** socket and scoped to the org, not to a
273
+ * subscription: the whole point is to reach somebody who is *not* currently
274
+ * looking at the thing that broke.
275
+ */
276
+ /**
277
+ * Why a live camera session ended (W9a).
278
+ *
279
+ * **The reason travels WITH the ending, and that is the whole point of this
280
+ * enum existing rather than a state somebody reads afterwards.** W6a put a
281
+ * `cause` on the wire, the console named the real reason, and the lead
282
+ * observed the gate step and closed it — and the review then found it still
283
+ * could not tell, for a different reason: `stopped_by_config_change` is
284
+ * **sticky**, nothing moves a camera out of it, and `LiveCameraRow` read that
285
+ * *current* state at the moment a stream ended. A config change at 10:00 and
286
+ * an unrelated release at 10:30 therefore reported the same cause (DEF-070).
287
+ *
288
+ * A state read after the fact answers "what is true now". A viewer needs
289
+ * "what happened to my session", and only an event carries that.
290
+ */
291
+ export const liveSessionEndReason = z.enum([
292
+ /** Another holder of this camera released it — another tab, or another client. */
293
+ 'released_by_peer',
294
+ /** The robot's configuration was published and this camera changed with it. */
295
+ 'config_changed',
296
+ /** The robot said it could not publish. `detail` carries its own words. */
297
+ 'publish_failed',
298
+ /** The bridge stopped answering. */
299
+ 'robot_offline',
300
+ /** The grant this session was minted under was withdrawn. */
301
+ 'revoked',
302
+ /** The session's own lifetime ran out. */
303
+ 'expired',
304
+ /** The robot was deleted out from under the session. */
305
+ 'robot_deleted',
306
+ /**
307
+ * The cloud ended it and cannot say which of the above applied. **Kept
308
+ * deliberately**: a channel that cannot say "I do not know" will say
309
+ * something false instead, and this project has paid for that four times in
310
+ * the camera path alone.
311
+ */
312
+ 'unknown',
313
+ ]);
314
+ /**
315
+ * A live camera session ended, told to the **client that holds it** (W9a).
316
+ *
317
+ * This is the channel `DEF-051`, `DEF-052`, `DEF-053` and `DEF-070` each
318
+ * described from a different direction across four waves. Until now the only
319
+ * vehicle was `camera_state`, which the cloud stores in `publishState` and
320
+ * reads in exactly one place — refusing a *later* joiner — so reporting a
321
+ * failure would have written to a dead end.
322
+ *
323
+ * Unlike `resourceHealthEvent`, which is developer-only and org-scoped, this
324
+ * one is addressed to the **holder of the session**: it names `session_id`
325
+ * (W6b gave `liveSessionResponse` one precisely so a session could be
326
+ * addressed) and is delivered only to the identity that session was minted
327
+ * for. A developer watching the same robot learns about the *resource* health;
328
+ * the viewer learns about *their own session*. Two questions, two channels,
329
+ * on purpose.
330
+ */
331
+ export const liveSessionEvent = z.object({
332
+ type: z.literal('live_session'),
333
+ robot_id: z.uuid(),
334
+ slug,
335
+ session_id: z.uuid(),
336
+ state: z.literal('ended'),
337
+ reason: liveSessionEndReason,
338
+ /**
339
+ * **Classified text the cloud produced, never text the robot sent.**
340
+ *
341
+ * An earlier draft of this comment said *"the robot's own words when it has
342
+ * any"*, which reads as permission to pass `bridgeCameraState.error.message`
343
+ * straight through. Nothing sanitises that field, and this codebase has a
344
+ * documented incident of a password reaching a developer surface through
345
+ * exactly that route — `camera-health.ts`'s fixed-string `REASON` discipline
346
+ * exists because of it. Nimbus-W9a stopped at the sentence and asked rather
347
+ * than taking the permission it appeared to give (2026-08-19).
348
+ *
349
+ * So: `null` unless the cloud itself has something classified to say. If a
350
+ * developer needs the robot's own diagnosis later, it arrives as a mapped
351
+ * code with fixed text, the way camera health already does it — not as
352
+ * forwarded foreign text on a channel a client reads.
353
+ */
354
+ detail: z.string().max(200).nullable(),
355
+ /** When it ended — not when this frame was sent. Same reasoning as `changed_at_ms`. */
356
+ ended_at_ms: z.number().int().nonnegative(),
357
+ });
358
+ /**
359
+ * A resource's health entry was **withdrawn** (W9a, DEF-071).
360
+ *
361
+ * The store's `invalidate()` deliberately emitted nothing, reasoning that
362
+ * "withdrawing a claim nobody can currently stand behind is not new
363
+ * information — the next `GET` already reflects it." That holds for a page
364
+ * that loads later. **It is false for a page that is already open, because
365
+ * there is no next `GET`:** `ensureSnapshot()` runs on `acquire` and nowhere
366
+ * else, there is no interval, and the event handler only ever *writes* keys.
367
+ * A camera retargeted to a source that never reports — which is the case the
368
+ * clearing exists for — leaves an open tab showing the old value indefinitely.
369
+ *
370
+ * **A separate event type rather than a nullable `state` on the existing
371
+ * one**, so a consumer's `switch` has to name it. A nullable field invites
372
+ * `if (state)` and fails silently when somebody forgets; an unhandled variant
373
+ * fails `tsc`, which is the difference between a rule and a mechanism.
374
+ */
375
+ export const resourceHealthCleared = z.object({
376
+ type: z.literal('resource_health_cleared'),
377
+ robot_id: z.uuid(),
378
+ kind: z.enum(['camera']),
379
+ ref: z.string().min(1).max(64),
380
+ facet: z.enum(['source', 'publish']),
381
+ cleared_at_ms: z.number().int().nonnegative(),
382
+ });
383
+ export const resourceHealthEvent = z.object({
384
+ type: z.literal('resource_health'),
385
+ robot_id: z.uuid(),
386
+ kind: z.enum(['camera']),
387
+ ref: z.string().min(1).max(64),
388
+ /** Which of the two questions this entry answers — see `resourceHealthState.facet`. */
389
+ facet: z.enum(['source', 'publish']),
390
+ state: z.enum(RESOURCE_HEALTH_STATES),
391
+ reason: z.string().max(200).nullable(),
392
+ changed_at_ms: z.number().int().nonnegative(),
393
+ });
394
+ /**
395
+ * One line of the developer console's activity panel (spec
396
+ * `2026-08-20-org-event-stream`).
397
+ *
398
+ * **This is an activity log for humans, not a complete feed.** It is throttled
399
+ * and sampled. `orgEventDropped` still reports a drop on the wire, but since
400
+ * FL-001 **no Fleetless surface renders it** — the console's gap banner was
401
+ * removed on request, and nothing replaced it. A reader of this stream
402
+ * therefore cannot tell a complete window from a sampled one, and this
403
+ * comment says so rather than implying a notice that exists only in the
404
+ * protocol. Anything that needs completeness reads the audit log or the
405
+ * job-run history, both durable, both 90 days.
406
+ */
407
+ export const ORG_EVENT_SAMPLE_INTERVAL_MS = 1_000;
408
+ /** The backstop above the per-slug cap: a fleet larger than the panel could serve anyway. */
409
+ export const ORG_EVENT_ORG_CEILING_PER_SECOND = 50;
410
+ /** Roughly 25 screens of scrollback. */
411
+ export const ORG_EVENT_BUFFER_SIZE = 200;
412
+ /** An org's buffer is dropped after this long without an event, so memory follows active orgs rather than all of them. */
413
+ export const ORG_EVENT_BUFFER_IDLE_MS = 3_600_000;
414
+ /** A log line, not a payload: a datapoint value is `unknown` and a LaserScan is megabytes. */
415
+ export const ORG_EVENT_DETAIL_MAX_BYTES = 4_096;
416
+ /**
417
+ * `'alert'` — a transition of a datapoint alert (`ok ⇄ firing`, spec
418
+ * `2026-08-28-alerts-and-datapoint-modal-design` D2). A firing event carries
419
+ * the alert's own `severity`; a resolved event is always `info` — resolving
420
+ * is good news regardless of how bad the firing was.
421
+ *
422
+ * `'datapoint'` — since FL-001, **no producer emits this kind**: the
423
+ * datapoint producer was made a deliberate no-op (Task 5/6, this stream's own
424
+ * per-slug sampling made it redundant with what the datapoint history route
425
+ * already serves). The member stays in the enum rather than being removed,
426
+ * because a reader may still hold a pre-deploy frame of this kind sitting in
427
+ * a buffer (a reconnect replay, a client that hasn't refreshed) and must be
428
+ * able to parse it rather than fail closed on an old, valid value. Same shape
429
+ * as the correction on `ORG_EVENT_SAMPLE_INTERVAL_MS`'s comment just above:
430
+ * name what the wire no longer does instead of leaving a value the cloud can
431
+ * never send undocumented.
432
+ */
433
+ export const orgEventKind = z.enum(['datapoint', 'health', 'job', 'bridge', 'audit', 'alert']);
434
+ /**
435
+ * Assigned by the **producer**, never derived by the reader. The console's
436
+ * `errors` filter cuts across all five kinds, and only the source knows whether
437
+ * an `auth_failed` is bad. A reader guessing from `detail` guesses differently
438
+ * for each source.
439
+ */
440
+ export const orgEventSeverity = z.enum(['info', 'warning', 'error']);
441
+ export const orgEvent = z
442
+ .object({
443
+ type: z.literal('org_event'),
444
+ /**
445
+ * **Per org, per process.** Like `job.seq` and unlike `job_runs.seq`, which
446
+ * is a postgres `bigserial` and durable. All three say which they are,
447
+ * because anyone who confuses them will confuse them in both directions.
448
+ */
449
+ seq: z.number().int().positive(),
450
+ at: z.iso.datetime(),
451
+ kind: orgEventKind,
452
+ severity: orgEventSeverity,
453
+ /** `null` for an org-level event — an invitation, a quota change — which belongs to no robot. */
454
+ robot_id: z.uuid().nullable(),
455
+ /** What the line is about: a slug, a camera, an actor's email. */
456
+ subject: z.string().min(1).max(200),
457
+ /**
458
+ * Kind-specific, and **capped at `ORG_EVENT_DETAIL_MAX_BYTES`** — above it
459
+ * the producer substitutes `{ omitted: 'too_large', bytes }`. Truncated,
460
+ * and saying so.
461
+ *
462
+ * Never a pre-formatted line: the reader decides language, number format
463
+ * and truncation, so changing how a line reads is not a cloud deploy.
464
+ */
465
+ detail: z.unknown().nullable(),
466
+ })
467
+ .strict();
468
+ /** Sent by a developer's socket to start the stream. Answered by `orgEventReplay`, then live `orgEvent`s. */
469
+ export const orgEventSubscribe = z.object({ type: z.literal('org_event_subscribe') }).strict();
470
+ export const orgEventUnsubscribe = z.object({ type: z.literal('org_event_unsubscribe') }).strict();
471
+ /**
472
+ * What the cloud still remembers, oldest first, sent once before the live
473
+ * stream starts — so the panel is filled on arrival rather than blank until
474
+ * something happens. A blank panel is indistinguishable from a broken one.
475
+ */
476
+ export const orgEventReplay = z
477
+ .object({
478
+ type: z.literal('org_event_replay'),
479
+ events: z.array(orgEvent).max(ORG_EVENT_BUFFER_SIZE),
480
+ /**
481
+ * **`false` means three different things, on purpose**: the buffer was
482
+ * already full, the cloud restarted, or this org's buffer had expired. All
483
+ * three mean the same thing to a reader — *something is missing above this
484
+ * line* — and a field separating them would claim a distinction nobody
485
+ * would act on differently.
486
+ */
487
+ complete: z.boolean(),
488
+ })
489
+ .strict();
490
+ /**
491
+ * Events this socket will never see. Two causes, reported alike: the cloud
492
+ * sampled them away, or this socket's send buffer was too far behind. Both mean
493
+ * *there was more than you are being shown*.
494
+ */
495
+ export const orgEventDropped = z
496
+ .object({
497
+ type: z.literal('org_event_dropped'),
498
+ /**
499
+ * **An epoch instant in milliseconds (`Date.now()`), not a duration.**
500
+ * The moment this socket last reported a drop — or the moment it
501
+ * subscribed, if this is its first such frame. The window the `dropped`
502
+ * count covers is `since_ms` to now, so a reader wanting an age
503
+ * subtracts: `Date.now() - since_ms`. Spelled out because the type
504
+ * admits both readings and the wrong one is silent: a consumer treating
505
+ * it as "milliseconds ago" renders a drop that happened seconds ago as
506
+ * having happened in 1970.
507
+ */
508
+ since_ms: z.number().int().nonnegative(),
509
+ /** Always at least one — a frame reporting nothing lost is noise on a channel built to be quiet. */
510
+ dropped: z.number().int().positive(),
511
+ })
512
+ .strict();