@fleetless/contracts 1.1.0 → 2.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 (66) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/artifacts/constants.json +30 -4
  3. package/artifacts/openapi.json +667 -98
  4. package/artifacts/routes.json +97 -6
  5. package/artifacts/schema/apply-error.schema.json +2 -1
  6. package/artifacts/schema/asset-list-response.schema.json +77 -12
  7. package/artifacts/schema/asset-sync-status.schema.json +35 -8
  8. package/artifacts/schema/asset.schema.json +2 -3
  9. package/artifacts/schema/assets-clear-response.schema.json +23 -0
  10. package/artifacts/schema/authorization-server-metadata.schema.json +1 -1
  11. package/artifacts/schema/bridge-asset-progress.schema.json +14 -8
  12. package/artifacts/schema/bridge-config-applied.schema.json +2 -1
  13. package/artifacts/schema/bridge-link-mode.schema.json +36 -0
  14. package/artifacts/schema/bridge-state.schema.json +6 -1
  15. package/artifacts/schema/client-robot-list-item.schema.json +6 -1
  16. package/artifacts/schema/client-robot-list-response.schema.json +6 -1
  17. package/artifacts/schema/cloud-config.schema.json +90 -5
  18. package/artifacts/schema/cloud-hello-ok.schema.json +45 -0
  19. package/artifacts/schema/cloud-ping.schema.json +27 -1
  20. package/artifacts/schema/config-draft-response.schema.json +90 -5
  21. package/artifacts/schema/config-state.schema.json +2 -1
  22. package/artifacts/schema/config-version-response.schema.json +90 -5
  23. package/artifacts/schema/datapoint-config.schema.json +5 -0
  24. package/artifacts/schema/datapoint-frame.schema.json +4 -0
  25. package/artifacts/schema/datapoint-list-response.schema.json +2 -2
  26. package/artifacts/schema/dynamic-client-registration-request.schema.json +1 -1
  27. package/artifacts/schema/dynamic-client-registration-response.schema.json +1 -1
  28. package/artifacts/schema/joint-state-put-request.schema.json +23 -0
  29. package/artifacts/schema/joint-state-put-response.schema.json +24 -0
  30. package/artifacts/schema/oauth-token-request.schema.json +79 -41
  31. package/artifacts/schema/oauth-token-response.schema.json +1 -1
  32. package/artifacts/schema/org-quota-usage-counts.schema.json +0 -5
  33. package/artifacts/schema/org-quota-usage.schema.json +1 -12
  34. package/artifacts/schema/org-quotas.schema.json +1 -7
  35. package/artifacts/schema/robot-config-doc.schema.json +90 -5
  36. package/artifacts/schema/robot-deletion-summary.schema.json +2 -1
  37. package/artifacts/schema/robot-detail-response.schema.json +63 -2
  38. package/artifacts/schema/robot-list-item.schema.json +15 -1
  39. package/artifacts/schema/robot-list-response.schema.json +15 -1
  40. package/artifacts/schema/robot-token-rotate-response.schema.json +15 -0
  41. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +14 -8
  42. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +2 -1
  43. package/artifacts/schema-outgoing/bridge-link-mode.schema.json +37 -0
  44. package/artifacts/schema-outgoing/datapoint-frame.schema.json +4 -0
  45. package/dist/assets.d.ts +85 -50
  46. package/dist/assets.js +152 -62
  47. package/dist/audit.d.ts +1 -1
  48. package/dist/audit.js +1 -1
  49. package/dist/client-robots.d.ts +2 -0
  50. package/dist/common.d.ts +10 -0
  51. package/dist/common.js +16 -1
  52. package/dist/config.d.ts +69 -1
  53. package/dist/config.js +86 -6
  54. package/dist/errors.d.ts +1 -1
  55. package/dist/errors.js +1 -8
  56. package/dist/index.d.ts +10 -10
  57. package/dist/index.js +5 -5
  58. package/dist/oauth.d.ts +34 -19
  59. package/dist/oauth.js +39 -24
  60. package/dist/protocol.d.ts +150 -71
  61. package/dist/protocol.js +144 -87
  62. package/dist/rest.d.ts +137 -35
  63. package/dist/rest.js +98 -66
  64. package/dist/routes.js +68 -19
  65. package/package.json +1 -1
  66. package/artifacts/schema/bridge-pressure.schema.json +0 -292
