@fleetless/sdk 3.1.0 → 4.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/dist/index.cjs CHANGED
@@ -266,7 +266,6 @@ function isRenderKind(kind) {
266
266
  case "texture":
267
267
  return true;
268
268
  case "urdf":
269
- case "other":
270
269
  return false;
271
270
  default: {
272
271
  const exhaustive = kind;
@@ -441,7 +440,7 @@ function createAssetsApi(http) {
441
440
  };
442
441
  }
443
442
 
444
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/common.js
443
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/common.js
445
444
  var import_zod = require("zod");
446
445
  var SLUG_RULE = "A name is lower-case: it starts with a letter, continues with letters and digits, and joins further words with a single underscore \u2014 `battery_voltage`. Capitals, dashes, dots, spaces, a leading digit and a doubled or trailing underscore are all refused.";
447
446
  var slug = import_zod.z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, SLUG_RULE);
@@ -456,7 +455,14 @@ var wireTimestampMs = import_zod.z.union([import_zod.z.string().regex(/^\d{1,15}
456
455
  const year = new Date(ms).getUTCFullYear();
457
456
  return Number.isFinite(year) && year >= 1 && year <= 9999;
458
457
  }, "must fall within years 1..9999"));
459
- var applyErrorKind = import_zod.z.enum(["datapoint", "action", "service", "publisher", "camera"]);
458
+ var applyErrorKind = import_zod.z.enum([
459
+ "datapoint",
460
+ "action",
461
+ "service",
462
+ "publisher",
463
+ "camera",
464
+ "low_bandwidth"
465
+ ]);
460
466
  var applyError = import_zod.z.object({
461
467
  slug: import_zod.z.string(),
462
468
  kind: applyErrorKind,
@@ -465,7 +471,7 @@ var applyError = import_zod.z.object({
465
471
  details: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
466
472
  });
467
473
 
468
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/mcp.js
474
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/mcp.js
469
475
  var import_zod2 = require("zod");
470
476
  function mcpAppEndpointPath(appIdentifier2) {
471
477
  return `/mcp/${appIdentifier2}`;
@@ -514,17 +520,17 @@ var mcpRolePreviewResponse = import_zod2.z.object({
514
520
  });
515
521
  var MCP_ASSET_LINK_TTL_MS = 15 * 60 * 1e3;
516
522
 
517
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/protocol.js
523
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/protocol.js
518
524
  var import_zod8 = require("zod");
519
525
 
520
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/assets.js
526
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/assets.js
521
527
  var import_zod3 = require("zod");
522
- var assetKind = import_zod3.z.enum(["urdf", "mesh", "texture", "other"]);
528
+ var assetKind = import_zod3.z.enum(["urdf", "mesh", "texture"]);
523
529
  var asset = import_zod3.z.object({
524
530
  id: import_zod3.z.uuid().meta({ description: "The asset's id in the store." }),
525
531
  robot_id: import_zod3.z.uuid().meta({ description: "The robot this asset belongs to." }),
526
532
  kind: assetKind.meta({
527
- description: "What the file is: the `urdf` itself, a `mesh` it references, a `texture` a mesh or the URDF paints with, or `other`. A renderer decides from this alone, before fetching anything, what to pre-fetch."
533
+ description: "What the file is: the `urdf` itself, a `mesh` it references, or a `texture` a mesh or the URDF paints with. A renderer decides from this alone, before fetching anything, what to pre-fetch."
528
534
  }),
529
535
  /**
530
536
  * What the robot called it — for a mesh, the `package://` URI the URDF
@@ -623,16 +629,18 @@ var assetSyncResponse = import_zod3.z.object({
623
629
  description: "The sync that has just started. A sync is long-running, so the answer is something to watch rather than a status that was true at the moment of asking."
624
630
  })
625
631
  });
626
- var ASSET_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
627
- var assetTooLargeDetails = import_zod3.z.object({
628
- limit_bytes: import_zod3.z.number().int().positive().meta({
629
- description: "The upload ceiling, in bytes."
632
+ var assetStoreRefusedDetails = import_zod3.z.object({
633
+ store_bytes: import_zod3.z.number().int().positive().meta({
634
+ description: "The robot's store, in bytes."
635
+ }),
636
+ used_bytes: import_zod3.z.number().int().nonnegative().meta({
637
+ description: "Bytes the robot's assets occupy before this upload."
630
638
  }),
631
639
  size_bytes: import_zod3.z.number().int().positive().meta({
632
- description: 'How large the refused file is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; "too large" alone answers neither.'
640
+ description: "The refused upload, in bytes."
633
641
  })
634
642
  });
635
- var assetFailureKind = import_zod3.z.enum(["unresolvable", "upload_failed", "refused", "too_large"]);
643
+ var assetFailureKind = import_zod3.z.enum(["unresolvable", "upload_failed", "refused"]);
636
644
  var assetFailure = import_zod3.z.object({
637
645
  /**
638
646
  * What could not be provided, verbatim — the same string `asset.name` would
@@ -645,33 +653,28 @@ var assetFailure = import_zod3.z.object({
645
653
  description: "What could not be provided, verbatim \u2014 the same string the asset would have been stored under, so a developer can match it against their own workspace by eye. For a failed URDF upload it is `robot_description`, which is **not** a mesh URI: a consumer must not assume every entry is one."
646
654
  }),
647
655
  kind: assetFailureKind.meta({
648
- description: "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** \u2014 the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`."
656
+ description: "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** \u2014 the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, and `refused` means it was never attempted, either because the robot's asset store had no room \u2014 then `details` carries the three numbers \u2014 or because a producer-side ceiling was hit."
649
657
  }),
650
658
  /**
651
- * **The two numbers, and why `too_large` is a kind of its own.**
659
+ * **The three numbers behind a full store.**
652
660
  *
653
- * `refused` means *never attempted, because a producer-side ceiling was
654
- * hit*. That fits a file skipped for its size **and** the single collective
655
- * entry a sync emits when it stops naming individual failures. Filing both
656
- * under one kind puts two facts on one key, each overwriting the other.
661
+ * A reason without numbers is not one a caller can act on. *"Refused"* does
662
+ * not answer whether to delete an old sync or shrink the mesh;
663
+ * `store_bytes`, `used_bytes` and `size_bytes` do.
657
664
  *
658
- * A reason without numbers is not one a caller can act on. *"Too large"*
659
- * does not answer whether to shrink the mesh or raise the limit;
660
- * `limit_bytes` and `size_bytes` do.
661
- *
662
- * Absent for every other kind — a forced `details: null` on every
663
- * `unresolvable` buys nothing. The pairing is **enforced** below, not merely
664
- * described: a field whose rule lives only in a comment is a request.
665
+ * **Present only on `refused`, and not on every `refused`.** The other half
666
+ * of that kind is the single collective entry a sync emits when it stops
667
+ * naming individual failures, and no store number describes it — requiring
668
+ * details there would mean inventing them. So the enforcement below is the
669
+ * half that can be enforced: details belong to `refused` and to nothing
670
+ * else. A forced `details: null` on every `unresolvable` buys nothing.
665
671
  */
666
- details: assetTooLargeDetails.nullish().meta({
667
- description: "The two numbers behind a `too_large` failure, and absent for every other kind \u2014 a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described."
672
+ details: assetStoreRefusedDetails.nullish().meta({
673
+ description: "The three numbers behind a `refused` entry the robot's store had no room for, and absent for every other kind \u2014 a forced `null` on every `unresolvable` entry buys nothing. A `refused` entry may also carry no details: the producer's own ceiling is the other half of that kind, and no store number describes it."
668
674
  })
669
675
  }).superRefine((f, ctx) => {
670
- if (f.kind === "too_large" && f.details == null) {
671
- ctx.addIssue({ code: "custom", path: ["details"], message: "`too_large` without limit_bytes/size_bytes says nothing a developer can act on" });
672
- }
673
- if (f.kind !== "too_large" && f.details != null) {
674
- ctx.addIssue({ code: "custom", path: ["details"], message: "size details belong to `too_large` only" });
676
+ if (f.kind !== "refused" && f.details != null) {
677
+ ctx.addIssue({ code: "custom", path: ["details"], message: "store details belong to `refused` only" });
675
678
  }
676
679
  });
677
680
  var assetSyncState = import_zod3.z.enum(["running", "succeeded", "failed"]);
@@ -717,6 +720,36 @@ var assetSyncStatus = import_zod3.z.object({
717
720
  reason: import_zod3.z.string().min(1).nullable().meta({
718
721
  description: "Why the sync ended as it did, when that is not a per-reference fact. `null` when `failed` already says everything there is to say."
719
722
  }),
723
+ /**
724
+ * **What the receiver counted, next to what the producer claimed.**
725
+ *
726
+ * `state` is the bridge's own terminal frame and nothing else. A dev stack
727
+ * with no object store answered `500` to every upload and the sync still
728
+ * read `succeeded` — the producer had genuinely sent every file, and no
729
+ * one had asked the store. These two numbers are the cloud's own count,
730
+ * taken after the terminal frame: how many of the announced files
731
+ * (`assets_available`'s URDF and mesh list) its store actually holds.
732
+ *
733
+ * They are a pair because neither alone answers anything. `stored` without
734
+ * `announced` cannot say whether four files is all of them or a tenth of
735
+ * them, and `announced` alone is what the producer said it had, which is
736
+ * the claim under examination.
737
+ *
738
+ * A partial store is still `succeeded`: some meshes were never going to
739
+ * resolve, and the per-reference `failed` entries say which. An empty one
740
+ * under a `succeeded` frame is `failed`, because no transport succeeds at
741
+ * nothing.
742
+ *
743
+ * **`stored` is `null` while the sync is still running.** The count is
744
+ * taken once, after the robot's terminal frame; a `0` before then would
745
+ * say the store is empty when nobody has looked.
746
+ */
747
+ stored: import_zod3.z.number().int().nonnegative().nullable().meta({
748
+ description: "How many of the announced files the cloud's store actually holds. Counted once, after the robot reports the sync done, and `null` until then \u2014 nobody has looked yet. Read it against `announced`: `state` is what the robot reported, this is what arrived."
749
+ }),
750
+ announced: import_zod3.z.number().int().nonnegative().meta({
751
+ description: "How many files the robot announced for this sync \u2014 the URDF, if it has one, plus every mesh URI its description references. `0` when the robot announced nothing, and also `0` until it has answered at all: read it beside `stored`, which stays `null` until the terminal frame."
752
+ }),
720
753
  started_at: import_zod3.z.iso.datetime().meta({
721
754
  description: "When the sync started, as an ISO 8601 timestamp."
722
755
  }),
@@ -760,6 +793,43 @@ var assetListResponse = import_zod3.z.object({
760
793
  */
761
794
  urdf_available: import_zod3.z.boolean().nullable().meta({
762
795
  description: 'What the connected bridge says it *could* transfer \u2014 deliberately separate from what has been transferred. `null` when no bridge is connected, distinct from `false`: "no robot is online to ask" and "the robot has no URDF" send a developer to different places. After a publisher is killed rather than shut down this can read `true` for some seconds, on the underlying DDS liveliness timeout rather than on any check made here.'
796
+ }),
797
+ /**
798
+ * **How full this robot's store is, on the list that already names what is
799
+ * in it.** A page showing assets is the page where "will the next sync fit"
800
+ * is asked, and a second round trip to some quota endpoint would answer it
801
+ * about the organisation instead — which is a different number about a
802
+ * different thing.
803
+ */
804
+ store: import_zod3.z.object({
805
+ bytes: import_zod3.z.number().int().positive().meta({
806
+ description: "The robot's asset store, `ROBOT_ASSET_STORE_BYTES`."
807
+ }),
808
+ used_bytes: import_zod3.z.number().int().nonnegative().meta({
809
+ description: "Bytes its assets occupy."
810
+ })
811
+ }).meta({ description: "How full this robot's store is." }),
812
+ /**
813
+ * The datapoint that moves the joints in a renderer, chosen by a developer
814
+ * and stored on the robot. It rides on this list because a client that has
815
+ * just fetched the URDF and the meshes needs exactly one more thing to
816
+ * animate them, and asking a second endpoint for one slug is a round trip
817
+ * that buys nothing.
818
+ *
819
+ * `null` is an ordinary answer: none was ever chosen, or a publish removed
820
+ * the datapoint it named and the cloud cleared the mapping rather than
821
+ * leave it pointing at something that no longer qualifies.
822
+ */
823
+ joint_state_slug: slug.nullable().meta({
824
+ description: "The whole-message `sensor_msgs/msg/JointState` datapoint that drives the console's URDF viewer; null when none is chosen or a publish removed it. Set through `PUT /api/robots/:id/urdf/joint-state`."
825
+ })
826
+ });
827
+ var assetsClearResponse = import_zod3.z.object({
828
+ deleted: import_zod3.z.number().int().nonnegative().meta({
829
+ description: "How many assets \u2014 URDF, meshes and textures together \u2014 were removed."
830
+ }),
831
+ bytes_freed: import_zod3.z.number().int().nonnegative().meta({
832
+ description: "The bytes the robot's store got back."
763
833
  })
764
834
  });
765
835
  var missingAssetQuery = import_zod3.z.object({
@@ -773,10 +843,10 @@ var assetSyncBusyDetails = import_zod3.z.object({
773
843
  started_at_ms: import_zod3.z.number().int().nonnegative()
774
844
  });
775
845
 
776
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/config.js
846
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/config.js
777
847
  var import_zod5 = require("zod");
778
848
 
779
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/alerts.js
849
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/alerts.js
780
850
  var import_zod4 = require("zod");
781
851
  var alertRowCondition = import_zod4.z.discriminatedUnion("kind", [
782
852
  import_zod4.z.strictObject({
@@ -846,7 +916,7 @@ var putDatapointDisplayRequest = import_zod4.z.object({
846
916
  y_max: import_zod4.z.number().finite().nullable()
847
917
  }).strict();
848
918
 
849
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/config.js
919
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/config.js
850
920
  var RTSP_URL_RULE = "The URL has to begin with `rtsp://` or `rtsps://` \u2014 `rtsp://cam-1.plant.local/stream1`. No other scheme is accepted: the bridge opens this with a library that would equally honour `file:`.";
851
921
  var MJPEG_URL_RULE = "The URL has to begin with `http://` or `https://` \u2014 `http://cam-1.plant.local/video.mjpg`. No other scheme is accepted: the bridge opens this with a library that would equally serve `file:`.";
852
922
  var DEVICE_PATH_RULE = "A capture device is a path under `/dev/`, and the character straight after it is a letter or a digit \u2014 `/dev/video0`, or a stable `/dev/v4l/by-id/...` symlink. Nothing outside `/dev/` is accepted: the string reaches OpenCV, which would as happily open an ordinary file.";
@@ -1237,6 +1307,9 @@ var datapointConfig = strictObject({
1237
1307
  description: "A ceiling on how often this datapoint is sent, in hertz. Omitted or `0` means no throttling. It is **a ceiling, not a clock**: a slow topic stays slow, a value is never repeated to manufacture a rate, and within a window the newest value wins. The bridge enforces it, so the robot's bandwidth is genuinely saved.",
1238
1308
  examples: [2, 0.5]
1239
1309
  }).optional(),
1310
+ low_bandwidth: import_zod5.z.literal("keep").optional().meta({
1311
+ description: "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses."
1312
+ }),
1240
1313
  description: serviceDescription.meta({
1241
1314
  description: "Prose about what this value is, for whoever meets it in the console. It changes nothing the robot does, so a publish that touches only it pushes no configuration at all \u2014 but it is carried verbatim into `robot_describe`, where a model that has never seen this robot reads it. The datapoint is offered whenever the role grants it; without one it is offered with `description: null` and the model has less to go on, as for actions, services, publishers and cameras. Omission is the only way to say nothing; an empty string is refused, here and on all five.",
1242
1315
  /**
@@ -1834,6 +1907,34 @@ var cameraConfig = strictObject({
1834
1907
  /** The value position of one entry — `front: ▮` under `cameras:`. */
1835
1908
  defaultSnippets: [CAMERA_SNIPPET]
1836
1909
  });
1910
+ var lowBandwidthMode = import_zod5.z.enum(["auto", "on", "off"]);
1911
+ var lowBandwidthCamera = import_zod5.z.enum(["reduce", "stop"]);
1912
+ var lowBandwidthSection = strictObject({
1913
+ mode: lowBandwidthMode.optional().meta({
1914
+ description: "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
1915
+ enumDescriptions: describeValues(lowBandwidthMode.options, {
1916
+ auto: "The bridge enters and leaves the mode on its own, from the lag the cloud reports and the dwell it measures in its own send queue. The setting to leave alone.",
1917
+ on: "The mode is held on, whatever the link is doing. For a robot on a link known to be poor, and for a test that would otherwise have to wait for a real one.",
1918
+ off: "The mode never engages, whatever the link is doing. The robot then sends at its configured rates over a link that cannot carry them, which is a choice and not a default."
1919
+ })
1920
+ }),
1921
+ enter_lag_ms: import_zod5.z.number().int().min(100).optional().meta({ description: "Lag or queue dwell above this enters the mode. Checked against exit_lag_ms only when both are in this document; a lone key composes with the bridge's parameter or the default on the robot, and a crossed pair is refused there when the configuration is applied, so name both when you change either." }),
1922
+ enter_after_s: import_zod5.z.number().int().min(1).optional().meta({ description: "The entry condition must hold this long." }),
1923
+ exit_lag_ms: import_zod5.z.number().int().min(0).optional().meta({ description: "Lag and dwell both at or below this leave the mode. Must be at or below enter_lag_ms: a crossed pair is a mode that leaves as it arrives. Checked here only when both keys are present; a lone key is checked on the robot against the parameter or default it composes with." }),
1924
+ exit_after_s: import_zod5.z.number().int().min(1).optional().meta({ description: "The exit condition must hold this long." }),
1925
+ datapoint_max_hz: import_zod5.z.number().gt(0).max(20).optional().meta({ description: "The long-run rate for every datapoint in the mode, unless the datapoint says `low_bandwidth: keep`. It is an average, not a minimum gap: after a quiet spell two samples may go out close together, and over any longer window the rate holds." }),
1926
+ camera: lowBandwidthCamera.optional().meta({
1927
+ description: "What happens to a running stream in the mode. New streams are refused either way.",
1928
+ enumDescriptions: describeValues(lowBandwidthCamera.options, {
1929
+ reduce: "A running stream is re-encoded at `camera_bitrate_kbps` and keeps running. A viewer sees a worse picture rather than none.",
1930
+ stop: "A running stream ends and the viewer is told why. The uplink is then free for datapoints, which is the right trade where video is the nice-to-have."
1931
+ })
1932
+ }),
1933
+ camera_bitrate_kbps: import_zod5.z.number().int().min(50).max(2e4).optional().meta({ description: "Bitrate applied to running streams under `reduce`." })
1934
+ }).superRefine((s, ctx) => {
1935
+ if (s.enter_lag_ms !== void 0 && s.exit_lag_ms !== void 0 && s.exit_lag_ms > s.enter_lag_ms)
1936
+ ctx.addIssue({ code: "custom", path: ["exit_lag_ms"], message: "low_bandwidth.exit_lag_ms must be at or below enter_lag_ms" });
1937
+ });
1837
1938
  var FLEETLESS_FORMAT_VERSION = 1;
1838
1939
  var capped = (entry, max, what) => slugKeyed(entry).refine((m) => Object.keys(m).length <= max, { message: `at most ${max} ${what}` });
1839
1940
  var robotConfigDoc = strictObject({
@@ -1845,28 +1946,42 @@ var robotConfigDoc = strictObject({
1845
1946
  defaultSnippets: [underSlug("${1:drive}", SHARED_MESSAGE_SNIPPET)]
1846
1947
  }).optional(),
1847
1948
  datapoints: capped(datapointConfig, 200, "datapoints").meta({
1848
- description: "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all four are refused when the document is validated.",
1949
+ description: "Values the robot publishes, each one field of one topic or a whole topic, and **never several topics**. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all three are refused when the document is validated.",
1849
1950
  defaultSnippets: [
1850
1951
  underSlug("${1:battery_voltage}", DATAPOINT_SNIPPET),
1851
1952
  underSlug("${1:battery}", NUMERIC_DATAPOINT_SNIPPET)
1852
1953
  ]
1853
1954
  }).optional(),
1854
1955
  actions: capped(actionConfig, 200, "actions").meta({
1855
- description: "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all four are refused when the document is validated.",
1956
+ description: "Things the robot does on request that take time, each reported as a job with progress. **At most one job runs per action slug**: a second call is refused `busy`, and every observer of that slug watches the same job. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all three are refused when the document is validated.",
1856
1957
  defaultSnippets: [underSlug("${1:navigate}", ACTION_SNIPPET)]
1857
1958
  }).optional(),
1858
1959
  services: capped(serviceConfig, 200, "services").meta({
1859
- description: "ROS service calls the robot answers \u2014 one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all four are refused when the document is validated.",
1960
+ description: "ROS service calls the robot answers \u2014 one request, one reply. Unlike an action a service reports **no progress** and the call returns with its result already on the job, so there is nothing left to observe; a second concurrent call is still refused `busy`, exactly as for an action. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all three are refused when the document is validated.",
1860
1961
  defaultSnippets: [underSlug("${1:reset_odometry}", SERVICE_SNIPPET)]
1861
1962
  }).optional(),
1862
1963
  publishers: capped(publisherConfig, 200, "publishers").meta({
1863
- description: "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all four are refused when the document is validated.",
1964
+ description: "Topics clients may send to, and where the format's whole safety story lives. The `message` template fixes every value a caller cannot change, and **`failsafe` is required**: once a client falls silent the bridge sends the failsafe message itself, so an operator whose window closed does not leave a robot driving. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all three are refused when the document is validated.",
1864
1965
  defaultSnippets: [underSlug("${1:drive}", PUBLISHER_SNIPPET)]
1865
1966
  }).optional(),
1866
1967
  cameras: capped(cameraConfig, 50, "cameras").meta({
1867
- description: "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures \u2014 they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state`, `robot_details` and `bridge_pressure` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all four are refused when the document is validated.",
1968
+ description: "Video the robot streams, and the still frames the cloud serves from it. `width`, `height`, `fps` and `bitrate_kbps` are what **the bridge produces before sending**, not what the camera captures \u2014 they live in the configuration rather than in a viewer's request precisely so that no viewer can make a robot send more. Keys are slugs, one namespace across all five exposure sections, which is what lets a role grant say `{robot, slug}` without naming a kind; `bridge_state` and `robot_details` are built-in, and `history` is reserved because `GET \u2026/jobs/history` would shadow an action of that name; all three are refused when the document is validated.",
1868
1969
  defaultSnippets: [underSlug("${1:front}", CAMERA_SNIPPET)]
1869
- }).optional()
1970
+ }).optional(),
1971
+ low_bandwidth: lowBandwidthSection.optional().meta({
1972
+ description: "Overrides for the bridge's low-bandwidth mode; see the section schema.",
1973
+ /**
1974
+ * The body is the three keys a developer actually reaches for — when the
1975
+ * mode engages, how hard it caps, and what it does to video. The five
1976
+ * timing keys stay out: they exist to be tuned once against a measured
1977
+ * link, and a skeleton that pre-fills them reads as a recommendation.
1978
+ */
1979
+ defaultSnippets: [{
1980
+ label: "low-bandwidth mode, tuned",
1981
+ description: "Enters after ten seconds above two seconds of lag, caps every datapoint to 1 Hz and lets running streams continue at a reduced bitrate.",
1982
+ body: { enter_lag_ms: 2e3, datapoint_max_hz: 1, camera: "reduce" }
1983
+ }]
1984
+ })
1870
1985
  });
1871
1986
  var validationIssue = import_zod5.z.object({
1872
1987
  path: import_zod5.z.string().min(1),
@@ -1893,7 +2008,7 @@ var configState = import_zod5.z.object({
1893
2008
  applied_errors: import_zod5.z.array(applyError).nullable()
1894
2009
  });
1895
2010
 
1896
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/introspection.js
2011
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/introspection.js
1897
2012
  var import_zod6 = require("zod");
1898
2013
  var rosGraphEntry = import_zod6.z.object({
1899
2014
  name: rosName,
@@ -1932,7 +2047,7 @@ var typeDefinition = import_zod6.z.discriminatedUnion("kind", [
1932
2047
  })
1933
2048
  ]);
1934
2049
 
1935
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/jobs.js
2050
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/jobs.js
1936
2051
  var import_zod7 = require("zod");
1937
2052
  var jobState = import_zod7.z.enum(["running", "succeeded", "failed", "cancelled", "lost"]);
1938
2053
  var job = import_zod7.z.object({
@@ -2150,7 +2265,8 @@ var jobRunSummary = import_zod7.z.object({
2150
2265
  since_ms: import_zod7.z.number().int().nonnegative()
2151
2266
  });
2152
2267
 
2153
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/protocol.js
2268
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/protocol.js
2269
+ var DAY_MS = 24 * 60 * 60 * 1e3;
2154
2270
  var MAX_PATIENCE_MS = 12e4;
2155
2271
  var MIN_PATIENCE_MS = 1e3;
2156
2272
  var activeJob = import_zod8.z.object({
@@ -2186,7 +2302,20 @@ var bridgeHello = import_zod8.z.object({
2186
2302
  });
2187
2303
  var cloudHelloOk = import_zod8.z.object({
2188
2304
  type: import_zod8.z.literal("hello_ok"),
2189
- robot_id: import_zod8.z.uuid()
2305
+ robot_id: import_zod8.z.uuid(),
2306
+ protocol: import_zod8.z.object({
2307
+ status: import_zod8.z.enum(["current", "deprecated"]).meta({
2308
+ description: "`current` or `deprecated` \u2014 never `unsupported`, which is a `hello_error`."
2309
+ }),
2310
+ sunset_at: import_zod8.z.iso.date().nullable().meta({
2311
+ description: "ISO date a deprecated version stops being served; `null` when current."
2312
+ })
2313
+ }).optional().meta({ description: "The cloud's verdict on the announced protocol version; absent from an older cloud." }),
2314
+ bridge: import_zod8.z.object({
2315
+ latest_version: import_zod8.z.string().min(1).meta({
2316
+ description: "The newest published fleetless-bridge package version, for the bridge's own upgrade hint."
2317
+ })
2318
+ }).optional().meta({ description: "What the cloud knows about bridge packages; absent from an older cloud." })
2190
2319
  });
2191
2320
  var cloudHelloError = import_zod8.z.object({
2192
2321
  type: import_zod8.z.literal("hello_error"),
@@ -2197,16 +2326,25 @@ var datapointFrame = import_zod8.z.object({
2197
2326
  type: import_zod8.z.literal("datapoint"),
2198
2327
  slug,
2199
2328
  value: import_zod8.z.unknown(),
2200
- timestamp_ms: import_zod8.z.number().int().nonnegative()
2329
+ timestamp_ms: import_zod8.z.number().int().nonnegative(),
2330
+ backfill: import_zod8.z.boolean().optional().meta({ description: "true when the sample was captured while the bridge was disconnected and is being replayed after the reconnect. The cloud keeps such a sample out of its lag measure; absent means live." })
2201
2331
  });
2202
2332
  var cloudPing = import_zod8.z.object({
2203
2333
  type: import_zod8.z.literal("ping"),
2204
- ts_ms: import_zod8.z.number().int().nonnegative()
2334
+ ts_ms: import_zod8.z.number().int().nonnegative(),
2335
+ latency_ms: import_zod8.z.number().nonnegative().nullable().meta({ description: "Round trip of the last pong in milliseconds; null before the first." }),
2336
+ lag_ms: import_zod8.z.number().nonnegative().nullable().meta({ description: "Datapoint lag over the link: median of the last five seconds minus the ten-minute minimum, in milliseconds; null until a sample exists, and null again whenever no live sample arrived in the last five seconds, because a stale median would be a lie." })
2205
2337
  });
2206
2338
  var bridgePong = import_zod8.z.object({
2207
2339
  type: import_zod8.z.literal("pong"),
2208
2340
  ts_ms: import_zod8.z.number().int().nonnegative()
2209
2341
  });
2342
+ var bridgeLinkMode = import_zod8.z.object({
2343
+ type: import_zod8.z.literal("link_mode"),
2344
+ low_bandwidth: import_zod8.z.boolean().meta({ description: "Whether the mode is active after this transition." }),
2345
+ reason: import_zod8.z.enum(["lag", "dwell", "forced", "recovered"]).meta({ description: "`lag`: the cloud-measured lag crossed the threshold; `dwell`: the bridge-measured queue dwell did; `forced`: `mode: on` or `off`; `recovered`: both measures stayed at or below the exit threshold." }),
2346
+ at_ms: import_zod8.z.number().int().nonnegative().meta({ description: "Bridge time of the transition, epoch milliseconds." })
2347
+ });
2210
2348
  var cloudConfig = import_zod8.z.object({
2211
2349
  type: import_zod8.z.literal("config"),
2212
2350
  version: import_zod8.z.number().int().nonnegative(),
@@ -2316,75 +2454,8 @@ var bridgeTypeDefinitions = import_zod8.z.object({
2316
2454
  });
2317
2455
  var bridgeState = import_zod8.z.object({
2318
2456
  online: import_zod8.z.boolean(),
2319
- latency_ms: import_zod8.z.number().nonnegative().nullable()
2320
- });
2321
- var bridgePressureTier = import_zod8.z.object({
2322
- sent: import_zod8.z.number().int().nonnegative(),
2323
- bytes: import_zod8.z.number().int().nonnegative(),
2324
- drops: import_zod8.z.number().int().nonnegative(),
2325
- high_water: import_zod8.z.number().int().nonnegative()
2326
- });
2327
- var bridgePressure = import_zod8.z.object({
2328
- link: import_zod8.z.object({
2329
- /** bytes/s the socket demonstrably drains, from sends >= 64 KiB
2330
- * only; null until the first large send of the session. */
2331
- rate_bps: import_zod8.z.number().nonnegative().nullable(),
2332
- /**
2333
- * the byte target snapshots are currently encoded to fit.
2334
- *
2335
- * `.nonnegative()`, not `.positive()`: the target is derived from
2336
- * `rate_bps`, and a link measured below 0.5 B/s floors to 0 here. A
2337
- * schema that rejects 0 does not prevent that link — it only makes the
2338
- * frame reporting it unparseable, and a console that cannot parse a
2339
- * pressure frame shows "no feed", i.e. reports a struggling robot as an
2340
- * *old* one. Zero is a legitimate reading and says something true.
2341
- */
2342
- snapshot_max_bytes: import_zod8.z.number().int().nonnegative()
2343
- }),
2344
- /**
2345
- * String keys "0".."5" because JSON has no integer keys. Counters are
2346
- * cumulative per session and reset on reconnect; clients window by
2347
- * differencing two samples.
2348
- *
2349
- * **What this schema does not decide:** it does not guarantee all six
2350
- * keys are present (`z.record` over the six literals is exhaustive in
2351
- * zod 4 — tested here, it required every key and rejected none, the
2352
- * opposite of what a partial sample needs — so this is a
2353
- * `.strictObject().partial()` over the same six literal keys instead, a
2354
- * deliberate deviation from the originally sketched `z.record` shape with
2355
- * the same runtime behaviour). A missing tier key reads as zeros; the
2356
- * schema names what it cannot decide rather than implying a completeness
2357
- * it cannot check.
2358
- */
2359
- tiers: import_zod8.z.strictObject({
2360
- "0": bridgePressureTier,
2361
- "1": bridgePressureTier,
2362
- "2": bridgePressureTier,
2363
- "3": bridgePressureTier,
2364
- "4": bridgePressureTier,
2365
- "5": bridgePressureTier
2366
- }).partial(),
2367
- video: import_zod8.z.object({
2368
- active_streams: import_zod8.z.number().int().nonnegative(),
2369
- bitrate_sum_kbps: import_zod8.z.number().int().nonnegative(),
2370
- /**
2371
- * The uplink budget the bridge was configured with
2372
- * (`FLEETLESS_UPLINK_KBPS`), or `null` when none was set.
2373
- *
2374
- * `.nonnegative()`, not `.positive()`: `FLEETLESS_UPLINK_KBPS=0` is a
2375
- * documented setting meaning "no video budget at all", and the bridge
2376
- * emits that 0 verbatim. `.positive()` made every frame from such a
2377
- * robot fail the console's `safeParse`, which renders an unparseable
2378
- * frame as "no pressure feed" — so the one robot that had *deliberately*
2379
- * turned video off was the one diagnosed as running a bridge too old to
2380
- * report pressure. A value the producer legitimately sends must parse;
2381
- * `null` is the only "not set" this field has.
2382
- */
2383
- uplink_kbps: import_zod8.z.number().int().nonnegative().nullable(),
2384
- override_kbps: import_zod8.z.number().int().nonnegative().nullable(),
2385
- video_budget_kbps: import_zod8.z.number().int().nonnegative().nullable(),
2386
- reserve_kbps: import_zod8.z.number().int().nonnegative()
2387
- })
2457
+ latency_ms: import_zod8.z.number().nonnegative().nullable(),
2458
+ low_bandwidth: import_zod8.z.boolean().meta({ description: "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported." })
2388
2459
  });
2389
2460
  var snapshotHeader = import_zod8.z.object({
2390
2461
  type: import_zod8.z.literal("snapshot"),
@@ -2548,11 +2619,11 @@ var bridgeCameraState = import_zod8.z.object({
2548
2619
  request_id: import_zod8.z.string().min(1).max(64).nullable()
2549
2620
  });
2550
2621
 
2551
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/config-issues.js
2622
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/config-issues.js
2552
2623
  var EXPOSURE_SECTIONS = ["datapoints", "actions", "services", "publishers", "cameras"];
2553
2624
  var EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
2554
2625
 
2555
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/rest.js
2626
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/rest.js
2556
2627
  var import_zod9 = require("zod");
2557
2628
  var robot = import_zod9.z.object({
2558
2629
  id: import_zod9.z.uuid().meta({
@@ -2578,6 +2649,21 @@ var createRobotResponse = import_zod9.z.object({
2578
2649
  robot,
2579
2650
  token: robotToken
2580
2651
  });
2652
+ var robotTokenRotateResponse = import_zod9.z.object({
2653
+ token: robotToken.meta({
2654
+ description: "The robot's new bridge token. Returned exactly once; the previous token stops working at the bridge's next hello."
2655
+ })
2656
+ });
2657
+ var jointStatePutRequest = import_zod9.z.object({
2658
+ slug: slug.nullable().meta({
2659
+ description: "The datapoint to read joint positions from, or `null` to choose none. It must name a whole-message `sensor_msgs/msg/JointState` datapoint of the published configuration; anything else is a `validation_error` naming the rule."
2660
+ })
2661
+ });
2662
+ var jointStatePutResponse = import_zod9.z.object({
2663
+ joint_state_slug: slug.nullable().meta({
2664
+ description: "The stored mapping after the call, `null` when none is chosen. The same value `assetListResponse.joint_state_slug` carries."
2665
+ })
2666
+ });
2581
2667
  var exposureCounts = import_zod9.z.object({
2582
2668
  datapoints: import_zod9.z.number().int().nonnegative(),
2583
2669
  actions: import_zod9.z.number().int().nonnegative(),
@@ -2585,11 +2671,15 @@ var exposureCounts = import_zod9.z.object({
2585
2671
  publishers: import_zod9.z.number().int().nonnegative(),
2586
2672
  cameras: import_zod9.z.number().int().nonnegative()
2587
2673
  });
2674
+ var protocolStatusValue = import_zod9.z.enum(["current", "deprecated", "refused"]);
2588
2675
  var robotListItem = import_zod9.z.object({
2589
2676
  ...robot.shape,
2590
2677
  bridge_state: bridgeState,
2591
2678
  /** Required, not optional: "we did not look" and "it exposes nothing" must not render the same. */
2592
- exposes: exposureCounts
2679
+ exposes: exposureCounts,
2680
+ protocol_status: protocolStatusValue.optional().meta({
2681
+ description: "Where this robot's bridge stands against the protocol window: `current`, `deprecated` (still served, sunset date on the detail), or `refused` (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as `current`."
2682
+ })
2593
2683
  });
2594
2684
  var robotListResponse = import_zod9.z.object({
2595
2685
  robots: import_zod9.z.array(robotListItem)
@@ -2606,6 +2696,15 @@ var datapointValue = import_zod9.z.object({
2606
2696
  var robotDetailResponse = import_zod9.z.object({
2607
2697
  ...robotListItem.shape,
2608
2698
  bridge_version: import_zod9.z.string().min(1).nullable(),
2699
+ protocol_version: import_zod9.z.number().int().positive().nullable().optional().meta({
2700
+ description: "The protocol version the bridge announced in its last accepted hello; `null` before the first. Absent from a cloud older than 0.21.0."
2701
+ }),
2702
+ protocol: import_zod9.z.object({
2703
+ status: protocolStatusValue.meta({ description: "Same values as `protocol_status`." }),
2704
+ sunset_at: import_zod9.z.iso.date().nullable().meta({
2705
+ description: "ISO date the announced version stops being served; `null` when current or unknown."
2706
+ })
2707
+ }).optional().meta({ description: "The window verdict for `protocol_version`." }),
2609
2708
  /**
2610
2709
  * Cleared (set back to null) by the next successful hello from this
2611
2710
  * robot's bridge — a warning that outlives the condition it warns
@@ -2659,7 +2758,7 @@ var fetchTypesResponse = import_zod9.z.object({
2659
2758
  var datapointDescriptor = import_zod9.z.object({
2660
2759
  slug: slug.meta({ description: "The name a client reads this datapoint by." }),
2661
2760
  builtin: import_zod9.z.boolean().meta({
2662
- description: "`true` for the datapoints every robot has \u2014 `bridge_state`, `robot_details` and `bridge_pressure` \u2014 and `false` for everything the published configuration adds."
2761
+ description: "`true` for the datapoints every robot has \u2014 `bridge_state` and `robot_details` \u2014 and `false` for everything the published configuration adds."
2663
2762
  }),
2664
2763
  unit: import_zod9.z.string().nullable().meta({
2665
2764
  description: "The unit the value carries **after** any scale and offset, shown beside the number so nobody has to guess whether `15` means percent, volts or minutes. `null` when the configuration names none."
@@ -2677,7 +2776,7 @@ var datapointDescriptor = import_zod9.z.object({
2677
2776
  });
2678
2777
  var datapointListResponse = import_zod9.z.object({
2679
2778
  datapoints: import_zod9.z.array(datapointDescriptor).meta({
2680
- description: "Everything a client may read on this robot: the three built-ins, plus every datapoint the published configuration exposes and the caller's role grants."
2779
+ description: "Everything a client may read on this robot: the two built-ins, plus every datapoint the published configuration exposes and the caller's role grants."
2681
2780
  })
2682
2781
  });
2683
2782
  var robotDetailsDoc = import_zod9.z.record(import_zod9.z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/), import_zod9.z.union([import_zod9.z.string().max(4096), import_zod9.z.number(), import_zod9.z.boolean(), import_zod9.z.array(import_zod9.z.unknown()), import_zod9.z.record(import_zod9.z.string(), import_zod9.z.unknown())]));
@@ -3031,22 +3130,25 @@ var robotDeletionSummary = import_zod9.z.object({
3031
3130
  bytes_freed: import_zod9.z.number().int().nonnegative(),
3032
3131
  cameras: import_zod9.z.array(slug),
3033
3132
  /**
3034
- * Assets destroyed with the robot, and **`asset_bytes_freed` is what
3035
- * this org actually gets back** — not the sum of the assets' sizes.
3133
+ * Assets destroyed with the robot, and **`asset_bytes_freed` is what the
3134
+ * robot's own store gives back** — every distinct mesh or texture blob it
3135
+ * holds, counted once, URDF excluded.
3036
3136
  *
3037
- * Storage is content-addressed, so a mesh two robots share survives the
3038
- * deletion of one of them and frees nothing. Reporting the total would tell
3039
- * a developer they are about to recover 400 MB and hand back 4, on the one
3040
- * screen whose entire justification is naming what an irreversible click
3041
- * destroys. Same reasoning that keeps `cameras` out of `slug_count`: this
3042
- * summary is read aloud to a human, and a number that is nearly right is
3043
- * worse here than an absent one.
3137
+ * Storage is content-addressed, but the store and its 1 GB ceiling are now
3138
+ * per robot: a blob another robot also references stays in the object
3139
+ * store but is still credited here, because each robot's counter carries
3140
+ * it regardless of what else points at the same bytes. Same reasoning that
3141
+ * keeps `cameras` out of `slug_count`: this summary is read aloud to a
3142
+ * human, and a number that is nearly right is worse here than an absent
3143
+ * one.
3044
3144
  *
3045
3145
  * `asset_count` is the plain count of the robot's asset rows, all of which
3046
3146
  * do go away.
3047
3147
  */
3048
3148
  asset_count: import_zod9.z.number().int().nonnegative(),
3049
- asset_bytes_freed: import_zod9.z.number().int().nonnegative(),
3149
+ asset_bytes_freed: import_zod9.z.number().int().nonnegative().meta({
3150
+ description: "What the robot's store gives back: every distinct mesh or texture blob it holds, counted once, URDF excluded; a blob another robot also references stays in the object store but is still credited here, because each robot's counter carries it."
3151
+ }),
3050
3152
  /**
3051
3153
  * How many rows of run history go with the robot — every recorded
3052
3154
  * invocation of one of its actions or services, up to
@@ -3185,39 +3287,13 @@ var orgQuotas = import_zod9.z.object({
3185
3287
  max_end_users: import_zod9.z.number().int().positive(),
3186
3288
  max_retention_bytes: import_zod9.z.number().int().nonnegative(),
3187
3289
  max_retention_writes_per_minute: import_zod9.z.number().int().nonnegative(),
3188
- max_realtime_connections: import_zod9.z.number().int().positive(),
3189
- /**
3190
- * Asset storage — **its own dial, not part of `max_retention_bytes`.** A
3191
- * sync grows storage in jumps and time series grow steadily; one dial would
3192
- * let the first crowd out the second, and the org that hit its limit would be
3193
- * told to look at the wrong thing.
3194
- *
3195
- * **Counted per distinct blob *this org references* — not per asset row, and
3196
- * not per object the platform stores on its behalf.** The two readings are
3197
- * indistinguishable from the number alone and a customer is entitled to know
3198
- * which one they are being charged for.
3199
- *
3200
- * Within an org, sharing is free: two robots referencing the same mesh cost
3201
- * one copy, which is what dedup means to a customer, and anything else
3202
- * charges an org twice for a fleet of identical robots — the normal case.
3203
- *
3204
- * **Across orgs, sharing is not free.** Storage stays globally
3205
- * content-addressed (one object per sha256; that efficiency is real), but
3206
- * accounting is per-org: an org is charged for each distinct blob it
3207
- * references and credited when its own last reference goes, whether or not
3208
- * the blob survives for somebody else. Global refcounting would make the
3209
- * first org to sync a blob pay for it forever while every later org stored it
3210
- * free — a quota evadable by anyone whose mesh someone else had already
3211
- * uploaded, and an org's own number would depend on who got there first.
3212
- */
3213
- max_asset_storage_bytes: import_zod9.z.number().int().nonnegative()
3290
+ max_realtime_connections: import_zod9.z.number().int().positive()
3214
3291
  });
3215
3292
  var orgQuotaUsageCounts = import_zod9.z.object({
3216
3293
  max_robots: import_zod9.z.number().int().nonnegative(),
3217
3294
  max_apps: import_zod9.z.number().int().nonnegative(),
3218
3295
  max_end_users: import_zod9.z.number().int().nonnegative(),
3219
3296
  max_retention_bytes: import_zod9.z.number().int().nonnegative(),
3220
- max_asset_storage_bytes: import_zod9.z.number().int().nonnegative(),
3221
3297
  max_retention_writes_per_minute: import_zod9.z.number().int().nonnegative(),
3222
3298
  max_realtime_connections: import_zod9.z.number().int().nonnegative()
3223
3299
  }).partial();
@@ -3312,13 +3388,13 @@ var slugUsageResponse = import_zod9.z.object({
3312
3388
  alert_count: import_zod9.z.number().int().nonnegative()
3313
3389
  });
3314
3390
 
3315
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/realtime.js
3391
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/realtime.js
3316
3392
  var import_zod14 = require("zod");
3317
3393
 
3318
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/client-auth.js
3394
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/client-auth.js
3319
3395
  var import_zod13 = require("zod");
3320
3396
 
3321
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/apps.js
3397
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/apps.js
3322
3398
  var import_zod10 = require("zod");
3323
3399
  var appIdentifier = slug;
3324
3400
  var app = import_zod10.z.object({
@@ -3500,10 +3576,10 @@ var rolePermissions = import_zod10.z.object({
3500
3576
  })
3501
3577
  });
3502
3578
 
3503
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/app-users.js
3579
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/app-users.js
3504
3580
  var import_zod12 = require("zod");
3505
3581
 
3506
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/identity.js
3582
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/identity.js
3507
3583
  var import_zod11 = require("zod");
3508
3584
  var password = import_zod11.z.string().min(12).max(256);
3509
3585
  var USER_DISPLAY_NAME_MAX = 120;
@@ -3665,7 +3741,7 @@ var authMeResponse = import_zod11.z.object({ org, user: fleetlessUser });
3665
3741
  var patchOrgRequest = import_zod11.z.object({ name: import_zod11.z.string().min(1).max(120) }).strict();
3666
3742
  var patchAuthMeRequest = import_zod11.z.object({ display_name: import_zod11.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable() }).strict();
3667
3743
 
3668
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/app-users.js
3744
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/app-users.js
3669
3745
  var APP_USER_DISPLAY_NAME_MAX = 120;
3670
3746
  var providerSlug = import_zod12.z.string().max(40).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "must be lowercase and hyphen-separated, starting with a letter");
3671
3747
  var appUserStatus = import_zod12.z.enum(["pending_verification", "active", "blocked"]);
@@ -3916,7 +3992,7 @@ var mailOutcome = import_zod12.z.object({
3916
3992
  })
3917
3993
  });
3918
3994
 
3919
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/client-auth.js
3995
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/client-auth.js
3920
3996
  var clientLoginRequest = import_zod13.z.object({
3921
3997
  app_identifier: appIdentifier.meta({
3922
3998
  description: "The app being logged in to: its globally unique, lowercase, underscore-separated identifier, chosen by the developer at creation. There is no organisation context at login, so this is what decides which app the credentials are checked for."
@@ -4105,7 +4181,7 @@ var clientIdentity = import_zod13.z.object({
4105
4181
  })
4106
4182
  });
4107
4183
 
4108
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/realtime.js
4184
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/realtime.js
4109
4185
  var clientAuth = import_zod14.z.object({
4110
4186
  type: import_zod14.z.literal("auth"),
4111
4187
  token: import_zod14.z.string().min(1)
@@ -4383,7 +4459,7 @@ var orgEventDropped = import_zod14.z.object({
4383
4459
  dropped: import_zod14.z.number().int().positive()
4384
4460
  }).strict();
4385
4461
 
4386
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/client-robots.js
4462
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/client-robots.js
4387
4463
  var import_zod15 = require("zod");
4388
4464
  var clientRobotListItem = import_zod15.z.object({
4389
4465
  ...robot.shape,
@@ -4400,7 +4476,7 @@ var clientRobotListResponse = import_zod15.z.object({
4400
4476
  })
4401
4477
  });
4402
4478
 
4403
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/audit.js
4479
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/audit.js
4404
4480
  var import_zod16 = require("zod");
4405
4481
  var auditActor = import_zod16.z.object({
4406
4482
  kind: import_zod16.z.enum(["developer", "end_user", "app_user", "server_key", "bridge"]),
@@ -4530,7 +4606,7 @@ var auditListResponse = import_zod16.z.object({
4530
4606
  next_cursor: import_zod16.z.number().int().positive().nullable()
4531
4607
  });
4532
4608
 
4533
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/errors.js
4609
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/errors.js
4534
4610
  var import_zod17 = require("zod");
4535
4611
  var apiError = import_zod17.z.object({
4536
4612
  code: import_zod17.z.string().min(1),
@@ -4547,7 +4623,7 @@ var parameterInvalidDetails = import_zod17.z.object({
4547
4623
  violations: import_zod17.z.array(parameterViolation).min(1)
4548
4624
  });
4549
4625
 
4550
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/oauth.js
4626
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/oauth.js
4551
4627
  var import_zod18 = require("zod");
4552
4628
  var oauthErrorCode = import_zod18.z.enum([
4553
4629
  "invalid_request",
@@ -4614,7 +4690,7 @@ var dynamicClientRegistrationRequest = import_zod18.z.object({
4614
4690
  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 \u2014 mandatory PKCE (`S256`) is the defence."
4615
4691
  }),
4616
4692
  grant_types: import_zod18.z.array(import_zod18.z.enum(["authorization_code", "refresh_token"])).optional().meta({
4617
- description: "Accepted for conformance with RFC 7591 and then **ignored**. What comes back is what was actually granted, which \xA73.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."
4693
+ 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 (\xA73.2.1) rather than what was asked."
4618
4694
  }),
4619
4695
  response_types: import_zod18.z.array(import_zod18.z.enum(["code"])).optional().meta({
4620
4696
  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."
@@ -4636,7 +4712,7 @@ var dynamicClientRegistrationResponse = import_zod18.z.object({
4636
4712
  description: "The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else."
4637
4713
  }),
4638
4714
  grant_types: import_zod18.z.array(import_zod18.z.string()).meta({
4639
- description: 'The grants this client may use. Always exactly `["authorization_code"]` \u2014 a client that asked for `refresh_token` is registered and told here that it did not get one, which is the substitution RFC 7591 \xA73.2.1 permits.'
4715
+ description: 'The grants this client may use. Always exactly `["authorization_code", "refresh_token"]` \u2014 an exchange mints a refresh token and the token endpoint rotates it.'
4640
4716
  }),
4641
4717
  response_types: import_zod18.z.array(import_zod18.z.string()).meta({
4642
4718
  description: "The response types this client may ask for: `code`."
@@ -4651,9 +4727,9 @@ var dynamicClientRegistrationResponse = import_zod18.z.object({
4651
4727
  description: "Always `0`, which is RFC 7591's way of saying the client secret never expires \u2014 there is none. The **registration** itself does expire: a self-registered client that never completes a flow is an unauthenticated write somebody left behind."
4652
4728
  })
4653
4729
  });
4654
- var oauthTokenRequest = import_zod18.z.object({
4730
+ var oauthCodeTokenRequest = import_zod18.z.object({
4655
4731
  grant_type: import_zod18.z.literal("authorization_code").meta({
4656
- description: "Always `authorization_code`: this request exchanges the code from the authorize redirect for tokens. Any other value \u2014 `refresh_token` included \u2014 is `unsupported_grant_type`, refused before the code is looked up."
4732
+ description: "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
4657
4733
  }),
4658
4734
  code: import_zod18.z.string().min(1).max(500).meta({
4659
4735
  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."
@@ -4673,6 +4749,25 @@ var oauthTokenRequest = import_zod18.z.object({
4673
4749
  }).meta({
4674
4750
  description: "RFC 6749 \xA74.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per \xA74.1.3, though the server accepts a JSON body too."
4675
4751
  });
4752
+ var oauthRefreshTokenRequest = import_zod18.z.object({
4753
+ grant_type: import_zod18.z.literal("refresh_token").meta({
4754
+ 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."
4755
+ }),
4756
+ refresh_token: import_zod18.z.string().min(1).max(500).meta({
4757
+ 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."
4758
+ }),
4759
+ client_id: import_zod18.z.string().min(1).max(200).meta({
4760
+ description: "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
4761
+ }),
4762
+ resource: import_zod18.z.url().optional().meta({
4763
+ 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."
4764
+ })
4765
+ }).meta({
4766
+ description: "RFC 6749 \xA76'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."
4767
+ });
4768
+ var oauthTokenRequest = import_zod18.z.discriminatedUnion("grant_type", [oauthCodeTokenRequest, oauthRefreshTokenRequest]).meta({
4769
+ 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."
4770
+ });
4676
4771
  var oauthTokenResponse = import_zod18.z.object({
4677
4772
  access_token: import_zod18.z.string().min(1).meta({
4678
4773
  description: "The bearer token. It is the same token the client login mints \u2014 only the envelope differs, because an RFC-compliant client parses this one and knows nothing about Fleetless."
@@ -4684,7 +4779,7 @@ var oauthTokenResponse = import_zod18.z.object({
4684
4779
  description: "How long the access token is valid, in **seconds**, per RFC 6749 \xA75.1. Not a timestamp, and not milliseconds."
4685
4780
  }),
4686
4781
  refresh_token: import_zod18.z.string().min(1).optional().meta({
4687
- description: "The refresh token, when one was issued. It rotates on every use."
4782
+ 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 \u2014 a block, a password change, a withdrawn consent. The console's own OAuth portal issues none."
4688
4783
  }),
4689
4784
  scope: import_zod18.z.string().max(500).optional().meta({
4690
4785
  description: "The scopes the issued token actually carries, space-separated."
@@ -4707,7 +4802,7 @@ var authorizationServerMetadata = import_zod18.z.object({
4707
4802
  description: "The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1."
4708
4803
  }),
4709
4804
  grant_types_supported: import_zod18.z.array(import_zod18.z.enum(["authorization_code", "refresh_token"])).meta({
4710
- description: "The grants this server offers. OAuth 2.1 removes the implicit and password grants, so neither appears here."
4805
+ description: "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
4711
4806
  }),
4712
4807
  code_challenge_methods_supported: import_zod18.z.array(codeChallengeMethod).meta({
4713
4808
  description: "The PKCE challenge methods accepted: `S256` only. `plain` is not offered \u2014 a challenge equal to its verifier defends against nothing, and offering it would make a downgrade negotiable."
@@ -4768,7 +4863,7 @@ var oauthAuthorizeQuery = import_zod18.z.object({
4768
4863
  description: "The authorization request an MCP client sends, per RFC 6749 \xA74.1.1 with mandatory PKCE. The handler reads it parameter by parameter rather than through one parse, because the answers differ: `client_id` and `redirect_uri` are refused flat, with no redirect, since until both are confirmed there is no trusted target to bounce a browser to, and everything after them is reported to the client's own callback as query parameters."
4769
4864
  });
4770
4865
 
4771
- // node_modules/.pnpm/@fleetless+contracts@1.1.0/node_modules/@fleetless/contracts/dist/routes.js
4866
+ // node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/routes.js
4772
4867
  var MCP_APP = MCP_APP_PATHS(":appIdentifier");
4773
4868
  var APP_IDENTIFIER = {
4774
4869
  name: "appIdentifier",
@@ -6041,7 +6136,7 @@ var ROUTES = [
6041
6136
  response: dynamicClientRegistrationResponse,
6042
6137
  errors: ["rate_limited"],
6043
6138
  transport: "http",
6044
- notes: "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because \xA73.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with \xA73.1 rather than a gap in it \u2014 a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which \xA73.2.1 allows a server to substitute \u2014 this authorization server issues `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`."
6139
+ notes: "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because \xA73.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with \xA73.1 rather than a gap in it \u2014 a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which \xA73.2.1 allows a server to substitute \u2014 this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`."
6045
6140
  },
6046
6141
  {
6047
6142
  method: "GET",
@@ -6167,7 +6262,7 @@ var ROUTES = [
6167
6262
  response: oauthTokenResponse,
6168
6263
  errors: [],
6169
6264
  transport: "http",
6170
- notes: "Only `authorization_code` is supported \u2014 there is no refresh grant here, so a session ends when its token expires and the client signs in again. Refusals are RFC 6749 \xA75.2's `oauthError`, so this route emits none of the codes in this reference. The response carries no `refresh_token`; the shape is the same `oauthTokenResponse` the app flow answers, whose refresh field is optional. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for."
6265
+ notes: "`authorization_code` mints an `mcp_session` access token bound to the central resource and a refresh token; `refresh_token` rotates that pair, and the presented refresh token is consumed \u2014 a second presentation revokes the session, as on `/api/auth/refresh`. The refresh token lives ninety days from its last use and is bound to the `client_id` it was issued to. A refresh re-reads the Fleetless user, so a removed account cannot refresh. Refusals are RFC 6749 \xA75.2's `oauthError`, so this route emits none of the codes in this reference. The code is single-use, PKCE-verified, and its `resource` must match the audience it was authorized for; a `resource` on a refresh must match the session's audience, and is checked before the token is consumed."
6171
6266
  },
6172
6267
  /* ------------------------------- developer auth (the console\'s OAuth portal) */
6173
6268
  {
@@ -6458,7 +6553,7 @@ var ROUTES = [
6458
6553
  response: dynamicClientRegistrationResponse,
6459
6554
  errors: ["rate_limited", "not_found"],
6460
6555
  transport: "http",
6461
- notes: "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` \u2014 one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: \xA73.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which \xA73.2.1 allows \u2014 `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from \u2014 a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not."
6556
+ notes: "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` \u2014 one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: \xA73.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which \xA73.2.1 allows \u2014 this authorization server grants `authorization_code` and `refresh_token` to every registration. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from \u2014 a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not."
6462
6557
  },
6463
6558
  {
6464
6559
  method: "GET",
@@ -6494,7 +6589,7 @@ var ROUTES = [
6494
6589
  response: oauthTokenResponse,
6495
6590
  errors: [],
6496
6591
  transport: "http",
6497
- notes: "Only `authorization_code`, PKCE-verified and single-use. There is no refresh grant here either, so a session ends when its token expires and the client signs in again; the shape is the same `oauthTokenResponse` the central endpoint answers, whose refresh field is optional and stays empty. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it \u2014 that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 \xA75.2's `oauthError`, so this route emits none of the codes in this reference \u2014 including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash."
6592
+ notes: "`authorization_code`, PKCE-verified and single-use, and `refresh_token`, which rotates the pair the exchange minted; the refresh token lives ninety days from its last use, is bound to its client and to this app, and a refresh re-reads the app user's status and their standing consent to the client, so a block or a withdrawn consent ends the session at its next refresh at the latest. **The `aud` is this app's endpoint URL on the canonical public base**, and the code's `resource` must match it \u2014 that is the whole of what stops a token minted for one app being spent at another's endpoint. \n\n**Every refusal is RFC 6749 \xA75.2's `oauthError`, so this route emits none of the codes in this reference \u2014 including the ones about the app.** An unknown identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The difference is who reads the answer: authorize is walked by a browser and its refusal is read by a person, while this endpoint is called by a client's own code in the middle of a flow, and handing that code an envelope its OAuth library cannot parse turns a clean refusal into an unexplained crash."
6498
6593
  },
6499
6594
  /* -------------------------------------------------- app-user (client) auth */
6500
6595
  {
@@ -6879,6 +6974,42 @@ var ROUTES = [
6879
6974
  transport: "http",
6880
6975
  notes: "`token` is the only moment the raw bridge token exists outside the caller's hands \u2014 the cloud stores a hash, so nothing can read it back and a caller who loses it rotates rather than recovers. Audited: this mints a credential that can speak for the org from anywhere, and the event carries no `details`, because the one interesting value here is the token. `max_robots` is checked before anything is created, which is only safe because robot deletion exists."
6881
6976
  },
6977
+ {
6978
+ method: "POST",
6979
+ path: "/api/robots/:id/token/rotate",
6980
+ section: "robots",
6981
+ summary: "Mints a new bridge token for the robot and invalidates the old one.",
6982
+ audience: "developer",
6983
+ auth: "developer",
6984
+ rateLimited: false,
6985
+ ownerTier: true,
6986
+ status: 201,
6987
+ params: [{ name: "id", description: "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`." }],
6988
+ query: null,
6989
+ request: null,
6990
+ response: robotTokenRotateResponse,
6991
+ errors: [...DEVELOPER_GUARD, "tier_required", "invalid_uuid", "not_found"],
6992
+ transport: "http",
6993
+ notes: "Owner tier, behind the org-scoped lookup, so a developer-tier admin sees the `404` a stranger would for a robot outside their org rather than a tier refusal that confirms the id exists. `token` is the only moment the new secret exists outside the caller's hands \u2014 the cloud stores a hash \u2014 so a caller who loses it rotates again. Audited as `robot.token_rotated`, with no `details`: the one interesting value here is the token. \n\n**It stops the bridge that is connected right now.** The old secret is gone the instant the hash is replaced, so the cloud closes that socket with `CLOSE_TOKEN_ROTATED` rather than leaving a bridge speaking on a credential nothing would accept again. A bridge that does not know the code reconnects and is refused at hello as `invalid_token`, which is the honest answer and ends the same way. **The robot is offline until somebody puts the new token on it** \u2014 this is a deliberate interruption, not a background rekey, and a fleet cannot be rotated without a visit to each robot."
6994
+ },
6995
+ {
6996
+ method: "PUT",
6997
+ path: "/api/robots/:id/urdf/joint-state",
6998
+ section: "robots",
6999
+ summary: "Chooses the datapoint whose joint positions move the robot's URDF, or clears it.",
7000
+ audience: "developer",
7001
+ auth: "developer",
7002
+ rateLimited: false,
7003
+ ownerTier: false,
7004
+ status: 200,
7005
+ params: [{ name: "id", description: "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`." }],
7006
+ query: null,
7007
+ request: jointStatePutRequest,
7008
+ response: jointStatePutResponse,
7009
+ errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error"],
7010
+ transport: "http",
7011
+ notes: '**What qualifies**: a datapoint of the **published** configuration whose ROS type is `sensor_msgs/msg/JointState` and which carries no `field` \u2014 the whole message, because positions and names arrive together and a single extracted field is half of a pose. Anything else is a `validation_error` naming that rule rather than a stored mapping that renders a battery reading as a robot. `{ "slug": null }` clears it, which is why the field is required and nullable rather than optional. \n\n**The mapping cannot outlive what it points at.** Every successful publish re-checks it against the new document and clears it when it no longer qualifies, recording `robot.joint_state_cleared` with the version that did it; a slug rename rewrites it like every other reference the editor already rewrites; deleting the robot takes it along. Every write through this route \u2014 a slug or `null` \u2014 is on the record too, as `robot.joint_state_set` with the actor and the slug, so a clear a person made is never mistaken for one a publish made. The stored value reads back on `GET /api/robots/:id/assets` as `joint_state_slug`, so a renderer fetches the URDF, the meshes and the mapping from one place.'
7012
+ },
6882
7013
  {
6883
7014
  method: "GET",
6884
7015
  path: "/api/robots",
@@ -7764,6 +7895,24 @@ var ROUTES = [
7764
7895
  transport: "http",
7765
7896
  notes: "Developer sessions only, like starting a sync: the guard admits three caller kinds and the handler answers `401 unauthorized` to the other two. A sync belonging to another robot reads exactly like one that never existed, which is why the robot is resolved first."
7766
7897
  },
7898
+ {
7899
+ method: "DELETE",
7900
+ path: "/api/robots/:id/assets",
7901
+ section: "assets",
7902
+ summary: "Empties a robot's asset store: every URDF, mesh and texture, gone at once.",
7903
+ audience: "client",
7904
+ auth: "developer_or_client",
7905
+ rateLimited: false,
7906
+ ownerTier: true,
7907
+ status: 200,
7908
+ params: [{ name: "id", description: "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`." }],
7909
+ query: null,
7910
+ request: null,
7911
+ response: assetsClearResponse,
7912
+ errors: [...CLIENT_GUARD, "tier_required", "invalid_uuid", "not_found", "busy"],
7913
+ transport: "http",
7914
+ notes: "The store's escape hatch: a full store is never a dead end, and this is the blunt third of the three answers to it \u2014 the URDF upload is exempt from the gate, reconcile after a sync already frees what the new URDF stopped referencing, and this route lets an Owner clear the robot outright. Owner tier, unconditionally, like starting a sync. Removes every asset of the robot and resets its store to `0`; the next sync fills it again. It does not touch the bridge's availability report \u2014 `urdf_available` still answers from the connected robot, unrelated to what this cloud happens to have stored. A clear while a sync is running is `409 busy` naming that sync's details, the same refusal starting a second sync gets, because deleting under a running upload would leave the store counter wrong."
7915
+ },
7767
7916
  /* ------------------------------------------------ org (quotas and fleet reads) */
7768
7917
  {
7769
7918
  method: "GET",
@@ -7888,9 +8037,9 @@ var ROUTES = [
7888
8037
  query: null,
7889
8038
  request: null,
7890
8039
  response: asset,
7891
- errors: ["unauthorized", "rate_limited", "asset_too_large", "validation_error", "not_found", "quota_exceeded", "bad_request"],
8040
+ errors: ["unauthorized", "rate_limited", "validation_error", "not_found", "quota_exceeded", "bad_request"],
7892
8041
  transport: "http",
7893
- notes: "The body is the **raw file bytes**, not JSON, so it has no request schema; everything about the file \u2014 its kind, its name, its sync id and its announced size \u2014 rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. The announced size is refused there too, before a single byte is read \u2014 it is an announcement and not a proof, so it only ever rejects early and never accepts early, and a body that lies small is still caught by the real length check. Past both, the server's own body limit answers a bare `413 bad_request` with neither ceiling nor size in it. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route."
8042
+ notes: "The body is the **raw file bytes**, not JSON, so it has no request schema; everything about the file \u2014 its kind, its name, its sync id and its announced size \u2014 rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. **Nothing is refused for its own size** \u2014 the robot's asset store is the only limit, so the announced size is checked there against `ROBOT_ASSET_STORE_BYTES` and a file with no room left answers `409 quota_exceeded` carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Past that, the server's own body limit answers a bare `413 bad_request` with none of those numbers in it. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The URDF itself is never refused for the store; only meshes and textures are charged against it."
7894
8043
  },
7895
8044
  /* ------------------------------------ realtime and bridge transports */
7896
8045
  {
@@ -8165,13 +8314,16 @@ function oidcErrorFromCallbackParams(params) {
8165
8314
  `The federated sign-in ended without a session (${known.data}). See ClientOidcErrorCode in @fleetless/contracts for what each code means, and branch on error.code rather than on this message.`
8166
8315
  );
8167
8316
  }
8168
- function serverKeyRefusal(method, needs) {
8317
+ function sessionlessRefusal(option, method, needs) {
8169
8318
  throw new FleetlessError(
8170
8319
  "invalid_option",
8171
- `auth.${method} is not available on a client constructed with a serverKey \u2014 ${needs}. Build a client with a tokenStore (an app user's own session) for this call.`
8320
+ `auth.${method} is not available on a client constructed with ${option === "serverKey" ? "a serverKey" : "a credentials source"} \u2014 ${needs}. Build a client with a tokenStore (an app user's own session) for this call.`
8172
8321
  );
8173
8322
  }
8174
- function createServerKeyAuth(http, appIdentifier2) {
8323
+ function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
8324
+ const serverKeyRefusal = (method, needs) => sessionlessRefusal(option, method, needs);
8325
+ const subject = option === "serverKey" ? "a server key" : "a supplied credential";
8326
+ const holder = option === "serverKey" ? "a server-key client" : "a client with a supplied credential";
8175
8327
  return {
8176
8328
  // **`register`, `resendVerification` and `requestPasswordReset` are
8177
8329
  // allowed here** — see `createPublicAuthCalls`. They are public routes
@@ -8180,25 +8332,25 @@ function createServerKeyAuth(http, appIdentifier2) {
8180
8332
  // already has.
8181
8333
  ...createPublicAuthCalls(http, appIdentifier2),
8182
8334
  async verifyEmail() {
8183
- serverKeyRefusal("verifyEmail", "the route answers a session and a server-key client has nowhere to store it, so the session would be silently discarded");
8335
+ serverKeyRefusal("verifyEmail", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
8184
8336
  },
8185
8337
  async login() {
8186
- serverKeyRefusal("login", "a server key IS the credential; there is nothing to exchange");
8338
+ serverKeyRefusal("login", `${subject} IS the credential; there is nothing to exchange`);
8187
8339
  },
8188
8340
  async logout() {
8189
- serverKeyRefusal("logout", "a server key holds no session to end");
8341
+ serverKeyRefusal("logout", `${subject} holds no session to end`);
8190
8342
  },
8191
8343
  async me() {
8192
8344
  return http.request("/api/client/me", {});
8193
8345
  },
8194
8346
  async changePassword() {
8195
- serverKeyRefusal("changePassword", "a server key has no password; rotate the key in the console instead");
8347
+ serverKeyRefusal("changePassword", `${subject} has no password${option === "serverKey" ? "; rotate the key in the console instead" : ""}`);
8196
8348
  },
8197
8349
  async confirmPasswordReset() {
8198
- serverKeyRefusal("confirmPasswordReset", "the route answers a session and a server-key client has nowhere to store it, so the session would be silently discarded");
8350
+ serverKeyRefusal("confirmPasswordReset", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
8199
8351
  },
8200
8352
  async acceptInvitation() {
8201
- serverKeyRefusal("acceptInvitation", "the route answers a session and a server-key client has nowhere to store it, so the session would be silently discarded");
8353
+ serverKeyRefusal("acceptInvitation", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
8202
8354
  },
8203
8355
  async listProviders() {
8204
8356
  const query = new URLSearchParams({ app_identifier: appIdentifier2 });
@@ -8218,16 +8370,16 @@ function createServerKeyAuth(http, appIdentifier2) {
8218
8370
  return http.request(`/api/client/mcp/interactions/${pathSegment(id)}`, {});
8219
8371
  },
8220
8372
  async approveMcpInteraction() {
8221
- serverKeyRefusal("approveMcpInteraction", "a consent is a person's decision, and a server key is not a person");
8373
+ serverKeyRefusal("approveMcpInteraction", `a consent is a person's decision, and ${subject} is not a person`);
8222
8374
  },
8223
8375
  async denyMcpInteraction() {
8224
- serverKeyRefusal("denyMcpInteraction", "a consent is a person's decision, and a server key is not a person");
8376
+ serverKeyRefusal("denyMcpInteraction", `a consent is a person's decision, and ${subject} is not a person`);
8225
8377
  },
8226
8378
  async listMcpGrants() {
8227
- serverKeyRefusal("listMcpGrants", "a server key never went through a consent screen, so it has no grants of its own");
8379
+ serverKeyRefusal("listMcpGrants", `${subject} never went through a consent screen, so it has no grants of its own`);
8228
8380
  },
8229
8381
  async revokeMcpGrant() {
8230
- serverKeyRefusal("revokeMcpGrant", "a server key never went through a consent screen, so it has no grants of its own");
8382
+ serverKeyRefusal("revokeMcpGrant", `${subject} never went through a consent screen, so it has no grants of its own`);
8231
8383
  }
8232
8384
  };
8233
8385
  }
@@ -8967,9 +9119,10 @@ var InMemoryTokenStore = class {
8967
9119
 
8968
9120
  // src/client.ts
8969
9121
  function createClient(options) {
8970
- if (options.serverKey !== void 0 && options.tokenStore !== void 0) {
9122
+ const chosen = ["tokenStore", "serverKey", "credentials"].filter((name) => options[name] !== void 0);
9123
+ if (chosen.length > 1) {
8971
9124
  throw new Error(
8972
- "createClient: pass either `tokenStore` (end-user login) or `serverKey` (a server-side caller), not both."
9125
+ `createClient: pass exactly one of \`tokenStore\` (end-user login), \`serverKey\` (a server-side caller) or \`credentials\` (a bearer the caller owns) \u2014 got ${chosen.join(" and ")}.`
8973
9126
  );
8974
9127
  }
8975
9128
  const config = Object.freeze({
@@ -8985,7 +9138,11 @@ function createClient(options) {
8985
9138
  let auth;
8986
9139
  let http;
8987
9140
  let credentials;
8988
- if (options.serverKey !== void 0) {
9141
+ if (options.credentials !== void 0) {
9142
+ credentials = options.credentials;
9143
+ http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials });
9144
+ auth = createServerKeyAuth(http, config.appIdentifier, "credentials");
9145
+ } else if (options.serverKey !== void 0) {
8989
9146
  credentials = new ServerKeyCredentials(options.serverKey);
8990
9147
  http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials });
8991
9148
  auth = createServerKeyAuth(http, config.appIdentifier);
@@ -9008,7 +9165,7 @@ function createClient(options) {
9008
9165
  const jobs = createJobsApi(http);
9009
9166
  const assets = createAssetsApi(http);
9010
9167
  const robots = createRobotsApi(http);
9011
- if (options.serverKey === void 0) {
9168
+ if (options.serverKey === void 0 && options.credentials === void 0) {
9012
9169
  const baseLogout = auth.logout.bind(auth);
9013
9170
  auth = {
9014
9171
  ...auth,