@fleetless/sdk 3.1.1 → 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/CHANGELOG.md +15 -1
- package/README.md +4 -4
- package/dist/index.cjs +359 -202
- package/dist/index.d.cts +68 -11
- package/dist/index.d.ts +68 -11
- package/dist/index.js +359 -202
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -236,7 +236,6 @@ function isRenderKind(kind) {
|
|
|
236
236
|
case "texture":
|
|
237
237
|
return true;
|
|
238
238
|
case "urdf":
|
|
239
|
-
case "other":
|
|
240
239
|
return false;
|
|
241
240
|
default: {
|
|
242
241
|
const exhaustive = kind;
|
|
@@ -411,7 +410,7 @@ function createAssetsApi(http) {
|
|
|
411
410
|
};
|
|
412
411
|
}
|
|
413
412
|
|
|
414
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
413
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/common.js
|
|
415
414
|
import { z } from "zod";
|
|
416
415
|
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.";
|
|
417
416
|
var slug = z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, SLUG_RULE);
|
|
@@ -426,7 +425,14 @@ var wireTimestampMs = z.union([z.string().regex(/^\d{1,15}$/), z.number().int()]
|
|
|
426
425
|
const year = new Date(ms).getUTCFullYear();
|
|
427
426
|
return Number.isFinite(year) && year >= 1 && year <= 9999;
|
|
428
427
|
}, "must fall within years 1..9999"));
|
|
429
|
-
var applyErrorKind = z.enum([
|
|
428
|
+
var applyErrorKind = z.enum([
|
|
429
|
+
"datapoint",
|
|
430
|
+
"action",
|
|
431
|
+
"service",
|
|
432
|
+
"publisher",
|
|
433
|
+
"camera",
|
|
434
|
+
"low_bandwidth"
|
|
435
|
+
]);
|
|
430
436
|
var applyError = z.object({
|
|
431
437
|
slug: z.string(),
|
|
432
438
|
kind: applyErrorKind,
|
|
@@ -435,7 +441,7 @@ var applyError = z.object({
|
|
|
435
441
|
details: z.record(z.string(), z.unknown()).optional()
|
|
436
442
|
});
|
|
437
443
|
|
|
438
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
444
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/mcp.js
|
|
439
445
|
import { z as z2 } from "zod";
|
|
440
446
|
function mcpAppEndpointPath(appIdentifier2) {
|
|
441
447
|
return `/mcp/${appIdentifier2}`;
|
|
@@ -484,17 +490,17 @@ var mcpRolePreviewResponse = z2.object({
|
|
|
484
490
|
});
|
|
485
491
|
var MCP_ASSET_LINK_TTL_MS = 15 * 60 * 1e3;
|
|
486
492
|
|
|
487
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
493
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/protocol.js
|
|
488
494
|
import { z as z8 } from "zod";
|
|
489
495
|
|
|
490
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
496
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/assets.js
|
|
491
497
|
import { z as z3 } from "zod";
|
|
492
|
-
var assetKind = z3.enum(["urdf", "mesh", "texture"
|
|
498
|
+
var assetKind = z3.enum(["urdf", "mesh", "texture"]);
|
|
493
499
|
var asset = z3.object({
|
|
494
500
|
id: z3.uuid().meta({ description: "The asset's id in the store." }),
|
|
495
501
|
robot_id: z3.uuid().meta({ description: "The robot this asset belongs to." }),
|
|
496
502
|
kind: assetKind.meta({
|
|
497
|
-
description: "What the file is: the `urdf` itself, a `mesh` it references, a `texture` a mesh or the URDF paints with
|
|
503
|
+
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."
|
|
498
504
|
}),
|
|
499
505
|
/**
|
|
500
506
|
* What the robot called it — for a mesh, the `package://` URI the URDF
|
|
@@ -593,16 +599,18 @@ var assetSyncResponse = z3.object({
|
|
|
593
599
|
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."
|
|
594
600
|
})
|
|
595
601
|
});
|
|
596
|
-
var
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
602
|
+
var assetStoreRefusedDetails = z3.object({
|
|
603
|
+
store_bytes: z3.number().int().positive().meta({
|
|
604
|
+
description: "The robot's store, in bytes."
|
|
605
|
+
}),
|
|
606
|
+
used_bytes: z3.number().int().nonnegative().meta({
|
|
607
|
+
description: "Bytes the robot's assets occupy before this upload."
|
|
600
608
|
}),
|
|
601
609
|
size_bytes: z3.number().int().positive().meta({
|
|
602
|
-
description:
|
|
610
|
+
description: "The refused upload, in bytes."
|
|
603
611
|
})
|
|
604
612
|
});
|
|
605
|
-
var assetFailureKind = z3.enum(["unresolvable", "upload_failed", "refused"
|
|
613
|
+
var assetFailureKind = z3.enum(["unresolvable", "upload_failed", "refused"]);
|
|
606
614
|
var assetFailure = z3.object({
|
|
607
615
|
/**
|
|
608
616
|
* What could not be provided, verbatim — the same string `asset.name` would
|
|
@@ -615,33 +623,28 @@ var assetFailure = z3.object({
|
|
|
615
623
|
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."
|
|
616
624
|
}),
|
|
617
625
|
kind: assetFailureKind.meta({
|
|
618
|
-
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
|
|
626
|
+
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."
|
|
619
627
|
}),
|
|
620
628
|
/**
|
|
621
|
-
* **The
|
|
629
|
+
* **The three numbers behind a full store.**
|
|
622
630
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
* under one kind puts two facts on one key, each overwriting the other.
|
|
631
|
+
* A reason without numbers is not one a caller can act on. *"Refused"* does
|
|
632
|
+
* not answer whether to delete an old sync or shrink the mesh;
|
|
633
|
+
* `store_bytes`, `used_bytes` and `size_bytes` do.
|
|
627
634
|
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
* described: a field whose rule lives only in a comment is a request.
|
|
635
|
+
* **Present only on `refused`, and not on every `refused`.** The other half
|
|
636
|
+
* of that kind is the single collective entry a sync emits when it stops
|
|
637
|
+
* naming individual failures, and no store number describes it — requiring
|
|
638
|
+
* details there would mean inventing them. So the enforcement below is the
|
|
639
|
+
* half that can be enforced: details belong to `refused` and to nothing
|
|
640
|
+
* else. A forced `details: null` on every `unresolvable` buys nothing.
|
|
635
641
|
*/
|
|
636
|
-
details:
|
|
637
|
-
description: "The
|
|
642
|
+
details: assetStoreRefusedDetails.nullish().meta({
|
|
643
|
+
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."
|
|
638
644
|
})
|
|
639
645
|
}).superRefine((f, ctx) => {
|
|
640
|
-
if (f.kind
|
|
641
|
-
ctx.addIssue({ code: "custom", path: ["details"], message: "
|
|
642
|
-
}
|
|
643
|
-
if (f.kind !== "too_large" && f.details != null) {
|
|
644
|
-
ctx.addIssue({ code: "custom", path: ["details"], message: "size details belong to `too_large` only" });
|
|
646
|
+
if (f.kind !== "refused" && f.details != null) {
|
|
647
|
+
ctx.addIssue({ code: "custom", path: ["details"], message: "store details belong to `refused` only" });
|
|
645
648
|
}
|
|
646
649
|
});
|
|
647
650
|
var assetSyncState = z3.enum(["running", "succeeded", "failed"]);
|
|
@@ -687,6 +690,36 @@ var assetSyncStatus = z3.object({
|
|
|
687
690
|
reason: z3.string().min(1).nullable().meta({
|
|
688
691
|
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."
|
|
689
692
|
}),
|
|
693
|
+
/**
|
|
694
|
+
* **What the receiver counted, next to what the producer claimed.**
|
|
695
|
+
*
|
|
696
|
+
* `state` is the bridge's own terminal frame and nothing else. A dev stack
|
|
697
|
+
* with no object store answered `500` to every upload and the sync still
|
|
698
|
+
* read `succeeded` — the producer had genuinely sent every file, and no
|
|
699
|
+
* one had asked the store. These two numbers are the cloud's own count,
|
|
700
|
+
* taken after the terminal frame: how many of the announced files
|
|
701
|
+
* (`assets_available`'s URDF and mesh list) its store actually holds.
|
|
702
|
+
*
|
|
703
|
+
* They are a pair because neither alone answers anything. `stored` without
|
|
704
|
+
* `announced` cannot say whether four files is all of them or a tenth of
|
|
705
|
+
* them, and `announced` alone is what the producer said it had, which is
|
|
706
|
+
* the claim under examination.
|
|
707
|
+
*
|
|
708
|
+
* A partial store is still `succeeded`: some meshes were never going to
|
|
709
|
+
* resolve, and the per-reference `failed` entries say which. An empty one
|
|
710
|
+
* under a `succeeded` frame is `failed`, because no transport succeeds at
|
|
711
|
+
* nothing.
|
|
712
|
+
*
|
|
713
|
+
* **`stored` is `null` while the sync is still running.** The count is
|
|
714
|
+
* taken once, after the robot's terminal frame; a `0` before then would
|
|
715
|
+
* say the store is empty when nobody has looked.
|
|
716
|
+
*/
|
|
717
|
+
stored: z3.number().int().nonnegative().nullable().meta({
|
|
718
|
+
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."
|
|
719
|
+
}),
|
|
720
|
+
announced: z3.number().int().nonnegative().meta({
|
|
721
|
+
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."
|
|
722
|
+
}),
|
|
690
723
|
started_at: z3.iso.datetime().meta({
|
|
691
724
|
description: "When the sync started, as an ISO 8601 timestamp."
|
|
692
725
|
}),
|
|
@@ -730,6 +763,43 @@ var assetListResponse = z3.object({
|
|
|
730
763
|
*/
|
|
731
764
|
urdf_available: z3.boolean().nullable().meta({
|
|
732
765
|
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.'
|
|
766
|
+
}),
|
|
767
|
+
/**
|
|
768
|
+
* **How full this robot's store is, on the list that already names what is
|
|
769
|
+
* in it.** A page showing assets is the page where "will the next sync fit"
|
|
770
|
+
* is asked, and a second round trip to some quota endpoint would answer it
|
|
771
|
+
* about the organisation instead — which is a different number about a
|
|
772
|
+
* different thing.
|
|
773
|
+
*/
|
|
774
|
+
store: z3.object({
|
|
775
|
+
bytes: z3.number().int().positive().meta({
|
|
776
|
+
description: "The robot's asset store, `ROBOT_ASSET_STORE_BYTES`."
|
|
777
|
+
}),
|
|
778
|
+
used_bytes: z3.number().int().nonnegative().meta({
|
|
779
|
+
description: "Bytes its assets occupy."
|
|
780
|
+
})
|
|
781
|
+
}).meta({ description: "How full this robot's store is." }),
|
|
782
|
+
/**
|
|
783
|
+
* The datapoint that moves the joints in a renderer, chosen by a developer
|
|
784
|
+
* and stored on the robot. It rides on this list because a client that has
|
|
785
|
+
* just fetched the URDF and the meshes needs exactly one more thing to
|
|
786
|
+
* animate them, and asking a second endpoint for one slug is a round trip
|
|
787
|
+
* that buys nothing.
|
|
788
|
+
*
|
|
789
|
+
* `null` is an ordinary answer: none was ever chosen, or a publish removed
|
|
790
|
+
* the datapoint it named and the cloud cleared the mapping rather than
|
|
791
|
+
* leave it pointing at something that no longer qualifies.
|
|
792
|
+
*/
|
|
793
|
+
joint_state_slug: slug.nullable().meta({
|
|
794
|
+
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`."
|
|
795
|
+
})
|
|
796
|
+
});
|
|
797
|
+
var assetsClearResponse = z3.object({
|
|
798
|
+
deleted: z3.number().int().nonnegative().meta({
|
|
799
|
+
description: "How many assets \u2014 URDF, meshes and textures together \u2014 were removed."
|
|
800
|
+
}),
|
|
801
|
+
bytes_freed: z3.number().int().nonnegative().meta({
|
|
802
|
+
description: "The bytes the robot's store got back."
|
|
733
803
|
})
|
|
734
804
|
});
|
|
735
805
|
var missingAssetQuery = z3.object({
|
|
@@ -743,10 +813,10 @@ var assetSyncBusyDetails = z3.object({
|
|
|
743
813
|
started_at_ms: z3.number().int().nonnegative()
|
|
744
814
|
});
|
|
745
815
|
|
|
746
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
816
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/config.js
|
|
747
817
|
import { z as z5 } from "zod";
|
|
748
818
|
|
|
749
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
819
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/alerts.js
|
|
750
820
|
import { z as z4 } from "zod";
|
|
751
821
|
var alertRowCondition = z4.discriminatedUnion("kind", [
|
|
752
822
|
z4.strictObject({
|
|
@@ -816,7 +886,7 @@ var putDatapointDisplayRequest = z4.object({
|
|
|
816
886
|
y_max: z4.number().finite().nullable()
|
|
817
887
|
}).strict();
|
|
818
888
|
|
|
819
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
889
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/config.js
|
|
820
890
|
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:`.";
|
|
821
891
|
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:`.";
|
|
822
892
|
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.";
|
|
@@ -1207,6 +1277,9 @@ var datapointConfig = strictObject({
|
|
|
1207
1277
|
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.",
|
|
1208
1278
|
examples: [2, 0.5]
|
|
1209
1279
|
}).optional(),
|
|
1280
|
+
low_bandwidth: z5.literal("keep").optional().meta({
|
|
1281
|
+
description: "`keep` exempts this datapoint from the low-bandwidth rate cap; its backfill still pauses."
|
|
1282
|
+
}),
|
|
1210
1283
|
description: serviceDescription.meta({
|
|
1211
1284
|
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.",
|
|
1212
1285
|
/**
|
|
@@ -1804,6 +1877,34 @@ var cameraConfig = strictObject({
|
|
|
1804
1877
|
/** The value position of one entry — `front: ▮` under `cameras:`. */
|
|
1805
1878
|
defaultSnippets: [CAMERA_SNIPPET]
|
|
1806
1879
|
});
|
|
1880
|
+
var lowBandwidthMode = z5.enum(["auto", "on", "off"]);
|
|
1881
|
+
var lowBandwidthCamera = z5.enum(["reduce", "stop"]);
|
|
1882
|
+
var lowBandwidthSection = strictObject({
|
|
1883
|
+
mode: lowBandwidthMode.optional().meta({
|
|
1884
|
+
description: "`auto` decides from the measured lag; `on` and `off` force the mode, for tests and for an operator who knows the link.",
|
|
1885
|
+
enumDescriptions: describeValues(lowBandwidthMode.options, {
|
|
1886
|
+
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.",
|
|
1887
|
+
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.",
|
|
1888
|
+
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."
|
|
1889
|
+
})
|
|
1890
|
+
}),
|
|
1891
|
+
enter_lag_ms: z5.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." }),
|
|
1892
|
+
enter_after_s: z5.number().int().min(1).optional().meta({ description: "The entry condition must hold this long." }),
|
|
1893
|
+
exit_lag_ms: z5.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." }),
|
|
1894
|
+
exit_after_s: z5.number().int().min(1).optional().meta({ description: "The exit condition must hold this long." }),
|
|
1895
|
+
datapoint_max_hz: z5.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." }),
|
|
1896
|
+
camera: lowBandwidthCamera.optional().meta({
|
|
1897
|
+
description: "What happens to a running stream in the mode. New streams are refused either way.",
|
|
1898
|
+
enumDescriptions: describeValues(lowBandwidthCamera.options, {
|
|
1899
|
+
reduce: "A running stream is re-encoded at `camera_bitrate_kbps` and keeps running. A viewer sees a worse picture rather than none.",
|
|
1900
|
+
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."
|
|
1901
|
+
})
|
|
1902
|
+
}),
|
|
1903
|
+
camera_bitrate_kbps: z5.number().int().min(50).max(2e4).optional().meta({ description: "Bitrate applied to running streams under `reduce`." })
|
|
1904
|
+
}).superRefine((s, ctx) => {
|
|
1905
|
+
if (s.enter_lag_ms !== void 0 && s.exit_lag_ms !== void 0 && s.exit_lag_ms > s.enter_lag_ms)
|
|
1906
|
+
ctx.addIssue({ code: "custom", path: ["exit_lag_ms"], message: "low_bandwidth.exit_lag_ms must be at or below enter_lag_ms" });
|
|
1907
|
+
});
|
|
1807
1908
|
var FLEETLESS_FORMAT_VERSION = 1;
|
|
1808
1909
|
var capped = (entry, max, what) => slugKeyed(entry).refine((m) => Object.keys(m).length <= max, { message: `at most ${max} ${what}` });
|
|
1809
1910
|
var robotConfigDoc = strictObject({
|
|
@@ -1815,28 +1916,42 @@ var robotConfigDoc = strictObject({
|
|
|
1815
1916
|
defaultSnippets: [underSlug("${1:drive}", SHARED_MESSAGE_SNIPPET)]
|
|
1816
1917
|
}).optional(),
|
|
1817
1918
|
datapoints: capped(datapointConfig, 200, "datapoints").meta({
|
|
1818
|
-
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
|
|
1919
|
+
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.",
|
|
1819
1920
|
defaultSnippets: [
|
|
1820
1921
|
underSlug("${1:battery_voltage}", DATAPOINT_SNIPPET),
|
|
1821
1922
|
underSlug("${1:battery}", NUMERIC_DATAPOINT_SNIPPET)
|
|
1822
1923
|
]
|
|
1823
1924
|
}).optional(),
|
|
1824
1925
|
actions: capped(actionConfig, 200, "actions").meta({
|
|
1825
|
-
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
|
|
1926
|
+
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.",
|
|
1826
1927
|
defaultSnippets: [underSlug("${1:navigate}", ACTION_SNIPPET)]
|
|
1827
1928
|
}).optional(),
|
|
1828
1929
|
services: capped(serviceConfig, 200, "services").meta({
|
|
1829
|
-
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
|
|
1930
|
+
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.",
|
|
1830
1931
|
defaultSnippets: [underSlug("${1:reset_odometry}", SERVICE_SNIPPET)]
|
|
1831
1932
|
}).optional(),
|
|
1832
1933
|
publishers: capped(publisherConfig, 200, "publishers").meta({
|
|
1833
|
-
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
|
|
1934
|
+
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.",
|
|
1834
1935
|
defaultSnippets: [underSlug("${1:drive}", PUBLISHER_SNIPPET)]
|
|
1835
1936
|
}).optional(),
|
|
1836
1937
|
cameras: capped(cameraConfig, 50, "cameras").meta({
|
|
1837
|
-
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
|
|
1938
|
+
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.",
|
|
1838
1939
|
defaultSnippets: [underSlug("${1:front}", CAMERA_SNIPPET)]
|
|
1839
|
-
}).optional()
|
|
1940
|
+
}).optional(),
|
|
1941
|
+
low_bandwidth: lowBandwidthSection.optional().meta({
|
|
1942
|
+
description: "Overrides for the bridge's low-bandwidth mode; see the section schema.",
|
|
1943
|
+
/**
|
|
1944
|
+
* The body is the three keys a developer actually reaches for — when the
|
|
1945
|
+
* mode engages, how hard it caps, and what it does to video. The five
|
|
1946
|
+
* timing keys stay out: they exist to be tuned once against a measured
|
|
1947
|
+
* link, and a skeleton that pre-fills them reads as a recommendation.
|
|
1948
|
+
*/
|
|
1949
|
+
defaultSnippets: [{
|
|
1950
|
+
label: "low-bandwidth mode, tuned",
|
|
1951
|
+
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.",
|
|
1952
|
+
body: { enter_lag_ms: 2e3, datapoint_max_hz: 1, camera: "reduce" }
|
|
1953
|
+
}]
|
|
1954
|
+
})
|
|
1840
1955
|
});
|
|
1841
1956
|
var validationIssue = z5.object({
|
|
1842
1957
|
path: z5.string().min(1),
|
|
@@ -1863,7 +1978,7 @@ var configState = z5.object({
|
|
|
1863
1978
|
applied_errors: z5.array(applyError).nullable()
|
|
1864
1979
|
});
|
|
1865
1980
|
|
|
1866
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
1981
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/introspection.js
|
|
1867
1982
|
import { z as z6 } from "zod";
|
|
1868
1983
|
var rosGraphEntry = z6.object({
|
|
1869
1984
|
name: rosName,
|
|
@@ -1902,7 +2017,7 @@ var typeDefinition = z6.discriminatedUnion("kind", [
|
|
|
1902
2017
|
})
|
|
1903
2018
|
]);
|
|
1904
2019
|
|
|
1905
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2020
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/jobs.js
|
|
1906
2021
|
import { z as z7 } from "zod";
|
|
1907
2022
|
var jobState = z7.enum(["running", "succeeded", "failed", "cancelled", "lost"]);
|
|
1908
2023
|
var job = z7.object({
|
|
@@ -2120,7 +2235,8 @@ var jobRunSummary = z7.object({
|
|
|
2120
2235
|
since_ms: z7.number().int().nonnegative()
|
|
2121
2236
|
});
|
|
2122
2237
|
|
|
2123
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2238
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/protocol.js
|
|
2239
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2124
2240
|
var MAX_PATIENCE_MS = 12e4;
|
|
2125
2241
|
var MIN_PATIENCE_MS = 1e3;
|
|
2126
2242
|
var activeJob = z8.object({
|
|
@@ -2156,7 +2272,20 @@ var bridgeHello = z8.object({
|
|
|
2156
2272
|
});
|
|
2157
2273
|
var cloudHelloOk = z8.object({
|
|
2158
2274
|
type: z8.literal("hello_ok"),
|
|
2159
|
-
robot_id: z8.uuid()
|
|
2275
|
+
robot_id: z8.uuid(),
|
|
2276
|
+
protocol: z8.object({
|
|
2277
|
+
status: z8.enum(["current", "deprecated"]).meta({
|
|
2278
|
+
description: "`current` or `deprecated` \u2014 never `unsupported`, which is a `hello_error`."
|
|
2279
|
+
}),
|
|
2280
|
+
sunset_at: z8.iso.date().nullable().meta({
|
|
2281
|
+
description: "ISO date a deprecated version stops being served; `null` when current."
|
|
2282
|
+
})
|
|
2283
|
+
}).optional().meta({ description: "The cloud's verdict on the announced protocol version; absent from an older cloud." }),
|
|
2284
|
+
bridge: z8.object({
|
|
2285
|
+
latest_version: z8.string().min(1).meta({
|
|
2286
|
+
description: "The newest published fleetless-bridge package version, for the bridge's own upgrade hint."
|
|
2287
|
+
})
|
|
2288
|
+
}).optional().meta({ description: "What the cloud knows about bridge packages; absent from an older cloud." })
|
|
2160
2289
|
});
|
|
2161
2290
|
var cloudHelloError = z8.object({
|
|
2162
2291
|
type: z8.literal("hello_error"),
|
|
@@ -2167,16 +2296,25 @@ var datapointFrame = z8.object({
|
|
|
2167
2296
|
type: z8.literal("datapoint"),
|
|
2168
2297
|
slug,
|
|
2169
2298
|
value: z8.unknown(),
|
|
2170
|
-
timestamp_ms: z8.number().int().nonnegative()
|
|
2299
|
+
timestamp_ms: z8.number().int().nonnegative(),
|
|
2300
|
+
backfill: z8.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." })
|
|
2171
2301
|
});
|
|
2172
2302
|
var cloudPing = z8.object({
|
|
2173
2303
|
type: z8.literal("ping"),
|
|
2174
|
-
ts_ms: z8.number().int().nonnegative()
|
|
2304
|
+
ts_ms: z8.number().int().nonnegative(),
|
|
2305
|
+
latency_ms: z8.number().nonnegative().nullable().meta({ description: "Round trip of the last pong in milliseconds; null before the first." }),
|
|
2306
|
+
lag_ms: z8.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." })
|
|
2175
2307
|
});
|
|
2176
2308
|
var bridgePong = z8.object({
|
|
2177
2309
|
type: z8.literal("pong"),
|
|
2178
2310
|
ts_ms: z8.number().int().nonnegative()
|
|
2179
2311
|
});
|
|
2312
|
+
var bridgeLinkMode = z8.object({
|
|
2313
|
+
type: z8.literal("link_mode"),
|
|
2314
|
+
low_bandwidth: z8.boolean().meta({ description: "Whether the mode is active after this transition." }),
|
|
2315
|
+
reason: z8.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." }),
|
|
2316
|
+
at_ms: z8.number().int().nonnegative().meta({ description: "Bridge time of the transition, epoch milliseconds." })
|
|
2317
|
+
});
|
|
2180
2318
|
var cloudConfig = z8.object({
|
|
2181
2319
|
type: z8.literal("config"),
|
|
2182
2320
|
version: z8.number().int().nonnegative(),
|
|
@@ -2286,75 +2424,8 @@ var bridgeTypeDefinitions = z8.object({
|
|
|
2286
2424
|
});
|
|
2287
2425
|
var bridgeState = z8.object({
|
|
2288
2426
|
online: z8.boolean(),
|
|
2289
|
-
latency_ms: z8.number().nonnegative().nullable()
|
|
2290
|
-
})
|
|
2291
|
-
var bridgePressureTier = z8.object({
|
|
2292
|
-
sent: z8.number().int().nonnegative(),
|
|
2293
|
-
bytes: z8.number().int().nonnegative(),
|
|
2294
|
-
drops: z8.number().int().nonnegative(),
|
|
2295
|
-
high_water: z8.number().int().nonnegative()
|
|
2296
|
-
});
|
|
2297
|
-
var bridgePressure = z8.object({
|
|
2298
|
-
link: z8.object({
|
|
2299
|
-
/** bytes/s the socket demonstrably drains, from sends >= 64 KiB
|
|
2300
|
-
* only; null until the first large send of the session. */
|
|
2301
|
-
rate_bps: z8.number().nonnegative().nullable(),
|
|
2302
|
-
/**
|
|
2303
|
-
* the byte target snapshots are currently encoded to fit.
|
|
2304
|
-
*
|
|
2305
|
-
* `.nonnegative()`, not `.positive()`: the target is derived from
|
|
2306
|
-
* `rate_bps`, and a link measured below 0.5 B/s floors to 0 here. A
|
|
2307
|
-
* schema that rejects 0 does not prevent that link — it only makes the
|
|
2308
|
-
* frame reporting it unparseable, and a console that cannot parse a
|
|
2309
|
-
* pressure frame shows "no feed", i.e. reports a struggling robot as an
|
|
2310
|
-
* *old* one. Zero is a legitimate reading and says something true.
|
|
2311
|
-
*/
|
|
2312
|
-
snapshot_max_bytes: z8.number().int().nonnegative()
|
|
2313
|
-
}),
|
|
2314
|
-
/**
|
|
2315
|
-
* String keys "0".."5" because JSON has no integer keys. Counters are
|
|
2316
|
-
* cumulative per session and reset on reconnect; clients window by
|
|
2317
|
-
* differencing two samples.
|
|
2318
|
-
*
|
|
2319
|
-
* **What this schema does not decide:** it does not guarantee all six
|
|
2320
|
-
* keys are present (`z.record` over the six literals is exhaustive in
|
|
2321
|
-
* zod 4 — tested here, it required every key and rejected none, the
|
|
2322
|
-
* opposite of what a partial sample needs — so this is a
|
|
2323
|
-
* `.strictObject().partial()` over the same six literal keys instead, a
|
|
2324
|
-
* deliberate deviation from the originally sketched `z.record` shape with
|
|
2325
|
-
* the same runtime behaviour). A missing tier key reads as zeros; the
|
|
2326
|
-
* schema names what it cannot decide rather than implying a completeness
|
|
2327
|
-
* it cannot check.
|
|
2328
|
-
*/
|
|
2329
|
-
tiers: z8.strictObject({
|
|
2330
|
-
"0": bridgePressureTier,
|
|
2331
|
-
"1": bridgePressureTier,
|
|
2332
|
-
"2": bridgePressureTier,
|
|
2333
|
-
"3": bridgePressureTier,
|
|
2334
|
-
"4": bridgePressureTier,
|
|
2335
|
-
"5": bridgePressureTier
|
|
2336
|
-
}).partial(),
|
|
2337
|
-
video: z8.object({
|
|
2338
|
-
active_streams: z8.number().int().nonnegative(),
|
|
2339
|
-
bitrate_sum_kbps: z8.number().int().nonnegative(),
|
|
2340
|
-
/**
|
|
2341
|
-
* The uplink budget the bridge was configured with
|
|
2342
|
-
* (`FLEETLESS_UPLINK_KBPS`), or `null` when none was set.
|
|
2343
|
-
*
|
|
2344
|
-
* `.nonnegative()`, not `.positive()`: `FLEETLESS_UPLINK_KBPS=0` is a
|
|
2345
|
-
* documented setting meaning "no video budget at all", and the bridge
|
|
2346
|
-
* emits that 0 verbatim. `.positive()` made every frame from such a
|
|
2347
|
-
* robot fail the console's `safeParse`, which renders an unparseable
|
|
2348
|
-
* frame as "no pressure feed" — so the one robot that had *deliberately*
|
|
2349
|
-
* turned video off was the one diagnosed as running a bridge too old to
|
|
2350
|
-
* report pressure. A value the producer legitimately sends must parse;
|
|
2351
|
-
* `null` is the only "not set" this field has.
|
|
2352
|
-
*/
|
|
2353
|
-
uplink_kbps: z8.number().int().nonnegative().nullable(),
|
|
2354
|
-
override_kbps: z8.number().int().nonnegative().nullable(),
|
|
2355
|
-
video_budget_kbps: z8.number().int().nonnegative().nullable(),
|
|
2356
|
-
reserve_kbps: z8.number().int().nonnegative()
|
|
2357
|
-
})
|
|
2427
|
+
latency_ms: z8.number().nonnegative().nullable(),
|
|
2428
|
+
low_bandwidth: z8.boolean().meta({ description: "Whether the bridge is in its low-bandwidth mode: datapoints capped, cameras reduced or stopped. Bridge-reported." })
|
|
2358
2429
|
});
|
|
2359
2430
|
var snapshotHeader = z8.object({
|
|
2360
2431
|
type: z8.literal("snapshot"),
|
|
@@ -2518,11 +2589,11 @@ var bridgeCameraState = z8.object({
|
|
|
2518
2589
|
request_id: z8.string().min(1).max(64).nullable()
|
|
2519
2590
|
});
|
|
2520
2591
|
|
|
2521
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2592
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/config-issues.js
|
|
2522
2593
|
var EXPOSURE_SECTIONS = ["datapoints", "actions", "services", "publishers", "cameras"];
|
|
2523
2594
|
var EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
|
|
2524
2595
|
|
|
2525
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
2596
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/rest.js
|
|
2526
2597
|
import { z as z9 } from "zod";
|
|
2527
2598
|
var robot = z9.object({
|
|
2528
2599
|
id: z9.uuid().meta({
|
|
@@ -2548,6 +2619,21 @@ var createRobotResponse = z9.object({
|
|
|
2548
2619
|
robot,
|
|
2549
2620
|
token: robotToken
|
|
2550
2621
|
});
|
|
2622
|
+
var robotTokenRotateResponse = z9.object({
|
|
2623
|
+
token: robotToken.meta({
|
|
2624
|
+
description: "The robot's new bridge token. Returned exactly once; the previous token stops working at the bridge's next hello."
|
|
2625
|
+
})
|
|
2626
|
+
});
|
|
2627
|
+
var jointStatePutRequest = z9.object({
|
|
2628
|
+
slug: slug.nullable().meta({
|
|
2629
|
+
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."
|
|
2630
|
+
})
|
|
2631
|
+
});
|
|
2632
|
+
var jointStatePutResponse = z9.object({
|
|
2633
|
+
joint_state_slug: slug.nullable().meta({
|
|
2634
|
+
description: "The stored mapping after the call, `null` when none is chosen. The same value `assetListResponse.joint_state_slug` carries."
|
|
2635
|
+
})
|
|
2636
|
+
});
|
|
2551
2637
|
var exposureCounts = z9.object({
|
|
2552
2638
|
datapoints: z9.number().int().nonnegative(),
|
|
2553
2639
|
actions: z9.number().int().nonnegative(),
|
|
@@ -2555,11 +2641,15 @@ var exposureCounts = z9.object({
|
|
|
2555
2641
|
publishers: z9.number().int().nonnegative(),
|
|
2556
2642
|
cameras: z9.number().int().nonnegative()
|
|
2557
2643
|
});
|
|
2644
|
+
var protocolStatusValue = z9.enum(["current", "deprecated", "refused"]);
|
|
2558
2645
|
var robotListItem = z9.object({
|
|
2559
2646
|
...robot.shape,
|
|
2560
2647
|
bridge_state: bridgeState,
|
|
2561
2648
|
/** Required, not optional: "we did not look" and "it exposes nothing" must not render the same. */
|
|
2562
|
-
exposes: exposureCounts
|
|
2649
|
+
exposes: exposureCounts,
|
|
2650
|
+
protocol_status: protocolStatusValue.optional().meta({
|
|
2651
|
+
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`."
|
|
2652
|
+
})
|
|
2563
2653
|
});
|
|
2564
2654
|
var robotListResponse = z9.object({
|
|
2565
2655
|
robots: z9.array(robotListItem)
|
|
@@ -2576,6 +2666,15 @@ var datapointValue = z9.object({
|
|
|
2576
2666
|
var robotDetailResponse = z9.object({
|
|
2577
2667
|
...robotListItem.shape,
|
|
2578
2668
|
bridge_version: z9.string().min(1).nullable(),
|
|
2669
|
+
protocol_version: z9.number().int().positive().nullable().optional().meta({
|
|
2670
|
+
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."
|
|
2671
|
+
}),
|
|
2672
|
+
protocol: z9.object({
|
|
2673
|
+
status: protocolStatusValue.meta({ description: "Same values as `protocol_status`." }),
|
|
2674
|
+
sunset_at: z9.iso.date().nullable().meta({
|
|
2675
|
+
description: "ISO date the announced version stops being served; `null` when current or unknown."
|
|
2676
|
+
})
|
|
2677
|
+
}).optional().meta({ description: "The window verdict for `protocol_version`." }),
|
|
2579
2678
|
/**
|
|
2580
2679
|
* Cleared (set back to null) by the next successful hello from this
|
|
2581
2680
|
* robot's bridge — a warning that outlives the condition it warns
|
|
@@ -2629,7 +2728,7 @@ var fetchTypesResponse = z9.object({
|
|
|
2629
2728
|
var datapointDescriptor = z9.object({
|
|
2630
2729
|
slug: slug.meta({ description: "The name a client reads this datapoint by." }),
|
|
2631
2730
|
builtin: z9.boolean().meta({
|
|
2632
|
-
description: "`true` for the datapoints every robot has \u2014 `bridge_state
|
|
2731
|
+
description: "`true` for the datapoints every robot has \u2014 `bridge_state` and `robot_details` \u2014 and `false` for everything the published configuration adds."
|
|
2633
2732
|
}),
|
|
2634
2733
|
unit: z9.string().nullable().meta({
|
|
2635
2734
|
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."
|
|
@@ -2647,7 +2746,7 @@ var datapointDescriptor = z9.object({
|
|
|
2647
2746
|
});
|
|
2648
2747
|
var datapointListResponse = z9.object({
|
|
2649
2748
|
datapoints: z9.array(datapointDescriptor).meta({
|
|
2650
|
-
description: "Everything a client may read on this robot: the
|
|
2749
|
+
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."
|
|
2651
2750
|
})
|
|
2652
2751
|
});
|
|
2653
2752
|
var robotDetailsDoc = z9.record(z9.string().regex(/^[a-z][a-z0-9_-]{0,63}$/), z9.union([z9.string().max(4096), z9.number(), z9.boolean(), z9.array(z9.unknown()), z9.record(z9.string(), z9.unknown())]));
|
|
@@ -3001,22 +3100,25 @@ var robotDeletionSummary = z9.object({
|
|
|
3001
3100
|
bytes_freed: z9.number().int().nonnegative(),
|
|
3002
3101
|
cameras: z9.array(slug),
|
|
3003
3102
|
/**
|
|
3004
|
-
* Assets destroyed with the robot, and **`asset_bytes_freed` is what
|
|
3005
|
-
*
|
|
3103
|
+
* Assets destroyed with the robot, and **`asset_bytes_freed` is what the
|
|
3104
|
+
* robot's own store gives back** — every distinct mesh or texture blob it
|
|
3105
|
+
* holds, counted once, URDF excluded.
|
|
3006
3106
|
*
|
|
3007
|
-
* Storage is content-addressed,
|
|
3008
|
-
*
|
|
3009
|
-
*
|
|
3010
|
-
*
|
|
3011
|
-
*
|
|
3012
|
-
*
|
|
3013
|
-
*
|
|
3107
|
+
* Storage is content-addressed, but the store and its 1 GB ceiling are now
|
|
3108
|
+
* per robot: a blob another robot also references stays in the object
|
|
3109
|
+
* store but is still credited here, because each robot's counter carries
|
|
3110
|
+
* it regardless of what else points at the same bytes. Same reasoning that
|
|
3111
|
+
* keeps `cameras` out of `slug_count`: this summary is read aloud to a
|
|
3112
|
+
* human, and a number that is nearly right is worse here than an absent
|
|
3113
|
+
* one.
|
|
3014
3114
|
*
|
|
3015
3115
|
* `asset_count` is the plain count of the robot's asset rows, all of which
|
|
3016
3116
|
* do go away.
|
|
3017
3117
|
*/
|
|
3018
3118
|
asset_count: z9.number().int().nonnegative(),
|
|
3019
|
-
asset_bytes_freed: z9.number().int().nonnegative()
|
|
3119
|
+
asset_bytes_freed: z9.number().int().nonnegative().meta({
|
|
3120
|
+
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."
|
|
3121
|
+
}),
|
|
3020
3122
|
/**
|
|
3021
3123
|
* How many rows of run history go with the robot — every recorded
|
|
3022
3124
|
* invocation of one of its actions or services, up to
|
|
@@ -3155,39 +3257,13 @@ var orgQuotas = z9.object({
|
|
|
3155
3257
|
max_end_users: z9.number().int().positive(),
|
|
3156
3258
|
max_retention_bytes: z9.number().int().nonnegative(),
|
|
3157
3259
|
max_retention_writes_per_minute: z9.number().int().nonnegative(),
|
|
3158
|
-
max_realtime_connections: z9.number().int().positive()
|
|
3159
|
-
/**
|
|
3160
|
-
* Asset storage — **its own dial, not part of `max_retention_bytes`.** A
|
|
3161
|
-
* sync grows storage in jumps and time series grow steadily; one dial would
|
|
3162
|
-
* let the first crowd out the second, and the org that hit its limit would be
|
|
3163
|
-
* told to look at the wrong thing.
|
|
3164
|
-
*
|
|
3165
|
-
* **Counted per distinct blob *this org references* — not per asset row, and
|
|
3166
|
-
* not per object the platform stores on its behalf.** The two readings are
|
|
3167
|
-
* indistinguishable from the number alone and a customer is entitled to know
|
|
3168
|
-
* which one they are being charged for.
|
|
3169
|
-
*
|
|
3170
|
-
* Within an org, sharing is free: two robots referencing the same mesh cost
|
|
3171
|
-
* one copy, which is what dedup means to a customer, and anything else
|
|
3172
|
-
* charges an org twice for a fleet of identical robots — the normal case.
|
|
3173
|
-
*
|
|
3174
|
-
* **Across orgs, sharing is not free.** Storage stays globally
|
|
3175
|
-
* content-addressed (one object per sha256; that efficiency is real), but
|
|
3176
|
-
* accounting is per-org: an org is charged for each distinct blob it
|
|
3177
|
-
* references and credited when its own last reference goes, whether or not
|
|
3178
|
-
* the blob survives for somebody else. Global refcounting would make the
|
|
3179
|
-
* first org to sync a blob pay for it forever while every later org stored it
|
|
3180
|
-
* free — a quota evadable by anyone whose mesh someone else had already
|
|
3181
|
-
* uploaded, and an org's own number would depend on who got there first.
|
|
3182
|
-
*/
|
|
3183
|
-
max_asset_storage_bytes: z9.number().int().nonnegative()
|
|
3260
|
+
max_realtime_connections: z9.number().int().positive()
|
|
3184
3261
|
});
|
|
3185
3262
|
var orgQuotaUsageCounts = z9.object({
|
|
3186
3263
|
max_robots: z9.number().int().nonnegative(),
|
|
3187
3264
|
max_apps: z9.number().int().nonnegative(),
|
|
3188
3265
|
max_end_users: z9.number().int().nonnegative(),
|
|
3189
3266
|
max_retention_bytes: z9.number().int().nonnegative(),
|
|
3190
|
-
max_asset_storage_bytes: z9.number().int().nonnegative(),
|
|
3191
3267
|
max_retention_writes_per_minute: z9.number().int().nonnegative(),
|
|
3192
3268
|
max_realtime_connections: z9.number().int().nonnegative()
|
|
3193
3269
|
}).partial();
|
|
@@ -3282,13 +3358,13 @@ var slugUsageResponse = z9.object({
|
|
|
3282
3358
|
alert_count: z9.number().int().nonnegative()
|
|
3283
3359
|
});
|
|
3284
3360
|
|
|
3285
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3361
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/realtime.js
|
|
3286
3362
|
import { z as z14 } from "zod";
|
|
3287
3363
|
|
|
3288
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3364
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
3289
3365
|
import { z as z13 } from "zod";
|
|
3290
3366
|
|
|
3291
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3367
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/apps.js
|
|
3292
3368
|
import { z as z10 } from "zod";
|
|
3293
3369
|
var appIdentifier = slug;
|
|
3294
3370
|
var app = z10.object({
|
|
@@ -3470,10 +3546,10 @@ var rolePermissions = z10.object({
|
|
|
3470
3546
|
})
|
|
3471
3547
|
});
|
|
3472
3548
|
|
|
3473
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3549
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/app-users.js
|
|
3474
3550
|
import { z as z12 } from "zod";
|
|
3475
3551
|
|
|
3476
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3552
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/identity.js
|
|
3477
3553
|
import { z as z11 } from "zod";
|
|
3478
3554
|
var password = z11.string().min(12).max(256);
|
|
3479
3555
|
var USER_DISPLAY_NAME_MAX = 120;
|
|
@@ -3635,7 +3711,7 @@ var authMeResponse = z11.object({ org, user: fleetlessUser });
|
|
|
3635
3711
|
var patchOrgRequest = z11.object({ name: z11.string().min(1).max(120) }).strict();
|
|
3636
3712
|
var patchAuthMeRequest = z11.object({ display_name: z11.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable() }).strict();
|
|
3637
3713
|
|
|
3638
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3714
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/app-users.js
|
|
3639
3715
|
var APP_USER_DISPLAY_NAME_MAX = 120;
|
|
3640
3716
|
var providerSlug = z12.string().max(40).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "must be lowercase and hyphen-separated, starting with a letter");
|
|
3641
3717
|
var appUserStatus = z12.enum(["pending_verification", "active", "blocked"]);
|
|
@@ -3886,7 +3962,7 @@ var mailOutcome = z12.object({
|
|
|
3886
3962
|
})
|
|
3887
3963
|
});
|
|
3888
3964
|
|
|
3889
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
3965
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
3890
3966
|
var clientLoginRequest = z13.object({
|
|
3891
3967
|
app_identifier: appIdentifier.meta({
|
|
3892
3968
|
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."
|
|
@@ -4075,7 +4151,7 @@ var clientIdentity = z13.object({
|
|
|
4075
4151
|
})
|
|
4076
4152
|
});
|
|
4077
4153
|
|
|
4078
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4154
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/realtime.js
|
|
4079
4155
|
var clientAuth = z14.object({
|
|
4080
4156
|
type: z14.literal("auth"),
|
|
4081
4157
|
token: z14.string().min(1)
|
|
@@ -4353,7 +4429,7 @@ var orgEventDropped = z14.object({
|
|
|
4353
4429
|
dropped: z14.number().int().positive()
|
|
4354
4430
|
}).strict();
|
|
4355
4431
|
|
|
4356
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4432
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/client-robots.js
|
|
4357
4433
|
import { z as z15 } from "zod";
|
|
4358
4434
|
var clientRobotListItem = z15.object({
|
|
4359
4435
|
...robot.shape,
|
|
@@ -4370,7 +4446,7 @@ var clientRobotListResponse = z15.object({
|
|
|
4370
4446
|
})
|
|
4371
4447
|
});
|
|
4372
4448
|
|
|
4373
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4449
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/audit.js
|
|
4374
4450
|
import { z as z16 } from "zod";
|
|
4375
4451
|
var auditActor = z16.object({
|
|
4376
4452
|
kind: z16.enum(["developer", "end_user", "app_user", "server_key", "bridge"]),
|
|
@@ -4500,7 +4576,7 @@ var auditListResponse = z16.object({
|
|
|
4500
4576
|
next_cursor: z16.number().int().positive().nullable()
|
|
4501
4577
|
});
|
|
4502
4578
|
|
|
4503
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4579
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/errors.js
|
|
4504
4580
|
import { z as z17 } from "zod";
|
|
4505
4581
|
var apiError = z17.object({
|
|
4506
4582
|
code: z17.string().min(1),
|
|
@@ -4517,7 +4593,7 @@ var parameterInvalidDetails = z17.object({
|
|
|
4517
4593
|
violations: z17.array(parameterViolation).min(1)
|
|
4518
4594
|
});
|
|
4519
4595
|
|
|
4520
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4596
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/oauth.js
|
|
4521
4597
|
import { z as z18 } from "zod";
|
|
4522
4598
|
var oauthErrorCode = z18.enum([
|
|
4523
4599
|
"invalid_request",
|
|
@@ -4584,7 +4660,7 @@ var dynamicClientRegistrationRequest = z18.object({
|
|
|
4584
4660
|
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."
|
|
4585
4661
|
}),
|
|
4586
4662
|
grant_types: z18.array(z18.enum(["authorization_code", "refresh_token"])).optional().meta({
|
|
4587
|
-
description: "Accepted for conformance with RFC 7591 and then **ignored
|
|
4663
|
+
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."
|
|
4588
4664
|
}),
|
|
4589
4665
|
response_types: z18.array(z18.enum(["code"])).optional().meta({
|
|
4590
4666
|
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."
|
|
@@ -4606,7 +4682,7 @@ var dynamicClientRegistrationResponse = z18.object({
|
|
|
4606
4682
|
description: "The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else."
|
|
4607
4683
|
}),
|
|
4608
4684
|
grant_types: z18.array(z18.string()).meta({
|
|
4609
|
-
description: 'The grants this client may use. Always exactly `["authorization_code"]` \u2014
|
|
4685
|
+
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.'
|
|
4610
4686
|
}),
|
|
4611
4687
|
response_types: z18.array(z18.string()).meta({
|
|
4612
4688
|
description: "The response types this client may ask for: `code`."
|
|
@@ -4621,9 +4697,9 @@ var dynamicClientRegistrationResponse = z18.object({
|
|
|
4621
4697
|
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."
|
|
4622
4698
|
})
|
|
4623
4699
|
});
|
|
4624
|
-
var
|
|
4700
|
+
var oauthCodeTokenRequest = z18.object({
|
|
4625
4701
|
grant_type: z18.literal("authorization_code").meta({
|
|
4626
|
-
description: "
|
|
4702
|
+
description: "`authorization_code`: this request exchanges the code from the authorize redirect for an access token and a refresh token."
|
|
4627
4703
|
}),
|
|
4628
4704
|
code: z18.string().min(1).max(500).meta({
|
|
4629
4705
|
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."
|
|
@@ -4643,6 +4719,25 @@ var oauthTokenRequest = z18.object({
|
|
|
4643
4719
|
}).meta({
|
|
4644
4720
|
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."
|
|
4645
4721
|
});
|
|
4722
|
+
var oauthRefreshTokenRequest = z18.object({
|
|
4723
|
+
grant_type: z18.literal("refresh_token").meta({
|
|
4724
|
+
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."
|
|
4725
|
+
}),
|
|
4726
|
+
refresh_token: z18.string().min(1).max(500).meta({
|
|
4727
|
+
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."
|
|
4728
|
+
}),
|
|
4729
|
+
client_id: z18.string().min(1).max(200).meta({
|
|
4730
|
+
description: "The client the refresh token was issued to, as registered. A refresh token is not transferable between clients."
|
|
4731
|
+
}),
|
|
4732
|
+
resource: z18.url().optional().meta({
|
|
4733
|
+
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."
|
|
4734
|
+
})
|
|
4735
|
+
}).meta({
|
|
4736
|
+
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."
|
|
4737
|
+
});
|
|
4738
|
+
var oauthTokenRequest = z18.discriminatedUnion("grant_type", [oauthCodeTokenRequest, oauthRefreshTokenRequest]).meta({
|
|
4739
|
+
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."
|
|
4740
|
+
});
|
|
4646
4741
|
var oauthTokenResponse = z18.object({
|
|
4647
4742
|
access_token: z18.string().min(1).meta({
|
|
4648
4743
|
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."
|
|
@@ -4654,7 +4749,7 @@ var oauthTokenResponse = z18.object({
|
|
|
4654
4749
|
description: "How long the access token is valid, in **seconds**, per RFC 6749 \xA75.1. Not a timestamp, and not milliseconds."
|
|
4655
4750
|
}),
|
|
4656
4751
|
refresh_token: z18.string().min(1).optional().meta({
|
|
4657
|
-
description: "The refresh token
|
|
4752
|
+
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."
|
|
4658
4753
|
}),
|
|
4659
4754
|
scope: z18.string().max(500).optional().meta({
|
|
4660
4755
|
description: "The scopes the issued token actually carries, space-separated."
|
|
@@ -4677,7 +4772,7 @@ var authorizationServerMetadata = z18.object({
|
|
|
4677
4772
|
description: "The response types this server offers: `code` only, the implicit grant being gone with OAuth 2.1."
|
|
4678
4773
|
}),
|
|
4679
4774
|
grant_types_supported: z18.array(z18.enum(["authorization_code", "refresh_token"])).meta({
|
|
4680
|
-
description: "The grants this server offers
|
|
4775
|
+
description: "The grants this server offers: `authorization_code` and `refresh_token`. OAuth 2.1 removes the implicit and password grants, so neither appears here."
|
|
4681
4776
|
}),
|
|
4682
4777
|
code_challenge_methods_supported: z18.array(codeChallengeMethod).meta({
|
|
4683
4778
|
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."
|
|
@@ -4738,7 +4833,7 @@ var oauthAuthorizeQuery = z18.object({
|
|
|
4738
4833
|
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."
|
|
4739
4834
|
});
|
|
4740
4835
|
|
|
4741
|
-
// node_modules/.pnpm/@fleetless+contracts@
|
|
4836
|
+
// node_modules/.pnpm/@fleetless+contracts@2.0.0/node_modules/@fleetless/contracts/dist/routes.js
|
|
4742
4837
|
var MCP_APP = MCP_APP_PATHS(":appIdentifier");
|
|
4743
4838
|
var APP_IDENTIFIER = {
|
|
4744
4839
|
name: "appIdentifier",
|
|
@@ -6011,7 +6106,7 @@ var ROUTES = [
|
|
|
6011
6106
|
response: dynamicClientRegistrationResponse,
|
|
6012
6107
|
errors: ["rate_limited"],
|
|
6013
6108
|
transport: "http",
|
|
6014
|
-
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
|
|
6109
|
+
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`."
|
|
6015
6110
|
},
|
|
6016
6111
|
{
|
|
6017
6112
|
method: "GET",
|
|
@@ -6137,7 +6232,7 @@ var ROUTES = [
|
|
|
6137
6232
|
response: oauthTokenResponse,
|
|
6138
6233
|
errors: [],
|
|
6139
6234
|
transport: "http",
|
|
6140
|
-
notes: "
|
|
6235
|
+
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."
|
|
6141
6236
|
},
|
|
6142
6237
|
/* ------------------------------- developer auth (the console\'s OAuth portal) */
|
|
6143
6238
|
{
|
|
@@ -6428,7 +6523,7 @@ var ROUTES = [
|
|
|
6428
6523
|
response: dynamicClientRegistrationResponse,
|
|
6429
6524
|
errors: ["rate_limited", "not_found"],
|
|
6430
6525
|
transport: "http",
|
|
6431
|
-
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
|
|
6526
|
+
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."
|
|
6432
6527
|
},
|
|
6433
6528
|
{
|
|
6434
6529
|
method: "GET",
|
|
@@ -6464,7 +6559,7 @@ var ROUTES = [
|
|
|
6464
6559
|
response: oauthTokenResponse,
|
|
6465
6560
|
errors: [],
|
|
6466
6561
|
transport: "http",
|
|
6467
|
-
notes: "
|
|
6562
|
+
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."
|
|
6468
6563
|
},
|
|
6469
6564
|
/* -------------------------------------------------- app-user (client) auth */
|
|
6470
6565
|
{
|
|
@@ -6849,6 +6944,42 @@ var ROUTES = [
|
|
|
6849
6944
|
transport: "http",
|
|
6850
6945
|
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."
|
|
6851
6946
|
},
|
|
6947
|
+
{
|
|
6948
|
+
method: "POST",
|
|
6949
|
+
path: "/api/robots/:id/token/rotate",
|
|
6950
|
+
section: "robots",
|
|
6951
|
+
summary: "Mints a new bridge token for the robot and invalidates the old one.",
|
|
6952
|
+
audience: "developer",
|
|
6953
|
+
auth: "developer",
|
|
6954
|
+
rateLimited: false,
|
|
6955
|
+
ownerTier: true,
|
|
6956
|
+
status: 201,
|
|
6957
|
+
params: [{ name: "id", description: "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`." }],
|
|
6958
|
+
query: null,
|
|
6959
|
+
request: null,
|
|
6960
|
+
response: robotTokenRotateResponse,
|
|
6961
|
+
errors: [...DEVELOPER_GUARD, "tier_required", "invalid_uuid", "not_found"],
|
|
6962
|
+
transport: "http",
|
|
6963
|
+
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."
|
|
6964
|
+
},
|
|
6965
|
+
{
|
|
6966
|
+
method: "PUT",
|
|
6967
|
+
path: "/api/robots/:id/urdf/joint-state",
|
|
6968
|
+
section: "robots",
|
|
6969
|
+
summary: "Chooses the datapoint whose joint positions move the robot's URDF, or clears it.",
|
|
6970
|
+
audience: "developer",
|
|
6971
|
+
auth: "developer",
|
|
6972
|
+
rateLimited: false,
|
|
6973
|
+
ownerTier: false,
|
|
6974
|
+
status: 200,
|
|
6975
|
+
params: [{ name: "id", description: "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`." }],
|
|
6976
|
+
query: null,
|
|
6977
|
+
request: jointStatePutRequest,
|
|
6978
|
+
response: jointStatePutResponse,
|
|
6979
|
+
errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found", "validation_error"],
|
|
6980
|
+
transport: "http",
|
|
6981
|
+
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.'
|
|
6982
|
+
},
|
|
6852
6983
|
{
|
|
6853
6984
|
method: "GET",
|
|
6854
6985
|
path: "/api/robots",
|
|
@@ -7734,6 +7865,24 @@ var ROUTES = [
|
|
|
7734
7865
|
transport: "http",
|
|
7735
7866
|
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."
|
|
7736
7867
|
},
|
|
7868
|
+
{
|
|
7869
|
+
method: "DELETE",
|
|
7870
|
+
path: "/api/robots/:id/assets",
|
|
7871
|
+
section: "assets",
|
|
7872
|
+
summary: "Empties a robot's asset store: every URDF, mesh and texture, gone at once.",
|
|
7873
|
+
audience: "client",
|
|
7874
|
+
auth: "developer_or_client",
|
|
7875
|
+
rateLimited: false,
|
|
7876
|
+
ownerTier: true,
|
|
7877
|
+
status: 200,
|
|
7878
|
+
params: [{ name: "id", description: "The robot's uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`." }],
|
|
7879
|
+
query: null,
|
|
7880
|
+
request: null,
|
|
7881
|
+
response: assetsClearResponse,
|
|
7882
|
+
errors: [...CLIENT_GUARD, "tier_required", "invalid_uuid", "not_found", "busy"],
|
|
7883
|
+
transport: "http",
|
|
7884
|
+
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."
|
|
7885
|
+
},
|
|
7737
7886
|
/* ------------------------------------------------ org (quotas and fleet reads) */
|
|
7738
7887
|
{
|
|
7739
7888
|
method: "GET",
|
|
@@ -7858,9 +8007,9 @@ var ROUTES = [
|
|
|
7858
8007
|
query: null,
|
|
7859
8008
|
request: null,
|
|
7860
8009
|
response: asset,
|
|
7861
|
-
errors: ["unauthorized", "rate_limited", "
|
|
8010
|
+
errors: ["unauthorized", "rate_limited", "validation_error", "not_found", "quota_exceeded", "bad_request"],
|
|
7862
8011
|
transport: "http",
|
|
7863
|
-
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.
|
|
8012
|
+
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."
|
|
7864
8013
|
},
|
|
7865
8014
|
/* ------------------------------------ realtime and bridge transports */
|
|
7866
8015
|
{
|
|
@@ -8135,13 +8284,16 @@ function oidcErrorFromCallbackParams(params) {
|
|
|
8135
8284
|
`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.`
|
|
8136
8285
|
);
|
|
8137
8286
|
}
|
|
8138
|
-
function
|
|
8287
|
+
function sessionlessRefusal(option, method, needs) {
|
|
8139
8288
|
throw new FleetlessError(
|
|
8140
8289
|
"invalid_option",
|
|
8141
|
-
`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.`
|
|
8290
|
+
`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.`
|
|
8142
8291
|
);
|
|
8143
8292
|
}
|
|
8144
|
-
function createServerKeyAuth(http, appIdentifier2) {
|
|
8293
|
+
function createServerKeyAuth(http, appIdentifier2, option = "serverKey") {
|
|
8294
|
+
const serverKeyRefusal = (method, needs) => sessionlessRefusal(option, method, needs);
|
|
8295
|
+
const subject = option === "serverKey" ? "a server key" : "a supplied credential";
|
|
8296
|
+
const holder = option === "serverKey" ? "a server-key client" : "a client with a supplied credential";
|
|
8145
8297
|
return {
|
|
8146
8298
|
// **`register`, `resendVerification` and `requestPasswordReset` are
|
|
8147
8299
|
// allowed here** — see `createPublicAuthCalls`. They are public routes
|
|
@@ -8150,25 +8302,25 @@ function createServerKeyAuth(http, appIdentifier2) {
|
|
|
8150
8302
|
// already has.
|
|
8151
8303
|
...createPublicAuthCalls(http, appIdentifier2),
|
|
8152
8304
|
async verifyEmail() {
|
|
8153
|
-
serverKeyRefusal("verifyEmail",
|
|
8305
|
+
serverKeyRefusal("verifyEmail", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
8154
8306
|
},
|
|
8155
8307
|
async login() {
|
|
8156
|
-
serverKeyRefusal("login",
|
|
8308
|
+
serverKeyRefusal("login", `${subject} IS the credential; there is nothing to exchange`);
|
|
8157
8309
|
},
|
|
8158
8310
|
async logout() {
|
|
8159
|
-
serverKeyRefusal("logout",
|
|
8311
|
+
serverKeyRefusal("logout", `${subject} holds no session to end`);
|
|
8160
8312
|
},
|
|
8161
8313
|
async me() {
|
|
8162
8314
|
return http.request("/api/client/me", {});
|
|
8163
8315
|
},
|
|
8164
8316
|
async changePassword() {
|
|
8165
|
-
serverKeyRefusal("changePassword",
|
|
8317
|
+
serverKeyRefusal("changePassword", `${subject} has no password${option === "serverKey" ? "; rotate the key in the console instead" : ""}`);
|
|
8166
8318
|
},
|
|
8167
8319
|
async confirmPasswordReset() {
|
|
8168
|
-
serverKeyRefusal("confirmPasswordReset",
|
|
8320
|
+
serverKeyRefusal("confirmPasswordReset", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
8169
8321
|
},
|
|
8170
8322
|
async acceptInvitation() {
|
|
8171
|
-
serverKeyRefusal("acceptInvitation",
|
|
8323
|
+
serverKeyRefusal("acceptInvitation", `the route answers a session and ${holder} has nowhere to store it, so the session would be silently discarded`);
|
|
8172
8324
|
},
|
|
8173
8325
|
async listProviders() {
|
|
8174
8326
|
const query = new URLSearchParams({ app_identifier: appIdentifier2 });
|
|
@@ -8188,16 +8340,16 @@ function createServerKeyAuth(http, appIdentifier2) {
|
|
|
8188
8340
|
return http.request(`/api/client/mcp/interactions/${pathSegment(id)}`, {});
|
|
8189
8341
|
},
|
|
8190
8342
|
async approveMcpInteraction() {
|
|
8191
|
-
serverKeyRefusal("approveMcpInteraction",
|
|
8343
|
+
serverKeyRefusal("approveMcpInteraction", `a consent is a person's decision, and ${subject} is not a person`);
|
|
8192
8344
|
},
|
|
8193
8345
|
async denyMcpInteraction() {
|
|
8194
|
-
serverKeyRefusal("denyMcpInteraction",
|
|
8346
|
+
serverKeyRefusal("denyMcpInteraction", `a consent is a person's decision, and ${subject} is not a person`);
|
|
8195
8347
|
},
|
|
8196
8348
|
async listMcpGrants() {
|
|
8197
|
-
serverKeyRefusal("listMcpGrants",
|
|
8349
|
+
serverKeyRefusal("listMcpGrants", `${subject} never went through a consent screen, so it has no grants of its own`);
|
|
8198
8350
|
},
|
|
8199
8351
|
async revokeMcpGrant() {
|
|
8200
|
-
serverKeyRefusal("revokeMcpGrant",
|
|
8352
|
+
serverKeyRefusal("revokeMcpGrant", `${subject} never went through a consent screen, so it has no grants of its own`);
|
|
8201
8353
|
}
|
|
8202
8354
|
};
|
|
8203
8355
|
}
|
|
@@ -8937,9 +9089,10 @@ var InMemoryTokenStore = class {
|
|
|
8937
9089
|
|
|
8938
9090
|
// src/client.ts
|
|
8939
9091
|
function createClient(options) {
|
|
8940
|
-
|
|
9092
|
+
const chosen = ["tokenStore", "serverKey", "credentials"].filter((name) => options[name] !== void 0);
|
|
9093
|
+
if (chosen.length > 1) {
|
|
8941
9094
|
throw new Error(
|
|
8942
|
-
|
|
9095
|
+
`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 ")}.`
|
|
8943
9096
|
);
|
|
8944
9097
|
}
|
|
8945
9098
|
const config = Object.freeze({
|
|
@@ -8955,7 +9108,11 @@ function createClient(options) {
|
|
|
8955
9108
|
let auth;
|
|
8956
9109
|
let http;
|
|
8957
9110
|
let credentials;
|
|
8958
|
-
if (options.
|
|
9111
|
+
if (options.credentials !== void 0) {
|
|
9112
|
+
credentials = options.credentials;
|
|
9113
|
+
http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials });
|
|
9114
|
+
auth = createServerKeyAuth(http, config.appIdentifier, "credentials");
|
|
9115
|
+
} else if (options.serverKey !== void 0) {
|
|
8959
9116
|
credentials = new ServerKeyCredentials(options.serverKey);
|
|
8960
9117
|
http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials });
|
|
8961
9118
|
auth = createServerKeyAuth(http, config.appIdentifier);
|
|
@@ -8978,7 +9135,7 @@ function createClient(options) {
|
|
|
8978
9135
|
const jobs = createJobsApi(http);
|
|
8979
9136
|
const assets = createAssetsApi(http);
|
|
8980
9137
|
const robots = createRobotsApi(http);
|
|
8981
|
-
if (options.serverKey === void 0) {
|
|
9138
|
+
if (options.serverKey === void 0 && options.credentials === void 0) {
|
|
8982
9139
|
const baseLogout = auth.logout.bind(auth);
|
|
8983
9140
|
auth = {
|
|
8984
9141
|
...auth,
|