package/dist/oauth.js CHANGED
@@ -188,7 +188,7 @@ export const dynamicClientRegistrationRequest = z
188
188
  description: '`none`, RFC 7591\'s value for a public client, and the only value either server registers. Any other value is **refused rather than silently downgraded**: a client that believes it holds a secret and does not has a wrong mental model of its own security. There is no client secret to hold — mandatory PKCE (`S256`) is the defence.',
189
189
  }),
190
190
  grant_types: z.array(z.enum(['authorization_code', 'refresh_token'])).optional().meta({
191
- description: 'Accepted for conformance with RFC 7591 and then **ignored**. What comes back is what was actually granted, which §3.2.1 permits a server to substitute: `authorization_code` and nothing else, so a client that asks for `refresh_token` is registered and told plainly that it did not get one.',
191
+ description: 'Accepted for conformance with RFC 7591 and then **ignored**: both MCP authorization servers grant `authorization_code` and `refresh_token` to every registration, and the answer states what was granted (§3.2.1) rather than what was asked.',
192
192
  }),
193
193
  response_types: z.array(z.enum(['code'])).optional().meta({
194
194
  description: 'Accepted for conformance and then **ignored**; the response names `code`, which is the only response type OAuth 2.1 leaves, the implicit grant having been removed.',
@@ -211,7 +211,7 @@ export const dynamicClientRegistrationResponse = z.object({
211
211
  description: 'The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else.',
212
212
  }),
213
213
  grant_types: z.array(z.string()).meta({
214
- description: 'The grants this client may use. Always exactly `["authorization_code"]` — a client that asked for `refresh_token` is registered and told here that it did not get one, which is the substitution RFC 7591 §3.2.1 permits.',
214
+ description: 'The grants this client may use. Always exactly `["authorization_code", "refresh_token"]` — an exchange mints a refresh token and the token endpoint rotates it.',
215
215
  }),
216
216
  response_types: z.array(z.string()).meta({
217
217
  description: 'The response types this client may ask for: `code`.',
@@ -227,26 +227,20 @@ export const dynamicClientRegistrationResponse = z.object({
227
227
  }),
228
228
  });
229
229
  /**
230
- * **The MCP token endpoint's request — one grant, because the servers serve
231
- * one.**
230
+ * **The MCP token endpoint's request — two grants, one per half of a
231
+ * session.**
232
232
  *
233
- * Both authorization servers, central and per-app, exchange through one
234
- * implementation, whose first act is to refuse anything but
235
- * `authorization_code` before a single lookup happens. There is no refresh grant here: a session ends when its token
236
- * expires and the client signs in again.
233
+ * `authorization_code` mints the first access token and a refresh token;
234
+ * `refresh_token` rotates that refresh token into a new pair. Both MCP
235
+ * authorization servers, central and per-app, answer both; the console's own
236
+ * OAuth portal answers the code grant only.
237
237
  *
238
- * **This was a `discriminatedUnion` with a `refresh_token` branch, and that
239
- * branch had no producer left.** It described the app-level OAuth surface,
240
- * which is deleted; an app user's refresh runs through `POST
241
- * /api/client/refresh` and `refreshRequest`, a different wire on a different
242
- * route. Keeping it would have published, to every MCP client author reading
243
- * `/openapi.json`, a grant the endpoint answers `unsupported_grant_type` to.
244
- * The argument the branch carried is worth keeping even though the branch is
245
- * not: **RFC 8707's `resource` has to survive rotation**, because a refresh
246
- * that drops the audience mints a successor with no `aud`, and the validating
247
- * resource then refuses a token the caller obtained legitimately — one token
248
- * lifetime after a login that worked, to somebody who did nothing wrong. If a
249
- * refresh grant is ever added here, it carries `resource`.
238
+ * **RFC 8707's `resource` has to survive rotation**: a refresh that drops the
239
+ * audience mints a successor with no `aud`, and the validating resource then
240
+ * refuses a token the caller obtained legitimately. So the server keeps the
241
+ * audience on the refresh token's own row, and a `resource` named here must
242
+ * match it or the answer is `invalid_target` — before the token is consumed,
243
+ * so a typo costs nothing.
250
244
  *
251
245
  * `code_verifier`'s bounds are RFC 7636 §4.1's, charset included. A verifier
252
246
  * is compared, not parsed, so a length nobody checks is a length an attacker
@@ -260,10 +254,10 @@ export const dynamicClientRegistrationResponse = z.object({
260
254
  * **stripped**, which bit the test for this schema: `safeParse().success`
261
255
  * cannot tell a present field from an absent one. Assert on the parsed value.
262
256
  */
263
- export const oauthTokenRequest = z
257
+ export const oauthCodeTokenRequest = z
264
258
  .object({
265
259
  grant_type: z.literal('authorization_code').meta({
266
- description: 'Always `authorization_code`: this request exchanges the code from the authorize redirect for tokens. Any other value — `refresh_token` included — is `unsupported_grant_type`, refused before the code is looked up.',
260
+ description: '`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token.',
267
261
  }),
268
262
  code: z.string().min(1).max(500).meta({
269
263
  description: 'The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets.',
@@ -284,6 +278,27 @@ export const oauthTokenRequest = z
284
278
  .meta({
285
279
  description: "RFC 6749 §4.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per §4.1.3, though the server accepts a JSON body too.",
286
280
  });
281
+ export const oauthRefreshTokenRequest = z
282
+ .object({
283
+ grant_type: z.literal('refresh_token').meta({
284
+ description: '`refresh_token`: this request rotates a refresh token into a new access token and a new refresh token. The presented token is consumed; presenting it again revokes the whole session.',
285
+ }),
286
+ refresh_token: z.string().min(1).max(500).meta({
287
+ description: 'The refresh token from the last token response. Bound to the client that received it and to one identity space: presented by another client, or at the other MCP server, it is `invalid_grant` and stays unconsumed.',
288
+ }),
289
+ client_id: z.string().min(1).max(200).meta({
290
+ description: 'The client the refresh token was issued to, as registered. A refresh token is not transferable between clients.',
291
+ }),
292
+ resource: z.url().optional().meta({
293
+ description: 'The resource the new token is for, per RFC 8707. Optional; when named it must be the audience the session was issued for, or the answer is `invalid_target` and the refresh token is left untouched. The successor carries the same audience either way.',
294
+ }),
295
+ })
296
+ .meta({
297
+ description: "RFC 6749 §6's refresh, as either MCP authorization server reads it. Every use rotates: the answer carries a new refresh token and the presented one is dead.",
298
+ });
299
+ export const oauthTokenRequest = z.discriminatedUnion('grant_type', [oauthCodeTokenRequest, oauthRefreshTokenRequest]).meta({
300
+ description: 'What an MCP token endpoint accepts: the authorization-code exchange, or a refresh. Any other `grant_type` is `unsupported_grant_type`, refused before a lookup happens.',
301
+ });
287
302
  /**
288
303
  * RFC 6749 §5.1's success envelope — **the second deliberate dialect, and this
289
304
  * one is a success shape rather than an error shape.**
@@ -311,7 +326,7 @@ export const oauthTokenResponse = z.object({
311
326
  description: 'How long the access token is valid, in **seconds**, per RFC 6749 §5.1. Not a timestamp, and not milliseconds.',
312
327
  }),
313
328
  refresh_token: z.string().min(1).optional().meta({
314
- description: 'The refresh token, when one was issued. It rotates on every use.',
329
+ description: 'The refresh token. Both MCP token endpoints issue one on every exchange and every refresh; it rotates on every use, lives ninety days from its last use, and dies with the account\'s sessions — a block, a password change, a withdrawn consent. The console\'s own OAuth portal issues none.',
315
330
  }),
316
331
  scope: z.string().max(500).optional().meta({
317
332
  description: 'The scopes the issued token actually carries, space-separated.',
@@ -335,7 +350,7 @@ export const authorizationServerMetadata = z.object({
335
350
  description: 'The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1.',
336
351
  }),
337
352
  grant_types_supported: z.array(z.enum(['authorization_code', 'refresh_token'])).meta({
338
- description: 'The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here.',
353
+ description: 'The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here.',
339
354
  }),
340
355
  code_challenge_methods_supported: z.array(codeChallengeMethod).meta({
341
356
  description: 'The PKCE challenge methods accepted: `S256` only. `plain` is not offered — a challenge equal to its verifier defends against nothing, and offering it would make a downgrade negotiable.',
@@ -1,18 +1,67 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
3
  /**
4
- * Bridge <-> cloud protocol, version 2.
4
+ * Bridge <-> cloud protocol, version 3.
5
5
  *
6
- * The version is exchanged in the hello handshake; the cloud refuses an
7
- * incompatible bridge: `protocol_mismatch`, which names both versions and
8
- * reaches the robot's detail view as `last_hello_error`.
6
+ * The version is exchanged in the hello handshake. Since 2026-09 the cloud
7
+ * serves a **window** of versions, not one: every entry of
8
+ * `PROTOCOL_VERSIONS` whose sunset has not passed. A version is deprecated
9
+ * by the cloud release that supersedes it and sunset `PROTOCOL_SUNSET_DAYS`
10
+ * later. Outside the window the cloud refuses with `protocol_mismatch`,
11
+ * which names the window and reaches the robot's detail view as
12
+ * `last_hello_error`.
13
+ *
14
+ * **3 (2026-09-22):** the ping carries `latency_ms` and `lag_ms`, the bridge
15
+ * sends `link_mode`, `bridge_state` gains `low_bandwidth`, and the
16
+ * `bridge_pressure` datapoint is gone. A protocol-2 bridge is served until
17
+ * its sunset, and the cloud's protocol-2 adapter owes it two translations on
18
+ * the way in: it drops its pressure datapoints, and it rewrites an
19
+ * `asset_progress` failure of kind `too_large` — a kind protocol 3 no longer
20
+ * has — to `refused` with `details: null`, because a 2.0.0 `bridgeAssetProgress`
21
+ * refuses the frame outright otherwise.
9
22
  *
10
23
  * **2 (2026-08-21):** `config_applied.errors` entries gained `kind` and `code`
11
- * beside `message`. The check is `!==`, not a floor, so a bridge that is not
12
- * exactly this version is refused entirely. That is deliberate: a cloud and a
13
- * bridge that disagree about the wire should not pretend otherwise.
24
+ * beside `message`.
25
+ */
26
+ export declare const PROTOCOL_VERSION = 3;
27
+ /** Days between a version's deprecation and its sunset. */
28
+ export declare const PROTOCOL_SUNSET_DAYS = 90;
29
+ export interface ProtocolVersionEntry {
30
+ version: number;
31
+ /** The first bridge package version that speaks this protocol. */
32
+ bridge_from: string;
33
+ /** ISO date of the cloud release that superseded it; null while current. */
34
+ deprecated_at: string | null;
35
+ }
36
+ /**
37
+ * Every protocol version the cloud has served, oldest first. A test keeps
38
+ * exactly one entry current and equal to `PROTOCOL_VERSION`; `test/changelog.test.ts`
39
+ * requires the CHANGELOG's current section to name the newest `bridge_from`
40
+ * and the previous entry's `sunsetOf(...)` date, and `scripts/verify-version-tag.mjs`
41
+ * requires a dated heading for the tag being released.
14
42
  */
15
- export declare const PROTOCOL_VERSION = 2;
43
+ export declare const PROTOCOL_VERSIONS: readonly ProtocolVersionEntry[];
44
+ /** The newest bridge package. The cloud mails organisations still below it. */
45
+ export declare const LATEST_BRIDGE_VERSION = "4.0.0";
46
+ export interface ProtocolStatus {
47
+ status: 'current' | 'deprecated' | 'unsupported';
48
+ /** ISO date, or null for a current or unknown version. */
49
+ sunset_at: string | null;
50
+ }
51
+ export declare function sunsetOf(entry: ProtocolVersionEntry): string | null;
52
+ /**
53
+ * The window rule itself: `protocolStatus` and `minimumProtocolVersion` are
54
+ * this function over `PROTOCOL_VERSIONS`, and the cloud calls it directly.
55
+ *
56
+ * The table is a parameter because callers pass one with a deprecated entry —
57
+ * tests, and any caller reasoning about a sunset. The real table has none
58
+ * until the first bump, so a hard-coded `PROTOCOL_VERSIONS` would leave the
59
+ * deprecated and unsupported branches unreachable.
60
+ */
61
+ export declare function statusFromTable(table: readonly ProtocolVersionEntry[], version: number, today: Date): ProtocolStatus;
62
+ export declare function protocolStatus(version: number, today?: Date): ProtocolStatus;
63
+ /** The lowest version still inside its window today. */
64
+ export declare function minimumProtocolVersion(today?: Date): number;
16
65
  /**
17
66
  * The bridge socket close code for "this robot no longer exists".
18
67
  *
@@ -24,6 +73,20 @@ export declare const PROTOCOL_VERSION = 2;
24
73
  * more informative than "connection closed".
25
74
  */
26
75
  export declare const CLOSE_ROBOT_DELETED = 4004;
76
+ /**
77
+ * The bridge socket close code for "the token you connected with is gone".
78
+ *
79
+ * The cloud closes a robot's live socket with this after the robot's token was
80
+ * rotated. A 4.0.0 bridge reads it the way it reads `invalid_token` — stop,
81
+ * exit 2 — because the secret it holds is no longer a secret anyone accepts,
82
+ * and no amount of reconnecting produces the new one. A 3.x bridge does not
83
+ * know the code, reconnects, and is refused at hello; that ends the same way,
84
+ * one round trip later.
85
+ *
86
+ * Its own code rather than `CLOSE_ROBOT_DELETED`, which would tell an operator
87
+ * their robot had been deleted when it very much still exists.
88
+ */
89
+ export declare const CLOSE_TOKEN_ROTATED = 4005;
27
90
  /**
28
91
  * How long a command waits for its answer when the caller names no patience
29
92
  * of its own.
@@ -113,10 +176,26 @@ export declare const bridgeHello: z.ZodObject<{
113
176
  }, z.core.$strip>>>;
114
177
  }, z.core.$strip>;
115
178
  export type BridgeHello = z.infer<typeof bridgeHello>;
116
- /** Cloud accepts the bridge: the robot is online from here on. */
179
+ /**
180
+ * Cloud accepts the bridge: the robot is online from here on.
181
+ *
182
+ * `protocol` and `bridge` are optional so that a bridge parsing `hello_ok`
183
+ * strictly still parses one from an older cloud. `status` here is never
184
+ * `unsupported`: an unsupported version gets `hello_error`, not this frame.
185
+ */
117
186
  export declare const cloudHelloOk: z.ZodObject<{
118
187
  type: z.ZodLiteral<"hello_ok">;
119
188
  robot_id: z.ZodUUID;
189
+ protocol: z.ZodOptional<z.ZodObject<{
190
+ status: z.ZodEnum<{
191
+ deprecated: "deprecated";
192
+ current: "current";
193
+ }>;
194
+ sunset_at: z.ZodNullable<z.ZodISODate>;
195
+ }, z.core.$strip>>;
196
+ bridge: z.ZodOptional<z.ZodObject<{
197
+ latest_version: z.ZodString;
198
+ }, z.core.$strip>>;
120
199
  }, z.core.$strip>;
121
200
  export type CloudHelloOk = z.infer<typeof cloudHelloOk>;
122
201
  /** Cloud refuses the bridge (bad token, incompatible protocol, ...). */
@@ -129,22 +208,40 @@ export type CloudHelloError = z.infer<typeof cloudHelloError>;
129
208
  /**
130
209
  * One datapoint sample. `timestamp_ms` is the capture time at the bridge —
131
210
  * never the receive time — so clients compute age themselves.
211
+ *
212
+ * Which is exactly why `backfill` has to be on the frame. A replayed sample
213
+ * carries the capture time it had during the outage, so a cloud that measures
214
+ * lag from every arriving frame reads a two-hour disconnect as two hours of
215
+ * lag the moment the bridge reconnects — and reports a healthy link as the
216
+ * worst one it has ever seen. Only the bridge knows which frames came out of
217
+ * its buffer, so only the bridge can say.
132
218
  */
133
219
  export declare const datapointFrame: z.ZodObject<{
134
220
  type: z.ZodLiteral<"datapoint">;
135
221
  slug: z.ZodString;
136
222
  value: z.ZodUnknown;
137
223
  timestamp_ms: z.ZodNumber;
224
+ backfill: z.ZodOptional<z.ZodBoolean>;
138
225
  }, z.core.$strip>;
139
226
  export type DatapointFrame = z.infer<typeof datapointFrame>;
140
227
  /**
141
228
  * Latency probe, cloud → bridge. The cloud sends its own clock in `ts_ms`;
142
229
  * the bridge echoes it back untouched and the cloud derives the round-trip
143
230
  * latency shown as `bridge_state.latency_ms`.
231
+ *
232
+ * Sent every `pingIntervalMs`; the bridge answers with `pong`. Since protocol
233
+ * 3 it also carries what the cloud measured about this link, so the bridge
234
+ * can decide on its low-bandwidth mode with an end-to-end number: the
235
+ * round trip of the last pong, and the datapoint lag — the median over the
236
+ * last five seconds of (receive time − `timestamp_ms`) minus the minimum of
237
+ * the last ten minutes, which cancels the robot's clock offset. `null` until
238
+ * the cloud has a sample. A protocol-2 bridge reads only `ts_ms`.
144
239
  */
145
240
  export declare const cloudPing: z.ZodObject<{
146
241
  type: z.ZodLiteral<"ping">;
147
242
  ts_ms: z.ZodNumber;
243
+ latency_ms: z.ZodNullable<z.ZodNumber>;
244
+ lag_ms: z.ZodNullable<z.ZodNumber>;
148
245
  }, z.core.$strip>;
149
246
  export type CloudPing = z.infer<typeof cloudPing>;
150
247
  /** Immediate bridge answer to a `CloudPing`, `ts_ms` echoed unchanged. */
@@ -153,6 +250,24 @@ export declare const bridgePong: z.ZodObject<{
153
250
  ts_ms: z.ZodNumber;
154
251
  }, z.core.$strip>;
155
252
  export type BridgePong = z.infer<typeof bridgePong>;
253
+ /**
254
+ * The bridge's low-bandwidth mode changed. Sent on every transition and once
255
+ * after `hello_ok`, at tier 0 like the pong: the cloud folds it into
256
+ * `bridge_state.low_bandwidth`, and a frame that waited behind bulk would
257
+ * describe a state that is already over.
258
+ */
259
+ export declare const bridgeLinkMode: z.ZodObject<{
260
+ type: z.ZodLiteral<"link_mode">;
261
+ low_bandwidth: z.ZodBoolean;
262
+ reason: z.ZodEnum<{
263
+ lag: "lag";
264
+ dwell: "dwell";
265
+ forced: "forced";
266
+ recovered: "recovered";
267
+ }>;
268
+ at_ms: z.ZodNumber;
269
+ }, z.core.$strip>;
270
+ export type BridgeLinkMode = z.infer<typeof bridgeLinkMode>;
156
271
  /**
157
272
  * The published configuration, cloud → bridge — the bridge applies the
158
273
  * published version. Sent right after `hello_ok` and again on every publish,
@@ -191,6 +306,7 @@ export declare const cloudConfig: z.ZodObject<{
191
306
  type: z.ZodString;
192
307
  field: z.ZodOptional<z.ZodString>;
193
308
  rate_throttle_hz: z.ZodOptional<z.ZodNumber>;
309
+ low_bandwidth: z.ZodOptional<z.ZodLiteral<"keep">>;
194
310
  description: z.ZodOptional<z.ZodString>;
195
311
  numeric: z.ZodOptional<z.ZodObject<{
196
312
  scale: z.ZodOptional<z.ZodNumber>;
@@ -357,6 +473,23 @@ export declare const cloudConfig: z.ZodObject<{
357
473
  snapshot_interval_seconds: z.ZodNumber;
358
474
  description: z.ZodOptional<z.ZodString>;
359
475
  }, z.core.$strict>>>;
476
+ low_bandwidth: z.ZodOptional<z.ZodObject<{
477
+ mode: z.ZodOptional<z.ZodEnum<{
478
+ auto: "auto";
479
+ on: "on";
480
+ off: "off";
481
+ }>>;
482
+ enter_lag_ms: z.ZodOptional<z.ZodNumber>;
483
+ enter_after_s: z.ZodOptional<z.ZodNumber>;
484
+ exit_lag_ms: z.ZodOptional<z.ZodNumber>;
485
+ exit_after_s: z.ZodOptional<z.ZodNumber>;
486
+ datapoint_max_hz: z.ZodOptional<z.ZodNumber>;
487
+ camera: z.ZodOptional<z.ZodEnum<{
488
+ reduce: "reduce";
489
+ stop: "stop";
490
+ }>>;
491
+ camera_bitrate_kbps: z.ZodOptional<z.ZodNumber>;
492
+ }, z.core.$strict>>;
360
493
  }, z.core.$strict>;
361
494
  }, z.core.$strip>;
362
495
  export type CloudConfig = z.infer<typeof cloudConfig>;
@@ -377,6 +510,7 @@ export declare const bridgeConfigApplied: z.ZodObject<{
377
510
  service: "service";
378
511
  publisher: "publisher";
379
512
  camera: "camera";
513
+ low_bandwidth: "low_bandwidth";
380
514
  }>;
381
515
  code: z.ZodString;
382
516
  message: z.ZodString;
@@ -538,72 +672,17 @@ export type BridgeTypeDefinitions = z.infer<typeof bridgeTypeDefinitions>;
538
672
  /**
539
673
  * The built-in `bridge_state` datapoint every robot has: connection status
540
674
  * plus latency, the basis for offline-aware client UIs.
675
+ *
676
+ * `online` and `latency_ms` are cloud-observed (the socket, the pong);
677
+ * `low_bandwidth` is bridge-reported through `link_mode` and `false` for a
678
+ * bridge that never sends one.
541
679
  */
542
680
  export declare const bridgeState: z.ZodObject<{
543
681
  online: z.ZodBoolean;
544
682
  latency_ms: z.ZodNullable<z.ZodNumber>;
683
+ low_bandwidth: z.ZodBoolean;
545
684
  }, z.core.$strip>;
546
685
  export type BridgeState = z.infer<typeof bridgeState>;
547
- /**
548
- * The built-in `bridge_pressure` datapoint: the bridge's own
549
- * bandwidth-shaping state, sent on the same reserved-slug path as
550
- * `bridge_state` so history, realtime, REST and MCP exposure fall out of the
551
- * ordinary datapoint machinery for free.
552
- */
553
- export declare const bridgePressure: z.ZodObject<{
554
- link: z.ZodObject<{
555
- rate_bps: z.ZodNullable<z.ZodNumber>;
556
- snapshot_max_bytes: z.ZodNumber;
557
- }, z.core.$strip>;
558
- tiers: z.ZodObject<{
559
- '0': z.ZodOptional<z.ZodObject<{
560
- sent: z.ZodNumber;
561
- bytes: z.ZodNumber;
562
- drops: z.ZodNumber;
563
- high_water: z.ZodNumber;
564
- }, z.core.$strip>>;
565
- '1': z.ZodOptional<z.ZodObject<{
566
- sent: z.ZodNumber;
567
- bytes: z.ZodNumber;
568
- drops: z.ZodNumber;
569
- high_water: z.ZodNumber;
570
- }, z.core.$strip>>;
571
- '2': z.ZodOptional<z.ZodObject<{
572
- sent: z.ZodNumber;
573
- bytes: z.ZodNumber;
574
- drops: z.ZodNumber;
575
- high_water: z.ZodNumber;
576
- }, z.core.$strip>>;
577
- '3': z.ZodOptional<z.ZodObject<{
578
- sent: z.ZodNumber;
579
- bytes: z.ZodNumber;
580
- drops: z.ZodNumber;
581
- high_water: z.ZodNumber;
582
- }, z.core.$strip>>;
583
- '4': z.ZodOptional<z.ZodObject<{
584
- sent: z.ZodNumber;
585
- bytes: z.ZodNumber;
586
- drops: z.ZodNumber;
587
- high_water: z.ZodNumber;
588
- }, z.core.$strip>>;
589
- '5': z.ZodOptional<z.ZodObject<{
590
- sent: z.ZodNumber;
591
- bytes: z.ZodNumber;
592
- drops: z.ZodNumber;
593
- high_water: z.ZodNumber;
594
- }, z.core.$strip>>;
595
- }, z.core.$strict>;
596
- video: z.ZodObject<{
597
- active_streams: z.ZodNumber;
598
- bitrate_sum_kbps: z.ZodNumber;
599
- uplink_kbps: z.ZodNullable<z.ZodNumber>;
600
- override_kbps: z.ZodNullable<z.ZodNumber>;
601
- video_budget_kbps: z.ZodNullable<z.ZodNumber>;
602
- reserve_kbps: z.ZodNumber;
603
- }, z.core.$strip>;
604
- }, z.core.$strip>;
605
- export type BridgePressure = z.infer<typeof bridgePressure>;
606
- export declare const PRESSURE_SLUG: "bridge_pressure";
607
686
  /**
608
687
  * The header of a **binary** snapshot frame, bridge → cloud.
609
688
  *
@@ -724,10 +803,10 @@ export declare const bridgeAssetProgress: z.ZodObject<{
724
803
  unresolvable: "unresolvable";
725
804
  upload_failed: "upload_failed";
726
805
  refused: "refused";
727
- too_large: "too_large";
728
806
  }>;
729
807
  details: z.ZodOptional<z.ZodNullable<z.ZodObject<{
730
- limit_bytes: z.ZodNumber;
808
+ store_bytes: z.ZodNumber;
809
+ used_bytes: z.ZodNumber;
731
810
  size_bytes: z.ZodNumber;
732
811
  }, z.core.$strip>>>;
733
812
  }, z.core.$strip>>;