@fleetless/sdk 1.0.0 → 2.0.1
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 +113 -0
- package/LICENSE +21 -0
- package/README.md +153 -1134
- package/dist/index.cjs +2963 -1108
- package/dist/index.d.cts +421 -146
- package/dist/index.d.ts +421 -146
- package/dist/index.js +2963 -1108
- package/package.json +43 -7
package/dist/index.cjs
CHANGED
|
@@ -349,9 +349,17 @@ function createAssetsApi(http) {
|
|
|
349
349
|
const { body, headers } = await http.requestBinary(`/api/robots/${pathSegment(robotId)}/assets/${pathSegment(assetId)}`, { signal });
|
|
350
350
|
return { body, mime: mimeFromContentType(headers) };
|
|
351
351
|
}
|
|
352
|
+
async function syncStatus(robotId, syncId, signal) {
|
|
353
|
+
const response = await http.request(
|
|
354
|
+
`/api/robots/${pathSegment(robotId)}/assets/sync/${pathSegment(syncId)}`,
|
|
355
|
+
{ signal }
|
|
356
|
+
);
|
|
357
|
+
return response;
|
|
358
|
+
}
|
|
352
359
|
return {
|
|
353
360
|
list,
|
|
354
361
|
get,
|
|
362
|
+
syncStatus,
|
|
355
363
|
async urdf(robotId) {
|
|
356
364
|
const { body } = await http.requestBinary(`/api/robots/${pathSegment(robotId)}/urdf`);
|
|
357
365
|
return new TextDecoder().decode(body);
|
|
@@ -433,7 +441,7 @@ function createAssetsApi(http) {
|
|
|
433
441
|
if (pkg) knownPackages.add(pkg);
|
|
434
442
|
}
|
|
435
443
|
for (const ref of listResponse.urdf.missing) {
|
|
436
|
-
const pkg = packageNameOf(ref);
|
|
444
|
+
const pkg = packageNameOf(ref.uri);
|
|
437
445
|
if (pkg) knownPackages.add(pkg);
|
|
438
446
|
}
|
|
439
447
|
const refusedObjectUrl = URL.createObjectURL(new Blob([]));
|
|
@@ -478,62 +486,69 @@ function createAssetsApi(http) {
|
|
|
478
486
|
};
|
|
479
487
|
}
|
|
480
488
|
|
|
481
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
489
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/common.js
|
|
482
490
|
var import_zod = require("zod");
|
|
483
|
-
var
|
|
484
|
-
var
|
|
485
|
-
var
|
|
486
|
-
var
|
|
491
|
+
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.";
|
|
492
|
+
var slug = import_zod.z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, SLUG_RULE);
|
|
493
|
+
var ROS_NAME_RULE = "A ROS graph name is absolute: it begins with a slash, and each segment after a slash starts with a letter or an underscore and continues with letters, digits and underscores \u2014 `/camera/image_raw`. A relative name, a trailing slash, a dash or a dot is refused.";
|
|
494
|
+
var rosName = import_zod.z.string().max(255).regex(/^\/[A-Za-z_][A-Za-z0-9_]*(?:\/[A-Za-z_][A-Za-z0-9_]*)*$/, ROS_NAME_RULE);
|
|
495
|
+
var ROS_TYPE_NAME_RULE = "A ROS 2 type name has three segments: the package, then `msg`, `srv` or `action`, then the type \u2014 `sensor_msgs/msg/BatteryState`, `std_srvs/srv/Trigger`, `nav2_msgs/action/NavigateToPose`. The middle segment is the one usually left out. The package is lower-case with underscores; the type itself is letters and digits, conventionally CamelCase.";
|
|
496
|
+
var rosTypeName = import_zod.z.string().max(255).regex(/^[a-z][a-z0-9_]*\/(?:msg|srv|action)\/[A-Za-z][A-Za-z0-9]*$/, ROS_TYPE_NAME_RULE);
|
|
497
|
+
var FIELD_PATH_RULE = "A field path is dotted and lower-case, and each segment may index at most one array level \u2014 `voltage`, `pose.position.x`, `ranges[0]`. ROS 2 has no nested arrays, so a second index on one segment could name nothing that exists.";
|
|
498
|
+
var fieldPath = import_zod.z.string().max(255).regex(/^[a-z_][a-z0-9_]*(?:\[\d+\])?(?:\.[a-z_][a-z0-9_]*(?:\[\d+\])?)*$/, FIELD_PATH_RULE);
|
|
499
|
+
var wireSeqCursor = import_zod.z.union([import_zod.z.string().regex(/^\d{1,19}$/), import_zod.z.number().int()]).transform((v) => Number(v)).pipe(import_zod.z.number().int().positive());
|
|
500
|
+
var wireTimestampMs = import_zod.z.union([import_zod.z.string().regex(/^\d{1,15}$/), import_zod.z.number().int()]).transform((v) => Number(v)).pipe(import_zod.z.number().int().nonnegative().refine((ms) => {
|
|
501
|
+
const year = new Date(ms).getUTCFullYear();
|
|
502
|
+
return Number.isFinite(year) && year >= 1 && year <= 9999;
|
|
503
|
+
}, "must fall within years 1..9999"));
|
|
504
|
+
var applyErrorKind = import_zod.z.enum(["datapoint", "action", "service", "publisher", "camera"]);
|
|
505
|
+
var applyError = import_zod.z.object({
|
|
506
|
+
slug: import_zod.z.string(),
|
|
507
|
+
kind: applyErrorKind,
|
|
508
|
+
code: import_zod.z.string().min(1).max(40),
|
|
509
|
+
message: import_zod.z.string().min(1),
|
|
510
|
+
details: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
511
|
+
});
|
|
487
512
|
|
|
488
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
513
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/mcp.js
|
|
489
514
|
var import_zod2 = require("zod");
|
|
490
515
|
var mcpToolKind = import_zod2.z.enum(["datapoint", "service", "action", "publisher", "camera"]);
|
|
491
|
-
var
|
|
492
|
-
var mcpToolNamePattern = /^[a-z0-9][a-z0-9_-]*$/;
|
|
493
|
-
var mcpToolPreview = import_zod2.z.object({
|
|
494
|
-
name: import_zod2.z.string().min(1).max(MCP_TOOL_NAME_MAX).regex(mcpToolNamePattern),
|
|
495
|
-
/** The human-readable label — where the robot's actual name goes. */
|
|
496
|
-
title: import_zod2.z.string().min(1).max(200),
|
|
497
|
-
/**
|
|
498
|
-
* **Four thousand, not two thousand, and the difference is the point.**
|
|
499
|
-
*
|
|
500
|
-
* `serviceDescription` bounds what a *developer writes* at 2000. This bounds
|
|
501
|
-
* what the *generator produces*, which is that text **plus** what it folds
|
|
502
|
-
* in — a datapoint's `Unit:` and `Plausible range:`, a camera's fixed
|
|
503
|
-
* sentence about snapshots. Measured by Kassandra-W7c and Momus-W7c
|
|
504
|
-
* independently: a maximal description came back at 2036–2068 characters
|
|
505
|
-
* against a 2000 bound, so the cloud served a document its own contract
|
|
506
|
-
* rejected — silently, because the route returns a typed literal without
|
|
507
|
-
* parsing it.
|
|
508
|
-
*
|
|
509
|
-
* **Do not "tidy" these two numbers into agreement.** They describe
|
|
510
|
-
* different things, and making them equal reintroduces the defect: either
|
|
511
|
-
* the generator truncates a developer's own words, or the response
|
|
512
|
-
* overflows again. The gap is the room the generator needs.
|
|
513
|
-
*/
|
|
514
|
-
description: import_zod2.z.string().min(1).max(4e3),
|
|
515
|
-
robot_id: import_zod2.z.uuid(),
|
|
516
|
+
var mcpExposure = import_zod2.z.object({
|
|
516
517
|
slug,
|
|
517
518
|
kind: mcpToolKind,
|
|
518
|
-
|
|
519
|
+
description: import_zod2.z.string().max(2e3).nullable(),
|
|
520
|
+
/** A datapoint's `numeric.unit`, verbatim; `null` for every other kind and for a unitless datapoint. */
|
|
521
|
+
unit: import_zod2.z.string().max(32).nullable(),
|
|
522
|
+
/**
|
|
523
|
+
* A datapoint's `numeric.decimals`, verbatim; `null` for every other kind
|
|
524
|
+
* and for a datapoint that does not set it. Required-nullable rather than
|
|
525
|
+
* optional for the same reason as `unit`: an omitted field would make a
|
|
526
|
+
* producer that forgot the datapoint's configuration indistinguishable from
|
|
527
|
+
* one reporting a datapoint that has none.
|
|
528
|
+
*/
|
|
529
|
+
decimals: import_zod2.z.number().int().min(0).max(6).nullable(),
|
|
530
|
+
input_schema: import_zod2.z.unknown().nullable()
|
|
531
|
+
});
|
|
532
|
+
var mcpCapabilities = import_zod2.z.object({
|
|
533
|
+
action_history: import_zod2.z.boolean(),
|
|
534
|
+
assets: import_zod2.z.boolean()
|
|
519
535
|
});
|
|
520
|
-
var
|
|
536
|
+
var mcpRobotDatasheet = import_zod2.z.object({
|
|
521
537
|
robot_id: import_zod2.z.uuid(),
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
message: import_zod2.z.string().min(1)
|
|
538
|
+
robot_name: import_zod2.z.string().min(1).max(200),
|
|
539
|
+
capabilities: mcpCapabilities,
|
|
540
|
+
exposures: import_zod2.z.array(mcpExposure).max(2e3)
|
|
526
541
|
});
|
|
527
|
-
var
|
|
542
|
+
var mcpRolePreviewResponse = import_zod2.z.object({
|
|
528
543
|
role_id: import_zod2.z.uuid(),
|
|
529
|
-
|
|
530
|
-
omitted: import_zod2.z.array(mcpOmission).max(500)
|
|
544
|
+
robots: import_zod2.z.array(mcpRobotDatasheet).max(500)
|
|
531
545
|
});
|
|
546
|
+
var MCP_ASSET_LINK_TTL_MS = 15 * 60 * 1e3;
|
|
532
547
|
|
|
533
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
534
|
-
var
|
|
548
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/protocol.js
|
|
549
|
+
var import_zod8 = require("zod");
|
|
535
550
|
|
|
536
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
551
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/assets.js
|
|
537
552
|
var import_zod3 = require("zod");
|
|
538
553
|
var assetKind = import_zod3.z.enum(["urdf", "mesh", "texture", "other"]);
|
|
539
554
|
var asset = import_zod3.z.object({
|
|
@@ -593,39 +608,24 @@ var urdfCompleteness = import_zod3.z.object({
|
|
|
593
608
|
present: import_zod3.z.boolean(),
|
|
594
609
|
/** How many distinct meshes the URDF references. */
|
|
595
610
|
mesh_count: import_zod3.z.number().int().nonnegative(),
|
|
596
|
-
missing: import_zod3.z.array(import_zod3.z.string().min(1))
|
|
597
|
-
});
|
|
598
|
-
var assetListResponse = import_zod3.z.object({
|
|
599
|
-
assets: import_zod3.z.array(asset),
|
|
600
|
-
urdf: urdfCompleteness,
|
|
601
611
|
/**
|
|
602
|
-
*
|
|
603
|
-
* separate from what has been transferred (§4.6: the bridge "meldet nur
|
|
604
|
-
* Verfügbarkeit"). `null` when no bridge is connected — distinct from
|
|
605
|
-
* `false`, because "no robot is online to ask" and "the robot has no URDF"
|
|
606
|
-
* send a developer to two different places.
|
|
612
|
+
* **Was fehlt, und wovon (W9b, DEF-081).**
|
|
607
613
|
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
* it heard, forever, and `true` was sticky. The fix is an **active**
|
|
613
|
-
* `count_publishers` query on the bridge's own timer.
|
|
614
|
-
*
|
|
615
|
-
* **What a consumer still needs to know is the clock, not the gap.** An
|
|
616
|
-
* ungraceful loss — the publisher process killed rather than shut down — is
|
|
617
|
-
* noticed on **DDS's liveliness timeout**, not on the bridge's check
|
|
618
|
-
* interval. Measured against a real bridge: ~1.6 s when the publisher calls
|
|
619
|
-
* `destroy_node()`, **~19 s when it is `SIGKILL`ed**. So `true` can outlive
|
|
620
|
-
* the truth by some seconds after a crash, and no amount of polling on our
|
|
621
|
-
* side shortens it.
|
|
614
|
+
* Vorher ein blankes `string[]`. Die Console meldete daraufhin *„N meshes
|
|
615
|
+
* missing from the workspace"* — auch für eine fehlende **Textur**, während
|
|
616
|
+
* `mesh_count` daneben eine andere Zahl nannte: zwei Angaben über denselben
|
|
617
|
+
* Gegenstand, die einander widersprechen.
|
|
622
618
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
619
|
+
* Die Cloud wusste es die ganze Zeit: `extractReferencesByElement` markiert
|
|
620
|
+
* jede Referenz mit ihrem Element und `buildAssetListResponse` warf die
|
|
621
|
+
* Markierung wieder weg. **Die Antwort im Client zu raten wäre genau die
|
|
622
|
+
* „neue Kopie", die die Registerzeile ausdrücklich ablehnt** — eine zweite
|
|
623
|
+
* Herleitung derselben Tatsache, die von der ersten abweichen kann.
|
|
627
624
|
*/
|
|
628
|
-
|
|
625
|
+
missing: import_zod3.z.array(import_zod3.z.object({
|
|
626
|
+
uri: import_zod3.z.string().min(1).max(500),
|
|
627
|
+
element: import_zod3.z.enum(["mesh", "texture"])
|
|
628
|
+
}))
|
|
629
629
|
});
|
|
630
630
|
var assetSyncRequest = import_zod3.z.object({
|
|
631
631
|
source: import_zod3.z.enum(["bridge"])
|
|
@@ -633,7 +633,12 @@ var assetSyncRequest = import_zod3.z.object({
|
|
|
633
633
|
var assetSyncResponse = import_zod3.z.object({
|
|
634
634
|
sync_id: import_zod3.z.uuid()
|
|
635
635
|
});
|
|
636
|
-
var
|
|
636
|
+
var ASSET_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
|
|
637
|
+
var assetTooLargeDetails = import_zod3.z.object({
|
|
638
|
+
limit_bytes: import_zod3.z.number().int().positive(),
|
|
639
|
+
size_bytes: import_zod3.z.number().int().positive()
|
|
640
|
+
});
|
|
641
|
+
var assetFailureKind = import_zod3.z.enum(["unresolvable", "upload_failed", "refused", "too_large"]);
|
|
637
642
|
var assetFailure = import_zod3.z.object({
|
|
638
643
|
/**
|
|
639
644
|
* What could not be provided, verbatim — the same string `asset.name` would
|
|
@@ -643,7 +648,34 @@ var assetFailure = import_zod3.z.object({
|
|
|
643
648
|
* this list must not assume every entry is one.
|
|
644
649
|
*/
|
|
645
650
|
reference: import_zod3.z.string().min(1).max(500),
|
|
646
|
-
kind: assetFailureKind
|
|
651
|
+
kind: assetFailureKind,
|
|
652
|
+
/**
|
|
653
|
+
* **Die zwei Zahlen, und warum `too_large` eine eigene Art ist (W9b).**
|
|
654
|
+
*
|
|
655
|
+
* `refused` bedeutet *„nie versucht, weil eine Decke des Erzeugers erreicht
|
|
656
|
+
* wurde"* — das passt auf eine Datei, die wegen ihrer Größe gar nicht erst
|
|
657
|
+
* gelesen wurde, **und ebenso auf die Sammel-Sentinel**, mit der ein Sync
|
|
658
|
+
* aufhört, einzelne Fehler zu benennen. Beides unter eine Art zu legen wäre
|
|
659
|
+
* derselbe Fehler, den W9a eine Welle zuvor ausgeräumt hat: zwei Fakten auf
|
|
660
|
+
* einem Schlüssel, von denen jeder den anderen überschreibt.
|
|
661
|
+
*
|
|
662
|
+
* Und ein Grund ohne Zahlen ist kein Grund, mit dem jemand etwas anfangen
|
|
663
|
+
* kann. *„Zu groß"* beantwortet nicht, ob das Mesh zu verkleinern ist oder
|
|
664
|
+
* die Grenze zu heben — `limit_bytes` und `size_bytes` tun es.
|
|
665
|
+
*
|
|
666
|
+
* Abwesend für jede andere Art — ein erzwungenes `details: null` auf jedem
|
|
667
|
+
* `unresolvable` kauft nichts. Die Paarung ist unten **erzwungen**, nicht
|
|
668
|
+
* beschrieben: ein Feld, dessen Regel nur im Kommentar steht, ist eine
|
|
669
|
+
* Bitte.
|
|
670
|
+
*/
|
|
671
|
+
details: assetTooLargeDetails.nullish()
|
|
672
|
+
}).superRefine((f, ctx) => {
|
|
673
|
+
if (f.kind === "too_large" && f.details == null) {
|
|
674
|
+
ctx.addIssue({ code: "custom", path: ["details"], message: "`too_large` without limit_bytes/size_bytes says nothing a developer can act on" });
|
|
675
|
+
}
|
|
676
|
+
if (f.kind !== "too_large" && f.details != null) {
|
|
677
|
+
ctx.addIssue({ code: "custom", path: ["details"], message: "size details belong to `too_large` only" });
|
|
678
|
+
}
|
|
647
679
|
});
|
|
648
680
|
var assetSyncState = import_zod3.z.enum(["running", "succeeded", "failed"]);
|
|
649
681
|
var assetSyncStatus = import_zod3.z.object({
|
|
@@ -702,121 +734,959 @@ var assetSyncStatus = import_zod3.z.object({
|
|
|
702
734
|
started_at: import_zod3.z.iso.datetime(),
|
|
703
735
|
updated_at: import_zod3.z.iso.datetime()
|
|
704
736
|
});
|
|
705
|
-
var
|
|
706
|
-
|
|
707
|
-
|
|
737
|
+
var assetListResponse = import_zod3.z.object({
|
|
738
|
+
assets: import_zod3.z.array(asset),
|
|
739
|
+
/**
|
|
740
|
+
* Der gerade laufende Sync, oder `null` (W9b, DEF-147).
|
|
741
|
+
*
|
|
742
|
+
* **Der Fall, für den das hier steht, ist der Neuladen-Fall.** Die Console
|
|
743
|
+
* hielt die `sync_id` nur im Speicher; ein Reload verlor die Fortschritts-
|
|
744
|
+
* anzeige, und der Zustand war serverseitig da, über
|
|
745
|
+
* `GET .../assets/sync/<id>` abfragbar — nur erreichte ihn niemand mehr, der
|
|
746
|
+
* die id nicht aufgehoben hatte. Eine Seite, die frisch lädt, drückt keinen
|
|
747
|
+
* Knopf; sie fragt diese Liste. Also muss die Liste es sagen.
|
|
748
|
+
*/
|
|
749
|
+
active_sync: assetSyncStatus.nullable(),
|
|
750
|
+
urdf: urdfCompleteness,
|
|
751
|
+
/**
|
|
752
|
+
* What the connected bridge says it *could* transfer, which is deliberately
|
|
753
|
+
* separate from what has been transferred (§4.6: the bridge "meldet nur
|
|
754
|
+
* Verfügbarkeit"). `null` when no bridge is connected — distinct from
|
|
755
|
+
* `false`, because "no robot is online to ask" and "the robot has no URDF"
|
|
756
|
+
* send a developer to two different places.
|
|
757
|
+
*
|
|
758
|
+
* **All three states are reachable as of W7a (R7).** They were not: the bridge
|
|
759
|
+
* used to report availability from a subscription callback, which fires only
|
|
760
|
+
* when a publisher *sends* something, so it could notice presence and never
|
|
761
|
+
* absence — a robot that lost its URDF left the cloud holding the last thing
|
|
762
|
+
* it heard, forever, and `true` was sticky. The fix is an **active**
|
|
763
|
+
* `count_publishers` query on the bridge's own timer.
|
|
764
|
+
*
|
|
765
|
+
* **What a consumer still needs to know is the clock, not the gap.** An
|
|
766
|
+
* ungraceful loss — the publisher process killed rather than shut down — is
|
|
767
|
+
* noticed on **DDS's liveliness timeout**, not on the bridge's check
|
|
768
|
+
* interval. Measured against a real bridge: ~1.6 s when the publisher calls
|
|
769
|
+
* `destroy_node()`, **~19 s when it is `SIGKILL`ed**. So `true` can outlive
|
|
770
|
+
* the truth by some seconds after a crash, and no amount of polling on our
|
|
771
|
+
* side shortens it.
|
|
772
|
+
*
|
|
773
|
+
* The sticky-`true` gap was found by Rosie-W7 checking her own work against
|
|
774
|
+
* the camera-health row of identical shape; the DDS clock was measured by
|
|
775
|
+
* Rosie-W7a closing it, and this comment was still describing the gap a wave
|
|
776
|
+
* after it was fixed (Momus-W7a, W7a review).
|
|
777
|
+
*/
|
|
778
|
+
urdf_available: import_zod3.z.boolean().nullable()
|
|
708
779
|
});
|
|
780
|
+
var assetSyncBusyDetails = import_zod3.z.object({
|
|
781
|
+
sync_id: import_zod3.z.uuid(),
|
|
782
|
+
/** Wann er begann — damit „läuft noch" von „hängt seit einer Stunde" unterscheidbar ist. */
|
|
783
|
+
started_at_ms: import_zod3.z.number().int().nonnegative()
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/config.js
|
|
787
|
+
var import_zod5 = require("zod");
|
|
709
788
|
|
|
710
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
789
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/alerts.js
|
|
711
790
|
var import_zod4 = require("zod");
|
|
712
|
-
var
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
});
|
|
729
|
-
var datapointRate = import_zod4.z.discriminatedUnion("mode", [
|
|
730
|
-
import_zod4.z.object({ mode: import_zod4.z.literal("max_hz"), hz: import_zod4.z.number().positive().max(100) }),
|
|
731
|
-
import_zod4.z.object({ mode: import_zod4.z.literal("on_change") })
|
|
791
|
+
var alertRowCondition = import_zod4.z.discriminatedUnion("kind", [
|
|
792
|
+
import_zod4.z.strictObject({
|
|
793
|
+
kind: import_zod4.z.literal("above"),
|
|
794
|
+
threshold: import_zod4.z.number().finite(),
|
|
795
|
+
resolve_hysteresis: import_zod4.z.number().nonnegative().default(0)
|
|
796
|
+
}),
|
|
797
|
+
import_zod4.z.strictObject({
|
|
798
|
+
kind: import_zod4.z.literal("below"),
|
|
799
|
+
threshold: import_zod4.z.number().finite(),
|
|
800
|
+
resolve_hysteresis: import_zod4.z.number().nonnegative().default(0)
|
|
801
|
+
}),
|
|
802
|
+
import_zod4.z.strictObject({
|
|
803
|
+
kind: import_zod4.z.literal("equals"),
|
|
804
|
+
/** A JSON scalar, matching what a datapoint value actually is on the wire — never an object or array. */
|
|
805
|
+
value: import_zod4.z.union([import_zod4.z.number(), import_zod4.z.string(), import_zod4.z.boolean()])
|
|
806
|
+
})
|
|
732
807
|
]);
|
|
733
|
-
var
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
808
|
+
var alertSeverity = import_zod4.z.enum(["warning", "error"]);
|
|
809
|
+
var alertState = import_zod4.z.enum(["ok", "firing"]);
|
|
810
|
+
var datapointAlertRow = import_zod4.z.object({
|
|
811
|
+
id: import_zod4.z.uuid(),
|
|
812
|
+
robot_id: import_zod4.z.uuid(),
|
|
738
813
|
slug,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
* this field
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* the
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
814
|
+
name: import_zod4.z.string().min(1).max(120),
|
|
815
|
+
enabled: import_zod4.z.boolean(),
|
|
816
|
+
severity: alertSeverity,
|
|
817
|
+
condition: alertRowCondition,
|
|
818
|
+
state: alertState,
|
|
819
|
+
/** `null` only until the first evaluation writes a state; every alert is created `ok` (D2), so in practice this is set from creation onward. */
|
|
820
|
+
state_since: import_zod4.z.iso.datetime().nullable(),
|
|
821
|
+
/**
|
|
822
|
+
* The value at the alert's last state transition — written only when the
|
|
823
|
+
* alert fires or resolves, never on a per-sample basis. This is a
|
|
824
|
+
* deliberate cost trade, not an oversight: a per-sample write would turn
|
|
825
|
+
* every accepted sample into a DB write regardless of whether anything
|
|
826
|
+
* changed, which is exactly the hot-path cost the evaluator avoids
|
|
827
|
+
* everywhere else. It follows that this is NOT "the datapoint's current
|
|
828
|
+
* value" — for that, read the live snapshot (`datapointValue`, or the
|
|
829
|
+
* realtime datapoint stream), never this field. `null` before the
|
|
830
|
+
* alert's first transition, and returns to `null` when a `PATCH`
|
|
831
|
+
* replaces `condition` wholesale — the old value was judged against the
|
|
832
|
+
* old condition, and the store resets runtime state in that same write
|
|
833
|
+
* rather than let it survive a condition change it no longer means
|
|
834
|
+
* anything against.
|
|
835
|
+
*/
|
|
836
|
+
last_value: import_zod4.z.unknown().nullable(),
|
|
837
|
+
created_at: import_zod4.z.iso.datetime()
|
|
838
|
+
});
|
|
839
|
+
var alertListResponse = import_zod4.z.object({
|
|
840
|
+
alerts: import_zod4.z.array(datapointAlertRow)
|
|
841
|
+
});
|
|
842
|
+
var orgFiringAlertsResponse = import_zod4.z.object({
|
|
843
|
+
alerts: import_zod4.z.array(datapointAlertRow.extend({ robot_name: import_zod4.z.string().min(1).max(63) }))
|
|
844
|
+
});
|
|
845
|
+
var datapointDisplay = import_zod4.z.object({
|
|
846
|
+
y_min: import_zod4.z.number().finite().nullable(),
|
|
847
|
+
y_max: import_zod4.z.number().finite().nullable()
|
|
848
|
+
});
|
|
849
|
+
var putDatapointDisplayRequest = import_zod4.z.object({
|
|
850
|
+
y_min: import_zod4.z.number().finite().nullable(),
|
|
851
|
+
y_max: import_zod4.z.number().finite().nullable()
|
|
852
|
+
}).strict();
|
|
853
|
+
|
|
854
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/config.js
|
|
855
|
+
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:`.";
|
|
856
|
+
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:`.";
|
|
857
|
+
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.";
|
|
858
|
+
var mapKey = slug.meta({ patternErrorMessage: SLUG_RULE });
|
|
859
|
+
var saysItIsMissing = (field) => {
|
|
860
|
+
const meta = import_zod5.z.globalRegistry.get(field);
|
|
861
|
+
const def = { ...field._zod.def };
|
|
862
|
+
const inherited = def.error;
|
|
863
|
+
const carrier = def.type === "optional" || !field.safeParse(void 0).success ? field : import_zod5.z.nonoptional(field);
|
|
864
|
+
const carrierDef = carrier === field ? def : { ...carrier._zod.def };
|
|
865
|
+
carrierDef.error = (issue) => {
|
|
866
|
+
const key = issue.path?.[issue.path.length - 1];
|
|
867
|
+
if (typeof key === "string" && absent(issue, key))
|
|
868
|
+
return `Missing required key \`${key}\`.`;
|
|
869
|
+
return typeof inherited === "function" ? inherited(issue) : inherited;
|
|
870
|
+
};
|
|
871
|
+
const cloned = carrier.clone(carrierDef);
|
|
872
|
+
if (meta !== void 0)
|
|
873
|
+
import_zod5.z.globalRegistry.add(cloned, meta);
|
|
874
|
+
return cloned;
|
|
875
|
+
};
|
|
876
|
+
var absent = (issue, key) => issue.input === void 0 || issue.code === "invalid_union" && typeof issue.input === "object" && issue.input !== null && !Object.hasOwn(issue.input, key);
|
|
877
|
+
var namesItsAbsence = (shape) => Object.fromEntries(Object.entries(shape).map(([key, field]) => [key, saysItIsMissing(field)]));
|
|
878
|
+
var strictObject = (shape) => import_zod5.z.strictObject(namesItsAbsence(shape));
|
|
879
|
+
var slugKeyed = (entry) => import_zod5.z.record(mapKey, entry, {
|
|
880
|
+
error: (issue) => {
|
|
881
|
+
if (issue.code !== "invalid_key")
|
|
882
|
+
return void 0;
|
|
883
|
+
const why = issue.issues.map((inner) => inner.message).filter(Boolean).join(" ");
|
|
884
|
+
return why.length === 0 ? void 0 : `\`${String(issue.input)}\` is not a valid name. ${why}`;
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
var describeValues = (values, table) => values.map((value) => table[value]);
|
|
888
|
+
var param = (name) => `\\\${${name}}`;
|
|
889
|
+
var underSlug = (slugKey, snippet) => ({ ...snippet, body: { [slugKey]: snippet.body } });
|
|
890
|
+
var serviceDescription = import_zod5.z.string().min(1).max(2e3).optional();
|
|
891
|
+
var parameterDescription = import_zod5.z.string().min(1).max(500).optional();
|
|
892
|
+
var parameterType = import_zod5.z.enum([
|
|
893
|
+
"bool",
|
|
894
|
+
"byte",
|
|
895
|
+
"char",
|
|
896
|
+
"int8",
|
|
897
|
+
"uint8",
|
|
898
|
+
"int16",
|
|
899
|
+
"uint16",
|
|
900
|
+
"int32",
|
|
901
|
+
"uint32",
|
|
902
|
+
"int64",
|
|
903
|
+
"uint64",
|
|
904
|
+
"float32",
|
|
905
|
+
"float64",
|
|
906
|
+
"string",
|
|
907
|
+
"wstring"
|
|
908
|
+
]);
|
|
909
|
+
var INTEGER_TYPES = /* @__PURE__ */ new Set(["byte", "char", "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64"]);
|
|
910
|
+
var FLOAT_TYPES = /* @__PURE__ */ new Set(["float32", "float64"]);
|
|
911
|
+
var STRING_TYPES = /* @__PURE__ */ new Set(["string", "wstring"]);
|
|
912
|
+
var PARAMETER_SNIPPET = {
|
|
913
|
+
label: "a parameter, with its bounds",
|
|
914
|
+
description: "One hole a caller fills: what type it is, what values it may take, and what it means. Without a `default` it is required, and the bounds are enforced in the cloud before anything reaches the robot.",
|
|
915
|
+
body: {
|
|
916
|
+
type: "${2:float64}",
|
|
917
|
+
min_value: -0.5,
|
|
918
|
+
max_value: 0.5,
|
|
919
|
+
description: "${3:What a caller is choosing when they set this.}"
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
var parameterSpec = strictObject({
|
|
923
|
+
type: parameterType.meta({
|
|
924
|
+
description: "The ROS 2 primitive a value of this parameter must be, spelled the way ROS 2 spells it \u2014 `float64`, not `double`. It **decides which other constraints are allowed at all**: `min_value` and `max_value` need a numeric type, `regex` needs a string one, and a constraint on the wrong type is refused rather than quietly ignored.",
|
|
774
925
|
/**
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
926
|
+
* One sentence per value. The field's own paragraph is already the
|
|
927
|
+
* hover; these answer the different question the editor asks when the
|
|
928
|
+
* cursor is on **one** offer — what is this type, and what does choosing
|
|
929
|
+
* it allow. Written for the value, so `int32` states its range and
|
|
930
|
+
* `string` says it is the type a `regex` may constrain.
|
|
778
931
|
*/
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
932
|
+
enumDescriptions: describeValues(parameterType.options, {
|
|
933
|
+
bool: "A `true`/`false` flag. The one type that takes no constraint at all: no bounds, no `regex`, no `enum`.",
|
|
934
|
+
byte: "One raw octet, `0` to `255`, carrying no character meaning. It counts as an integer here, so bounds and an `enum` apply to it.",
|
|
935
|
+
char: "A single-octet character code, `0` to `255`. ROS 2 keeps it apart from `byte` although the width is the same, and it travels as a number rather than as a one-character string.",
|
|
936
|
+
int8: "A whole number from `-128` to `127`.",
|
|
937
|
+
uint8: "A whole number from `0` to `255`.",
|
|
938
|
+
int16: "A whole number from `-32768` to `32767`.",
|
|
939
|
+
uint16: "A whole number from `0` to `65535`.",
|
|
940
|
+
int32: "A whole number from `-2147483648` to `2147483647` \u2014 the usual choice for a count or an index.",
|
|
941
|
+
uint32: "A whole number from `0` to `4294967295`.",
|
|
942
|
+
int64: "A whole number from `-9223372036854775808` to `9223372036854775807`.",
|
|
943
|
+
uint64: "A whole number from `0` to `18446744073709551615`.",
|
|
944
|
+
float32: "A single-precision number, roughly seven significant digits.",
|
|
945
|
+
float64: "A double-precision number, roughly fifteen significant digits. This is what other languages call `double`; ROS 2 spells it `float64` and so does this field.",
|
|
946
|
+
string: "Text, carried as UTF-8. One of the two types a `regex` may constrain.",
|
|
947
|
+
wstring: "Text as wide characters, and rare \u2014 nearly every ROS 2 interface uses `string`. It takes a `regex` on the same terms."
|
|
948
|
+
})
|
|
949
|
+
}),
|
|
950
|
+
default: import_zod5.z.union([import_zod5.z.number(), import_zod5.z.string(), import_zod5.z.boolean()]).meta({
|
|
951
|
+
description: "The value used when a caller omits this parameter: **without a `default` the parameter is required**, because the message cannot be built without it. It must itself satisfy `min_value`, `max_value`, `enum` and `regex` \u2014 a default the constraints reject is refused here rather than becoming the one value that reaches the robot unchecked."
|
|
952
|
+
}).optional(),
|
|
953
|
+
min_value: import_zod5.z.number().meta({
|
|
954
|
+
description: "The lowest value a caller may send; numeric types only. It is **enforced in the cloud, before anything reaches the robot** \u2014 this is where a speed limit actually holds, rather than in the app that is supposed to respect it.",
|
|
955
|
+
examples: [-0.5]
|
|
956
|
+
}).optional(),
|
|
957
|
+
max_value: import_zod5.z.number().meta({
|
|
958
|
+
description: "The highest value a caller may send; numeric types only, and it may not sit below `min_value`. A reversed pair is refused at parse time, because nothing downstream catches it and every call would then fail against a bound no value can satisfy.",
|
|
959
|
+
examples: [0.5]
|
|
960
|
+
}).optional(),
|
|
961
|
+
enum: import_zod5.z.array(import_zod5.z.union([import_zod5.z.string(), import_zod5.z.number()])).min(1).meta({
|
|
962
|
+
description: "The complete set of values a caller may send. Integer and string types only \u2014 **never a float**, because equality on floating point is unreliable and an enumerated float list is a trap that only shows up in operation. Every entry must match `type`, and a `default` must be one of them."
|
|
963
|
+
}).optional(),
|
|
964
|
+
regex: import_zod5.z.string().min(1).meta({
|
|
965
|
+
description: "A pattern the value must match; string types only. It is compiled as a JavaScript regular expression and is **not anchored**, so `[a-z]+` accepts any value that merely contains a lowercase run \u2014 a pattern meant to cover the whole value writes its own `^` and `$`.",
|
|
966
|
+
examples: ["^[a-z_]+$"]
|
|
967
|
+
}).optional(),
|
|
968
|
+
description: parameterDescription.meta({
|
|
969
|
+
description: "What this parameter means, in the developer's own words, and documentation only \u2014 the robot does nothing with it. It travels into the input schema `robot_describe` publishes for this call, beside the bounds, so `type` and the range say what the value *is* and this is the only place that says what it *does*.",
|
|
970
|
+
/** The sentence `PARAMETER_SNIPPET` already places here, verbatim. */
|
|
971
|
+
examples: ["What a caller is choosing when they set this."]
|
|
972
|
+
})
|
|
973
|
+
}).superRefine((p, ctx) => {
|
|
974
|
+
const numeric = INTEGER_TYPES.has(p.type) || FLOAT_TYPES.has(p.type);
|
|
975
|
+
const refuse = (path, why, code) => ctx.addIssue({ code: "custom", path, message: why, ...code ? { params: { code } } : {} });
|
|
976
|
+
const matchesType = (v) => {
|
|
977
|
+
if (p.type === "bool")
|
|
978
|
+
return typeof v === "boolean";
|
|
979
|
+
if (STRING_TYPES.has(p.type))
|
|
980
|
+
return typeof v === "string";
|
|
981
|
+
if (INTEGER_TYPES.has(p.type))
|
|
982
|
+
return typeof v === "number" && Number.isInteger(v);
|
|
983
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
984
|
+
};
|
|
985
|
+
if (!numeric && (p.min_value !== void 0 || p.max_value !== void 0))
|
|
986
|
+
refuse(["min_value"], `min_value/max_value need a numeric type, not '${p.type}'`, "constraint_not_allowed_for_type");
|
|
987
|
+
if (!STRING_TYPES.has(p.type) && p.regex !== void 0)
|
|
988
|
+
refuse(["regex"], `regex needs a string type, not '${p.type}'`, "constraint_not_allowed_for_type");
|
|
989
|
+
if (p.enum !== void 0 && !(INTEGER_TYPES.has(p.type) || STRING_TYPES.has(p.type))) {
|
|
990
|
+
refuse(["enum"], `enum needs an integer or string type, not '${p.type}'`, "constraint_not_allowed_for_type");
|
|
991
|
+
} else if (p.enum !== void 0) {
|
|
992
|
+
p.enum.forEach((v, i) => {
|
|
993
|
+
if (!matchesType(v))
|
|
994
|
+
refuse(["enum", i], `enum entry does not match type '${p.type}'`, "value_type_mismatch");
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if (p.default !== void 0 && !matchesType(p.default))
|
|
998
|
+
refuse(["default"], `default does not match type '${p.type}'`, "value_type_mismatch");
|
|
999
|
+
if (p.min_value !== void 0 && p.max_value !== void 0 && p.min_value > p.max_value)
|
|
1000
|
+
refuse(["min_value"], "min_value is greater than max_value");
|
|
1001
|
+
if (p.default !== void 0 && matchesType(p.default)) {
|
|
1002
|
+
const d = p.default;
|
|
1003
|
+
if (typeof d === "number") {
|
|
1004
|
+
if (p.min_value !== void 0 && d < p.min_value)
|
|
1005
|
+
refuse(["default"], `default ${d} is below min_value ${p.min_value}`);
|
|
1006
|
+
if (p.max_value !== void 0 && d > p.max_value)
|
|
1007
|
+
refuse(["default"], `default ${d} is above max_value ${p.max_value}`);
|
|
1008
|
+
}
|
|
1009
|
+
if (p.enum !== void 0 && !p.enum.some((v) => v === d))
|
|
1010
|
+
refuse(["default"], "default is not one of the enum entries");
|
|
1011
|
+
if (p.regex !== void 0 && typeof d === "string") {
|
|
1012
|
+
let re;
|
|
1013
|
+
try {
|
|
1014
|
+
re = new RegExp(p.regex);
|
|
1015
|
+
} catch {
|
|
1016
|
+
}
|
|
1017
|
+
if (re && !re.test(d))
|
|
1018
|
+
refuse(["default"], "default does not match regex");
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}).meta({ defaultSnippets: [PARAMETER_SNIPPET] });
|
|
1022
|
+
var parameterMap = slugKeyed(parameterSpec).refine((m) => Object.keys(m).length <= 50, { message: "at most 50 parameters per entry" }).meta({
|
|
1023
|
+
description: "The holes in this entry's `message` that a caller fills, keyed by **parameter name** rather than by field path \u2014 so the name survives the field moving inside the message, and a caller sends something that means what it says. Every declared parameter must appear somewhere in the message and every `${name}` in the message must be declared; either half alone is an error.",
|
|
1024
|
+
/**
|
|
1025
|
+
* **One snippet reaching three positions.** `parameters:` under an action,
|
|
1026
|
+
* under a service and under a publisher are all this node, so the snippet
|
|
1027
|
+
* is authored once here rather than three times on the three sections.
|
|
1028
|
+
* Three copies that must agree is three chances to disagree, and the
|
|
1029
|
+
* export inlines this object into all three positions — which
|
|
1030
|
+
* `config-snippets.test.ts` asserts rather than assumes, because the way
|
|
1031
|
+
* this comes apart is somebody later giving one section a `parameters:`
|
|
1032
|
+
* snippet of its own.
|
|
1033
|
+
*
|
|
1034
|
+
* The body is `PARAMETER_SNIPPET` under its slug key — the same skeleton
|
|
1035
|
+
* the entry position below offers, and the reasons behind every value in
|
|
1036
|
+
* it are written there.
|
|
1037
|
+
*/
|
|
1038
|
+
defaultSnippets: [underSlug("${1:speed}", PARAMETER_SNIPPET)]
|
|
791
1039
|
});
|
|
792
|
-
var
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1040
|
+
var alertCondition = strictObject({
|
|
1041
|
+
fire_at: import_zod5.z.union([import_zod5.z.number().finite(), import_zod5.z.string(), import_zod5.z.boolean()]).meta({
|
|
1042
|
+
description: "The value at which the alert starts firing. Alone it is an **equality**: it fires while the value equals `fire_at` and is ok again as soon as it differs, which is what makes a boolean or a string condition meaningful. Adding `resolve_at` turns it into a threshold instead.",
|
|
1043
|
+
examples: [15, true]
|
|
1044
|
+
}),
|
|
1045
|
+
resolve_at: import_zod5.z.number().finite().meta({
|
|
1046
|
+
description: "The value at which a firing alert becomes ok again \u2014 allowed only when `fire_at` is a number, and it **must differ from it**. That gap is the hysteresis, and it makes the condition a threshold whose direction follows from which of the two values is higher. Without a gap a value sitting on the line flips on every sample.",
|
|
1047
|
+
examples: [18]
|
|
1048
|
+
}).optional()
|
|
1049
|
+
}).superRefine((c, ctx) => {
|
|
1050
|
+
if (c.resolve_at === void 0)
|
|
1051
|
+
return;
|
|
1052
|
+
if (typeof c.fire_at !== "number")
|
|
1053
|
+
ctx.addIssue({
|
|
1054
|
+
code: "custom",
|
|
1055
|
+
path: ["resolve_at"],
|
|
1056
|
+
message: "resolve_at is only allowed when fire_at is a number",
|
|
1057
|
+
params: { code: "invalid_condition" }
|
|
1058
|
+
});
|
|
1059
|
+
else if (c.resolve_at === c.fire_at)
|
|
1060
|
+
ctx.addIssue({
|
|
1061
|
+
code: "custom",
|
|
1062
|
+
path: ["resolve_at"],
|
|
1063
|
+
message: "resolve_at must differ from fire_at",
|
|
1064
|
+
params: { code: "invalid_condition" }
|
|
1065
|
+
});
|
|
798
1066
|
});
|
|
799
|
-
var
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1067
|
+
var ALERT_SNIPPET = {
|
|
1068
|
+
label: "an alert, with its condition",
|
|
1069
|
+
description: "One whole alert: the label shown in place of its key, and the threshold with the gap that keeps it from flipping on every sample.",
|
|
1070
|
+
body: {
|
|
1071
|
+
condition: { fire_at: 15, resolve_at: 18 },
|
|
1072
|
+
name: "${2:Battery low}"
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
var datapointAlert = strictObject({
|
|
1076
|
+
condition: alertCondition.meta({
|
|
1077
|
+
description: "When this alert fires and when it is ok again. It carries **no discriminator**: upper threshold, lower threshold or equality all follow from the two values in it. Editing it resets the alert to `ok` on the next publish, while a publish that leaves it untouched keeps the running state.",
|
|
1078
|
+
/**
|
|
1079
|
+
* **Two snippets, and the reason is the missing discriminator.** A
|
|
1080
|
+
* threshold and an equality are the same shape here — one field apart —
|
|
1081
|
+
* so a single skeleton would not merely be incomplete, it would hide one
|
|
1082
|
+
* of the two things this field exists to express behind a `resolve_at`
|
|
1083
|
+
* the developer has to know to delete. Offering both makes the choice the
|
|
1084
|
+
* schema deliberately does not name into a choice the editor does.
|
|
1085
|
+
*
|
|
1086
|
+
* Both values come from `fire_at`'s own `examples`, which carry exactly
|
|
1087
|
+
* this pair: `15` for the threshold and `true` for the equality.
|
|
1088
|
+
*
|
|
1089
|
+
* **The second snippet costs the `condition:` *key* completion its body,
|
|
1090
|
+
* and that price is paid knowingly.** monaco-yaml's
|
|
1091
|
+
* `getInsertTextForProperty` (`yaml.worker.js:8520`) takes
|
|
1092
|
+
* `defaultSnippets[0].body` only when a node carries **exactly one**
|
|
1093
|
+
* snippet, so accepting `condition` from the key list writes the bare key
|
|
1094
|
+
* here where every other node this wave touched writes its whole block.
|
|
1095
|
+
* The two stay anyway: the value position — a developer who has written
|
|
1096
|
+
* `condition:` and pressed ⏎ — is where the question "what goes here?" is
|
|
1097
|
+
* actually asked, and that is the position this wave exists to answer.
|
|
1098
|
+
* Merging them into one would buy back the key completion by deleting the
|
|
1099
|
+
* choice the schema deliberately does not name, which is the worse trade;
|
|
1100
|
+
* anyone tempted to make it should change the key-completion behaviour
|
|
1101
|
+
* knowingly rather than as a side effect of tidying two snippets into one.
|
|
1102
|
+
*/
|
|
1103
|
+
defaultSnippets: [
|
|
1104
|
+
{
|
|
1105
|
+
label: "a threshold, with its hysteresis",
|
|
1106
|
+
description: "Fires below 15 and is ok again above 18. The gap is what keeps a value sitting on the line from flipping on every sample; the direction follows from which of the two is higher, and nothing else declares it.",
|
|
1107
|
+
body: { fire_at: 15, resolve_at: 18 }
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
label: "an equality",
|
|
1111
|
+
description: "Fires while the value equals `fire_at` and is ok as soon as it differs \u2014 the only form a boolean or a string condition can take. No `resolve_at`: adding one would turn this into a threshold, and is refused unless `fire_at` is a number.",
|
|
1112
|
+
body: { fire_at: true }
|
|
1113
|
+
}
|
|
1114
|
+
]
|
|
1115
|
+
}),
|
|
1116
|
+
severity: alertSeverity.meta({
|
|
1117
|
+
description: "How bad it is when this alert fires; absent means `warning`. It changes no behaviour \u2014 nothing is escalated, retried or delivered differently \u2014 it travels with the org event and colours the alert wherever it is shown.",
|
|
1118
|
+
enumDescriptions: describeValues(alertSeverity.options, {
|
|
1119
|
+
warning: "Worth seeing. This is what an alert that names no severity gets.",
|
|
1120
|
+
error: "Worth acting on. The only difference from `warning` is how the alert is shown: the same event is written, at the same moment, to the same places."
|
|
1121
|
+
})
|
|
1122
|
+
}).optional(),
|
|
1123
|
+
name: import_zod5.z.string().min(1).max(120).meta({
|
|
1124
|
+
description: "A human-readable label shown wherever this alert appears, in place of its bare key. It is not the alert's identity \u2014 the key is \u2014 so the label can be reworded freely, while changing the key deletes one alert and creates another.",
|
|
1125
|
+
examples: ["Battery low"]
|
|
1126
|
+
}).optional(),
|
|
1127
|
+
enabled: import_zod5.z.boolean().meta({
|
|
1128
|
+
description: "Whether this alert is evaluated at all. Absent means on, the opposite of `retention.enabled`: an alert that is written down watches unless it is explicitly switched off, which is how one is silenced without losing the key that identifies it."
|
|
1129
|
+
}).optional()
|
|
1130
|
+
}).meta({
|
|
1131
|
+
/**
|
|
1132
|
+
* The value position of one entry — `battery_low: ▮` under `alerts:`, which
|
|
1133
|
+
* is where a developer adding a **second** alert by hand stands. The section
|
|
1134
|
+
* snippet fires on the empty `alerts:` and never again.
|
|
1135
|
+
*/
|
|
1136
|
+
defaultSnippets: [ALERT_SNIPPET]
|
|
1137
|
+
});
|
|
1138
|
+
var datapointNumeric = strictObject({
|
|
1139
|
+
scale: import_zod5.z.number().meta({
|
|
1140
|
+
description: "A factor the robot multiplies the raw value by before sending it (`value * scale + offset`). The arithmetic happens once, at the source, so REST, realtime and history can never disagree about a number.",
|
|
1141
|
+
examples: [100]
|
|
1142
|
+
}).optional(),
|
|
1143
|
+
offset: import_zod5.z.number().meta({
|
|
1144
|
+
description: "A constant the robot adds after `scale` (`value * scale + offset`), for a value whose zero sits in the wrong place. Like `scale` it is applied before sending, so history stores the converted value and a later correction cannot reach what is already stored.",
|
|
1145
|
+
examples: [-273.15]
|
|
1146
|
+
}).optional(),
|
|
1147
|
+
unit: import_zod5.z.string().max(32).meta({
|
|
1148
|
+
description: "The unit of the value **after** `scale` and `offset`, not the robot's own. It is shown beside the value and carried by `robot_describe` as its own field, so a model does not have to guess whether 15 means percent, volts or minutes.",
|
|
1149
|
+
examples: ["%"]
|
|
1150
|
+
}).optional(),
|
|
1151
|
+
decimals: import_zod5.z.number().int().min(0).max(6).meta({
|
|
1152
|
+
description: "How many fraction digits the console shows the value with \u2014 value tile, chart axis and tooltip, and the datapoint detail page \u2014 and the number `robot_describe` reports as its own field, so a model formats the value the way the console does. Presentation only: the stored value keeps the precision it arrived with, and absent means the console's own default rather than zero.",
|
|
1153
|
+
examples: [1]
|
|
1154
|
+
}).optional()
|
|
1155
|
+
});
|
|
1156
|
+
var datapointRetention = strictObject({
|
|
1157
|
+
enabled: import_zod5.z.boolean().meta({
|
|
1158
|
+
description: "Whether values are written to the time series and become queryable. Off by default: without it the value is live only, and nobody who was not watching will ever see it."
|
|
1159
|
+
}).optional(),
|
|
1160
|
+
interval_seconds: import_zod5.z.number().int().min(1).max(3600).meta({
|
|
1161
|
+
description: "How often a value is written to history, in seconds; absent means `300`. **Not** how often it is sent \u2014 that is `rate_throttle_hz`. Stored points are billed, so this is the direct lever on what a robot costs, and a bumper that is true for 200 ms does not appear unless a write falls inside it.",
|
|
1162
|
+
examples: [300, 60]
|
|
1163
|
+
}).optional(),
|
|
1164
|
+
max_buffer_values: import_zod5.z.number().int().min(1).max(1e5).meta({
|
|
1165
|
+
description: "How many values the robot holds while the bridge is disconnected, to be pushed once it reconnects. The catch-up runs behind live telemetry and job results at a limited rate, so closing a gap never delays the present; without it the series simply has a gap, which is an honest answer.",
|
|
1166
|
+
examples: [5e3]
|
|
1167
|
+
}).optional()
|
|
1168
|
+
});
|
|
1169
|
+
var chartStyle = import_zod5.z.enum(["line", "step"]);
|
|
1170
|
+
var datapointChart = strictObject({
|
|
1171
|
+
y_min: import_zod5.z.number().finite().meta({
|
|
1172
|
+
description: "A fixed floor for the chart's y axis; omitted, the axis scales to the data. `0` is a real floor and is read as `0`, never as unset.",
|
|
1173
|
+
examples: [0]
|
|
1174
|
+
}).optional(),
|
|
1175
|
+
y_max: import_zod5.z.number().finite().meta({
|
|
1176
|
+
description: "A fixed ceiling for the chart's y axis; omitted, the axis scales to the data. It may not sit below `y_min`: a reversed pair is refused here because nothing downstream catches it, and the chart would render empty.",
|
|
1177
|
+
examples: [100]
|
|
1178
|
+
}).optional(),
|
|
1179
|
+
style: chartStyle.meta({
|
|
1180
|
+
description: "How the drawing joins two samples, which is not a matter of taste. `line` claims the value moved evenly between them, roughly true of a temperature or a charge; `step` holds and then jumps, the only honest drawing for a mode, a switch or a counter, where a straight line would show values that never existed.",
|
|
1181
|
+
enumDescriptions: describeValues(chartStyle.options, {
|
|
1182
|
+
line: "Straight lines between samples, so the drawing claims the value moved evenly from one to the next. Right for a quantity that really is continuous \u2014 a temperature, a charge level \u2014 where a reading taken between two samples would have landed somewhere on that line.",
|
|
1183
|
+
step: "Each value is held until the next one arrives, then jumps to it. Right for anything that does not slide between its values \u2014 a mode, a state, a switch, a counter \u2014 where a sloped line would draw readings the robot never reported."
|
|
1184
|
+
})
|
|
1185
|
+
}).optional(),
|
|
1186
|
+
default_window_minutes: import_zod5.z.number().int().min(1).max(43200).meta({
|
|
1187
|
+
description: "How far back the chart reaches when it is first opened, in minutes; absent means `60`. Only the starting zoom: a viewer may look further, and nothing about what is stored follows from it.",
|
|
1188
|
+
examples: [1440]
|
|
1189
|
+
}).optional()
|
|
1190
|
+
}).superRefine((c, ctx) => {
|
|
1191
|
+
if (c.y_min !== void 0 && c.y_max !== void 0 && c.y_min > c.y_max)
|
|
1192
|
+
ctx.addIssue({ code: "custom", path: ["y_min"], message: "y_min is greater than y_max" });
|
|
1193
|
+
});
|
|
1194
|
+
var rateThrottleHz = import_zod5.z.number().nonnegative().max(20);
|
|
1195
|
+
var DATAPOINT_SNIPPET = {
|
|
1196
|
+
label: "a datapoint",
|
|
1197
|
+
description: "One value the robot publishes: one field of one topic.",
|
|
1198
|
+
body: {
|
|
1199
|
+
topic: "${2:/battery}",
|
|
1200
|
+
type: "${3:sensor_msgs/msg/BatteryState}",
|
|
1201
|
+
field: "${4:voltage}",
|
|
1202
|
+
description: "${5:What this value is, for whoever meets it in the console.}"
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
var NUMERIC_DATAPOINT_SNIPPET = {
|
|
1206
|
+
label: "a numeric datapoint, with history and a chart",
|
|
1207
|
+
description: "A number with its unit, what is kept of it and how it is drawn \u2014 the blocks a plain datapoint leaves out.",
|
|
1208
|
+
body: {
|
|
1209
|
+
topic: "${2:/battery}",
|
|
1210
|
+
type: "${3:sensor_msgs/msg/BatteryState}",
|
|
1211
|
+
field: "${4:percentage}",
|
|
1212
|
+
description: "${5:What this value is, for whoever meets it in the console.}",
|
|
1213
|
+
/**
|
|
1214
|
+
* The quotes inside `unit` are part of the inserted text and are not
|
|
1215
|
+
* decoration. A body string is written into the document verbatim, and `%`
|
|
1216
|
+
* is a YAML directive indicator: measured with `yaml` 2.9.0, `unit: %` is a
|
|
1217
|
+
* **syntax error** ("Plain value cannot start with directive indicator
|
|
1218
|
+
* character %") while `unit: "%"` parses to `%`. Nothing between here and
|
|
1219
|
+
* the buffer quotes a scalar for us.
|
|
1220
|
+
*/
|
|
1221
|
+
numeric: { scale: 100, unit: '"%"', decimals: 1 },
|
|
1222
|
+
retention: { enabled: true, interval_seconds: 300 },
|
|
1223
|
+
chart: { y_min: 0, y_max: 100, style: "line" }
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
var datapointConfig = strictObject({
|
|
1227
|
+
topic: rosName.meta({
|
|
1228
|
+
description: "The ROS topic this datapoint reads, as an absolute graph name. One datapoint reads **one** topic: a value assembled from two topics is not expressible here.",
|
|
1229
|
+
patternErrorMessage: ROS_NAME_RULE,
|
|
1230
|
+
examples: ["/battery"]
|
|
1231
|
+
}),
|
|
1232
|
+
type: rosTypeName.meta({
|
|
1233
|
+
description: "The message type carried by `topic`, spelled the way ROS 2 spells it, with the `msg` segment in the middle \u2014 `sensor_msgs/msg/BatteryState`, never `sensor_msgs/BatteryState`. It is declared here rather than discovered, so a configuration can be written for a robot that has never been connected; the cloud checks it against the robot's own message definitions only once one is there.",
|
|
1234
|
+
patternErrorMessage: ROS_TYPE_NAME_RULE,
|
|
1235
|
+
examples: ["sensor_msgs/msg/BatteryState"]
|
|
817
1236
|
}),
|
|
818
|
-
|
|
819
|
-
|
|
1237
|
+
field: fieldPath.meta({
|
|
1238
|
+
description: "A dotted path into the message naming the single value this datapoint carries, each segment indexing at most one array level \u2014 `ranges[0]`, never `ranges[0][1]`, because ROS 2 has no nested arrays. Without it the datapoint is the whole message, and `numeric`, `chart` and `alerts` are then refused.",
|
|
1239
|
+
patternErrorMessage: FIELD_PATH_RULE,
|
|
1240
|
+
examples: ["voltage", "pose.position.x", "ranges[0]"]
|
|
1241
|
+
}).optional(),
|
|
1242
|
+
rate_throttle_hz: rateThrottleHz.meta({
|
|
1243
|
+
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.",
|
|
1244
|
+
examples: [2, 0.5]
|
|
1245
|
+
}).optional(),
|
|
1246
|
+
description: serviceDescription.meta({
|
|
1247
|
+
description: "Prose about what this value is, for whoever meets it in the console later. 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.",
|
|
1248
|
+
/**
|
|
1249
|
+
* The sentence both datapoint snippets already place here, verbatim. Its
|
|
1250
|
+
* four siblings — an action's, a service's, a publisher's, a camera's —
|
|
1251
|
+
* each carry the sentence from their own snippet body, so this position
|
|
1252
|
+
* was the one description in the format offering nothing; a second wording
|
|
1253
|
+
* invented here would have been the drift instead.
|
|
1254
|
+
*/
|
|
1255
|
+
examples: ["What this value is, for whoever meets it in the console."]
|
|
1256
|
+
}),
|
|
1257
|
+
numeric: datapointNumeric.meta({
|
|
1258
|
+
description: "Arithmetic and formatting for a numeric value. `scale` and `offset` are applied **on the robot**, before sending, which is why REST, realtime and history all carry identical numbers. `unit` and `decimals` change nothing the robot does, so a publish that touches only those pushes no configuration.",
|
|
1259
|
+
/**
|
|
1260
|
+
* The quotes inside `unit` are inserted text, not decoration, for the
|
|
1261
|
+
* reason spelled out on the `datapoints` snippet below: a body string is
|
|
1262
|
+
* written to the buffer verbatim and a bare `%` is a YAML directive
|
|
1263
|
+
* indicator, so `unit: %` is a syntax error where `unit: "%"` parses.
|
|
1264
|
+
*
|
|
1265
|
+
* **`offset` is not in the body, and that is a choice rather than an
|
|
1266
|
+
* oversight.** All four fields carry `examples`, but they were authored
|
|
1267
|
+
* per field and from two different conversions: `scale: 100` with
|
|
1268
|
+
* `unit: '%'` is a 0..1 fraction shown as a percentage, while
|
|
1269
|
+
* `offset: -273.15` is kelvin as celsius. A body holding both would
|
|
1270
|
+
* insert arithmetic that means nothing and that a developer has to
|
|
1271
|
+
* unpick before it means anything. The rule this file follows is that a
|
|
1272
|
+
* skeleton carries what a developer opening the block almost certainly
|
|
1273
|
+
* wants, at the node's own example values; the remaining keys arrive by
|
|
1274
|
+
* ordinary key completion, which works here and never stopped working —
|
|
1275
|
+
* the position that was silent is the *value* after `numeric:`.
|
|
1276
|
+
*/
|
|
1277
|
+
defaultSnippets: [{
|
|
1278
|
+
label: "a unit, and the arithmetic that produces it",
|
|
1279
|
+
description: "A 0..1 fraction sent as a percentage to one decimal. `scale` is applied on the robot before sending, so history stores the converted value and a later correction cannot reach what is already stored.",
|
|
1280
|
+
body: { scale: 100, unit: '"%"', decimals: 1 }
|
|
1281
|
+
}]
|
|
1282
|
+
}).optional(),
|
|
1283
|
+
retention: datapointRetention.meta({
|
|
1284
|
+
description: "What outlives the moment: whether this value is written to the time series, how often, and how many points the robot buffers while the bridge is away. Absent means no history at all \u2014 the value is live only.",
|
|
1285
|
+
/**
|
|
1286
|
+
* `enabled: true` is the only value that makes opening this block mean
|
|
1287
|
+
* anything — absent already means off, so a skeleton inserting `false`
|
|
1288
|
+
* would be a block that does nothing. It is a boolean and carries no
|
|
1289
|
+
* `examples`; the direction comes from the schema comment above, which
|
|
1290
|
+
* says why absent-means-off is the deliberate one.
|
|
1291
|
+
*
|
|
1292
|
+
* `interval_seconds: 300` restates the format's own default, on purpose:
|
|
1293
|
+
* stored points are what a customer is billed for, so this is the direct
|
|
1294
|
+
* lever on what a robot costs, and a developer who never sees the field
|
|
1295
|
+
* never tunes it.
|
|
1296
|
+
*
|
|
1297
|
+
* **This body carries `max_buffer_values` and the composite `datapoints`
|
|
1298
|
+
* snippet's `retention:` does not, deliberately.** The two answer
|
|
1299
|
+
* different questions and the difference is the answer to each: the
|
|
1300
|
+
* composite says *what a datapoint looks like*, where retention is one
|
|
1301
|
+
* of three sub-blocks and the robot-side buffer is a tuning detail that
|
|
1302
|
+
* would bury the shape it is there to show; this node is reached only by
|
|
1303
|
+
* a developer who has written `retention:` and asked what goes in it, and
|
|
1304
|
+
* for that question the buffer is a third of the answer. Neither is the
|
|
1305
|
+
* corrected version of the other.
|
|
1306
|
+
*/
|
|
1307
|
+
defaultSnippets: [{
|
|
1308
|
+
label: "history, on, with its interval and buffer",
|
|
1309
|
+
description: "Writes this value to the time series every 300 seconds and holds 5000 points on the robot while the bridge is away. Stored points are billed, so both numbers are worth choosing rather than inheriting.",
|
|
1310
|
+
body: { enabled: true, interval_seconds: 300, max_buffer_values: 5e3 }
|
|
1311
|
+
}]
|
|
1312
|
+
}).optional(),
|
|
1313
|
+
chart: datapointChart.meta({
|
|
1314
|
+
description: "How the console draws this value over time: axis bounds, whether the line interpolates or steps, and the window a chart opens on. **Display only** \u2014 it changes no stored value, no alert and nothing the robot does, so a publish that touches only it pushes no configuration.",
|
|
1315
|
+
/**
|
|
1316
|
+
* `style` is a **choice**, not a literal, for the reason the camera
|
|
1317
|
+
* source's `type` is one: the format offers a closed pair, the right
|
|
1318
|
+
* answer depends on what the datapoint is, and the snippet cannot know.
|
|
1319
|
+
* Its own description says the two are not a matter of taste — `line`
|
|
1320
|
+
* claims the value moved evenly between two samples, `step` holds and
|
|
1321
|
+
* jumps, and `step` is the only honest drawing for a mode, a switch or a
|
|
1322
|
+
* counter. A snippet that picked `line` would draw values that never
|
|
1323
|
+
* existed, and nothing would object: both are valid, no diagnostic
|
|
1324
|
+
* fires, and the chart looks plausible.
|
|
1325
|
+
*
|
|
1326
|
+
* `Choice.toString()` is the first option, so a developer who tabs past
|
|
1327
|
+
* this gets `line`, which is right for the continuous values most charts
|
|
1328
|
+
* carry; one who opens the picker sees that `step` exists at all.
|
|
1329
|
+
*
|
|
1330
|
+
* The composite snippet on `datapoints` writes `style: 'line'` as a
|
|
1331
|
+
* literal and stays that way — its body is a battery percentage, where
|
|
1332
|
+
* `line` is not a guess.
|
|
1333
|
+
*
|
|
1334
|
+
* **`default_window_minutes` is left out, on the same rule that leaves
|
|
1335
|
+
* `offset` out of `numeric` above**, and it is said here so that the two
|
|
1336
|
+
* omissions read alike: it has its own `examples` (`1440`) and this
|
|
1337
|
+
* node's description names it, but it is the one field of the four that
|
|
1338
|
+
* decides nothing about the drawing — absent means 60, a viewer may look
|
|
1339
|
+
* further whatever it says, and nothing about what is stored follows from
|
|
1340
|
+
* it. Key completion offers it inside the block the moment anyone wants
|
|
1341
|
+
* it; the position that was silent is the *value* after `chart:`.
|
|
1342
|
+
*/
|
|
1343
|
+
defaultSnippets: [{
|
|
1344
|
+
label: "axis bounds, and how two samples are joined",
|
|
1345
|
+
description: "A fixed 0..100 axis rather than one that scales to the data, and a choice between interpolating and stepping between samples \u2014 which is not a matter of taste.",
|
|
1346
|
+
body: { y_min: 0, y_max: 100, style: "${1|line,step|}" }
|
|
1347
|
+
}]
|
|
1348
|
+
}).optional(),
|
|
1349
|
+
alerts: slugKeyed(datapointAlert).meta({
|
|
1350
|
+
description: "Alerts watching this value, keyed by slug; each moves between `ok` and `firing` and writes an org event on every transition. No mail is sent. **The key is the identity**, so renaming an alert is a delete plus a create: its runtime state is lost, and an alert that is still true fires again.",
|
|
1351
|
+
/**
|
|
1352
|
+
* The body is `ALERT_SNIPPET` under its slug key — the same skeleton the
|
|
1353
|
+
* entry position offers, and the reasons behind every value in it are
|
|
1354
|
+
* written there. **The key is in the wrapper**, and it is the alert's
|
|
1355
|
+
* identity: renaming it is a delete plus a create.
|
|
1356
|
+
*/
|
|
1357
|
+
defaultSnippets: [underSlug("${1:battery_low}", ALERT_SNIPPET)]
|
|
1358
|
+
}).optional()
|
|
1359
|
+
}).superRefine((d, ctx) => {
|
|
1360
|
+
if (d.field !== void 0)
|
|
1361
|
+
return;
|
|
1362
|
+
for (const group of ["numeric", "chart", "alerts"]) {
|
|
1363
|
+
if (d[group] !== void 0)
|
|
1364
|
+
ctx.addIssue({
|
|
1365
|
+
code: "custom",
|
|
1366
|
+
path: [group],
|
|
1367
|
+
message: `${group} needs a single field; without 'field' the value is the whole message`,
|
|
1368
|
+
params: { code: "requires_single_field" }
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}).meta({ defaultSnippets: [DATAPOINT_SNIPPET, NUMERIC_DATAPOINT_SNIPPET] });
|
|
1372
|
+
function holdsExplicitNull(node) {
|
|
1373
|
+
const stack = [node];
|
|
1374
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1375
|
+
while (stack.length > 0) {
|
|
1376
|
+
const current = stack.pop();
|
|
1377
|
+
if (current === null)
|
|
1378
|
+
return true;
|
|
1379
|
+
if (typeof current !== "object")
|
|
1380
|
+
continue;
|
|
1381
|
+
if (seen.has(current))
|
|
1382
|
+
continue;
|
|
1383
|
+
seen.add(current);
|
|
1384
|
+
stack.push(...Array.isArray(current) ? current : Object.values(current));
|
|
1385
|
+
}
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
var messageTemplate = import_zod5.z.unknown().refine((v) => !holdsExplicitNull(v), {
|
|
1389
|
+
message: "null is not a value; omit the key instead",
|
|
1390
|
+
params: { code: "explicit_null" }
|
|
1391
|
+
}).meta({
|
|
1392
|
+
description: 'The message as it will be sent, written out in full: literals are fixed, `${name}` is a hole a caller fills, and a field written `0.0` is one no client can change. Directly after `message:` a `${name}` standing alone names a shared message instead; anywhere inside a body it is a parameter. `null` is refused **at every depth** \u2014 omitting a key is the only spelling of "not set".'
|
|
1393
|
+
});
|
|
1394
|
+
var PLACEHOLDER_RE = /^\$\{([a-z][a-z0-9]*(?:_[a-z0-9]+)*)\}$/;
|
|
1395
|
+
var messageRef = import_zod5.z.string().regex(PLACEHOLDER_RE).meta({
|
|
1396
|
+
description: "A reference to a shared message: `${name}` and nothing else, which is what separates a reference from a literal \u2014 a bare word stays a literal even when it happens to match a declared name. Whether that name is declared, and whether it points at a body holding a second reference, are questions about the whole document and are answered in the cloud."
|
|
1397
|
+
});
|
|
1398
|
+
var messageBody = messageTemplate;
|
|
1399
|
+
function placeholderNames(node, found = /* @__PURE__ */ new Set()) {
|
|
1400
|
+
const stack = [node];
|
|
1401
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1402
|
+
while (stack.length > 0) {
|
|
1403
|
+
const current = stack.pop();
|
|
1404
|
+
if (typeof current === "string") {
|
|
1405
|
+
const m = PLACEHOLDER_RE.exec(current);
|
|
1406
|
+
if (m)
|
|
1407
|
+
found.add(m[1]);
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (!current || typeof current !== "object")
|
|
1411
|
+
continue;
|
|
1412
|
+
if (seen.has(current))
|
|
1413
|
+
continue;
|
|
1414
|
+
seen.add(current);
|
|
1415
|
+
if (Array.isArray(current)) {
|
|
1416
|
+
for (const item of current)
|
|
1417
|
+
stack.push(item);
|
|
1418
|
+
} else {
|
|
1419
|
+
for (const value of Object.values(current))
|
|
1420
|
+
stack.push(value);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return found;
|
|
1424
|
+
}
|
|
1425
|
+
var SHARED_MESSAGE_SNIPPET = {
|
|
1426
|
+
label: "a shared message",
|
|
1427
|
+
description: "One reusable body, with one parameter hole in it.",
|
|
1428
|
+
body: {
|
|
1429
|
+
linear: { x: param("speed") },
|
|
1430
|
+
angular: { z: 0 }
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
var sharedMessageBody = messageTemplate.meta({ defaultSnippets: [SHARED_MESSAGE_SNIPPET] });
|
|
1434
|
+
var messageMap = slugKeyed(sharedMessageBody).refine((m) => Object.keys(m).length <= 200, { message: "at most 200 shared messages" }).meta({
|
|
1435
|
+
description: "Reusable message bodies, keyed by name. A body is inserted by writing `${name}` directly after `message:`, may hold placeholders of its own, and **may not insert another** \u2014 which rules out cycles and lets every check look at exactly one body."
|
|
1436
|
+
});
|
|
1437
|
+
var ACTION_SNIPPET = {
|
|
1438
|
+
label: "an action",
|
|
1439
|
+
description: "One thing the robot does on request, reported as a job with progress.",
|
|
1440
|
+
body: {
|
|
1441
|
+
ros_name: "${2:/navigate_to_pose}",
|
|
1442
|
+
type: "${3:nav2_msgs/action/NavigateToPose}",
|
|
1443
|
+
description: "${4:Drives to a target pose on the map.}"
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
var actionConfig = strictObject({
|
|
1447
|
+
ros_name: rosName.meta({
|
|
1448
|
+
description: "The action server on the robot, as an absolute graph name \u2014 this is what the bridge sends the goal to. Clients never see it: they address this entry by its slug, so a server can be renamed on the robot without a single app changing.",
|
|
1449
|
+
patternErrorMessage: ROS_NAME_RULE,
|
|
1450
|
+
examples: ["/navigate_to_pose"]
|
|
1451
|
+
}),
|
|
1452
|
+
type: rosTypeName.meta({
|
|
1453
|
+
description: "The action type `ros_name` implements, with the `action` segment in the middle \u2014 `nav2_msgs/action/NavigateToPose`, never `nav2_msgs/NavigateToPose`. Declared rather than introspected, so an action can be configured for a robot that has never connected; the cloud checks it against the robot's own definitions only once one is there.",
|
|
1454
|
+
patternErrorMessage: ROS_TYPE_NAME_RULE,
|
|
1455
|
+
examples: ["nav2_msgs/action/NavigateToPose"]
|
|
1456
|
+
}),
|
|
1457
|
+
message: messageBody.optional(),
|
|
1458
|
+
parameters: parameterMap.optional(),
|
|
1459
|
+
description: serviceDescription.meta({
|
|
1460
|
+
description: "What this action does, in the developer's own words \u2014 documentation for the console and for MCP clients, which is all it is: the robot does nothing with it. It is carried verbatim into `robot_describe` and read by a model that has never seen this robot. The action is offered whenever the role grants it; without one it is offered with `description: null`, and the model has nothing but the slug.",
|
|
1461
|
+
examples: ["Drives to a target pose on the map."]
|
|
1462
|
+
})
|
|
1463
|
+
}).meta({
|
|
1464
|
+
/** The value position of one entry — `navigate: ▮` under `actions:`. */
|
|
1465
|
+
defaultSnippets: [ACTION_SNIPPET]
|
|
1466
|
+
});
|
|
1467
|
+
var SERVICE_SNIPPET = {
|
|
1468
|
+
label: "a service",
|
|
1469
|
+
description: "One request, one reply, no progress in between.",
|
|
1470
|
+
body: {
|
|
1471
|
+
ros_name: "${2:/reset_odometry}",
|
|
1472
|
+
type: "${3:std_srvs/srv/Trigger}",
|
|
1473
|
+
description: "${4:Resets odometry to the origin.}"
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
var serviceConfig = strictObject({
|
|
1477
|
+
ros_name: rosName.meta({
|
|
1478
|
+
description: "The ROS service the robot answers on, as an absolute graph name. The call is one request and one reply with no progress in between, so whatever this service does has to finish inside that reply; anything long-running belongs in `actions`.",
|
|
1479
|
+
patternErrorMessage: ROS_NAME_RULE,
|
|
1480
|
+
examples: ["/reset_odometry"]
|
|
1481
|
+
}),
|
|
1482
|
+
type: rosTypeName.meta({
|
|
1483
|
+
description: "The service type `ros_name` implements, with the `srv` segment in the middle \u2014 `std_srvs/srv/Trigger`. A type whose request has no fields, like `Trigger`, needs neither `message` nor `parameters`: there is nothing to fill.",
|
|
1484
|
+
patternErrorMessage: ROS_TYPE_NAME_RULE,
|
|
1485
|
+
examples: ["std_srvs/srv/Trigger"]
|
|
1486
|
+
}),
|
|
1487
|
+
message: messageBody.optional(),
|
|
1488
|
+
parameters: parameterMap.optional(),
|
|
1489
|
+
description: serviceDescription.meta({
|
|
1490
|
+
description: "What this service does, in the developer's own words. The robot does nothing with it \u2014 the readers are the console and MCP clients, and without one the service is still offered, with `description: null`, exactly as for an action. It sits on the configuration rather than on the app, so one wording is true for every app that reaches this robot.",
|
|
1491
|
+
examples: ["Resets odometry to the origin."]
|
|
1492
|
+
})
|
|
1493
|
+
}).meta({
|
|
1494
|
+
/** The value position of one entry — `reset_odometry: ▮` under `services:`. */
|
|
1495
|
+
defaultSnippets: [SERVICE_SNIPPET]
|
|
1496
|
+
});
|
|
1497
|
+
var PUBLISHER_SNIPPET = {
|
|
1498
|
+
label: "a publisher, with its parameters and its failsafe",
|
|
1499
|
+
description: "A topic clients may send to: what is fixed, what a caller fills, and what the bridge sends by itself once the caller falls silent.",
|
|
1500
|
+
body: {
|
|
1501
|
+
topic: "${2:/cmd_vel}",
|
|
1502
|
+
type: "${3:geometry_msgs/msg/Twist}",
|
|
1503
|
+
message: {
|
|
1504
|
+
linear: { x: param("speed") },
|
|
1505
|
+
angular: { z: param("turn") }
|
|
1506
|
+
},
|
|
1507
|
+
parameters: {
|
|
1508
|
+
speed: { type: "float64", min_value: -0.5, max_value: 0.5, default: 0 },
|
|
1509
|
+
turn: { type: "float64", min_value: -0.5, max_value: 0.5, default: 0 }
|
|
1510
|
+
},
|
|
1511
|
+
failsafe: {
|
|
1512
|
+
timeout_ms: 500,
|
|
1513
|
+
message: {
|
|
1514
|
+
linear: { x: 0 },
|
|
1515
|
+
angular: { z: 0 }
|
|
1516
|
+
}
|
|
1517
|
+
},
|
|
1518
|
+
quiet_timeout_ms: 2e3,
|
|
1519
|
+
description: "${4:Velocity command. If sending stops, the robot stops.}"
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
var publisherConfig = strictObject({
|
|
1523
|
+
topic: rosName.meta({
|
|
1524
|
+
description: "The ROS topic the message is published onto, as an absolute graph name. **No client ever names a topic**: a caller addresses this entry by its slug, so the topics an app can write to are exactly the ones written in this file.",
|
|
1525
|
+
patternErrorMessage: ROS_NAME_RULE,
|
|
1526
|
+
examples: ["/cmd_vel"]
|
|
1527
|
+
}),
|
|
1528
|
+
type: rosTypeName.meta({
|
|
1529
|
+
description: "The message type of `topic`, spelled the way ROS 2 spells it, with the `msg` segment. It fixes the shape that `message` and `failsafe.message` must both fill, which is why one publisher carries one type and a second type needs a second publisher.",
|
|
1530
|
+
patternErrorMessage: ROS_TYPE_NAME_RULE,
|
|
1531
|
+
examples: ["geometry_msgs/msg/Twist"]
|
|
1532
|
+
}),
|
|
1533
|
+
message: messageBody,
|
|
1534
|
+
parameters: parameterMap.optional(),
|
|
1535
|
+
failsafe: strictObject({
|
|
1536
|
+
timeout_ms: import_zod5.z.number().int().positive().max(6e4).meta({
|
|
1537
|
+
description: "How long the bridge waits for the client's next send before sending the failsafe message itself, in milliseconds. The deadline runs **on the robot**, so it still fires when the link to the cloud is what failed \u2014 which is the case it exists for.",
|
|
1538
|
+
examples: [500, 1e3]
|
|
1539
|
+
}),
|
|
1540
|
+
message: messageBody.meta({
|
|
1541
|
+
description: "What the bridge sends once `timeout_ms` runs out \u2014 for a drive command, a zero twist. It must be safe in **every** state, because it is sent precisely when nobody is watching any more, and it may hold no placeholder: there is no caller left to fill one."
|
|
1542
|
+
})
|
|
1543
|
+
}).refine((f) => typeof f.message === "string" || placeholderNames(f.message).size === 0, {
|
|
1544
|
+
message: "the failsafe message must contain no placeholder: it is sent with no caller to fill one",
|
|
1545
|
+
path: ["message"],
|
|
1546
|
+
params: { code: "failsafe_has_parameters" }
|
|
1547
|
+
}).meta({
|
|
1548
|
+
description: "What the bridge sends **by itself** once a client stops sending, and how long it waits first. This is the format's safety story in one field: a client that crashes, loses its connection or whose operator closes the window does not leave a robot driving. The message may hold no placeholder, inline or through a shared message \u2014 there is nobody left to fill one.",
|
|
1549
|
+
/**
|
|
1550
|
+
* Both fields are required, so the body carries both: a `failsafe:` with
|
|
1551
|
+
* only one of them is a publisher the format refuses, and this is the
|
|
1552
|
+
* field where a document that does not publish is the least useful thing
|
|
1553
|
+
* to hand somebody.
|
|
1554
|
+
*
|
|
1555
|
+
* The zero twist is the message this field's own description names, and
|
|
1556
|
+
* the same body the composite `publishers` snippet inserts. Neither
|
|
1557
|
+
* knows the publisher's ROS type — nothing at this position does — so
|
|
1558
|
+
* the snippet offers the format's canonical safe message rather than
|
|
1559
|
+
* guessing a shape. Every value in it is a literal: a placeholder here
|
|
1560
|
+
* is refused outright (`failsafe_has_parameters`), because the message
|
|
1561
|
+
* is sent with no caller left to fill one.
|
|
1562
|
+
*/
|
|
1563
|
+
defaultSnippets: [{
|
|
1564
|
+
label: "a deadline, and the message it sends",
|
|
1565
|
+
description: "Half a second of silence and then a zero twist. The deadline runs on the robot, so it still fires when the link to the cloud is what failed \u2014 which is the case it exists for.",
|
|
1566
|
+
body: {
|
|
1567
|
+
timeout_ms: 500,
|
|
1568
|
+
message: {
|
|
1569
|
+
linear: { x: 0 },
|
|
1570
|
+
angular: { z: 0 }
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}]
|
|
1574
|
+
}),
|
|
1575
|
+
quiet_timeout_ms: import_zod5.z.number().int().nonnegative().max(6e5).meta({
|
|
1576
|
+
description: "How long this publisher must stay silent before a **different** user may send to it. Whoever sends holds it implicitly exclusive, with no session and no lock, so this one number is the whole handover policy: too short and two operators fight over one robot, too long and a crashed client blocks it for everyone.",
|
|
1577
|
+
examples: [2e3]
|
|
1578
|
+
}),
|
|
1579
|
+
description: serviceDescription.meta({
|
|
1580
|
+
description: "What sending to this publisher does, in the developer's own words. It is documentation for the console and for MCP clients \u2014 the robot does nothing with it \u2014 and as for actions and services, the publisher is offered whether or not one is written, with `description: null` when it is not. A caller sends here repeatedly and continuously rather than once, which is why this kind alone carries `failsafe` and `quiet_timeout_ms`.",
|
|
1581
|
+
examples: ["Velocity command. If sending stops, the robot stops."]
|
|
1582
|
+
})
|
|
1583
|
+
}).meta({
|
|
1584
|
+
/** The value position of one entry — `drive: ▮` under `publishers:`. */
|
|
1585
|
+
defaultSnippets: [PUBLISHER_SNIPPET]
|
|
1586
|
+
});
|
|
1587
|
+
var cameraCredentials = strictObject({
|
|
1588
|
+
username: import_zod5.z.string().min(1).max(128).optional().meta({
|
|
1589
|
+
description: "The account name the camera expects. For MJPEG the bridge sends a real HTTP `Authorization: Basic` header and leaves the URL untouched. RTSP offers no such channel through ffmpeg, so there the name goes inside the connect URL instead \u2014 built fresh for that one call and never written back into the stored document.",
|
|
1590
|
+
examples: ["ops"]
|
|
1591
|
+
}),
|
|
1592
|
+
password: import_zod5.z.string().min(1).max(128).optional().meta({
|
|
1593
|
+
description: "The password for `username`. **There is no secret store behind this**: the value written here is the value stored, so treat it as readable by everyone who may read this robot's configuration, now and in its history."
|
|
1594
|
+
})
|
|
1595
|
+
}).meta({
|
|
1596
|
+
description: "Username and password for the stream, standing **in clear text in the document**. A published version is immutable, so a password here cannot be removed from history or rotated without republishing \u2014 which is why the publish audit event carries only the version number and never the document body. Userinfo in the `url` works too; an explicit block here wins over it.",
|
|
1597
|
+
defaultSnippets: [{
|
|
1598
|
+
label: "username and password",
|
|
1599
|
+
description: "Both fields, in clear text \u2014 which is what this block is. The password default is deliberately not a password: `CHANGE-ME` is stored like any other value, but it is **visible** rather than plausible, so a reviewer reading the diff sees it and the camera rejects it at connect time \u2014 where a default that looked like a password would simply be published and kept.",
|
|
1600
|
+
/**
|
|
1601
|
+
* `CHANGE-ME`, and not a plausible-looking password, because of what the
|
|
1602
|
+
* comment above this schema records: a published version is immutable,
|
|
1603
|
+
* so a password written here cannot be removed from history or rotated
|
|
1604
|
+
* without republishing. This snippet is the one thing in the file that
|
|
1605
|
+
* could manufacture such a version by itself — a developer who tabs past
|
|
1606
|
+
* the placeholder publishes whatever the default was.
|
|
1607
|
+
*
|
|
1608
|
+
* What `CHANGE-ME` buys is **visibility, not a refusal**. It is stored
|
|
1609
|
+
* exactly like any other value; nothing at publish time objects. What it
|
|
1610
|
+
* does is fail at the camera, at connect time, and read wrong to anyone
|
|
1611
|
+
* looking at the diff — where a plausible default is published and kept.
|
|
1612
|
+
*
|
|
1613
|
+
* The two alternatives were both worse. A plausible default (`secret`)
|
|
1614
|
+
* reads in a diff like a value somebody chose, so nobody looks twice. A
|
|
1615
|
+
* bare `$2` inserts the empty string, which `min(1)` refuses — that is
|
|
1616
|
+
* loud, but it makes this the only snippet in the format that knowingly
|
|
1617
|
+
* inserts an invalid document, and the guard that says none of them do
|
|
1618
|
+
* would need an exception carved for it. A guard with an exception is not
|
|
1619
|
+
* a guard. So the default stays valid and stays obviously wrong: no
|
|
1620
|
+
* camera accepts it, and no reviewer reads past it.
|
|
1621
|
+
*
|
|
1622
|
+
* Both fields are `.optional()` — a bare `{}` parses — so nothing forces
|
|
1623
|
+
* a default here at all. It is offered because a developer who opened
|
|
1624
|
+
* this block wants both fields, and the snippet exists to save them the
|
|
1625
|
+
* typing, not to decide anything.
|
|
1626
|
+
*/
|
|
1627
|
+
body: {
|
|
1628
|
+
username: "${1:ops}",
|
|
1629
|
+
password: "${2:CHANGE-ME}"
|
|
1630
|
+
}
|
|
1631
|
+
}]
|
|
1632
|
+
});
|
|
1633
|
+
var rtspTransport = import_zod5.z.enum(["tcp", "udp"]);
|
|
1634
|
+
var cameraSource = import_zod5.z.discriminatedUnion("kind", [
|
|
1635
|
+
strictObject({
|
|
1636
|
+
kind: import_zod5.z.literal("ros").meta({
|
|
1637
|
+
description: "Selects the ROS image-topic source: this camera then carries `topic` and `type`, and no field of another kind."
|
|
1638
|
+
}),
|
|
1639
|
+
topic: rosName.meta({
|
|
1640
|
+
description: "The ROS image topic the bridge subscribes to, as an absolute graph name. Clients never name it \u2014 they address the camera by its slug \u2014 so the topic can be renamed on the robot without an app changing.",
|
|
1641
|
+
patternErrorMessage: ROS_NAME_RULE,
|
|
1642
|
+
examples: ["/camera/image_raw"]
|
|
1643
|
+
}),
|
|
1644
|
+
type: rosTypeName.meta({
|
|
1645
|
+
description: "The message type of `topic`: `sensor_msgs/msg/Image` for raw frames, `sensor_msgs/msg/CompressedImage` for a camera that already encodes. Declared here rather than introspected, so a camera can be configured for a robot that has never connected.",
|
|
1646
|
+
patternErrorMessage: ROS_TYPE_NAME_RULE,
|
|
1647
|
+
examples: ["sensor_msgs/msg/Image"]
|
|
1648
|
+
})
|
|
1649
|
+
}).meta({
|
|
1650
|
+
description: "Frames come from an image topic the robot already publishes. It is the only source the bridge **subscribes** to rather than opens, so it needs no URL, no device and nobody to authenticate to.",
|
|
1651
|
+
defaultSnippets: [{
|
|
1652
|
+
label: "ros \u2014 an image topic the robot already publishes",
|
|
1653
|
+
description: "Subscribes to a topic that is already there; nothing is opened and there is nobody to authenticate to.",
|
|
1654
|
+
/**
|
|
1655
|
+
* `type` is a **choice**, not a literal, and that is a correction: it was
|
|
1656
|
+
* written out on the rule that a field the format fixes is written out,
|
|
1657
|
+
* and `type` is not such a field. Its own description names two values
|
|
1658
|
+
* and says which applies when — `Image` for raw frames,
|
|
1659
|
+
* `CompressedImage` for a camera that encodes itself. A snippet that
|
|
1660
|
+
* picks one picks wrong for half the cameras, and picks it invisibly:
|
|
1661
|
+
* `rosTypeName` accepts either, publish accepts either, no diagnostic
|
|
1662
|
+
* fires anywhere, and the bridge then subscribes with the wrong type and
|
|
1663
|
+
* delivers no frames. A snippet supplying a wrong answer where it could
|
|
1664
|
+
* have supplied a question is this project's *check that cannot fire*,
|
|
1665
|
+
* arriving through a hint the developer trusts.
|
|
1666
|
+
*
|
|
1667
|
+
* `kind: 'ros'` stays a literal, because the branch really does fix it.
|
|
1668
|
+
*
|
|
1669
|
+
* Measured through the actual pipeline rather than assumed, because
|
|
1670
|
+
* choice syntax is the one construct here that three layers must each
|
|
1671
|
+
* pass through unharmed: yaml-language-server's `stringifyObject` emits
|
|
1672
|
+
* the body verbatim, and monaco-editor 0.52.2's `SnippetParser` parses
|
|
1673
|
+
* `${2|a,b|}` into a placeholder carrying both options whose
|
|
1674
|
+
* `toString()` — the text on the buffer before anyone chooses — is the
|
|
1675
|
+
* first one. So a developer who tabs past this gets a document
|
|
1676
|
+
* byte-identical to the literal it replaced, and one who opens the
|
|
1677
|
+
* picker gets `CompressedImage`; both parse.
|
|
1678
|
+
*/
|
|
1679
|
+
body: {
|
|
1680
|
+
kind: "ros",
|
|
1681
|
+
topic: "${1:/camera/image_raw}",
|
|
1682
|
+
type: "${2|sensor_msgs/msg/Image,sensor_msgs/msg/CompressedImage|}"
|
|
1683
|
+
}
|
|
1684
|
+
}]
|
|
1685
|
+
}),
|
|
1686
|
+
strictObject({
|
|
1687
|
+
kind: import_zod5.z.literal("rtsp").meta({
|
|
1688
|
+
description: "Selects the RTSP source: this camera then carries `url`, and optionally `transport` and `credentials`."
|
|
1689
|
+
}),
|
|
820
1690
|
/**
|
|
821
1691
|
* Scheme-constrained deliberately. The playbook drafted `z.string().url()`
|
|
822
1692
|
* here and the shipped contract was `z.string().min(1).max(2048)` — nobody
|
|
@@ -829,19 +1699,63 @@ var cameraSource = import_zod4.z.discriminatedUnion("kind", [
|
|
|
829
1699
|
* by intent. The bridge re-checks this too — a robot must not become a
|
|
830
1700
|
* file server because a validator changed.
|
|
831
1701
|
*/
|
|
832
|
-
url:
|
|
1702
|
+
url: import_zod5.z.string().min(1).max(2048).regex(/^rtsps?:\/\//i, RTSP_URL_RULE).meta({
|
|
1703
|
+
description: "Where the stream lives, reached from the robot rather than from the cloud. **`rtsp://` or `rtsps://` only** \u2014 the bridge opens this with a library that would equally honour `file:`, so an unconstrained URL would turn a configuration document into arbitrary file access on the robot. The bridge re-checks the scheme itself, so a validator that changed could not make a robot serve files.",
|
|
1704
|
+
patternErrorMessage: RTSP_URL_RULE,
|
|
1705
|
+
examples: ["rtsp://cam-1.plant.local/stream1"]
|
|
1706
|
+
}),
|
|
833
1707
|
/** TCP by default: UDP loses frames on a congested link, silently. */
|
|
834
|
-
transport:
|
|
835
|
-
|
|
1708
|
+
transport: rtspTransport.optional().meta({
|
|
1709
|
+
description: "How the RTSP payload is carried. Omitted means `tcp`: `udp` loses frames on a congested link and loses them silently, so the result looks like a failing camera rather than like a choice made here.",
|
|
1710
|
+
enumDescriptions: describeValues(rtspTransport.options, {
|
|
1711
|
+
tcp: "The frames are interleaved into the RTSP connection itself, which is what a congested or lossy link needs \u2014 nothing is dropped on the way. This is what an omitted `transport` means.",
|
|
1712
|
+
udp: "The frames travel in their own UDP stream: lower latency on a quiet network, and silent frame loss on any other."
|
|
1713
|
+
})
|
|
1714
|
+
}),
|
|
1715
|
+
credentials: cameraCredentials.optional()
|
|
1716
|
+
}).meta({
|
|
1717
|
+
description: "Frames come from an RTSP stream the robot itself can reach \u2014 a network camera on its own LAN. The bridge opens the connection; the cloud never does, and never needs a route to the camera.",
|
|
1718
|
+
defaultSnippets: [{
|
|
1719
|
+
label: "rtsp \u2014 a network camera the robot itself can reach",
|
|
1720
|
+
description: "A stream the bridge opens over RTSP. The scheme is written out because the format constrains it; the host and the path are what vary.",
|
|
1721
|
+
body: {
|
|
1722
|
+
kind: "rtsp",
|
|
1723
|
+
url: "rtsp://${1:cam-1.plant.local}/${2:stream1}"
|
|
1724
|
+
}
|
|
1725
|
+
}]
|
|
836
1726
|
}),
|
|
837
|
-
|
|
838
|
-
kind:
|
|
1727
|
+
strictObject({
|
|
1728
|
+
kind: import_zod5.z.literal("mjpeg").meta({
|
|
1729
|
+
description: "Selects the MJPEG-over-HTTP source: this camera then carries `url`, and optionally `credentials`."
|
|
1730
|
+
}),
|
|
839
1731
|
/** `http:`/`https:` only — see the `rtsp` variant above for why. */
|
|
840
|
-
url:
|
|
841
|
-
|
|
1732
|
+
url: import_zod5.z.string().min(1).max(2048).regex(/^https?:\/\//i, MJPEG_URL_RULE).meta({
|
|
1733
|
+
description: "Where the stream lives. **`http://` or `https://` only** \u2014 as for the `rtsp` URL, the bridge opens it with a library that would also serve `file:`. Plain `http://` is permitted because these cameras usually sit on the robot's own network, but Basic credentials on such a URL then travel in the clear.",
|
|
1734
|
+
patternErrorMessage: MJPEG_URL_RULE,
|
|
1735
|
+
/**
|
|
1736
|
+
* The host and the path this branch's own snippet body inserts, and the
|
|
1737
|
+
* URL its rule sentence names — one answer to "what goes here?", not a
|
|
1738
|
+
* third. The sibling `rtsp` url had an `examples` from the first day and
|
|
1739
|
+
* this position was the format's only silent URL (§1.1).
|
|
1740
|
+
*/
|
|
1741
|
+
examples: ["http://cam-1.plant.local/video.mjpg"]
|
|
1742
|
+
}),
|
|
1743
|
+
credentials: cameraCredentials.optional()
|
|
1744
|
+
}).meta({
|
|
1745
|
+
description: "Frames come from an MJPEG stream over HTTP \u2014 one JPEG after another, the simplest network source there is. Unlike `rtsp` there is no `transport` to choose: it is HTTP, and any `credentials` therefore travel as HTTP Basic.",
|
|
1746
|
+
defaultSnippets: [{
|
|
1747
|
+
label: "mjpeg \u2014 one JPEG after another over HTTP",
|
|
1748
|
+
description: "The simplest network source there is. `https://` is accepted too, and is what any credentials on this URL need.",
|
|
1749
|
+
body: {
|
|
1750
|
+
kind: "mjpeg",
|
|
1751
|
+
url: "http://${1:cam-1.plant.local}/${2:video.mjpg}"
|
|
1752
|
+
}
|
|
1753
|
+
}]
|
|
842
1754
|
}),
|
|
843
|
-
|
|
844
|
-
kind:
|
|
1755
|
+
strictObject({
|
|
1756
|
+
kind: import_zod5.z.literal("v4l2").meta({
|
|
1757
|
+
description: "Selects the local capture-device source: this camera then carries `device` and nothing else."
|
|
1758
|
+
}),
|
|
845
1759
|
/**
|
|
846
1760
|
* e.g. `/dev/video0`, or a stable `/dev/v4l/by-id/...` symlink. Resolved
|
|
847
1761
|
* on the robot, never by the cloud.
|
|
@@ -865,103 +1779,182 @@ var cameraSource = import_zod4.z.discriminatedUnion("kind", [
|
|
|
865
1779
|
* The bridge re-derives this constraint rather than trusting the wire
|
|
866
1780
|
* (`validate_device_path`), exactly as it re-derives the URL scheme.
|
|
867
1781
|
*/
|
|
868
|
-
device:
|
|
1782
|
+
device: import_zod5.z.string().min(1).max(128).regex(/^\/dev\/[A-Za-z0-9][A-Za-z0-9._/-]*$/, DEVICE_PATH_RULE).refine((v) => !v.split("/").includes(".."), "must not contain a `..` path segment").refine((v) => !v.endsWith("/"), "must name a device, not a directory").meta({
|
|
1783
|
+
description: "The capture device, resolved on the robot and never by the cloud; a `/dev/v4l/by-id/...` symlink survives a reboot that renumbers `/dev/video0`. **Constrained to `/dev/`** \u2014 the string reaches OpenCV, which will just as happily open an ordinary video file or an `http://` URL and publish its pixels to the cloud. The bridge re-derives the same constraint rather than trusting the wire.",
|
|
1784
|
+
patternErrorMessage: DEVICE_PATH_RULE,
|
|
1785
|
+
examples: ["/dev/video0"]
|
|
1786
|
+
})
|
|
1787
|
+
}).meta({
|
|
1788
|
+
description: "Frames come from a capture device attached to the robot itself, such as a USB camera on `/dev/video0`. Nothing leaves the robot to fetch them, and there is nothing to authenticate to, so this source takes no `credentials`.",
|
|
1789
|
+
defaultSnippets: [{
|
|
1790
|
+
label: "v4l2 \u2014 a capture device attached to the robot",
|
|
1791
|
+
description: "A USB camera on the robot itself. A `/dev/v4l/by-id/...` symlink survives a reboot that renumbers `/dev/video0`.",
|
|
1792
|
+
/**
|
|
1793
|
+
* The default is not decoration. `device` is required and the path is
|
|
1794
|
+
* constrained to `/dev/`, so a bare `$1` would insert the empty string
|
|
1795
|
+
* and offer a camera the format refuses.
|
|
1796
|
+
*/
|
|
1797
|
+
body: {
|
|
1798
|
+
kind: "v4l2",
|
|
1799
|
+
device: "${1:/dev/video0}"
|
|
1800
|
+
}
|
|
1801
|
+
}]
|
|
869
1802
|
})
|
|
870
1803
|
]);
|
|
871
|
-
var
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1804
|
+
var snapshotIntervalSeconds = import_zod5.z.number().int().min(1).max(3600);
|
|
1805
|
+
var CAMERA_SNIPPET = {
|
|
1806
|
+
label: "a camera",
|
|
1807
|
+
description: "A complete camera entry with every required field.",
|
|
1808
|
+
body: {
|
|
1809
|
+
source: { kind: "v4l2", device: "${2:/dev/video0}" },
|
|
1810
|
+
width: 1280,
|
|
1811
|
+
height: 720,
|
|
1812
|
+
fps: 15,
|
|
1813
|
+
bitrate_kbps: 2e3,
|
|
1814
|
+
snapshot_interval_seconds: 5,
|
|
1815
|
+
description: "${3:Forward-facing camera on the mast.}"
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
var cameraConfig = strictObject({
|
|
1819
|
+
source: cameraSource.meta({
|
|
1820
|
+
description: "Where this camera's frames come from. `kind` picks one of four sources and fixes which other fields the source may carry, so an impossible camera is unrepresentable rather than merely invalid \u2014 there is no way to write an RTSP camera with a ROS topic."
|
|
1821
|
+
}),
|
|
1822
|
+
width: import_zod5.z.number().int().positive().max(7680).meta({
|
|
1823
|
+
description: "The width the bridge scales frames to before sending, in pixels \u2014 what the bridge produces, not what the sensor captures; a snapshot can arrive narrower, since the bridge reduces both dimensions together to fit its JPEG byte ceiling. It stands in the configuration and never in a viewer's request, so no client can make the robot encode a larger frame than the developer allowed.",
|
|
1824
|
+
examples: [1280]
|
|
1825
|
+
}),
|
|
1826
|
+
height: import_zod5.z.number().int().positive().max(4320).meta({
|
|
1827
|
+
description: "The height the bridge scales every frame to, in pixels; with `width` it is the size the live stream carries. A snapshot can arrive **smaller** than this \u2014 its JPEG has a byte ceiling, and the bridge gives up quality first and then resolution to fit, reporting the size it actually encoded.",
|
|
1828
|
+
examples: [720]
|
|
1829
|
+
}),
|
|
1830
|
+
fps: import_zod5.z.number().int().positive().max(60).meta({
|
|
1831
|
+
description: "How many frames a second the bridge forwards, at most. It is a ceiling, not a clock: a camera that delivers ten frames a second stays at ten. Both modes read the same throttled pipeline, so this also bounds how fresh a snapshot can be.",
|
|
1832
|
+
examples: [15]
|
|
1833
|
+
}),
|
|
1834
|
+
bitrate_kbps: import_zod5.z.number().int().positive().max(5e4).meta({
|
|
1835
|
+
description: "The ceiling for the **live** encoding, in kilobits per second \u2014 this is what bounds a watched camera against the robot's uplink. Snapshots are not covered by it: they are JPEGs under their own byte ceiling. Raising `width`, `height` or `fps` against a fixed bitrate buys blur, not detail.",
|
|
1836
|
+
examples: [2e3]
|
|
1837
|
+
}),
|
|
1838
|
+
snapshot_interval_seconds: snapshotIntervalSeconds.meta({
|
|
1839
|
+
description: "How often a still frame is captured, in seconds. **It runs whether or not anyone is watching**, unlike the live stream, which the cloud refcounts \u2014 first viewer starts it, last one ends it. The cloud caches the one frame and serves every reader from it, so a hundred pollers cost the robot exactly one image per interval.",
|
|
1840
|
+
examples: [5]
|
|
1841
|
+
}),
|
|
1842
|
+
description: serviceDescription.meta({
|
|
1843
|
+
description: "What this camera shows, in the developer's own words \u2014 documentation for whoever reads the configuration, for the console and for MCP clients; the robot does nothing with it. A camera without one is still offered, with `description: null`, as for actions, services and publishers. What `camera_snapshot` serves is the latest snapshot with its age; a live session is never a tool.",
|
|
1844
|
+
examples: ["Forward-facing camera on the mast."]
|
|
1845
|
+
})
|
|
1846
|
+
}).meta({
|
|
1847
|
+
/** The value position of one entry — `front: ▮` under `cameras:`. */
|
|
1848
|
+
defaultSnippets: [CAMERA_SNIPPET]
|
|
1849
|
+
});
|
|
1850
|
+
var FLEETLESS_FORMAT_VERSION = 1;
|
|
1851
|
+
var capped = (entry, max, what) => slugKeyed(entry).refine((m) => Object.keys(m).length <= max, { message: `at most ${max} ${what}` });
|
|
1852
|
+
var robotConfigDoc = strictObject({
|
|
1853
|
+
fleetless: import_zod5.z.literal(FLEETLESS_FORMAT_VERSION).meta({
|
|
1854
|
+
description: "The format version, and the first line of the file. It decides how everything below is read, so a file that omits it \u2014 or names a version this cloud does not know \u2014 is **refused rather than half understood**."
|
|
1855
|
+
}),
|
|
1856
|
+
messages: messageMap.meta({
|
|
1857
|
+
description: "Reusable message bodies, keyed by name, inserted elsewhere by writing `${name}` directly after `message:`. A shared body may hold placeholders and whoever inserts it declares the parameters, so two publishers can send the same message under different bounds. **A shared message may not insert another**, so a `${name}` inside a body is always a parameter and never a second message.",
|
|
1858
|
+
defaultSnippets: [underSlug("${1:drive}", SHARED_MESSAGE_SNIPPET)]
|
|
1859
|
+
}).optional(),
|
|
1860
|
+
datapoints: capped(datapointConfig, 200, "datapoints").meta({
|
|
1861
|
+
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 refused here.",
|
|
1862
|
+
defaultSnippets: [
|
|
1863
|
+
underSlug("${1:battery_voltage}", DATAPOINT_SNIPPET),
|
|
1864
|
+
underSlug("${1:battery}", NUMERIC_DATAPOINT_SNIPPET)
|
|
1865
|
+
]
|
|
1866
|
+
}).optional(),
|
|
1867
|
+
actions: capped(actionConfig, 200, "actions").meta({
|
|
1868
|
+
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 refused here.",
|
|
1869
|
+
defaultSnippets: [underSlug("${1:navigate}", ACTION_SNIPPET)]
|
|
1870
|
+
}).optional(),
|
|
1871
|
+
services: capped(serviceConfig, 200, "services").meta({
|
|
1872
|
+
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 refused here.",
|
|
1873
|
+
defaultSnippets: [underSlug("${1:reset_odometry}", SERVICE_SNIPPET)]
|
|
1874
|
+
}).optional(),
|
|
1875
|
+
publishers: capped(publisherConfig, 200, "publishers").meta({
|
|
1876
|
+
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 refused here.",
|
|
1877
|
+
defaultSnippets: [underSlug("${1:drive}", PUBLISHER_SNIPPET)]
|
|
1878
|
+
}).optional(),
|
|
1879
|
+
cameras: capped(cameraConfig, 50, "cameras").meta({
|
|
1880
|
+
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 refused here.",
|
|
1881
|
+
defaultSnippets: [underSlug("${1:front}", CAMERA_SNIPPET)]
|
|
1882
|
+
}).optional()
|
|
1883
|
+
});
|
|
1884
|
+
var validationIssue = import_zod5.z.object({
|
|
1885
|
+
path: import_zod5.z.string().min(1),
|
|
1886
|
+
slug: import_zod5.z.string().nullable(),
|
|
1887
|
+
code: import_zod5.z.string().min(1),
|
|
1888
|
+
message: import_zod5.z.string().min(1),
|
|
1889
|
+
severity: import_zod5.z.enum(["error", "warning"])
|
|
1890
|
+
});
|
|
1891
|
+
var configState = import_zod5.z.object({
|
|
1892
|
+
published_version: import_zod5.z.number().int().positive().nullable(),
|
|
1893
|
+
published_at: import_zod5.z.iso.datetime().nullable(),
|
|
1894
|
+
draft_updated_at: import_zod5.z.iso.datetime().nullable(),
|
|
1895
|
+
applied_version: import_zod5.z.number().int().nonnegative().nullable(),
|
|
1896
|
+
applied_ok: import_zod5.z.boolean().nullable(),
|
|
1897
|
+
/**
|
|
1898
|
+
* The bridge's own `bridgeConfigApplied.errors` (`protocol.ts`), read back
|
|
1899
|
+
* verbatim. **Reuses `applyError` rather than restating `{ slug, message
|
|
1900
|
+
* }`** — a narrower local copy here used to silently strip `kind`, `code`
|
|
1901
|
+
* and `details` on every read: `configState.safeParse` dropped every field
|
|
1902
|
+
* a caller did not ask for, and `robotDetailResponse` embeds `configState`
|
|
1903
|
+
* (`useCloudApi.ts`'s `getRobot`), so the console lost the fields one
|
|
1904
|
+
* layer before anyone could see them.
|
|
1905
|
+
*/
|
|
1906
|
+
applied_errors: import_zod5.z.array(applyError).nullable()
|
|
914
1907
|
});
|
|
915
1908
|
|
|
916
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
917
|
-
var
|
|
918
|
-
var rosGraphEntry =
|
|
1909
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/introspection.js
|
|
1910
|
+
var import_zod6 = require("zod");
|
|
1911
|
+
var rosGraphEntry = import_zod6.z.object({
|
|
919
1912
|
name: rosName,
|
|
920
|
-
types:
|
|
921
|
-
});
|
|
922
|
-
var rosGraph =
|
|
923
|
-
topics:
|
|
924
|
-
services:
|
|
925
|
-
actions:
|
|
926
|
-
captured_at_ms:
|
|
927
|
-
});
|
|
928
|
-
var typeField =
|
|
929
|
-
name:
|
|
930
|
-
type:
|
|
931
|
-
array:
|
|
932
|
-
fields:
|
|
1913
|
+
types: import_zod6.z.array(rosTypeName).min(1)
|
|
1914
|
+
});
|
|
1915
|
+
var rosGraph = import_zod6.z.object({
|
|
1916
|
+
topics: import_zod6.z.array(rosGraphEntry),
|
|
1917
|
+
services: import_zod6.z.array(rosGraphEntry),
|
|
1918
|
+
actions: import_zod6.z.array(rosGraphEntry),
|
|
1919
|
+
captured_at_ms: import_zod6.z.number().int().nonnegative()
|
|
1920
|
+
});
|
|
1921
|
+
var typeField = import_zod6.z.lazy(() => import_zod6.z.object({
|
|
1922
|
+
name: import_zod6.z.string().min(1).max(128),
|
|
1923
|
+
type: import_zod6.z.string().min(1).max(255),
|
|
1924
|
+
array: import_zod6.z.boolean(),
|
|
1925
|
+
fields: import_zod6.z.array(typeField).nullable()
|
|
933
1926
|
}));
|
|
934
|
-
var typeDefinition =
|
|
935
|
-
|
|
1927
|
+
var typeDefinition = import_zod6.z.discriminatedUnion("kind", [
|
|
1928
|
+
import_zod6.z.object({
|
|
936
1929
|
name: rosTypeName,
|
|
937
|
-
kind:
|
|
938
|
-
fields:
|
|
1930
|
+
kind: import_zod6.z.literal("msg"),
|
|
1931
|
+
fields: import_zod6.z.array(typeField)
|
|
939
1932
|
}),
|
|
940
|
-
|
|
1933
|
+
import_zod6.z.object({
|
|
941
1934
|
name: rosTypeName,
|
|
942
|
-
kind:
|
|
943
|
-
request:
|
|
944
|
-
response:
|
|
1935
|
+
kind: import_zod6.z.literal("srv"),
|
|
1936
|
+
request: import_zod6.z.array(typeField),
|
|
1937
|
+
response: import_zod6.z.array(typeField)
|
|
945
1938
|
}),
|
|
946
|
-
|
|
1939
|
+
import_zod6.z.object({
|
|
947
1940
|
name: rosTypeName,
|
|
948
|
-
kind:
|
|
949
|
-
goal:
|
|
950
|
-
result:
|
|
951
|
-
feedback:
|
|
1941
|
+
kind: import_zod6.z.literal("action"),
|
|
1942
|
+
goal: import_zod6.z.array(typeField),
|
|
1943
|
+
result: import_zod6.z.array(typeField),
|
|
1944
|
+
feedback: import_zod6.z.array(typeField)
|
|
952
1945
|
})
|
|
953
1946
|
]);
|
|
954
1947
|
|
|
955
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
956
|
-
var
|
|
957
|
-
var jobState =
|
|
958
|
-
var job =
|
|
959
|
-
id:
|
|
960
|
-
robot_id:
|
|
1948
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/jobs.js
|
|
1949
|
+
var import_zod7 = require("zod");
|
|
1950
|
+
var jobState = import_zod7.z.enum(["running", "succeeded", "failed", "cancelled", "lost"]);
|
|
1951
|
+
var job = import_zod7.z.object({
|
|
1952
|
+
id: import_zod7.z.uuid(),
|
|
1953
|
+
robot_id: import_zod7.z.uuid(),
|
|
961
1954
|
slug,
|
|
962
1955
|
state: jobState,
|
|
963
|
-
started_at:
|
|
964
|
-
updated_at:
|
|
1956
|
+
started_at: import_zod7.z.iso.datetime(),
|
|
1957
|
+
updated_at: import_zod7.z.iso.datetime(),
|
|
965
1958
|
/**
|
|
966
1959
|
* A monotonic counter, ascending in mint order (W7), and the **named**
|
|
967
1960
|
* tiebreaker for any listing that claims an order.
|
|
@@ -978,9 +1971,9 @@ var job = import_zod6.z.object({
|
|
|
978
1971
|
* orders jobs that coexist in one registry — and stated, because a reader
|
|
979
1972
|
* who assumed `auditEvent.seq`'s durable semantics would be wrong.
|
|
980
1973
|
*/
|
|
981
|
-
seq:
|
|
1974
|
+
seq: import_zod7.z.number().int().positive(),
|
|
982
1975
|
/** Present once the job succeeded; shape is the ROS result's. */
|
|
983
|
-
result:
|
|
1976
|
+
result: import_zod7.z.unknown().nullable(),
|
|
984
1977
|
/**
|
|
985
1978
|
* Present on `failed`; a human message, plus a code where one exists.
|
|
986
1979
|
*
|
|
@@ -998,52 +1991,122 @@ var job = import_zod6.z.object({
|
|
|
998
1991
|
* code has a documented payload — `job_queue_full` has
|
|
999
1992
|
* `jobQueueFullDetails` — it belongs here, not in the sentence.
|
|
1000
1993
|
*/
|
|
1001
|
-
error:
|
|
1002
|
-
code:
|
|
1003
|
-
message:
|
|
1004
|
-
details:
|
|
1994
|
+
error: import_zod7.z.object({
|
|
1995
|
+
code: import_zod7.z.string().min(1),
|
|
1996
|
+
message: import_zod7.z.string().min(1),
|
|
1997
|
+
details: import_zod7.z.unknown().optional()
|
|
1005
1998
|
}).nullable()
|
|
1006
1999
|
});
|
|
1007
|
-
var jobEvent =
|
|
1008
|
-
type:
|
|
1009
|
-
robot_id:
|
|
2000
|
+
var jobEvent = import_zod7.z.object({
|
|
2001
|
+
type: import_zod7.z.literal("job"),
|
|
2002
|
+
robot_id: import_zod7.z.uuid(),
|
|
1010
2003
|
slug,
|
|
1011
2004
|
job,
|
|
1012
2005
|
/** Action feedback, if this update carries any. */
|
|
1013
|
-
feedback:
|
|
2006
|
+
feedback: import_zod7.z.unknown().nullable(),
|
|
1014
2007
|
/** 0..1 when the action reports progress; null when it does not. */
|
|
1015
|
-
progress:
|
|
1016
|
-
timestamp_ms:
|
|
2008
|
+
progress: import_zod7.z.number().min(0).max(1).nullable(),
|
|
2009
|
+
timestamp_ms: import_zod7.z.number().int().nonnegative()
|
|
1017
2010
|
});
|
|
1018
|
-
var busyDetails =
|
|
2011
|
+
var busyDetails = import_zod7.z.object({
|
|
1019
2012
|
running: job
|
|
1020
2013
|
});
|
|
1021
|
-
var publisherBusyDetails =
|
|
2014
|
+
var publisherBusyDetails = import_zod7.z.object({
|
|
1022
2015
|
/** The configured silence a holder must leave before anyone else may publish. */
|
|
1023
|
-
quiet_timeout_ms:
|
|
2016
|
+
quiet_timeout_ms: import_zod7.z.number().int().nonnegative(),
|
|
1024
2017
|
/** How much of that silence is still outstanding, now. */
|
|
1025
|
-
retry_after_ms:
|
|
2018
|
+
retry_after_ms: import_zod7.z.number().int().nonnegative()
|
|
1026
2019
|
});
|
|
1027
|
-
var jobQueueFullDetails =
|
|
2020
|
+
var jobQueueFullDetails = import_zod7.z.object({
|
|
1028
2021
|
/** The bridge's bound on queued jobs. */
|
|
1029
|
-
limit:
|
|
2022
|
+
limit: import_zod7.z.number().int().positive(),
|
|
1030
2023
|
/** How many are queued right now — `>= limit` when this refusal is sent. */
|
|
1031
|
-
queued:
|
|
2024
|
+
queued: import_zod7.z.number().int().nonnegative()
|
|
2025
|
+
});
|
|
2026
|
+
var JOB_RUN_PAGE_MAX = 200;
|
|
2027
|
+
var jobActor = import_zod7.z.object({
|
|
2028
|
+
kind: import_zod7.z.enum(["developer", "end_user", "server_key"]),
|
|
2029
|
+
id: import_zod7.z.uuid(),
|
|
2030
|
+
/**
|
|
2031
|
+
* The email for a developer or end user, the key's `name` for a server key.
|
|
2032
|
+
* A display snapshot taken at invoke time: renaming a key afterwards does not
|
|
2033
|
+
* rewrite history, which is the point of storing it rather than joining.
|
|
2034
|
+
*/
|
|
2035
|
+
label: import_zod7.z.string().min(1).max(200)
|
|
2036
|
+
});
|
|
2037
|
+
var jobRunKind = import_zod7.z.enum(["action", "service"]);
|
|
2038
|
+
var jobRun = import_zod7.z.object({
|
|
2039
|
+
id: import_zod7.z.uuid(),
|
|
2040
|
+
robot_id: import_zod7.z.uuid(),
|
|
2041
|
+
slug,
|
|
2042
|
+
kind: jobRunKind,
|
|
2043
|
+
state: jobState,
|
|
2044
|
+
started_at: import_zod7.z.iso.datetime(),
|
|
2045
|
+
/** `null` while `running` — a run has an end only once it has one. */
|
|
2046
|
+
ended_at: import_zod7.z.iso.datetime().nullable(),
|
|
2047
|
+
/** `null` while `running`. Not "0 so far". */
|
|
2048
|
+
duration_ms: import_zod7.z.number().int().nonnegative().nullable(),
|
|
2049
|
+
result: import_zod7.z.unknown().nullable(),
|
|
2050
|
+
error: import_zod7.z.object({ code: import_zod7.z.string().min(1), message: import_zod7.z.string().min(1), details: import_zod7.z.unknown().optional() }).nullable(),
|
|
2051
|
+
actor: jobActor,
|
|
2052
|
+
/**
|
|
2053
|
+
* **Durable, unlike `job.seq`.** That one is a per-process counter that
|
|
2054
|
+
* restarts with the cloud; this is a postgres `bigserial` and is the cursor
|
|
2055
|
+
* `before_seq` walks.
|
|
2056
|
+
*/
|
|
2057
|
+
seq: import_zod7.z.number().int().positive(),
|
|
2058
|
+
/**
|
|
2059
|
+
* Live-only, read from the in-memory registry for rows that are still
|
|
2060
|
+
* running. `null` means **"not known right now"** — after a cloud restart,
|
|
2061
|
+
* before the bridge reconnects — and never "0 %". A fraction, as in
|
|
2062
|
+
* `jobEvent.progress`, not a percentage.
|
|
2063
|
+
*/
|
|
2064
|
+
progress: import_zod7.z.number().min(0).max(1).nullable(),
|
|
2065
|
+
feedback: import_zod7.z.unknown().nullable()
|
|
2066
|
+
});
|
|
2067
|
+
var jobRunQuery = import_zod7.z.object({
|
|
2068
|
+
/** Only runs with a smaller `seq` — the next, older page. */
|
|
2069
|
+
before_seq: wireSeqCursor.optional(),
|
|
2070
|
+
limit: import_zod7.z.union([import_zod7.z.string().regex(/^\d{1,4}$/), import_zod7.z.number().int()]).transform((v) => Number(v)).pipe(import_zod7.z.number().int().positive().max(JOB_RUN_PAGE_MAX)).optional(),
|
|
2071
|
+
robot_id: import_zod7.z.uuid().optional(),
|
|
2072
|
+
slug: slug.optional(),
|
|
2073
|
+
state: jobState.optional(),
|
|
2074
|
+
kind: jobRunKind.optional(),
|
|
2075
|
+
/** Half-open `[from, to)`, the same rule the history shapes follow (DEF-062). */
|
|
2076
|
+
from_ms: wireTimestampMs.optional(),
|
|
2077
|
+
to_ms: wireTimestampMs.optional()
|
|
2078
|
+
}).strict();
|
|
2079
|
+
var jobRunListResponse = import_zod7.z.object({
|
|
2080
|
+
runs: import_zod7.z.array(jobRun),
|
|
2081
|
+
/**
|
|
2082
|
+
* The `seq` a caller sends as `before_seq` to keep reading — or `null` when
|
|
2083
|
+
* there is nothing further. **`null` means the end, and that is a promise
|
|
2084
|
+
* rather than an observation.** A caller who instead compares `runs.length`
|
|
2085
|
+
* against `limit` is wrong the moment a filter makes a page thin.
|
|
2086
|
+
*/
|
|
2087
|
+
next_cursor: import_zod7.z.number().int().positive().nullable()
|
|
2088
|
+
});
|
|
2089
|
+
var jobRunSummaryQuery = import_zod7.z.object({ since_ms: wireTimestampMs }).strict();
|
|
2090
|
+
var jobRunSummary = import_zod7.z.object({
|
|
2091
|
+
running: import_zod7.z.number().int().nonnegative(),
|
|
2092
|
+
started: import_zod7.z.number().int().nonnegative(),
|
|
2093
|
+
failed: import_zod7.z.number().int().nonnegative(),
|
|
2094
|
+
since_ms: import_zod7.z.number().int().nonnegative()
|
|
1032
2095
|
});
|
|
1033
2096
|
|
|
1034
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
2097
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/protocol.js
|
|
1035
2098
|
var MAX_PATIENCE_MS = 12e4;
|
|
1036
2099
|
var MIN_PATIENCE_MS = 1e3;
|
|
1037
|
-
var activeJob =
|
|
1038
|
-
job_id:
|
|
2100
|
+
var activeJob = import_zod8.z.object({
|
|
2101
|
+
job_id: import_zod8.z.uuid(),
|
|
1039
2102
|
slug,
|
|
1040
2103
|
state: jobState
|
|
1041
2104
|
});
|
|
1042
|
-
var bridgeHello =
|
|
1043
|
-
type:
|
|
1044
|
-
protocol_version:
|
|
1045
|
-
token:
|
|
1046
|
-
bridge_version:
|
|
2105
|
+
var bridgeHello = import_zod8.z.object({
|
|
2106
|
+
type: import_zod8.z.literal("hello"),
|
|
2107
|
+
protocol_version: import_zod8.z.number().int().positive(),
|
|
2108
|
+
token: import_zod8.z.string().min(1),
|
|
2109
|
+
bridge_version: import_zod8.z.string().min(1),
|
|
1047
2110
|
/**
|
|
1048
2111
|
* Every job this bridge still knows about, right now (spec §6.1, W4).
|
|
1049
2112
|
*
|
|
@@ -1071,60 +2134,68 @@ var bridgeHello = import_zod7.z.object({
|
|
|
1071
2134
|
* accepted alongside the new one: two accepted spellings would have to be
|
|
1072
2135
|
* supported and reconciled forever, and nobody is asking for that.
|
|
1073
2136
|
*/
|
|
1074
|
-
active_jobs:
|
|
2137
|
+
active_jobs: import_zod8.z.array(activeJob).max(500).default([])
|
|
1075
2138
|
});
|
|
1076
|
-
var cloudHelloOk =
|
|
1077
|
-
type:
|
|
1078
|
-
robot_id:
|
|
2139
|
+
var cloudHelloOk = import_zod8.z.object({
|
|
2140
|
+
type: import_zod8.z.literal("hello_ok"),
|
|
2141
|
+
robot_id: import_zod8.z.uuid()
|
|
1079
2142
|
});
|
|
1080
|
-
var cloudHelloError =
|
|
1081
|
-
type:
|
|
1082
|
-
code:
|
|
1083
|
-
message:
|
|
2143
|
+
var cloudHelloError = import_zod8.z.object({
|
|
2144
|
+
type: import_zod8.z.literal("hello_error"),
|
|
2145
|
+
code: import_zod8.z.string().min(1),
|
|
2146
|
+
message: import_zod8.z.string().min(1)
|
|
1084
2147
|
});
|
|
1085
|
-
var datapointFrame =
|
|
1086
|
-
type:
|
|
2148
|
+
var datapointFrame = import_zod8.z.object({
|
|
2149
|
+
type: import_zod8.z.literal("datapoint"),
|
|
1087
2150
|
slug,
|
|
1088
|
-
value:
|
|
1089
|
-
timestamp_ms:
|
|
2151
|
+
value: import_zod8.z.unknown(),
|
|
2152
|
+
timestamp_ms: import_zod8.z.number().int().nonnegative()
|
|
1090
2153
|
});
|
|
1091
|
-
var cloudPing =
|
|
1092
|
-
type:
|
|
1093
|
-
ts_ms:
|
|
2154
|
+
var cloudPing = import_zod8.z.object({
|
|
2155
|
+
type: import_zod8.z.literal("ping"),
|
|
2156
|
+
ts_ms: import_zod8.z.number().int().nonnegative()
|
|
1094
2157
|
});
|
|
1095
|
-
var bridgePong =
|
|
1096
|
-
type:
|
|
1097
|
-
ts_ms:
|
|
2158
|
+
var bridgePong = import_zod8.z.object({
|
|
2159
|
+
type: import_zod8.z.literal("pong"),
|
|
2160
|
+
ts_ms: import_zod8.z.number().int().nonnegative()
|
|
1098
2161
|
});
|
|
1099
|
-
var cloudConfig =
|
|
1100
|
-
type:
|
|
1101
|
-
version:
|
|
1102
|
-
doc: robotConfigDoc
|
|
1103
|
-
credentials: import_zod7.z.record(credentialRef, import_zod7.z.object({ username: import_zod7.z.string(), password: import_zod7.z.string() })).default({})
|
|
2162
|
+
var cloudConfig = import_zod8.z.object({
|
|
2163
|
+
type: import_zod8.z.literal("config"),
|
|
2164
|
+
version: import_zod8.z.number().int().nonnegative(),
|
|
2165
|
+
doc: robotConfigDoc
|
|
1104
2166
|
});
|
|
1105
|
-
var bridgeConfigApplied =
|
|
1106
|
-
type:
|
|
1107
|
-
version:
|
|
1108
|
-
ok:
|
|
1109
|
-
errors:
|
|
2167
|
+
var bridgeConfigApplied = import_zod8.z.object({
|
|
2168
|
+
type: import_zod8.z.literal("config_applied"),
|
|
2169
|
+
version: import_zod8.z.number().int().nonnegative(),
|
|
2170
|
+
ok: import_zod8.z.boolean(),
|
|
2171
|
+
errors: import_zod8.z.array(applyError)
|
|
1110
2172
|
});
|
|
1111
|
-
var cloudInvoke =
|
|
1112
|
-
type:
|
|
1113
|
-
job_id:
|
|
2173
|
+
var cloudInvoke = import_zod8.z.object({
|
|
2174
|
+
type: import_zod8.z.literal("invoke"),
|
|
2175
|
+
job_id: import_zod8.z.uuid(),
|
|
1114
2176
|
slug,
|
|
1115
2177
|
/**
|
|
1116
2178
|
* Already validated against §4.4 rules; the bridge validates structurally.
|
|
1117
2179
|
*
|
|
1118
|
-
* **Flat, keyed by
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
2180
|
+
* **Flat, keyed by parameter name** — `{"target_x": 1}`. The key is a key of
|
|
2181
|
+
* the entry's `parameters` mapping, not a path into the message. Those were
|
|
2182
|
+
* the same thing until FL-002 and are now deliberately decoupled: a
|
|
2183
|
+
* parameter keeps its name when the field it fills moves in the message
|
|
2184
|
+
* tree, which is the same reason a slug is not a topic name.
|
|
2185
|
+
*
|
|
2186
|
+
* Three things follow, and the last one got stronger rather than weaker:
|
|
2187
|
+
* the key a caller sends is the key a rule names, so a `parameter_invalid`
|
|
2188
|
+
* reports something the caller can find; the console binds one input per
|
|
2189
|
+
* parameter; and a position the template does not mark with `${…}` cannot
|
|
2190
|
+
* be set by any caller at all. That last one used to be a rule about what
|
|
2191
|
+
* no `parameterSpec` declared. It is now structural — the value has nowhere
|
|
2192
|
+
* to go.
|
|
2193
|
+
*
|
|
2194
|
+
* The bridge substitutes these values into the entry's `message` template
|
|
2195
|
+
* at its placeholder positions. It no longer unflattens a dotted path;
|
|
2196
|
+
* there is no dotted path to unflatten.
|
|
1126
2197
|
*/
|
|
1127
|
-
params:
|
|
2198
|
+
params: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown()),
|
|
1128
2199
|
/**
|
|
1129
2200
|
* How long this one call is worth waiting for (W6b), already resolved by
|
|
1130
2201
|
* the cloud — the caller's `invokeRequest.patience_ms`, or
|
|
@@ -1139,81 +2210,151 @@ var cloudInvoke = import_zod7.z.object({
|
|
|
1139
2210
|
* 15.0 s, agreeing only by accident, with no way to tell whose deadline a
|
|
1140
2211
|
* caller had actually hit.
|
|
1141
2212
|
*/
|
|
1142
|
-
patience_ms:
|
|
2213
|
+
patience_ms: import_zod8.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS)
|
|
1143
2214
|
});
|
|
1144
|
-
var cloudCancel =
|
|
1145
|
-
type:
|
|
2215
|
+
var cloudCancel = import_zod8.z.object({
|
|
2216
|
+
type: import_zod8.z.literal("cancel"),
|
|
1146
2217
|
slug,
|
|
1147
|
-
job_id:
|
|
2218
|
+
job_id: import_zod8.z.uuid().nullable()
|
|
1148
2219
|
});
|
|
1149
|
-
var cloudPublish =
|
|
1150
|
-
type:
|
|
2220
|
+
var cloudPublish = import_zod8.z.object({
|
|
2221
|
+
type: import_zod8.z.literal("publish"),
|
|
1151
2222
|
slug,
|
|
1152
2223
|
/**
|
|
1153
|
-
* Flat and keyed by
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
2224
|
+
* Flat and keyed by parameter name, exactly like `cloudInvoke.params` — a
|
|
2225
|
+
* publisher declares parameters and takes the same validation, so it takes
|
|
2226
|
+
* the same shape.
|
|
2227
|
+
*
|
|
2228
|
+
* This is *not* the shape of `publisherConfig.failsafe.message`, which is a
|
|
2229
|
+
* complete ROS message template. The failsafe is authored once against the
|
|
2230
|
+
* type, sent by the bridge with no caller present, and refused outright if
|
|
2231
|
+
* it contains a placeholder — there would be nobody to fill it.
|
|
1159
2232
|
*/
|
|
1160
|
-
message:
|
|
2233
|
+
message: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown())
|
|
1161
2234
|
});
|
|
1162
|
-
var bridgeJobUpdate =
|
|
1163
|
-
type:
|
|
1164
|
-
job_id:
|
|
2235
|
+
var bridgeJobUpdate = import_zod8.z.object({
|
|
2236
|
+
type: import_zod8.z.literal("job_update"),
|
|
2237
|
+
job_id: import_zod8.z.uuid(),
|
|
1165
2238
|
slug,
|
|
1166
2239
|
state: jobState,
|
|
1167
|
-
feedback:
|
|
1168
|
-
progress:
|
|
1169
|
-
result:
|
|
2240
|
+
feedback: import_zod8.z.unknown().nullable(),
|
|
2241
|
+
progress: import_zod8.z.number().min(0).max(1).nullable(),
|
|
2242
|
+
result: import_zod8.z.unknown().nullable(),
|
|
1170
2243
|
/** Same shape as `job.error`, `details` included — see `jobs.ts`. */
|
|
1171
|
-
error:
|
|
1172
|
-
timestamp_ms:
|
|
2244
|
+
error: import_zod8.z.object({ code: import_zod8.z.string().min(1), message: import_zod8.z.string().min(1), details: import_zod8.z.unknown().optional() }).nullable(),
|
|
2245
|
+
timestamp_ms: import_zod8.z.number().int().nonnegative()
|
|
1173
2246
|
});
|
|
1174
|
-
var bridgeJobLost =
|
|
1175
|
-
type:
|
|
1176
|
-
job_ids:
|
|
2247
|
+
var bridgeJobLost = import_zod8.z.object({
|
|
2248
|
+
type: import_zod8.z.literal("job_lost"),
|
|
2249
|
+
job_ids: import_zod8.z.array(import_zod8.z.uuid())
|
|
1177
2250
|
});
|
|
1178
|
-
var cloudIntrospectRequest =
|
|
1179
|
-
type:
|
|
1180
|
-
request_id:
|
|
2251
|
+
var cloudIntrospectRequest = import_zod8.z.object({
|
|
2252
|
+
type: import_zod8.z.literal("introspect_request"),
|
|
2253
|
+
request_id: import_zod8.z.string().min(1).max(64)
|
|
1181
2254
|
});
|
|
1182
|
-
var bridgeIntrospect =
|
|
1183
|
-
type:
|
|
1184
|
-
request_id:
|
|
2255
|
+
var bridgeIntrospect = import_zod8.z.object({
|
|
2256
|
+
type: import_zod8.z.literal("introspect"),
|
|
2257
|
+
request_id: import_zod8.z.string().min(1).max(64),
|
|
1185
2258
|
graph: rosGraph
|
|
1186
2259
|
});
|
|
1187
|
-
var cloudTypeRequest =
|
|
1188
|
-
type:
|
|
1189
|
-
request_id:
|
|
1190
|
-
type_names:
|
|
2260
|
+
var cloudTypeRequest = import_zod8.z.object({
|
|
2261
|
+
type: import_zod8.z.literal("type_request"),
|
|
2262
|
+
request_id: import_zod8.z.string().min(1).max(64),
|
|
2263
|
+
type_names: import_zod8.z.array(rosTypeName).min(1).max(50)
|
|
1191
2264
|
});
|
|
1192
|
-
var bridgeTypeDefinitions =
|
|
1193
|
-
type:
|
|
1194
|
-
request_id:
|
|
1195
|
-
definitions:
|
|
1196
|
-
unresolved:
|
|
2265
|
+
var bridgeTypeDefinitions = import_zod8.z.object({
|
|
2266
|
+
type: import_zod8.z.literal("type_definitions"),
|
|
2267
|
+
request_id: import_zod8.z.string().min(1).max(64),
|
|
2268
|
+
definitions: import_zod8.z.array(typeDefinition),
|
|
2269
|
+
unresolved: import_zod8.z.array(import_zod8.z.string())
|
|
1197
2270
|
});
|
|
1198
|
-
var bridgeState =
|
|
1199
|
-
online:
|
|
1200
|
-
latency_ms:
|
|
2271
|
+
var bridgeState = import_zod8.z.object({
|
|
2272
|
+
online: import_zod8.z.boolean(),
|
|
2273
|
+
latency_ms: import_zod8.z.number().nonnegative().nullable()
|
|
2274
|
+
});
|
|
2275
|
+
var bridgePressureTier = import_zod8.z.object({
|
|
2276
|
+
sent: import_zod8.z.number().int().nonnegative(),
|
|
2277
|
+
bytes: import_zod8.z.number().int().nonnegative(),
|
|
2278
|
+
drops: import_zod8.z.number().int().nonnegative(),
|
|
2279
|
+
high_water: import_zod8.z.number().int().nonnegative()
|
|
2280
|
+
});
|
|
2281
|
+
var bridgePressure = import_zod8.z.object({
|
|
2282
|
+
link: import_zod8.z.object({
|
|
2283
|
+
/** bytes/s the socket demonstrably drains, from sends >= 64 KiB
|
|
2284
|
+
* only; null until the first large send of the session. */
|
|
2285
|
+
rate_bps: import_zod8.z.number().nonnegative().nullable(),
|
|
2286
|
+
/**
|
|
2287
|
+
* the byte target snapshots are currently encoded to fit.
|
|
2288
|
+
*
|
|
2289
|
+
* `.nonnegative()`, not `.positive()`: the target is derived from
|
|
2290
|
+
* `rate_bps`, and a link measured below 0.5 B/s floors to 0 here. A
|
|
2291
|
+
* schema that rejects 0 does not prevent that link — it only makes the
|
|
2292
|
+
* frame reporting it unparseable, and a console that cannot parse a
|
|
2293
|
+
* pressure frame shows "no feed", i.e. reports a struggling robot as an
|
|
2294
|
+
* *old* one. Zero is a legitimate reading and says something true.
|
|
2295
|
+
*/
|
|
2296
|
+
snapshot_max_bytes: import_zod8.z.number().int().nonnegative()
|
|
2297
|
+
}),
|
|
2298
|
+
/**
|
|
2299
|
+
* String keys "0".."5" because JSON has no integer keys. Counters are
|
|
2300
|
+
* cumulative per session and reset on reconnect; clients window by
|
|
2301
|
+
* differencing two samples.
|
|
2302
|
+
*
|
|
2303
|
+
* **What this schema does not decide:** it does not guarantee all six
|
|
2304
|
+
* keys are present (`z.record` over the six literals is exhaustive in
|
|
2305
|
+
* zod 4 — tested here, it required every key and rejected none, the
|
|
2306
|
+
* opposite of what a partial sample needs — so this is a
|
|
2307
|
+
* `.strictObject().partial()` over the same six literal keys instead, a
|
|
2308
|
+
* deliberate deviation from the originally sketched `z.record` shape with
|
|
2309
|
+
* the same runtime behaviour). A missing tier key reads as zeros; the
|
|
2310
|
+
* schema names what it cannot decide rather than implying a completeness
|
|
2311
|
+
* it cannot check.
|
|
2312
|
+
*/
|
|
2313
|
+
tiers: import_zod8.z.strictObject({
|
|
2314
|
+
"0": bridgePressureTier,
|
|
2315
|
+
"1": bridgePressureTier,
|
|
2316
|
+
"2": bridgePressureTier,
|
|
2317
|
+
"3": bridgePressureTier,
|
|
2318
|
+
"4": bridgePressureTier,
|
|
2319
|
+
"5": bridgePressureTier
|
|
2320
|
+
}).partial(),
|
|
2321
|
+
video: import_zod8.z.object({
|
|
2322
|
+
active_streams: import_zod8.z.number().int().nonnegative(),
|
|
2323
|
+
bitrate_sum_kbps: import_zod8.z.number().int().nonnegative(),
|
|
2324
|
+
/**
|
|
2325
|
+
* The uplink budget the bridge was configured with
|
|
2326
|
+
* (`FLEETLESS_UPLINK_KBPS`), or `null` when none was set.
|
|
2327
|
+
*
|
|
2328
|
+
* `.nonnegative()`, not `.positive()`: `FLEETLESS_UPLINK_KBPS=0` is a
|
|
2329
|
+
* documented setting meaning "no video budget at all", and the bridge
|
|
2330
|
+
* emits that 0 verbatim. `.positive()` made every frame from such a
|
|
2331
|
+
* robot fail the console's `safeParse`, which renders an unparseable
|
|
2332
|
+
* frame as "no pressure feed" — so the one robot that had *deliberately*
|
|
2333
|
+
* turned video off was the one diagnosed as running a bridge too old to
|
|
2334
|
+
* report pressure. A value the producer legitimately sends must parse;
|
|
2335
|
+
* `null` is the only "not set" this field has.
|
|
2336
|
+
*/
|
|
2337
|
+
uplink_kbps: import_zod8.z.number().int().nonnegative().nullable(),
|
|
2338
|
+
override_kbps: import_zod8.z.number().int().nonnegative().nullable(),
|
|
2339
|
+
video_budget_kbps: import_zod8.z.number().int().nonnegative().nullable(),
|
|
2340
|
+
reserve_kbps: import_zod8.z.number().int().nonnegative()
|
|
2341
|
+
})
|
|
1201
2342
|
});
|
|
1202
|
-
var snapshotHeader =
|
|
1203
|
-
type:
|
|
2343
|
+
var snapshotHeader = import_zod8.z.object({
|
|
2344
|
+
type: import_zod8.z.literal("snapshot"),
|
|
1204
2345
|
slug,
|
|
1205
2346
|
/** `image/jpeg` in practice; stated so nothing has to sniff the bytes. */
|
|
1206
|
-
mime:
|
|
1207
|
-
width:
|
|
1208
|
-
height:
|
|
1209
|
-
timestamp_ms:
|
|
2347
|
+
mime: import_zod8.z.string().min(1).max(64),
|
|
2348
|
+
width: import_zod8.z.number().int().positive(),
|
|
2349
|
+
height: import_zod8.z.number().int().positive(),
|
|
2350
|
+
timestamp_ms: import_zod8.z.number().int().nonnegative()
|
|
1210
2351
|
});
|
|
1211
|
-
var cloudCameraStart =
|
|
1212
|
-
type:
|
|
2352
|
+
var cloudCameraStart = import_zod8.z.object({
|
|
2353
|
+
type: import_zod8.z.literal("camera_start"),
|
|
1213
2354
|
slug,
|
|
1214
|
-
url:
|
|
1215
|
-
room:
|
|
1216
|
-
token:
|
|
2355
|
+
url: import_zod8.z.string().min(1),
|
|
2356
|
+
room: import_zod8.z.string().min(1),
|
|
2357
|
+
token: import_zod8.z.string().min(1),
|
|
1217
2358
|
/**
|
|
1218
2359
|
* Names **this attempt** (W6b), and is echoed in the `camera_state` that
|
|
1219
2360
|
* answers it.
|
|
@@ -1225,33 +2366,33 @@ var cloudCameraStart = import_zod7.z.object({
|
|
|
1225
2366
|
* knows nothing about. The viewer is then told the running stream failed,
|
|
1226
2367
|
* for a reason belonging to an attempt that is already over.
|
|
1227
2368
|
*/
|
|
1228
|
-
request_id:
|
|
2369
|
+
request_id: import_zod8.z.string().min(1).max(64)
|
|
1229
2370
|
});
|
|
1230
|
-
var bridgeAssetsAvailable =
|
|
1231
|
-
type:
|
|
2371
|
+
var bridgeAssetsAvailable = import_zod8.z.object({
|
|
2372
|
+
type: import_zod8.z.literal("assets_available"),
|
|
1232
2373
|
/** Whether `/robot_description` (or the configured source) yielded a URDF. */
|
|
1233
|
-
urdf:
|
|
2374
|
+
urdf: import_zod8.z.boolean(),
|
|
1234
2375
|
/**
|
|
1235
2376
|
* Every `package://` URI the URDF references, verbatim and unresolved —
|
|
1236
2377
|
* including the ones this bridge cannot find in its workspace. Reporting
|
|
1237
2378
|
* only the resolvable ones would make an incomplete workspace look like a
|
|
1238
2379
|
* complete robot, and the cloud would have nothing to show as missing.
|
|
1239
2380
|
*/
|
|
1240
|
-
meshes:
|
|
2381
|
+
meshes: import_zod8.z.array(import_zod8.z.string().min(1))
|
|
1241
2382
|
});
|
|
1242
|
-
var cloudAssetRequest =
|
|
1243
|
-
type:
|
|
1244
|
-
sync_id:
|
|
1245
|
-
upload_url:
|
|
1246
|
-
token:
|
|
2383
|
+
var cloudAssetRequest = import_zod8.z.object({
|
|
2384
|
+
type: import_zod8.z.literal("asset_request"),
|
|
2385
|
+
sync_id: import_zod8.z.uuid(),
|
|
2386
|
+
upload_url: import_zod8.z.url(),
|
|
2387
|
+
token: import_zod8.z.string().min(1),
|
|
1247
2388
|
/** Which URIs to send. Empty means the URDF only. */
|
|
1248
|
-
meshes:
|
|
2389
|
+
meshes: import_zod8.z.array(import_zod8.z.string().min(1))
|
|
1249
2390
|
});
|
|
1250
|
-
var bridgeAssetProgress =
|
|
1251
|
-
type:
|
|
1252
|
-
sync_id:
|
|
1253
|
-
done:
|
|
1254
|
-
total:
|
|
2391
|
+
var bridgeAssetProgress = import_zod8.z.object({
|
|
2392
|
+
type: import_zod8.z.literal("asset_progress"),
|
|
2393
|
+
sync_id: import_zod8.z.uuid(),
|
|
2394
|
+
done: import_zod8.z.number().int().nonnegative(),
|
|
2395
|
+
total: import_zod8.z.number().int().nonnegative(),
|
|
1255
2396
|
/**
|
|
1256
2397
|
* **Each entry says why** — see `assetFailure` in `assets.ts` for the three
|
|
1257
2398
|
* kinds and why one word was not enough. The bound is `assets.ts`'s too: a
|
|
@@ -1261,7 +2402,7 @@ var bridgeAssetProgress = import_zod7.z.object({
|
|
|
1261
2402
|
* a file in its workspace (Kassandra-W7a). A producer at its own ceiling
|
|
1262
2403
|
* reports **one** `refused` entry naming the file, not one per reference.
|
|
1263
2404
|
*/
|
|
1264
|
-
failed:
|
|
2405
|
+
failed: import_zod8.z.array(assetFailure).max(1e3),
|
|
1265
2406
|
/**
|
|
1266
2407
|
* Three values, because a boolean `finished` had nowhere to put a refusal.
|
|
1267
2408
|
*
|
|
@@ -1283,19 +2424,19 @@ var bridgeAssetProgress = import_zod7.z.object({
|
|
|
1283
2424
|
* Raised by Rosie-W7, who found the gap by asking what a second request
|
|
1284
2425
|
* should do rather than picking the silent option.
|
|
1285
2426
|
*/
|
|
1286
|
-
state:
|
|
2427
|
+
state: import_zod8.z.enum(["running", "finished", "refused_busy"])
|
|
1287
2428
|
});
|
|
1288
|
-
var cloudCameraStop =
|
|
1289
|
-
type:
|
|
2429
|
+
var cloudCameraStop = import_zod8.z.object({
|
|
2430
|
+
type: import_zod8.z.literal("camera_stop"),
|
|
1290
2431
|
slug,
|
|
1291
2432
|
/** Names this stop, echoed by the `camera_state` that answers it — see `cloudCameraStart.request_id`. */
|
|
1292
|
-
request_id:
|
|
2433
|
+
request_id: import_zod8.z.string().min(1).max(64)
|
|
1293
2434
|
});
|
|
1294
|
-
var bridgeCameraState =
|
|
1295
|
-
type:
|
|
2435
|
+
var bridgeCameraState = import_zod8.z.object({
|
|
2436
|
+
type: import_zod8.z.literal("camera_state"),
|
|
1296
2437
|
slug,
|
|
1297
|
-
publishing:
|
|
1298
|
-
error:
|
|
2438
|
+
publishing: import_zod8.z.boolean(),
|
|
2439
|
+
error: import_zod8.z.object({ code: import_zod8.z.string().min(1), message: import_zod8.z.string().min(1) }).nullable(),
|
|
1299
2440
|
/**
|
|
1300
2441
|
* Why this frame was sent (W6a).
|
|
1301
2442
|
*
|
|
@@ -1325,7 +2466,7 @@ var bridgeCameraState = import_zod7.z.object({
|
|
|
1325
2466
|
* reason. Old bridges fail validation on this frame — acceptable while
|
|
1326
2467
|
* nothing is deployed, and W8 is the first deployment.
|
|
1327
2468
|
*/
|
|
1328
|
-
cause:
|
|
2469
|
+
cause: import_zod8.z.enum(["command", "source", "config_change", "live_lost"]),
|
|
1329
2470
|
/**
|
|
1330
2471
|
* When the **robot** observed this state — bridge capture time, never
|
|
1331
2472
|
* receive time, the same discipline `timestamp_ms` follows for samples
|
|
@@ -1343,7 +2484,7 @@ var bridgeCameraState = import_zod7.z.object({
|
|
|
1343
2484
|
* when the frame was sent. A bridge that re-states a failure it has held for
|
|
1344
2485
|
* an hour says so.
|
|
1345
2486
|
*/
|
|
1346
|
-
observed_at_ms:
|
|
2487
|
+
observed_at_ms: import_zod8.z.number().int().nonnegative(),
|
|
1347
2488
|
/**
|
|
1348
2489
|
* Which request this frame answers (W6b), or `null` when it answers none.
|
|
1349
2490
|
*
|
|
@@ -1369,96 +2510,122 @@ var bridgeCameraState = import_zod7.z.object({
|
|
|
1369
2510
|
* where the correlation is used, in the cloud's bridge frame handler, and
|
|
1370
2511
|
* stated here so nobody has to derive it from that code.
|
|
1371
2512
|
*/
|
|
1372
|
-
request_id:
|
|
2513
|
+
request_id: import_zod8.z.string().min(1).max(64).nullable()
|
|
1373
2514
|
});
|
|
1374
2515
|
|
|
1375
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
1376
|
-
var
|
|
1377
|
-
var
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
2516
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/config-issues.js
|
|
2517
|
+
var EXPOSURE_SECTIONS = ["datapoints", "actions", "services", "publishers", "cameras"];
|
|
2518
|
+
var EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
|
|
2519
|
+
|
|
2520
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/rest.js
|
|
2521
|
+
var import_zod9 = require("zod");
|
|
2522
|
+
var robot = import_zod9.z.object({
|
|
2523
|
+
id: import_zod9.z.uuid(),
|
|
2524
|
+
name: import_zod9.z.string().min(1).max(63),
|
|
2525
|
+
created_at: import_zod9.z.iso.datetime()
|
|
1381
2526
|
});
|
|
1382
|
-
var createRobotRequest =
|
|
1383
|
-
name:
|
|
2527
|
+
var createRobotRequest = import_zod9.z.object({
|
|
2528
|
+
name: import_zod9.z.string().min(1).max(63)
|
|
1384
2529
|
});
|
|
1385
|
-
var robotToken =
|
|
1386
|
-
var createRobotResponse =
|
|
2530
|
+
var robotToken = import_zod9.z.string().regex(/^frt_[0-9a-f]{32}$/);
|
|
2531
|
+
var createRobotResponse = import_zod9.z.object({
|
|
1387
2532
|
robot,
|
|
1388
2533
|
token: robotToken
|
|
1389
2534
|
});
|
|
1390
|
-
var
|
|
2535
|
+
var exposureCounts = import_zod9.z.object({
|
|
2536
|
+
datapoints: import_zod9.z.number().int().nonnegative(),
|
|
2537
|
+
actions: import_zod9.z.number().int().nonnegative(),
|
|
2538
|
+
services: import_zod9.z.number().int().nonnegative(),
|
|
2539
|
+
publishers: import_zod9.z.number().int().nonnegative(),
|
|
2540
|
+
cameras: import_zod9.z.number().int().nonnegative()
|
|
2541
|
+
});
|
|
2542
|
+
var robotListItem = import_zod9.z.object({
|
|
1391
2543
|
...robot.shape,
|
|
1392
|
-
bridge_state: bridgeState
|
|
2544
|
+
bridge_state: bridgeState,
|
|
2545
|
+
/** Required, not optional: "we did not look" and "it exposes nothing" must not render the same. */
|
|
2546
|
+
exposes: exposureCounts
|
|
1393
2547
|
});
|
|
1394
|
-
var robotListResponse =
|
|
1395
|
-
robots:
|
|
2548
|
+
var robotListResponse = import_zod9.z.object({
|
|
2549
|
+
robots: import_zod9.z.array(robotListItem)
|
|
1396
2550
|
});
|
|
1397
|
-
var datapointValue =
|
|
2551
|
+
var datapointValue = import_zod9.z.object({
|
|
1398
2552
|
slug,
|
|
1399
|
-
value:
|
|
1400
|
-
timestamp_ms:
|
|
2553
|
+
value: import_zod9.z.unknown(),
|
|
2554
|
+
timestamp_ms: import_zod9.z.number().int().nonnegative()
|
|
1401
2555
|
});
|
|
1402
|
-
var robotDetailResponse =
|
|
2556
|
+
var robotDetailResponse = import_zod9.z.object({
|
|
1403
2557
|
...robotListItem.shape,
|
|
1404
|
-
bridge_version:
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
2558
|
+
bridge_version: import_zod9.z.string().min(1).nullable(),
|
|
2559
|
+
/**
|
|
2560
|
+
* Cleared (set back to null) by the next successful hello from this
|
|
2561
|
+
* robot's bridge — a warning that outlives the condition it warns
|
|
2562
|
+
* about would be read as current state, and was.
|
|
2563
|
+
*/
|
|
2564
|
+
last_hello_error: import_zod9.z.object({
|
|
2565
|
+
code: import_zod9.z.string().min(1),
|
|
2566
|
+
message: import_zod9.z.string().min(1),
|
|
2567
|
+
at: import_zod9.z.iso.datetime()
|
|
1409
2568
|
}).nullable(),
|
|
1410
2569
|
config: configState
|
|
1411
2570
|
});
|
|
1412
|
-
var configDraftResponse =
|
|
1413
|
-
doc: robotConfigDoc,
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
var
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2571
|
+
var configDraftResponse = import_zod9.z.object({
|
|
2572
|
+
doc: robotConfigDoc.nullable(),
|
|
2573
|
+
source: import_zod9.z.string(),
|
|
2574
|
+
updated_at: import_zod9.z.iso.datetime().nullable(),
|
|
2575
|
+
issues: import_zod9.z.array(validationIssue)
|
|
2576
|
+
});
|
|
2577
|
+
var putConfigDraftRequest = import_zod9.z.object({ source: import_zod9.z.string().max(1e6) });
|
|
2578
|
+
var publishConfigResponse = import_zod9.z.object({
|
|
2579
|
+
version: import_zod9.z.number().int().positive(),
|
|
2580
|
+
published_at: import_zod9.z.iso.datetime()
|
|
2581
|
+
});
|
|
2582
|
+
var configVersionsResponse = import_zod9.z.object({
|
|
2583
|
+
versions: import_zod9.z.array(import_zod9.z.object({
|
|
2584
|
+
version: import_zod9.z.number().int().positive(),
|
|
2585
|
+
published_at: import_zod9.z.iso.datetime()
|
|
1426
2586
|
}))
|
|
1427
2587
|
});
|
|
1428
|
-
var configVersionResponse =
|
|
1429
|
-
version:
|
|
1430
|
-
published_at:
|
|
1431
|
-
doc: robotConfigDoc
|
|
2588
|
+
var configVersionResponse = import_zod9.z.object({
|
|
2589
|
+
version: import_zod9.z.number().int().positive(),
|
|
2590
|
+
published_at: import_zod9.z.iso.datetime(),
|
|
2591
|
+
doc: robotConfigDoc,
|
|
2592
|
+
source: import_zod9.z.string()
|
|
1432
2593
|
});
|
|
1433
|
-
var introspectionResponse =
|
|
2594
|
+
var introspectionResponse = import_zod9.z.object({
|
|
1434
2595
|
graph: rosGraph,
|
|
1435
|
-
fetched_at:
|
|
1436
|
-
stale:
|
|
2596
|
+
fetched_at: import_zod9.z.iso.datetime(),
|
|
2597
|
+
stale: import_zod9.z.boolean()
|
|
1437
2598
|
});
|
|
1438
|
-
var typesResponse =
|
|
1439
|
-
types:
|
|
2599
|
+
var typesResponse = import_zod9.z.object({
|
|
2600
|
+
types: import_zod9.z.array(typeDefinition)
|
|
1440
2601
|
});
|
|
1441
|
-
var fetchTypesRequest =
|
|
1442
|
-
type_names:
|
|
2602
|
+
var fetchTypesRequest = import_zod9.z.object({
|
|
2603
|
+
type_names: import_zod9.z.array(rosTypeName).min(1).max(50)
|
|
1443
2604
|
});
|
|
1444
|
-
var fetchTypesResponse =
|
|
1445
|
-
types:
|
|
1446
|
-
unresolved:
|
|
2605
|
+
var fetchTypesResponse = import_zod9.z.object({
|
|
2606
|
+
types: import_zod9.z.array(typeDefinition),
|
|
2607
|
+
unresolved: import_zod9.z.array(import_zod9.z.string())
|
|
1447
2608
|
});
|
|
1448
|
-
var datapointDescriptor =
|
|
2609
|
+
var datapointDescriptor = import_zod9.z.object({
|
|
1449
2610
|
slug,
|
|
1450
|
-
builtin:
|
|
1451
|
-
unit:
|
|
1452
|
-
|
|
1453
|
-
|
|
2611
|
+
builtin: import_zod9.z.boolean(),
|
|
2612
|
+
unit: import_zod9.z.string().nullable(),
|
|
2613
|
+
/**
|
|
2614
|
+
* `null` for a built-in and for a datapoint published with no throttle —
|
|
2615
|
+
* the same "no ceiling configured" fact `datapointConfig.rate_throttle_hz`
|
|
2616
|
+
* itself carries as `0` or absence, just re-spelled nullable rather than
|
|
2617
|
+
* optional because this shape is a read response, not a document a caller
|
|
2618
|
+
* writes. Reuses `rateThrottleHz` so the 20 Hz ceiling is written once.
|
|
2619
|
+
*/
|
|
2620
|
+
rate_throttle_hz: rateThrottleHz.nullable()
|
|
1454
2621
|
});
|
|
1455
|
-
var datapointListResponse =
|
|
1456
|
-
datapoints:
|
|
2622
|
+
var datapointListResponse = import_zod9.z.object({
|
|
2623
|
+
datapoints: import_zod9.z.array(datapointDescriptor)
|
|
1457
2624
|
});
|
|
1458
|
-
var robotDetailsDoc =
|
|
1459
|
-
var putRobotDetailsRequest =
|
|
1460
|
-
var invokeRequest =
|
|
1461
|
-
params:
|
|
2625
|
+
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())]));
|
|
2626
|
+
var putRobotDetailsRequest = import_zod9.z.object({ details: robotDetailsDoc });
|
|
2627
|
+
var invokeRequest = import_zod9.z.object({
|
|
2628
|
+
params: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.unknown()),
|
|
1462
2629
|
/**
|
|
1463
2630
|
* How long **this call** is worth waiting for, in milliseconds (W6b).
|
|
1464
2631
|
*
|
|
@@ -1485,39 +2652,39 @@ var invokeRequest = import_zod8.z.object({
|
|
|
1485
2652
|
* *acceptance* — once a goal is accepted the job runs as long as it runs,
|
|
1486
2653
|
* and is observed, not awaited.
|
|
1487
2654
|
*/
|
|
1488
|
-
patience_ms:
|
|
2655
|
+
patience_ms: import_zod9.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional()
|
|
1489
2656
|
});
|
|
1490
|
-
var cancelRequest =
|
|
1491
|
-
job_id:
|
|
2657
|
+
var cancelRequest = import_zod9.z.object({
|
|
2658
|
+
job_id: import_zod9.z.uuid().nullable().optional()
|
|
1492
2659
|
}).strict();
|
|
1493
|
-
var releaseLiveQuery =
|
|
1494
|
-
session_id:
|
|
2660
|
+
var releaseLiveQuery = import_zod9.z.object({
|
|
2661
|
+
session_id: import_zod9.z.uuid().optional()
|
|
1495
2662
|
}).strict();
|
|
1496
|
-
var invokeResponse =
|
|
2663
|
+
var invokeResponse = import_zod9.z.object({
|
|
1497
2664
|
job,
|
|
1498
2665
|
/** The slug's kind — see `commandResult.kind` for why the caller needs it. */
|
|
1499
|
-
kind:
|
|
2666
|
+
kind: import_zod9.z.enum(["action", "service"])
|
|
1500
2667
|
});
|
|
1501
|
-
var serviceCallResponse =
|
|
1502
|
-
result:
|
|
2668
|
+
var serviceCallResponse = import_zod9.z.object({
|
|
2669
|
+
result: import_zod9.z.unknown()
|
|
1503
2670
|
});
|
|
1504
|
-
var publishRequest =
|
|
1505
|
-
message:
|
|
2671
|
+
var publishRequest = import_zod9.z.object({
|
|
2672
|
+
message: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.unknown())
|
|
1506
2673
|
});
|
|
1507
|
-
var jobResponse =
|
|
1508
|
-
var rateLimitDetails =
|
|
1509
|
-
retry_after_ms:
|
|
2674
|
+
var jobResponse = import_zod9.z.object({ job: job.nullable() });
|
|
2675
|
+
var rateLimitDetails = import_zod9.z.object({
|
|
2676
|
+
retry_after_ms: import_zod9.z.number().int().nonnegative()
|
|
1510
2677
|
});
|
|
1511
|
-
var robotJobsResponse =
|
|
1512
|
-
jobs:
|
|
2678
|
+
var robotJobsResponse = import_zod9.z.object({
|
|
2679
|
+
jobs: import_zod9.z.array(job)
|
|
1513
2680
|
});
|
|
1514
|
-
var exposure =
|
|
2681
|
+
var exposure = import_zod9.z.object({
|
|
1515
2682
|
slug,
|
|
1516
|
-
kind:
|
|
1517
|
-
builtin:
|
|
2683
|
+
kind: import_zod9.z.enum(["datapoint", "action", "service", "publisher", "camera"]),
|
|
2684
|
+
builtin: import_zod9.z.boolean()
|
|
1518
2685
|
});
|
|
1519
|
-
var exposureListResponse =
|
|
1520
|
-
exposures:
|
|
2686
|
+
var exposureListResponse = import_zod9.z.object({
|
|
2687
|
+
exposures: import_zod9.z.array(exposure)
|
|
1521
2688
|
});
|
|
1522
2689
|
var SNAPSHOT_HEADERS = {
|
|
1523
2690
|
ageMs: "x-fleetless-age-ms",
|
|
@@ -1525,15 +2692,22 @@ var SNAPSHOT_HEADERS = {
|
|
|
1525
2692
|
width: "x-fleetless-width",
|
|
1526
2693
|
height: "x-fleetless-height"
|
|
1527
2694
|
};
|
|
1528
|
-
var cameraDescriptor =
|
|
2695
|
+
var cameraDescriptor = import_zod9.z.object({
|
|
1529
2696
|
slug,
|
|
1530
|
-
width:
|
|
1531
|
-
height:
|
|
1532
|
-
fps:
|
|
1533
|
-
|
|
2697
|
+
width: import_zod9.z.number().int().positive(),
|
|
2698
|
+
height: import_zod9.z.number().int().positive(),
|
|
2699
|
+
fps: import_zod9.z.number().int().positive(),
|
|
2700
|
+
/**
|
|
2701
|
+
* Seconds, as the document spells it, reusing `snapshotIntervalSeconds` so
|
|
2702
|
+
* the 1–3600 bound is written once. It was `snapshot_interval_ms` after the
|
|
2703
|
+
* document moved to seconds, which left the cloud converting the unit on
|
|
2704
|
+
* this descriptor and not on `datapointDescriptor` beside it — the same
|
|
2705
|
+
* drift `rateThrottleHz` was extracted to stop.
|
|
2706
|
+
*/
|
|
2707
|
+
snapshot_interval_seconds: snapshotIntervalSeconds
|
|
1534
2708
|
});
|
|
1535
|
-
var cameraListResponse =
|
|
1536
|
-
var liveSessionResponse =
|
|
2709
|
+
var cameraListResponse = import_zod9.z.object({ cameras: import_zod9.z.array(cameraDescriptor) });
|
|
2710
|
+
var liveSessionResponse = import_zod9.z.object({
|
|
1537
2711
|
/**
|
|
1538
2712
|
* This viewer's hold, and the **only** thing `DELETE` should be given
|
|
1539
2713
|
* (W6b).
|
|
@@ -1551,43 +2725,76 @@ var liveSessionResponse = import_zod8.z.object({
|
|
|
1551
2725
|
* is going away entirely, still needs a way to let go. It is the blunt
|
|
1552
2726
|
* form, and it is the one that strands other tabs; new callers pass the id.
|
|
1553
2727
|
*/
|
|
1554
|
-
session_id:
|
|
1555
|
-
url:
|
|
1556
|
-
room:
|
|
1557
|
-
token:
|
|
1558
|
-
expires_at:
|
|
2728
|
+
session_id: import_zod9.z.uuid(),
|
|
2729
|
+
url: import_zod9.z.string().min(1),
|
|
2730
|
+
room: import_zod9.z.string().min(1),
|
|
2731
|
+
token: import_zod9.z.string().min(1),
|
|
2732
|
+
expires_at: import_zod9.z.iso.datetime()
|
|
1559
2733
|
});
|
|
1560
|
-
var snapshotMetaResponse =
|
|
2734
|
+
var snapshotMetaResponse = import_zod9.z.object({
|
|
1561
2735
|
slug,
|
|
1562
|
-
timestamp_ms:
|
|
1563
|
-
age_ms:
|
|
1564
|
-
width:
|
|
1565
|
-
height:
|
|
1566
|
-
mime:
|
|
1567
|
-
});
|
|
1568
|
-
var historyQuery =
|
|
1569
|
-
from:
|
|
2736
|
+
timestamp_ms: import_zod9.z.number().int().nonnegative().nullable(),
|
|
2737
|
+
age_ms: import_zod9.z.number().int().nonnegative().nullable(),
|
|
2738
|
+
width: import_zod9.z.number().int().positive().nullable(),
|
|
2739
|
+
height: import_zod9.z.number().int().positive().nullable(),
|
|
2740
|
+
mime: import_zod9.z.string().nullable()
|
|
2741
|
+
});
|
|
2742
|
+
var historyQuery = import_zod9.z.object({
|
|
2743
|
+
from: import_zod9.z.string().min(1).max(32),
|
|
1570
2744
|
/** Defaults to now. */
|
|
1571
|
-
to:
|
|
2745
|
+
to: import_zod9.z.string().min(1).max(32).optional(),
|
|
1572
2746
|
/** Bucket width, e.g. `10s`, `1m`. Absent means raw samples. */
|
|
1573
|
-
window:
|
|
1574
|
-
agg:
|
|
2747
|
+
window: import_zod9.z.string().min(2).max(16).optional(),
|
|
2748
|
+
agg: import_zod9.z.enum(["min", "max", "avg"]).optional(),
|
|
1575
2749
|
/** A numeric field inside an object value, e.g. `pose.x` (§4.4 paths). */
|
|
1576
|
-
field:
|
|
2750
|
+
field: import_zod9.z.string().min(1).max(128).optional(),
|
|
1577
2751
|
/**
|
|
1578
|
-
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
2752
|
+
* **A union whose input branch IS the wire, not a coercion (W9d, DEF-059).**
|
|
2753
|
+
*
|
|
2754
|
+
* This was `z.coerce.number()`, for a good reason that stayed true: the
|
|
2755
|
+
* schema describes a **query string**, where every value arrives as text,
|
|
2756
|
+
* and a bare `z.number()` would make each route coerce by hand. What was
|
|
2757
|
+
* measured afterwards is that a coercion cannot be *published*: zod renders
|
|
2758
|
+
* a coercion's **result** in either `io` mode, so `io: 'input'` and
|
|
2759
|
+
* `io: 'output'` both emit `{"type":"integer"}` — an artifact describing a
|
|
2760
|
+
* shape a query string can never carry. Anyone validating a real request
|
|
2761
|
+
* against it rejects every one that sets `limit`.
|
|
2762
|
+
*
|
|
2763
|
+
* That is a **different** defect from the `.default()` class, which
|
|
2764
|
+
* `io: 'input'` genuinely does fix; `export-schemas.ts` once claimed one
|
|
2765
|
+
* remedy for both and has been corrected.
|
|
2766
|
+
*
|
|
2767
|
+
* A union states both truths honestly: the wire carries a numeric string,
|
|
2768
|
+
* a programmatic caller may pass a number, and the artifact can render the
|
|
2769
|
+
* input branch because there is one to render.
|
|
2770
|
+
*
|
|
2771
|
+
* **What the artifact no longer says, named here rather than left silent.**
|
|
2772
|
+
* The `1..10000` bound lives in the `.pipe()`, which is the *output* half, so
|
|
2773
|
+
* no input-mode artifact can express it as a constraint: the published shape
|
|
2774
|
+
* is `^\d{1,5}$` or a bare integer, and five digits is a weak echo of the
|
|
2775
|
+
* real ceiling. That is honest about the wire — the bound is enforced after
|
|
2776
|
+
* parsing, not by the shape of the text — but it is a **reduction**, and an
|
|
2777
|
+
* artifact that stops naming a bound reads as if there were none.
|
|
2778
|
+
*
|
|
2779
|
+
* So both branches carry the number in a `.describe()` (Nimbus-W9d's
|
|
2780
|
+
* proposal). It is **not** a constraint and nothing validates against it; it
|
|
2781
|
+
* means a generator, or a person reading only the published schema, sees the
|
|
2782
|
+
* actual ceiling instead of nothing. The gap is narrowed and named rather
|
|
2783
|
+
* than closed.
|
|
1583
2784
|
*/
|
|
1584
|
-
limit:
|
|
1585
|
-
});
|
|
1586
|
-
|
|
2785
|
+
limit: import_zod9.z.union([
|
|
2786
|
+
import_zod9.z.string().regex(/^\d{1,5}$/).describe("Positive integer, 1-10000. The pattern only bounds digit count; the real ceiling is enforced after parsing."),
|
|
2787
|
+
// The same sentence on the numeric branch, for the same reason and one
|
|
2788
|
+
// that is arguably stronger: without it the artifact publishes the full
|
|
2789
|
+
// safe-integer range, which reads as *nine quadrillion is fine*.
|
|
2790
|
+
import_zod9.z.number().int().describe("Positive integer, 1-10000. The ceiling is enforced after parsing, not by this type.")
|
|
2791
|
+
]).transform((v) => Number(v)).pipe(import_zod9.z.number().int().positive().max(1e4)).optional()
|
|
2792
|
+
});
|
|
2793
|
+
var historySamplesResponse = import_zod9.z.object({
|
|
1587
2794
|
slug,
|
|
1588
|
-
kind:
|
|
1589
|
-
samples:
|
|
1590
|
-
truncated:
|
|
2795
|
+
kind: import_zod9.z.literal("samples"),
|
|
2796
|
+
samples: import_zod9.z.array(import_zod9.z.object({ timestamp_ms: import_zod9.z.number().int().nonnegative(), value: import_zod9.z.unknown() })),
|
|
2797
|
+
truncated: import_zod9.z.boolean(),
|
|
1591
2798
|
/**
|
|
1592
2799
|
* Why it was cut, `null` when it was not — because the two causes have
|
|
1593
2800
|
* **different remedies** and a single boolean cannot tell them apart:
|
|
@@ -1606,15 +2813,15 @@ var historySamplesResponse = import_zod8.z.object({
|
|
|
1606
2813
|
* `required` in the JSON Schema artifacts, which is the contradiction this
|
|
1607
2814
|
* project has now hit five times.
|
|
1608
2815
|
*/
|
|
1609
|
-
truncated_by:
|
|
2816
|
+
truncated_by: import_zod9.z.enum(["limit", "bytes"]).nullable()
|
|
1610
2817
|
});
|
|
1611
|
-
var historyBucketsResponse =
|
|
2818
|
+
var historyBucketsResponse = import_zod9.z.object({
|
|
1612
2819
|
slug,
|
|
1613
|
-
kind:
|
|
1614
|
-
window_ms:
|
|
1615
|
-
agg:
|
|
1616
|
-
buckets:
|
|
1617
|
-
bucket_start_ms:
|
|
2820
|
+
kind: import_zod9.z.literal("buckets"),
|
|
2821
|
+
window_ms: import_zod9.z.number().int().positive(),
|
|
2822
|
+
agg: import_zod9.z.enum(["min", "max", "avg"]),
|
|
2823
|
+
buckets: import_zod9.z.array(import_zod9.z.object({
|
|
2824
|
+
bucket_start_ms: import_zod9.z.number().int().nonnegative(),
|
|
1618
2825
|
/**
|
|
1619
2826
|
* The aggregate over this bucket's **numeric** samples — or `null` when
|
|
1620
2827
|
* none of them were numeric, which is **not** the same as the bucket
|
|
@@ -1632,7 +2839,7 @@ var historyBucketsResponse = import_zod8.z.object({
|
|
|
1632
2839
|
* so the second row above was indistinguishable from the first and the
|
|
1633
2840
|
* console rendered "empty — no samples" over live data.
|
|
1634
2841
|
*/
|
|
1635
|
-
value:
|
|
2842
|
+
value: import_zod9.z.number().nullable(),
|
|
1636
2843
|
/**
|
|
1637
2844
|
* Every sample that landed in this bucket and inside the queried range,
|
|
1638
2845
|
* whether or not it contributed to `value` — which is the point of the
|
|
@@ -1649,10 +2856,10 @@ var historyBucketsResponse = import_zod8.z.object({
|
|
|
1649
2856
|
* and only in-range samples are counted. A low edge count is a
|
|
1650
2857
|
* boundary effect, not a quiet period.
|
|
1651
2858
|
*/
|
|
1652
|
-
sample_count:
|
|
2859
|
+
sample_count: import_zod9.z.number().int().nonnegative()
|
|
1653
2860
|
}))
|
|
1654
2861
|
});
|
|
1655
|
-
var robotDeletionSummary =
|
|
2862
|
+
var robotDeletionSummary = import_zod9.z.object({
|
|
1656
2863
|
/**
|
|
1657
2864
|
* Datapoints, actions, services and publishers in the **published**
|
|
1658
2865
|
* configuration — what the robot was actually running. **Cameras are not
|
|
@@ -1669,10 +2876,10 @@ var robotDeletionSummary = import_zod8.z.object({
|
|
|
1669
2876
|
* rather than by either of these: describing three things with two numbers
|
|
1670
2877
|
* would make each of them mean something else.
|
|
1671
2878
|
*/
|
|
1672
|
-
slug_count:
|
|
1673
|
-
sample_rows:
|
|
1674
|
-
bytes_freed:
|
|
1675
|
-
cameras:
|
|
2879
|
+
slug_count: import_zod9.z.number().int().nonnegative(),
|
|
2880
|
+
sample_rows: import_zod9.z.number().int().nonnegative(),
|
|
2881
|
+
bytes_freed: import_zod9.z.number().int().nonnegative(),
|
|
2882
|
+
cameras: import_zod9.z.array(slug),
|
|
1676
2883
|
/**
|
|
1677
2884
|
* Assets destroyed with the robot (W7), and **`asset_bytes_freed` is what
|
|
1678
2885
|
* this org actually gets back** — not the sum of the assets' sizes.
|
|
@@ -1688,9 +2895,40 @@ var robotDeletionSummary = import_zod8.z.object({
|
|
|
1688
2895
|
* `asset_count` is the plain count of the robot's asset rows, all of which
|
|
1689
2896
|
* do go away.
|
|
1690
2897
|
*/
|
|
1691
|
-
asset_count:
|
|
1692
|
-
asset_bytes_freed:
|
|
1693
|
-
|
|
2898
|
+
asset_count: import_zod9.z.number().int().nonnegative(),
|
|
2899
|
+
asset_bytes_freed: import_zod9.z.number().int().nonnegative(),
|
|
2900
|
+
/**
|
|
2901
|
+
* How many rows of run history go with the robot — every recorded
|
|
2902
|
+
* invocation of one of its actions or services, up to
|
|
2903
|
+
* `JOB_RUN_RETENTION_DAYS`.
|
|
2904
|
+
*
|
|
2905
|
+
* Its own number, never folded into `slug_count`, for the same reason
|
|
2906
|
+
* `cameras` is not: `slug_count` counts *configuration* — what the robot
|
|
2907
|
+
* was set up to do — and this counts *what was actually done*, over as
|
|
2908
|
+
* much as 90 days. One robot with four slugs can carry forty thousand
|
|
2909
|
+
* runs, and a sentence that added them would describe two unrelated
|
|
2910
|
+
* magnitudes with one number on the one screen whose entire justification
|
|
2911
|
+
* is naming what an irreversible click destroys.
|
|
2912
|
+
*
|
|
2913
|
+
* It is also the only field here that names *people*: a run row carries
|
|
2914
|
+
* the `jobActor` who invoked it — a developer's or end user's email,
|
|
2915
|
+
* snapshotted at invoke time. So this deletion destroys attributed history
|
|
2916
|
+
* of who asked the machine to do what, which is a different kind of loss
|
|
2917
|
+
* from a count of sample rows and deserves to be said out loud rather than
|
|
2918
|
+
* inferred.
|
|
2919
|
+
*
|
|
2920
|
+
* **Bridge latency buckets are deliberately not counted here, and this is
|
|
2921
|
+
* the note saying so** rather than leaving the asymmetry to be
|
|
2922
|
+
* rediscovered as an omission. They are platform telemetry with a seven-day
|
|
2923
|
+
* life (`BRIDGE_LATENCY_RETENTION_DAYS`), produced by the cloud's own
|
|
2924
|
+
* pinging rather than by anything the developer did, counted against no
|
|
2925
|
+
* retention quota, and worth nothing to anybody after the robot is gone.
|
|
2926
|
+
* This summary is read aloud to a human deciding whether to click, and its
|
|
2927
|
+
* value comes from naming what the *developer* loses; a number for
|
|
2928
|
+
* telemetry they never asked for and cannot use would dilute exactly that.
|
|
2929
|
+
*/
|
|
2930
|
+
job_run_count: import_zod9.z.number().int().nonnegative(),
|
|
2931
|
+
had_live_session: import_zod9.z.boolean(),
|
|
1694
2932
|
/**
|
|
1695
2933
|
* Whether an unpublished draft went with it — separately, because the
|
|
1696
2934
|
* counts above deliberately do not include it and a record that silently
|
|
@@ -1700,7 +2938,7 @@ var robotDeletionSummary = import_zod8.z.object({
|
|
|
1700
2938
|
* there the counts are zero and this is the only field saying anything
|
|
1701
2939
|
* was there at all.
|
|
1702
2940
|
*/
|
|
1703
|
-
had_unpublished_draft:
|
|
2941
|
+
had_unpublished_draft: import_zod9.z.boolean()
|
|
1704
2942
|
});
|
|
1705
2943
|
var RESOURCE_HEALTH_STATES = [
|
|
1706
2944
|
"ok",
|
|
@@ -1721,9 +2959,12 @@ var RESOURCE_HEALTH_STATES = [
|
|
|
1721
2959
|
* `unknown`, which means "the robot reported a failure we cannot classify"
|
|
1722
2960
|
* — a different fact with a different fix.
|
|
1723
2961
|
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
2962
|
+
* **Retiring with the credential store**, and not live behaviour to build
|
|
2963
|
+
* against. Its one producer was `cloud-config-frame.ts` tolerating an
|
|
2964
|
+
* unresolved `credentials_ref` at publish time; FL-002 deleted that field,
|
|
2965
|
+
* so nothing emits this today. It is kept only until the wave that removes
|
|
2966
|
+
* the store also removes these three credential states — `unreadable_credential`
|
|
2967
|
+
* and the `readable` fact on `credentialSummary` go the same way.
|
|
1727
2968
|
*/
|
|
1728
2969
|
"credential_missing",
|
|
1729
2970
|
/** A configuration change stopped this stream, deliberately. */
|
|
@@ -1742,31 +2983,50 @@ var RESOURCE_HEALTH_STATES = [
|
|
|
1742
2983
|
*/
|
|
1743
2984
|
"unknown"
|
|
1744
2985
|
];
|
|
1745
|
-
var resourceHealthState =
|
|
1746
|
-
robot_id:
|
|
1747
|
-
kind:
|
|
2986
|
+
var resourceHealthState = import_zod9.z.object({
|
|
2987
|
+
robot_id: import_zod9.z.uuid(),
|
|
2988
|
+
kind: import_zod9.z.enum(["camera"]),
|
|
1748
2989
|
/** The camera slug, or the credential name. */
|
|
1749
|
-
ref:
|
|
1750
|
-
|
|
2990
|
+
ref: import_zod9.z.string().min(1).max(64),
|
|
2991
|
+
/**
|
|
2992
|
+
* **Which of two questions this entry answers (W9a, DEF-072).**
|
|
2993
|
+
*
|
|
2994
|
+
* `'source'` — can the source be read at all? (`unreachable`, `auth_failed`,
|
|
2995
|
+
* `unreadable_credential`, `missing_credential`, `ok`, …)
|
|
2996
|
+
* `'publish'` — given a readable source, did publishing to LiveKit work?
|
|
2997
|
+
*
|
|
2998
|
+
* Before this, both went into one entry keyed `${robot} ${kind} ${ref}` with
|
|
2999
|
+
* one flat `state`, in which `publish_failed` answered *"can we publish"*
|
|
3000
|
+
* and every other value answered *"can the source be read"* — **same key,
|
|
3001
|
+
* same field, two questions**, so each overwrote the other. The conflation
|
|
3002
|
+
* was once an occasional race; W6a's reconnect restatement made it
|
|
3003
|
+
* guaranteed, on every reconnect, for any camera with an active viewer.
|
|
3004
|
+
*
|
|
3005
|
+
* The facet is part of the entry's identity: a camera can perfectly well be
|
|
3006
|
+
* readable and unpublishable at the same moment, and that pair is exactly
|
|
3007
|
+
* what a developer needs to see rather than whichever fact arrived last.
|
|
3008
|
+
*/
|
|
3009
|
+
facet: import_zod9.z.enum(["source", "publish"]),
|
|
3010
|
+
state: import_zod9.z.enum(RESOURCE_HEALTH_STATES),
|
|
1751
3011
|
/** A short human-readable reason, or `null`. Never an exception message. */
|
|
1752
|
-
reason:
|
|
3012
|
+
reason: import_zod9.z.string().max(200).nullable(),
|
|
1753
3013
|
/**
|
|
1754
3014
|
* When this state was entered — not when it was sent. A page that loads
|
|
1755
3015
|
* late must be able to tell a failure from a minute ago from one from
|
|
1756
3016
|
* yesterday, and a state with only a send time cannot.
|
|
1757
3017
|
*/
|
|
1758
|
-
changed_at_ms:
|
|
1759
|
-
});
|
|
1760
|
-
var resourceHealthListResponse =
|
|
1761
|
-
resources:
|
|
1762
|
-
});
|
|
1763
|
-
var orgQuotas =
|
|
1764
|
-
max_robots:
|
|
1765
|
-
max_apps:
|
|
1766
|
-
max_end_users:
|
|
1767
|
-
max_retention_bytes:
|
|
1768
|
-
max_retention_writes_per_minute:
|
|
1769
|
-
max_realtime_connections:
|
|
3018
|
+
changed_at_ms: import_zod9.z.number().int().nonnegative()
|
|
3019
|
+
});
|
|
3020
|
+
var resourceHealthListResponse = import_zod9.z.object({
|
|
3021
|
+
resources: import_zod9.z.array(resourceHealthState)
|
|
3022
|
+
});
|
|
3023
|
+
var orgQuotas = import_zod9.z.object({
|
|
3024
|
+
max_robots: import_zod9.z.number().int().positive(),
|
|
3025
|
+
max_apps: import_zod9.z.number().int().positive(),
|
|
3026
|
+
max_end_users: import_zod9.z.number().int().positive(),
|
|
3027
|
+
max_retention_bytes: import_zod9.z.number().int().nonnegative(),
|
|
3028
|
+
max_retention_writes_per_minute: import_zod9.z.number().int().nonnegative(),
|
|
3029
|
+
max_realtime_connections: import_zod9.z.number().int().positive(),
|
|
1770
3030
|
/**
|
|
1771
3031
|
* Asset storage (§4.6, W7) — **its own dial, not part of
|
|
1772
3032
|
* `max_retention_bytes`.** A sync grows storage in jumps and time series
|
|
@@ -1793,78 +3053,138 @@ var orgQuotas = import_zod8.z.object({
|
|
|
1793
3053
|
* which nobody can predict. Measured before the change: 342 bytes held by an
|
|
1794
3054
|
* org owning no assets, with no operation able to free them.
|
|
1795
3055
|
*/
|
|
1796
|
-
max_asset_storage_bytes:
|
|
1797
|
-
});
|
|
1798
|
-
var orgQuotaUsageCounts =
|
|
1799
|
-
max_robots:
|
|
1800
|
-
max_apps:
|
|
1801
|
-
max_end_users:
|
|
1802
|
-
max_retention_bytes:
|
|
1803
|
-
max_asset_storage_bytes:
|
|
1804
|
-
max_retention_writes_per_minute:
|
|
1805
|
-
max_realtime_connections:
|
|
3056
|
+
max_asset_storage_bytes: import_zod9.z.number().int().nonnegative()
|
|
3057
|
+
});
|
|
3058
|
+
var orgQuotaUsageCounts = import_zod9.z.object({
|
|
3059
|
+
max_robots: import_zod9.z.number().int().nonnegative(),
|
|
3060
|
+
max_apps: import_zod9.z.number().int().nonnegative(),
|
|
3061
|
+
max_end_users: import_zod9.z.number().int().nonnegative(),
|
|
3062
|
+
max_retention_bytes: import_zod9.z.number().int().nonnegative(),
|
|
3063
|
+
max_asset_storage_bytes: import_zod9.z.number().int().nonnegative(),
|
|
3064
|
+
max_retention_writes_per_minute: import_zod9.z.number().int().nonnegative(),
|
|
3065
|
+
max_realtime_connections: import_zod9.z.number().int().nonnegative()
|
|
1806
3066
|
}).partial();
|
|
1807
|
-
var orgQuotaUsage =
|
|
1808
|
-
var
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
*
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
3067
|
+
var orgQuotaUsage = import_zod9.z.object({ quotas: orgQuotas, usage: orgQuotaUsageCounts });
|
|
3068
|
+
var LATENCY_BUCKET_MS = 6e4;
|
|
3069
|
+
var latencyBucket = import_zod9.z.object({
|
|
3070
|
+
/** Truncated to the minute. */
|
|
3071
|
+
bucket_at: import_zod9.z.iso.datetime(),
|
|
3072
|
+
/**
|
|
3073
|
+
* `null` exactly when `samples` is 0. A minute in which the robot was offline
|
|
3074
|
+
* throughout has **no** latency; writing `0` would put the number meaning
|
|
3075
|
+
* "perfectly fast" into the state meaning "not there at all".
|
|
3076
|
+
*/
|
|
3077
|
+
min_ms: import_zod9.z.number().nonnegative().nullable(),
|
|
3078
|
+
avg_ms: import_zod9.z.number().nonnegative().nullable(),
|
|
3079
|
+
max_ms: import_zod9.z.number().nonnegative().nullable(),
|
|
3080
|
+
samples: import_zod9.z.number().int().nonnegative(),
|
|
3081
|
+
/**
|
|
3082
|
+
* Milliseconds of this bucket the cloud held the robot online.
|
|
3083
|
+
*
|
|
3084
|
+
* A duration and **not a ratio**: a ratio needs a denominator, and here that
|
|
3085
|
+
* would be expected pings per minute — `pingIntervalMs`, which is
|
|
3086
|
+
* configurable and is shrunk in tests. A stored value whose meaning depends
|
|
3087
|
+
* on a configuration variable is not comparable across the time it is stored
|
|
3088
|
+
* for. A client divides by `LATENCY_BUCKET_MS` if it wants a fraction.
|
|
3089
|
+
*/
|
|
3090
|
+
online_ms: import_zod9.z.number().int().min(0).max(LATENCY_BUCKET_MS)
|
|
3091
|
+
});
|
|
3092
|
+
var robotLatencySeries = import_zod9.z.object({
|
|
3093
|
+
robot_id: import_zod9.z.uuid(),
|
|
3094
|
+
buckets: import_zod9.z.array(latencyBucket)
|
|
3095
|
+
});
|
|
3096
|
+
var orgLatencyQuery = import_zod9.z.object({
|
|
3097
|
+
from_ms: wireTimestampMs,
|
|
3098
|
+
/** Exclusive — half-open `[from, to)`, the convention every other query here already follows (DEF-062). */
|
|
3099
|
+
to_ms: wireTimestampMs,
|
|
3100
|
+
/**
|
|
3101
|
+
* One robot's own sparkline. `z.uuid()`, because the column is one —
|
|
3102
|
+
* the same fix in the same place `auditQuery.actor_id` documents at
|
|
3103
|
+
* length: a non-uuid reaching Postgres as a uuid parameter answers
|
|
3104
|
+
* `500 internal_error`, and a 500 explains nothing.
|
|
3105
|
+
*/
|
|
3106
|
+
robot_id: import_zod9.z.uuid().optional()
|
|
3107
|
+
}).strict().refine((query) => query.from_ms < query.to_ms, {
|
|
3108
|
+
message: "from_ms must be strictly before to_ms",
|
|
3109
|
+
path: ["from_ms"]
|
|
3110
|
+
});
|
|
3111
|
+
var orgLatencyResponse = import_zod9.z.object({
|
|
3112
|
+
series: import_zod9.z.array(robotLatencySeries),
|
|
3113
|
+
from_ms: import_zod9.z.number().int().nonnegative(),
|
|
3114
|
+
to_ms: import_zod9.z.number().int().nonnegative(),
|
|
3115
|
+
truncated: import_zod9.z.boolean(),
|
|
3116
|
+
/**
|
|
3117
|
+
* Which ceiling cut the response short, `null` when nothing did — borrowed
|
|
3118
|
+
* from `historySamplesResponse.truncated_by` rather than invented a second
|
|
3119
|
+
* time, for its reason: one boolean cannot carry two different remedies.
|
|
3120
|
+
*/
|
|
3121
|
+
truncated_by: import_zod9.z.enum(["limit", "bytes"]).nullable()
|
|
3122
|
+
});
|
|
3123
|
+
var usageMetric = import_zod9.z.enum(["api_calls", "live_session_ms", "retention_bytes", "asset_bytes", "robot_online_ms"]);
|
|
3124
|
+
var usageDay = import_zod9.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be a UTC calendar day, YYYY-MM-DD").refine((day) => {
|
|
3125
|
+
const parsed = /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
|
|
3126
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === day;
|
|
3127
|
+
}, { message: "must be a UTC calendar day, YYYY-MM-DD" });
|
|
3128
|
+
var orgUsageQuery = import_zod9.z.object({ from_day: usageDay, to_day: usageDay }).strict().refine((query) => query.from_day <= query.to_day, {
|
|
3129
|
+
message: "from_day must not be after to_day",
|
|
3130
|
+
path: ["from_day"]
|
|
3131
|
+
});
|
|
3132
|
+
var usageRow = import_zod9.z.object({
|
|
3133
|
+
app_id: import_zod9.z.uuid().nullable(),
|
|
3134
|
+
app_name: import_zod9.z.string().nullable(),
|
|
3135
|
+
metric: usageMetric,
|
|
3136
|
+
day: usageDay,
|
|
3137
|
+
value: import_zod9.z.number().int().nonnegative()
|
|
3138
|
+
});
|
|
3139
|
+
var orgUsageResponse = import_zod9.z.object({
|
|
3140
|
+
rows: import_zod9.z.array(usageRow),
|
|
3141
|
+
from_day: usageDay,
|
|
3142
|
+
to_day: usageDay
|
|
3143
|
+
});
|
|
3144
|
+
var patchRobotRequest = import_zod9.z.object({ name: import_zod9.z.string().min(1).max(63) }).strict();
|
|
3145
|
+
var renameSlugRequest = import_zod9.z.object({ from: slug, to: slug }).strict();
|
|
3146
|
+
var renameSlugResponse = import_zod9.z.object({
|
|
3147
|
+
rewritten_grants: import_zod9.z.number().int().nonnegative(),
|
|
3148
|
+
history_moved: import_zod9.z.boolean(),
|
|
3149
|
+
requires_publish: import_zod9.z.literal(true)
|
|
3150
|
+
});
|
|
3151
|
+
var slugUsageResponse = import_zod9.z.object({
|
|
3152
|
+
grant_count: import_zod9.z.number().int().nonnegative(),
|
|
3153
|
+
app_identifiers: import_zod9.z.array(import_zod9.z.string()),
|
|
3154
|
+
has_recorded_history: import_zod9.z.boolean(),
|
|
3155
|
+
alert_count: import_zod9.z.number().int().nonnegative()
|
|
1850
3156
|
});
|
|
1851
3157
|
|
|
1852
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
3158
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/realtime.js
|
|
3159
|
+
var import_zod12 = require("zod");
|
|
3160
|
+
|
|
3161
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
1853
3162
|
var import_zod11 = require("zod");
|
|
1854
3163
|
|
|
1855
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
3164
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/apps.js
|
|
1856
3165
|
var import_zod10 = require("zod");
|
|
1857
|
-
|
|
1858
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/apps.js
|
|
1859
|
-
var import_zod9 = require("zod");
|
|
1860
3166
|
var appIdentifier = slug;
|
|
1861
|
-
var app =
|
|
1862
|
-
id:
|
|
1863
|
-
org_id:
|
|
1864
|
-
name:
|
|
3167
|
+
var app = import_zod10.z.object({
|
|
3168
|
+
id: import_zod10.z.uuid(),
|
|
3169
|
+
org_id: import_zod10.z.uuid(),
|
|
3170
|
+
name: import_zod10.z.string().min(1).max(120),
|
|
1865
3171
|
identifier: appIdentifier,
|
|
3172
|
+
/**
|
|
3173
|
+
* **Exactly one group owns this app** (2026-08-29 identity redesign, D2).
|
|
3174
|
+
* The app uses that group's auth provider, and only users of that group can
|
|
3175
|
+
* hold an assignment for it — which is why re-linking cascade-deletes the
|
|
3176
|
+
* assignments that stop being valid.
|
|
3177
|
+
*
|
|
3178
|
+
* Not nullable and not optional: "an app with no group" is not a state the
|
|
3179
|
+
* model has, and an optional field here would let a mapper that forgot the
|
|
3180
|
+
* column produce one anyway.
|
|
3181
|
+
*
|
|
3182
|
+
* Changing it is `PUT /api/apps/:id/group` with `putAppGroupRequest`, never
|
|
3183
|
+
* `updateAppRequest` — see the comment there.
|
|
3184
|
+
*/
|
|
3185
|
+
group_id: import_zod10.z.uuid(),
|
|
1866
3186
|
/** Robots are referenced individually; tags never grant rights (§12.2). */
|
|
1867
|
-
robot_ids:
|
|
3187
|
+
robot_ids: import_zod10.z.array(import_zod10.z.uuid()),
|
|
1868
3188
|
/**
|
|
1869
3189
|
* Whether this app accepts **self-registering** OAuth clients (RFC 7591).
|
|
1870
3190
|
*
|
|
@@ -1877,81 +3197,98 @@ var app = import_zod9.z.object({
|
|
|
1877
3197
|
*
|
|
1878
3198
|
* The flag is app state rather than a deployment setting so that turning it
|
|
1879
3199
|
* on is a decision somebody made about one app, visible in the console and
|
|
1880
|
-
* in the audit log.
|
|
1881
|
-
*
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
* The
|
|
1895
|
-
*
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
1905
|
-
*
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
3200
|
+
* in the audit log.
|
|
3201
|
+
*
|
|
3202
|
+
* **It is the only switch left on an app, and the sibling it used to have
|
|
3203
|
+
* is worth remembering.** W7c added `mcp_enabled` beside it — "whether this
|
|
3204
|
+
* app serves a remote MCP server at `/mcp/<identifier>`" — and the
|
|
3205
|
+
* central-MCP cut (D5, 2026-08-29) deleted the per-app endpoint it named.
|
|
3206
|
+
* The field outlived its endpoint by a release, gating nothing but one
|
|
3207
|
+
* `resource` branch of the legacy OAuth stub, and was removed on
|
|
3208
|
+
* 2026-08-29. `orgGroup.mcp_enabled` in `identity.ts` is a **different**
|
|
3209
|
+
* field with a live door behind it (`cloud/src/mcp-access.ts`); the two
|
|
3210
|
+
* shared a name and never a meaning.
|
|
3211
|
+
*/
|
|
3212
|
+
accepts_dynamic_clients: import_zod10.z.boolean(),
|
|
3213
|
+
/**
|
|
3214
|
+
* **The app's default role** (2026-08-29 identity redesign, D1: *"role +
|
|
3215
|
+
* rights matrix and default role in app settings"*).
|
|
3216
|
+
*
|
|
3217
|
+
* Two consumers, one field. The console prefills it when an admin assigns a
|
|
3218
|
+
* user (`putAssignmentRequest` still carries the role explicitly — a
|
|
3219
|
+
* prefill is not a default the server applies), and the cloud authorizes an
|
|
3220
|
+
* **org admin** with it: admins hold no assignments (see `appAssignment`),
|
|
3221
|
+
* so an app login by one has to get its role from somewhere, and until D4's
|
|
3222
|
+
* impersonation interstitial lands this is that somewhere.
|
|
3223
|
+
*
|
|
3224
|
+
* `null` — and nullable rather than absent — means *this app has not chosen
|
|
3225
|
+
* one*. That is a normal state, not an unset field: every app is created
|
|
3226
|
+
* before its roles are configured, and the cloud falls back to the
|
|
3227
|
+
* least-privileged builtin (`observe`) by name rather than picking a role by
|
|
3228
|
+
* position. An app whose default role is deleted lands back here.
|
|
3229
|
+
*
|
|
3230
|
+
* The role must belong to **this** app; the schema sees a uuid and cannot
|
|
3231
|
+
* check that, so `PATCH /api/apps/:id` does.
|
|
3232
|
+
*/
|
|
3233
|
+
default_role_id: import_zod10.z.uuid().nullable(),
|
|
3234
|
+
created_at: import_zod10.z.iso.datetime()
|
|
1909
3235
|
});
|
|
1910
|
-
var createAppRequest =
|
|
1911
|
-
name:
|
|
3236
|
+
var createAppRequest = import_zod10.z.object({
|
|
3237
|
+
name: import_zod10.z.string().min(1).max(120),
|
|
1912
3238
|
identifier: appIdentifier,
|
|
1913
|
-
robot_ids:
|
|
3239
|
+
robot_ids: import_zod10.z.array(import_zod10.z.uuid()).optional(),
|
|
1914
3240
|
/** Optional, defaulting to `false` — same reasoning as `robot_ids` above: setting it at creation is the obvious operation, and refusing it here would make a `.strict()` request reject the field the caller can plainly see on `app`. */
|
|
1915
|
-
accepts_dynamic_clients:
|
|
1916
|
-
/**
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
* `app`, which is the exact shape the line above rejects. One rule, both
|
|
1924
|
-
* switches.
|
|
1925
|
-
*/
|
|
1926
|
-
mcp_enabled: import_zod9.z.boolean().optional()
|
|
3241
|
+
accepts_dynamic_clients: import_zod10.z.boolean().optional(),
|
|
3242
|
+
/**
|
|
3243
|
+
* Required, unlike the switch above: an app belongs to exactly one
|
|
3244
|
+
* group from the moment it exists (D2), there is no sensible default — the
|
|
3245
|
+
* Org Admins group would be the one group whose members never hold
|
|
3246
|
+
* assignments — and a defaulted answer here decides who can log into the app.
|
|
3247
|
+
*/
|
|
3248
|
+
group_id: import_zod10.z.uuid()
|
|
1927
3249
|
}).strict();
|
|
1928
|
-
var updateAppRequest =
|
|
1929
|
-
name:
|
|
1930
|
-
robot_ids:
|
|
1931
|
-
accepts_dynamic_clients:
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
3250
|
+
var updateAppRequest = import_zod10.z.object({
|
|
3251
|
+
name: import_zod10.z.string().min(1).max(120).optional(),
|
|
3252
|
+
robot_ids: import_zod10.z.array(import_zod10.z.uuid()).optional(),
|
|
3253
|
+
accepts_dynamic_clients: import_zod10.z.boolean().optional(),
|
|
3254
|
+
/**
|
|
3255
|
+
* `app.default_role_id`'s write half — an app *setting*, which is where D1
|
|
3256
|
+
* put the default role, so it belongs on the app's own PATCH and not on a
|
|
3257
|
+
* route of its own.
|
|
3258
|
+
*
|
|
3259
|
+
* **`.nullable().optional()`, and the two mean different things.** Absent
|
|
3260
|
+
* leaves the current default alone; an explicit `null` clears it. A field
|
|
3261
|
+
* that could only be set and never unset would make "we changed our mind"
|
|
3262
|
+
* unreachable through the API — the same silence `group_id` above was made
|
|
3263
|
+
* strict to avoid, from the other direction.
|
|
3264
|
+
*
|
|
3265
|
+
* Unlike `group_id`, this carries no cascade: changing it invalidates no
|
|
3266
|
+
* assignment and cuts nobody off, so it needs no acknowledgement and no
|
|
3267
|
+
* route of its own.
|
|
3268
|
+
*/
|
|
3269
|
+
default_role_id: import_zod10.z.uuid().nullable().optional()
|
|
3270
|
+
}).strict();
|
|
3271
|
+
var serverKeyToken = import_zod10.z.string().regex(/^flk_[0-9a-f]{32}$/);
|
|
3272
|
+
var serverKey = import_zod10.z.object({
|
|
3273
|
+
id: import_zod10.z.uuid(),
|
|
3274
|
+
app_id: import_zod10.z.uuid(),
|
|
3275
|
+
name: import_zod10.z.string().min(1).max(120),
|
|
3276
|
+
created_at: import_zod10.z.iso.datetime(),
|
|
1940
3277
|
/** Null until first use — the cheapest way to spot a key nobody needs. */
|
|
1941
|
-
last_used_at:
|
|
3278
|
+
last_used_at: import_zod10.z.iso.datetime().nullable()
|
|
1942
3279
|
});
|
|
1943
|
-
var createServerKeyResponse =
|
|
3280
|
+
var createServerKeyResponse = import_zod10.z.object({
|
|
1944
3281
|
server_key: serverKey,
|
|
1945
3282
|
key: serverKeyToken
|
|
1946
3283
|
});
|
|
1947
|
-
var role =
|
|
1948
|
-
id:
|
|
1949
|
-
app_id:
|
|
1950
|
-
name:
|
|
1951
|
-
builtin:
|
|
3284
|
+
var role = import_zod10.z.object({
|
|
3285
|
+
id: import_zod10.z.uuid(),
|
|
3286
|
+
app_id: import_zod10.z.uuid(),
|
|
3287
|
+
name: import_zod10.z.string().min(1).max(60),
|
|
3288
|
+
builtin: import_zod10.z.boolean()
|
|
1952
3289
|
});
|
|
1953
|
-
var rolePermissions =
|
|
1954
|
-
role_id:
|
|
3290
|
+
var rolePermissions = import_zod10.z.object({
|
|
3291
|
+
role_id: import_zod10.z.uuid(),
|
|
1955
3292
|
/**
|
|
1956
3293
|
* **A slug is unique per robot across ALL service kinds** (spec §4.1:
|
|
1957
3294
|
* "Jeder Dienst erhält einen Slug" — one namespace, not one per kind), and
|
|
@@ -1964,91 +3301,169 @@ var rolePermissions = import_zod9.z.object({
|
|
|
1964
3301
|
* today it enumerates datapoints only, which is the seam that would
|
|
1965
3302
|
* otherwise force a rebuild.
|
|
1966
3303
|
*/
|
|
1967
|
-
grants:
|
|
1968
|
-
robot_id:
|
|
1969
|
-
slugs:
|
|
3304
|
+
grants: import_zod10.z.array(import_zod10.z.object({
|
|
3305
|
+
robot_id: import_zod10.z.uuid(),
|
|
3306
|
+
slugs: import_zod10.z.array(slug)
|
|
1970
3307
|
})),
|
|
1971
3308
|
/**
|
|
1972
3309
|
* App-wide abilities a role grants, as opposed to per-slug grants above.
|
|
1973
3310
|
*
|
|
1974
|
-
* **A capability here is a promise, and
|
|
1975
|
-
* `action_history` and `presence`
|
|
1976
|
-
* and
|
|
1977
|
-
* (register row 8). A console
|
|
1978
|
-
*
|
|
1979
|
-
* granted something.
|
|
3311
|
+
* **A capability here is a promise, and one of them is still not kept.**
|
|
3312
|
+
* `action_history` and `presence` were both gated by this object from W4
|
|
3313
|
+
* and implemented nowhere — no route, no SDK method, no realtime frame
|
|
3314
|
+
* (register row 8). A console could therefore switch them on and nothing
|
|
3315
|
+
* changed, which is worse than their absence: the developer believes they
|
|
3316
|
+
* granted something. **This paragraph stays** whatever the current tally
|
|
3317
|
+
* is: it is the only place that says a switch in the console may change
|
|
3318
|
+
* nothing, and it is how the next unkept capability gets caught.
|
|
1980
3319
|
*
|
|
1981
|
-
* `assets` (W7)
|
|
3320
|
+
* `assets` (W7) was the first one redeemed. It gates §4.6's asset store,
|
|
1982
3321
|
* which is not covered by `grants` because **assets are not slugs** — and it
|
|
1983
3322
|
* is its own decision rather than a side effect of reaching the robot,
|
|
1984
3323
|
* because a mesh set gives away the machine's build.
|
|
3324
|
+
*
|
|
3325
|
+
* **`action_history` is kept as of the run-history delta.** It gates
|
|
3326
|
+
* `GET /api/robots/:id/jobs/history` — an end user whose role lacks it is
|
|
3327
|
+
* refused `403 capability_required`, naming the capability so the developer
|
|
3328
|
+
* knows which switch is off. It was unkeepable while nothing durable
|
|
3329
|
+
* recorded what had run; `jobRun` and `job_runs` are that record.
|
|
3330
|
+
*
|
|
3331
|
+
* **What granting it discloses.** A `jobRun` names the actor who invoked
|
|
3332
|
+
* it, and `jobActor.label` is an email — so an end user holding this
|
|
3333
|
+
* capability learns which *other* people have been driving that robot.
|
|
3334
|
+
* That is inherent in "may read the history" rather than an oversight, and
|
|
3335
|
+
* it is written down here because the switch lives in the console while its
|
|
3336
|
+
* consequence does not.
|
|
3337
|
+
*
|
|
3338
|
+
* **`presence` is still not implemented.** Nothing in the cloud, the SDK or
|
|
3339
|
+
* the realtime protocol consults it. It remains exactly what the first
|
|
3340
|
+
* paragraph describes.
|
|
1985
3341
|
*/
|
|
1986
|
-
capabilities:
|
|
1987
|
-
action_history:
|
|
1988
|
-
presence:
|
|
1989
|
-
assets:
|
|
3342
|
+
capabilities: import_zod10.z.object({
|
|
3343
|
+
action_history: import_zod10.z.boolean(),
|
|
3344
|
+
presence: import_zod10.z.boolean(),
|
|
3345
|
+
assets: import_zod10.z.boolean()
|
|
1990
3346
|
})
|
|
1991
3347
|
});
|
|
1992
|
-
var
|
|
1993
|
-
end_user_id: import_zod9.z.uuid(),
|
|
1994
|
-
app_id: import_zod9.z.uuid(),
|
|
1995
|
-
role_id: import_zod9.z.uuid()
|
|
1996
|
-
});
|
|
1997
|
-
var brandingConfig = import_zod9.z.object({
|
|
3348
|
+
var brandingConfig = import_zod10.z.object({
|
|
1998
3349
|
/** `#rrggbb`, lowercase — one canonical spelling so two configs that look identical are identical. */
|
|
1999
|
-
primary_color:
|
|
3350
|
+
primary_color: import_zod10.z.string().regex(/^#[0-9a-f]{6}$/, "primary_color must be lowercase #rrggbb"),
|
|
2000
3351
|
/**
|
|
2001
3352
|
* 256 KiB of raw image at most. Base64 costs 4 bytes per 3, so the encoded
|
|
2002
3353
|
* ceiling is stated here in encoded characters — the unit the validator can
|
|
2003
3354
|
* actually count, rather than one it would have to infer.
|
|
2004
3355
|
*/
|
|
2005
|
-
logo_data_uri:
|
|
2006
|
-
footer_text:
|
|
3356
|
+
logo_data_uri: import_zod10.z.string().max(349528).regex(/^data:image\/(png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$/, "logo must be a base64 data URI of image/png or image/jpeg").optional(),
|
|
3357
|
+
footer_text: import_zod10.z.string().min(1).max(200).optional()
|
|
2007
3358
|
});
|
|
2008
3359
|
|
|
2009
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
2010
|
-
var clientLoginRequest =
|
|
3360
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/client-auth.js
|
|
3361
|
+
var clientLoginRequest = import_zod11.z.object({
|
|
2011
3362
|
app_identifier: appIdentifier,
|
|
2012
|
-
email:
|
|
2013
|
-
password:
|
|
2014
|
-
});
|
|
2015
|
-
var clientRefreshRequest =
|
|
2016
|
-
refresh_token:
|
|
2017
|
-
});
|
|
2018
|
-
var clientLogoutRequest =
|
|
2019
|
-
refresh_token:
|
|
2020
|
-
});
|
|
2021
|
-
var
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
3363
|
+
email: import_zod11.z.email(),
|
|
3364
|
+
password: import_zod11.z.string().min(1)
|
|
3365
|
+
});
|
|
3366
|
+
var clientRefreshRequest = import_zod11.z.object({
|
|
3367
|
+
refresh_token: import_zod11.z.string().min(1)
|
|
3368
|
+
});
|
|
3369
|
+
var clientLogoutRequest = import_zod11.z.object({
|
|
3370
|
+
refresh_token: import_zod11.z.string().min(1)
|
|
3371
|
+
});
|
|
3372
|
+
var clientLogoutResponse = import_zod11.z.object({
|
|
3373
|
+
idp_logout: import_zod11.z.discriminatedUnion("status", [
|
|
3374
|
+
import_zod11.z.object({
|
|
3375
|
+
status: import_zod11.z.literal("redirect"),
|
|
3376
|
+
/** Send the browser here. Built from the IdP's own `end_session_endpoint`. */
|
|
3377
|
+
url: import_zod11.z.url().max(2e3)
|
|
3378
|
+
}),
|
|
3379
|
+
/** This session did not come from an IdP — there is nothing else to end. */
|
|
3380
|
+
import_zod11.z.object({ status: import_zod11.z.literal("not_federated") }),
|
|
3381
|
+
/**
|
|
3382
|
+
* It did, and the IdP's discovery document names no `end_session_endpoint`
|
|
3383
|
+
* (RP-initiated logout is optional in OIDC). **The IdP session survives and
|
|
3384
|
+
* this platform cannot end it** — say so rather than implying success.
|
|
3385
|
+
*/
|
|
3386
|
+
import_zod11.z.object({ status: import_zod11.z.literal("unsupported_by_idp") }),
|
|
3387
|
+
/**
|
|
3388
|
+
* **Federated, the IdP *can* end the session, and we cannot ask it to
|
|
3389
|
+
* (W9c, Nimbus-W9c's proposal).**
|
|
3390
|
+
*
|
|
3391
|
+
* `id_token_hint` is missing: the stored value could not be decrypted, or
|
|
3392
|
+
* the session predates the fix that started keeping one. Deliberately not
|
|
3393
|
+
* folded into `unsupported_by_idp` — there the cause is **the IdP's**, here
|
|
3394
|
+
* it is **ours**, and an operator reading a rise in these needs to know
|
|
3395
|
+
* which of the two they are looking at.
|
|
3396
|
+
*
|
|
3397
|
+
* **No `url` field, and that is the whole point.** Sending somebody to a
|
|
3398
|
+
* bare `end_session_endpoint` produces a page that *asks* rather than one
|
|
3399
|
+
* that ends — measured against real Keycloak, which renders *"Do you want
|
|
3400
|
+
* to log out?"* and leaves the session alive. An outcome that promises
|
|
3401
|
+
* something it does not deliver is worse than one that admits it.
|
|
3402
|
+
*
|
|
3403
|
+
* **The two causes are one status on purpose.** A caller can do nothing
|
|
3404
|
+
* differently between "decryption failed" and "this session is older than
|
|
3405
|
+
* the fix" — both mean *the IdP session survives and you cannot end it
|
|
3406
|
+
* from here*. The distinction matters to whoever runs this platform, and
|
|
3407
|
+
* it belongs in the server's log, not on the wire: splitting a wire enum
|
|
3408
|
+
* to carry a fact no consumer can act on is how a contract grows keys
|
|
3409
|
+
* nobody reads.
|
|
3410
|
+
*/
|
|
3411
|
+
import_zod11.z.object({ status: import_zod11.z.literal("hint_unavailable") }),
|
|
3412
|
+
/**
|
|
3413
|
+
* **The server does not know this session, so it can say nothing about an
|
|
3414
|
+
* IdP** (W9 review, Argus-W9; André, 2026-08-19).
|
|
3415
|
+
*
|
|
3416
|
+
* The token was unknown, already superseded, revoked, or expired. There is
|
|
3417
|
+
* nothing to end here and — this is the whole point — **nothing to claim
|
|
3418
|
+
* either**. `not_federated` would be a statement about a login this server
|
|
3419
|
+
* never saw.
|
|
3420
|
+
*
|
|
3421
|
+
* Same shape of argument that produced `hint_unavailable`: *there the cause
|
|
3422
|
+
* is the IdP's, here it is ours.* Here it is neither — it is an **absence
|
|
3423
|
+
* of knowledge**, and a contract whose job on this route is to keep facts
|
|
3424
|
+
* apart should not spend a fact it does not have.
|
|
3425
|
+
*
|
|
3426
|
+
* **What a caller does with it: nothing, but not the same nothing as
|
|
3427
|
+
* `not_federated`.** A first logout in the same flow may well have returned
|
|
3428
|
+
* a `redirect` that is still worth following. Reading this as *"the user is
|
|
3429
|
+
* fully logged out"* is exactly the mistake a second logout invites.
|
|
3430
|
+
*/
|
|
3431
|
+
import_zod11.z.object({ status: import_zod11.z.literal("session_unknown") })
|
|
3432
|
+
])
|
|
3433
|
+
});
|
|
3434
|
+
var clientIdentity = import_zod11.z.object({
|
|
3435
|
+
kind: import_zod11.z.enum(["developer", "end_user", "server_key"]),
|
|
3436
|
+
developer_id: import_zod11.z.uuid().nullable(),
|
|
3437
|
+
end_user_id: import_zod11.z.uuid().nullable(),
|
|
3438
|
+
server_key_id: import_zod11.z.uuid().nullable(),
|
|
3439
|
+
app_id: import_zod11.z.uuid().nullable(),
|
|
3440
|
+
role_id: import_zod11.z.uuid().nullable(),
|
|
3441
|
+
email: import_zod11.z.email().nullable(),
|
|
3442
|
+
/** Present only under impersonation — the real admin's user id (D4, `act`-claim). */
|
|
3443
|
+
act: import_zod11.z.object({ admin_user_id: import_zod11.z.uuid() }).strict().optional()
|
|
2029
3444
|
});
|
|
2030
3445
|
|
|
2031
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
2032
|
-
var clientAuth =
|
|
2033
|
-
type:
|
|
2034
|
-
token:
|
|
3446
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/realtime.js
|
|
3447
|
+
var clientAuth = import_zod12.z.object({
|
|
3448
|
+
type: import_zod12.z.literal("auth"),
|
|
3449
|
+
token: import_zod12.z.string().min(1)
|
|
2035
3450
|
});
|
|
2036
|
-
var authOk =
|
|
2037
|
-
type:
|
|
3451
|
+
var authOk = import_zod12.z.object({
|
|
3452
|
+
type: import_zod12.z.literal("auth_ok"),
|
|
2038
3453
|
identity: clientIdentity
|
|
2039
3454
|
});
|
|
2040
|
-
var authError =
|
|
2041
|
-
type:
|
|
2042
|
-
code:
|
|
2043
|
-
message:
|
|
3455
|
+
var authError = import_zod12.z.object({
|
|
3456
|
+
type: import_zod12.z.literal("auth_error"),
|
|
3457
|
+
code: import_zod12.z.string().min(1),
|
|
3458
|
+
message: import_zod12.z.string().min(1)
|
|
2044
3459
|
});
|
|
2045
|
-
var clientInvoke =
|
|
2046
|
-
type:
|
|
2047
|
-
request_id:
|
|
2048
|
-
robot_id:
|
|
3460
|
+
var clientInvoke = import_zod12.z.object({
|
|
3461
|
+
type: import_zod12.z.literal("invoke"),
|
|
3462
|
+
request_id: import_zod12.z.string().min(1).max(64),
|
|
3463
|
+
robot_id: import_zod12.z.uuid(),
|
|
2049
3464
|
slug,
|
|
2050
3465
|
/** Parameters by field path, validated against the config's rules (§4.4). */
|
|
2051
|
-
params:
|
|
3466
|
+
params: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()),
|
|
2052
3467
|
/**
|
|
2053
3468
|
* How long this one call is worth waiting for (W6b) — the same field,
|
|
2054
3469
|
* meaning and cap as `invokeRequest.patience_ms`; absent means
|
|
@@ -2062,12 +3477,12 @@ var clientInvoke = import_zod11.z.object({
|
|
|
2062
3477
|
* methods no SDK caller could invoke; this is the same defect caught before
|
|
2063
3478
|
* it shipped, by the SDK owner rather than by a reviewer.
|
|
2064
3479
|
*/
|
|
2065
|
-
patience_ms:
|
|
3480
|
+
patience_ms: import_zod12.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional()
|
|
2066
3481
|
});
|
|
2067
|
-
var clientCancel =
|
|
2068
|
-
type:
|
|
2069
|
-
request_id:
|
|
2070
|
-
robot_id:
|
|
3482
|
+
var clientCancel = import_zod12.z.object({
|
|
3483
|
+
type: import_zod12.z.literal("cancel"),
|
|
3484
|
+
request_id: import_zod12.z.string().min(1).max(64),
|
|
3485
|
+
robot_id: import_zod12.z.uuid(),
|
|
2071
3486
|
/** Which slug — required, and the only address a cancel had until W6b. */
|
|
2072
3487
|
slug,
|
|
2073
3488
|
/**
|
|
@@ -2085,19 +3500,19 @@ var clientCancel = import_zod11.z.object({
|
|
|
2085
3500
|
* back to the slug. Falling back would be the platform deciding that the
|
|
2086
3501
|
* caller did not really mean the id they typed.
|
|
2087
3502
|
*/
|
|
2088
|
-
job_id:
|
|
3503
|
+
job_id: import_zod12.z.uuid().nullable()
|
|
2089
3504
|
});
|
|
2090
|
-
var clientPublish =
|
|
2091
|
-
type:
|
|
2092
|
-
request_id:
|
|
2093
|
-
robot_id:
|
|
3505
|
+
var clientPublish = import_zod12.z.object({
|
|
3506
|
+
type: import_zod12.z.literal("publish"),
|
|
3507
|
+
request_id: import_zod12.z.string().min(1).max(64),
|
|
3508
|
+
robot_id: import_zod12.z.uuid(),
|
|
2094
3509
|
slug,
|
|
2095
|
-
message:
|
|
3510
|
+
message: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown())
|
|
2096
3511
|
});
|
|
2097
|
-
var commandResult =
|
|
2098
|
-
type:
|
|
2099
|
-
request_id:
|
|
2100
|
-
ok:
|
|
3512
|
+
var commandResult = import_zod12.z.object({
|
|
3513
|
+
type: import_zod12.z.literal("command_result"),
|
|
3514
|
+
request_id: import_zod12.z.string().min(1).max(64),
|
|
3515
|
+
ok: import_zod12.z.boolean(),
|
|
2101
3516
|
/**
|
|
2102
3517
|
* The job this reply is *about* — which is not always the caller's own.
|
|
2103
3518
|
*
|
|
@@ -2121,9 +3536,9 @@ var commandResult = import_zod11.z.object({
|
|
|
2121
3536
|
* later. Returning the kind lets a client refuse its own mistake at once,
|
|
2122
3537
|
* instead of reporting it as the machine's.
|
|
2123
3538
|
*/
|
|
2124
|
-
kind:
|
|
2125
|
-
code:
|
|
2126
|
-
message:
|
|
3539
|
+
kind: import_zod12.z.enum(["datapoint", "action", "service", "publisher", "camera"]).nullable(),
|
|
3540
|
+
code: import_zod12.z.string().nullable(),
|
|
3541
|
+
message: import_zod12.z.string().nullable(),
|
|
2127
3542
|
/**
|
|
2128
3543
|
* The same payload the REST envelope carries in `apiError.details` — for
|
|
2129
3544
|
* `parameter_invalid`, a `parameterInvalidDetails`.
|
|
@@ -2135,16 +3550,16 @@ var commandResult = import_zod11.z.object({
|
|
|
2135
3550
|
* A client cannot bind an error to the input that caused it from a code
|
|
2136
3551
|
* alone — which is the entire point of the flat parameter shape.
|
|
2137
3552
|
*/
|
|
2138
|
-
details:
|
|
3553
|
+
details: import_zod12.z.unknown().optional()
|
|
2139
3554
|
});
|
|
2140
|
-
var errorFrame =
|
|
2141
|
-
type:
|
|
2142
|
-
code:
|
|
2143
|
-
message:
|
|
3555
|
+
var errorFrame = import_zod12.z.object({
|
|
3556
|
+
type: import_zod12.z.literal("error"),
|
|
3557
|
+
code: import_zod12.z.string().min(1),
|
|
3558
|
+
message: import_zod12.z.string().min(1)
|
|
2144
3559
|
});
|
|
2145
|
-
var clientSubscribe =
|
|
2146
|
-
type:
|
|
2147
|
-
robot_id:
|
|
3560
|
+
var clientSubscribe = import_zod12.z.object({
|
|
3561
|
+
type: import_zod12.z.literal("subscribe"),
|
|
3562
|
+
robot_id: import_zod12.z.uuid(),
|
|
2148
3563
|
slug,
|
|
2149
3564
|
/**
|
|
2150
3565
|
* What the subscriber expects, and how it wants it (W5).
|
|
@@ -2166,179 +3581,375 @@ var clientSubscribe = import_zod11.z.object({
|
|
|
2166
3581
|
* request was the same mistake as the silent wrong-verb subscribe this
|
|
2167
3582
|
* field was added to fix.
|
|
2168
3583
|
*/
|
|
2169
|
-
kind:
|
|
2170
|
-
options:
|
|
3584
|
+
kind: import_zod12.z.enum(["datapoint", "action", "service", "publisher", "camera"]).optional(),
|
|
3585
|
+
options: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).optional()
|
|
2171
3586
|
});
|
|
2172
|
-
var clientUnsubscribe =
|
|
2173
|
-
type:
|
|
2174
|
-
robot_id:
|
|
3587
|
+
var clientUnsubscribe = import_zod12.z.object({
|
|
3588
|
+
type: import_zod12.z.literal("unsubscribe"),
|
|
3589
|
+
robot_id: import_zod12.z.uuid(),
|
|
2175
3590
|
slug
|
|
2176
3591
|
});
|
|
2177
|
-
var subscribeError =
|
|
2178
|
-
type:
|
|
2179
|
-
robot_id:
|
|
2180
|
-
slug:
|
|
2181
|
-
code:
|
|
2182
|
-
message:
|
|
3592
|
+
var subscribeError = import_zod12.z.object({
|
|
3593
|
+
type: import_zod12.z.literal("subscribe_error"),
|
|
3594
|
+
robot_id: import_zod12.z.string(),
|
|
3595
|
+
slug: import_zod12.z.string(),
|
|
3596
|
+
code: import_zod12.z.string().min(1),
|
|
3597
|
+
message: import_zod12.z.string().min(1)
|
|
2183
3598
|
});
|
|
2184
|
-
var datapointEvent =
|
|
2185
|
-
type:
|
|
2186
|
-
robot_id:
|
|
3599
|
+
var datapointEvent = import_zod12.z.object({
|
|
3600
|
+
type: import_zod12.z.literal("datapoint"),
|
|
3601
|
+
robot_id: import_zod12.z.uuid(),
|
|
2187
3602
|
slug,
|
|
2188
|
-
value:
|
|
2189
|
-
timestamp_ms:
|
|
2190
|
-
});
|
|
2191
|
-
var
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
3603
|
+
value: import_zod12.z.unknown(),
|
|
3604
|
+
timestamp_ms: import_zod12.z.number().int().nonnegative()
|
|
3605
|
+
});
|
|
3606
|
+
var liveSessionEndReason = import_zod12.z.enum([
|
|
3607
|
+
/** Another holder of this camera released it — another tab, or another client. */
|
|
3608
|
+
"released_by_peer",
|
|
3609
|
+
/** The robot's configuration was published and this camera changed with it. */
|
|
3610
|
+
"config_changed",
|
|
3611
|
+
/** The robot said it could not publish. `detail` carries its own words. */
|
|
3612
|
+
"publish_failed",
|
|
3613
|
+
/** The bridge stopped answering. */
|
|
3614
|
+
"robot_offline",
|
|
3615
|
+
/** The grant this session was minted under was withdrawn. */
|
|
3616
|
+
"revoked",
|
|
3617
|
+
/** The session's own lifetime ran out. */
|
|
3618
|
+
"expired",
|
|
3619
|
+
/** The robot was deleted out from under the session. */
|
|
3620
|
+
"robot_deleted",
|
|
3621
|
+
/**
|
|
3622
|
+
* The cloud ended it and cannot say which of the above applied. **Kept
|
|
3623
|
+
* deliberately**: a channel that cannot say "I do not know" will say
|
|
3624
|
+
* something false instead, and this project has paid for that four times in
|
|
3625
|
+
* the camera path alone.
|
|
3626
|
+
*/
|
|
3627
|
+
"unknown"
|
|
3628
|
+
]);
|
|
3629
|
+
var liveSessionEvent = import_zod12.z.object({
|
|
3630
|
+
type: import_zod12.z.literal("live_session"),
|
|
3631
|
+
robot_id: import_zod12.z.uuid(),
|
|
3632
|
+
slug,
|
|
3633
|
+
session_id: import_zod12.z.uuid(),
|
|
3634
|
+
state: import_zod12.z.literal("ended"),
|
|
3635
|
+
reason: liveSessionEndReason,
|
|
3636
|
+
/**
|
|
3637
|
+
* **Classified text the cloud produced, never text the robot sent.**
|
|
3638
|
+
*
|
|
3639
|
+
* An earlier draft of this comment said *"the robot's own words when it has
|
|
3640
|
+
* any"*, which reads as permission to pass `bridgeCameraState.error.message`
|
|
3641
|
+
* straight through. Nothing sanitises that field, and this codebase has a
|
|
3642
|
+
* documented incident of a password reaching a developer surface through
|
|
3643
|
+
* exactly that route — `camera-health.ts`'s fixed-string `REASON` discipline
|
|
3644
|
+
* exists because of it. Nimbus-W9a stopped at the sentence and asked rather
|
|
3645
|
+
* than taking the permission it appeared to give (2026-08-19).
|
|
3646
|
+
*
|
|
3647
|
+
* So: `null` unless the cloud itself has something classified to say. If a
|
|
3648
|
+
* developer needs the robot's own diagnosis later, it arrives as a mapped
|
|
3649
|
+
* code with fixed text, the way camera health already does it — not as
|
|
3650
|
+
* forwarded foreign text on a channel a client reads.
|
|
3651
|
+
*/
|
|
3652
|
+
detail: import_zod12.z.string().max(200).nullable(),
|
|
3653
|
+
/** When it ended — not when this frame was sent. Same reasoning as `changed_at_ms`. */
|
|
3654
|
+
ended_at_ms: import_zod12.z.number().int().nonnegative()
|
|
3655
|
+
});
|
|
3656
|
+
var resourceHealthCleared = import_zod12.z.object({
|
|
3657
|
+
type: import_zod12.z.literal("resource_health_cleared"),
|
|
3658
|
+
robot_id: import_zod12.z.uuid(),
|
|
3659
|
+
kind: import_zod12.z.enum(["camera"]),
|
|
3660
|
+
ref: import_zod12.z.string().min(1).max(64),
|
|
3661
|
+
facet: import_zod12.z.enum(["source", "publish"]),
|
|
3662
|
+
cleared_at_ms: import_zod12.z.number().int().nonnegative()
|
|
3663
|
+
});
|
|
3664
|
+
var resourceHealthEvent = import_zod12.z.object({
|
|
3665
|
+
type: import_zod12.z.literal("resource_health"),
|
|
3666
|
+
robot_id: import_zod12.z.uuid(),
|
|
3667
|
+
kind: import_zod12.z.enum(["camera"]),
|
|
3668
|
+
ref: import_zod12.z.string().min(1).max(64),
|
|
3669
|
+
/** Which of the two questions this entry answers — see `resourceHealthState.facet`. */
|
|
3670
|
+
facet: import_zod12.z.enum(["source", "publish"]),
|
|
3671
|
+
state: import_zod12.z.enum(RESOURCE_HEALTH_STATES),
|
|
3672
|
+
reason: import_zod12.z.string().max(200).nullable(),
|
|
3673
|
+
changed_at_ms: import_zod12.z.number().int().nonnegative()
|
|
3674
|
+
});
|
|
3675
|
+
var ORG_EVENT_BUFFER_SIZE = 200;
|
|
3676
|
+
var orgEventKind = import_zod12.z.enum(["datapoint", "health", "job", "bridge", "audit", "alert"]);
|
|
3677
|
+
var orgEventSeverity = import_zod12.z.enum(["info", "warning", "error"]);
|
|
3678
|
+
var orgEvent = import_zod12.z.object({
|
|
3679
|
+
type: import_zod12.z.literal("org_event"),
|
|
3680
|
+
/**
|
|
3681
|
+
* **Per org, per process.** Like `job.seq` and unlike `job_runs.seq`, which
|
|
3682
|
+
* is a postgres `bigserial` and durable. All three say which they are,
|
|
3683
|
+
* because anyone who confuses them will confuse them in both directions.
|
|
3684
|
+
*/
|
|
3685
|
+
seq: import_zod12.z.number().int().positive(),
|
|
3686
|
+
at: import_zod12.z.iso.datetime(),
|
|
3687
|
+
kind: orgEventKind,
|
|
3688
|
+
severity: orgEventSeverity,
|
|
3689
|
+
/** `null` for an org-level event — an invitation, a quota change — which belongs to no robot. */
|
|
3690
|
+
robot_id: import_zod12.z.uuid().nullable(),
|
|
3691
|
+
/** What the line is about: a slug, a camera, an actor's email. */
|
|
3692
|
+
subject: import_zod12.z.string().min(1).max(200),
|
|
3693
|
+
/**
|
|
3694
|
+
* Kind-specific, and **capped at `ORG_EVENT_DETAIL_MAX_BYTES`** — above it
|
|
3695
|
+
* the producer substitutes `{ omitted: 'too_large', bytes }`. Truncated,
|
|
3696
|
+
* and saying so.
|
|
3697
|
+
*
|
|
3698
|
+
* Never a pre-formatted line: the reader decides language, number format
|
|
3699
|
+
* and truncation, so changing how a line reads is not a cloud deploy.
|
|
3700
|
+
*/
|
|
3701
|
+
detail: import_zod12.z.unknown().nullable()
|
|
3702
|
+
}).strict();
|
|
3703
|
+
var orgEventSubscribe = import_zod12.z.object({ type: import_zod12.z.literal("org_event_subscribe") }).strict();
|
|
3704
|
+
var orgEventUnsubscribe = import_zod12.z.object({ type: import_zod12.z.literal("org_event_unsubscribe") }).strict();
|
|
3705
|
+
var orgEventReplay = import_zod12.z.object({
|
|
3706
|
+
type: import_zod12.z.literal("org_event_replay"),
|
|
3707
|
+
events: import_zod12.z.array(orgEvent).max(ORG_EVENT_BUFFER_SIZE),
|
|
3708
|
+
/**
|
|
3709
|
+
* **`false` means three different things, on purpose**: the buffer was
|
|
3710
|
+
* already full, the cloud restarted, or this org's buffer had expired. All
|
|
3711
|
+
* three mean the same thing to a reader — *something is missing above this
|
|
3712
|
+
* line* — and a field separating them would claim a distinction nobody
|
|
3713
|
+
* would act on differently.
|
|
3714
|
+
*/
|
|
3715
|
+
complete: import_zod12.z.boolean()
|
|
3716
|
+
}).strict();
|
|
3717
|
+
var orgEventDropped = import_zod12.z.object({
|
|
3718
|
+
type: import_zod12.z.literal("org_event_dropped"),
|
|
3719
|
+
/**
|
|
3720
|
+
* **An epoch instant in milliseconds (`Date.now()`), not a duration.**
|
|
3721
|
+
* The moment this socket last reported a drop — or the moment it
|
|
3722
|
+
* subscribed, if this is its first such frame. The window the `dropped`
|
|
3723
|
+
* count covers is `since_ms` to now, so a reader wanting an age
|
|
3724
|
+
* subtracts: `Date.now() - since_ms`. Spelled out because the type
|
|
3725
|
+
* admits both readings and the wrong one is silent: a consumer treating
|
|
3726
|
+
* it as "milliseconds ago" renders a drop that happened seconds ago as
|
|
3727
|
+
* having happened in 1970.
|
|
3728
|
+
*/
|
|
3729
|
+
since_ms: import_zod12.z.number().int().nonnegative(),
|
|
3730
|
+
/** Always at least one — a frame reporting nothing lost is noise on a channel built to be quiet. */
|
|
3731
|
+
dropped: import_zod12.z.number().int().positive()
|
|
3732
|
+
}).strict();
|
|
2200
3733
|
|
|
2201
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
2202
|
-
var
|
|
2203
|
-
var password =
|
|
2204
|
-
var
|
|
2205
|
-
var
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
3734
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/identity.js
|
|
3735
|
+
var import_zod13 = require("zod");
|
|
3736
|
+
var password = import_zod13.z.string().min(12).max(256);
|
|
3737
|
+
var GROUP_NAME_MAX = 120;
|
|
3738
|
+
var USER_DISPLAY_NAME_MAX = 120;
|
|
3739
|
+
var orgAdminTier = import_zod13.z.enum(["owner", "developer"]);
|
|
3740
|
+
var mcpAccess = import_zod13.z.enum(["default", "allowed", "denied"]);
|
|
3741
|
+
var org = import_zod13.z.object({
|
|
3742
|
+
id: import_zod13.z.uuid(),
|
|
3743
|
+
name: import_zod13.z.string().min(1).max(120),
|
|
3744
|
+
created_at: import_zod13.z.iso.datetime()
|
|
3745
|
+
});
|
|
3746
|
+
var orgGroup = import_zod13.z.object({
|
|
3747
|
+
id: import_zod13.z.uuid(),
|
|
3748
|
+
org_id: import_zod13.z.uuid(),
|
|
3749
|
+
name: import_zod13.z.string().min(1).max(GROUP_NAME_MAX),
|
|
3750
|
+
/**
|
|
3751
|
+
* **A server fact, never a request field.** The Org Admins group is created
|
|
3752
|
+
* with the org and is the only path into the console, so a caller able to
|
|
3753
|
+
* set this could mint themselves console access. Neither
|
|
3754
|
+
* `createGroupRequest` nor `patchGroupRequest` carries it, and both are
|
|
3755
|
+
* `.strict()` so offering it is a refusal rather than a silent drop.
|
|
3756
|
+
*/
|
|
3757
|
+
is_org_admins: import_zod13.z.boolean(),
|
|
3758
|
+
/**
|
|
3759
|
+
* Whether this **customer** group's users may reach the central MCP server,
|
|
3760
|
+
* enforced since 0.5.0 at token issue and on every request
|
|
3761
|
+
* (`cloud/src/mcp-access.ts`), with the per-user `mcpAccess` override
|
|
3762
|
+
* winning over it in both directions.
|
|
3763
|
+
*
|
|
3764
|
+
* **On the Org Admins group it decides nothing.** The door permits a member
|
|
3765
|
+
* of `is_org_admins` unconditionally (Andre, 2026-08-29) before it reads
|
|
3766
|
+
* either this flag or the user's override; turning it off there refuses
|
|
3767
|
+
* nobody. Stored and settable all the same, so the group shape stays one
|
|
3768
|
+
* shape — but it is a customer-group control, and only that.
|
|
3769
|
+
*/
|
|
3770
|
+
mcp_enabled: import_zod13.z.boolean(),
|
|
3771
|
+
member_count: import_zod13.z.number().int().nonnegative(),
|
|
3772
|
+
app_count: import_zod13.z.number().int().nonnegative(),
|
|
3773
|
+
created_at: import_zod13.z.iso.datetime()
|
|
3774
|
+
});
|
|
3775
|
+
var orgUser = import_zod13.z.object({
|
|
3776
|
+
id: import_zod13.z.uuid(),
|
|
3777
|
+
org_id: import_zod13.z.uuid(),
|
|
3778
|
+
email: import_zod13.z.email(),
|
|
3779
|
+
/**
|
|
3780
|
+
* Optional human name, shown by the console instead of the email where
|
|
3781
|
+
* present. Self-service via `PATCH /api/auth/me`; never used for auth.
|
|
3782
|
+
*/
|
|
3783
|
+
display_name: import_zod13.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable(),
|
|
3784
|
+
/** Exactly one, always (D1). Moving is a deliberate act — see `moveUserGroupRequest`. */
|
|
3785
|
+
group_id: import_zod13.z.uuid(),
|
|
3786
|
+
/**
|
|
3787
|
+
* **The wire's only statement about the Fleetless credential, and it is one
|
|
3788
|
+
* bit on purpose.** No hash, no algorithm, no "last changed" — a response
|
|
3789
|
+
* that carries a hash puts it in every log that ever captured a response,
|
|
3790
|
+
* and this platform has already written that rule down for server keys and
|
|
3791
|
+
* IdP secrets.
|
|
3792
|
+
*
|
|
3793
|
+
* `false` means *this user has no Fleetless password* — an OIDC-provisioned
|
|
3794
|
+
* user, or an invitation not yet accepted. It does **not** mean blocked, and
|
|
3795
|
+
* it does **not** mean without access: such a user signs in through their
|
|
3796
|
+
* group's provider and a password reset answers them the same silent way as
|
|
3797
|
+
* an unknown address (D4).
|
|
3798
|
+
*
|
|
3799
|
+
* What it cannot say: whether the password is strong, old, or already known
|
|
3800
|
+
* to somebody else. Nothing on the wire can, and a field that looked like it
|
|
3801
|
+
* could would be read as an assurance.
|
|
3802
|
+
*/
|
|
3803
|
+
has_password: import_zod13.z.boolean(),
|
|
3804
|
+
mcp_access: mcpAccess,
|
|
3805
|
+
/**
|
|
3806
|
+
* **Present only for members of the Org Admins group — and absent means "not
|
|
3807
|
+
* applicable", never "unknown".**
|
|
3808
|
+
*
|
|
3809
|
+
* A tier is a statement about console powers, and a user in a customer group
|
|
3810
|
+
* has none to grade; giving them a `developer` tier would invent a rank the
|
|
3811
|
+
* model does not have, and defaulting one would make "we did not load it"
|
|
3812
|
+
* indistinguishable from "they are an ordinary user" on the field that
|
|
3813
|
+
* decides who may delete the org.
|
|
3814
|
+
*
|
|
3815
|
+
* The schema cannot check the pairing: it sees a `group_id`, not whether
|
|
3816
|
+
* that group is the Org Admins one. The cloud is what refuses a tier on a
|
|
3817
|
+
* user outside that group and requires one inside it.
|
|
3818
|
+
*/
|
|
3819
|
+
tier: orgAdminTier.optional(),
|
|
3820
|
+
created_at: import_zod13.z.iso.datetime()
|
|
3821
|
+
});
|
|
3822
|
+
var appAssignment = import_zod13.z.object({
|
|
3823
|
+
user_id: import_zod13.z.uuid(),
|
|
3824
|
+
app_id: import_zod13.z.uuid(),
|
|
3825
|
+
role_id: import_zod13.z.uuid()
|
|
3826
|
+
});
|
|
3827
|
+
var groupListResponse = import_zod13.z.object({ groups: import_zod13.z.array(orgGroup) });
|
|
3828
|
+
var orgUserListResponse = import_zod13.z.object({ users: import_zod13.z.array(orgUser) });
|
|
3829
|
+
var appAssignmentListResponse = import_zod13.z.object({ assignments: import_zod13.z.array(appAssignment) });
|
|
3830
|
+
var sessionTokens = import_zod13.z.object({
|
|
3831
|
+
access_token: import_zod13.z.string().min(1),
|
|
3832
|
+
refresh_token: import_zod13.z.string().min(1),
|
|
3833
|
+
expires_in: import_zod13.z.number().int().positive()
|
|
3834
|
+
});
|
|
3835
|
+
var refreshRequest = import_zod13.z.object({ refresh_token: import_zod13.z.string().min(1) });
|
|
3836
|
+
var signUpRequest = import_zod13.z.object({
|
|
3837
|
+
org_name: import_zod13.z.string().min(1).max(120),
|
|
3838
|
+
email: import_zod13.z.email(),
|
|
2226
3839
|
password
|
|
2227
3840
|
});
|
|
2228
|
-
var signUpResponse =
|
|
3841
|
+
var signUpResponse = import_zod13.z.object({
|
|
2229
3842
|
org,
|
|
2230
|
-
|
|
3843
|
+
user: orgUser,
|
|
2231
3844
|
tokens: sessionTokens
|
|
2232
3845
|
});
|
|
2233
|
-
var
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
email:
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
3846
|
+
var waitlistRequest = import_zod13.z.object({ email: import_zod13.z.email().max(254) });
|
|
3847
|
+
var developerLoginRequest = import_zod13.z.object({
|
|
3848
|
+
email: import_zod13.z.email(),
|
|
3849
|
+
password: import_zod13.z.string().min(1)
|
|
3850
|
+
});
|
|
3851
|
+
var mailStatus = import_zod13.z.enum(["sent", "not_configured", "failed"]);
|
|
3852
|
+
var createUserInviteRequest = import_zod13.z.object({
|
|
3853
|
+
email: import_zod13.z.email(),
|
|
3854
|
+
display_name: import_zod13.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable().optional(),
|
|
3855
|
+
group_id: import_zod13.z.uuid(),
|
|
3856
|
+
/** Org Admins group only — see above; the cloud, not the schema, enforces the pairing. */
|
|
3857
|
+
tier: orgAdminTier.optional(),
|
|
3858
|
+
/** Absent means `default`: follow the group. Data model only until the MCP plan. */
|
|
3859
|
+
mcp_access: mcpAccess.optional(),
|
|
3860
|
+
send_mail: import_zod13.z.boolean()
|
|
3861
|
+
}).strict();
|
|
3862
|
+
var userInvite = import_zod13.z.object({
|
|
3863
|
+
id: import_zod13.z.uuid(),
|
|
3864
|
+
email: import_zod13.z.email(),
|
|
3865
|
+
group_id: import_zod13.z.uuid(),
|
|
3866
|
+
expires_at: import_zod13.z.iso.datetime(),
|
|
3867
|
+
/** Bounded like `idpIssuer`: an unbounded URL on a shape that gets mailed, logged and rendered is a size nobody chose. */
|
|
3868
|
+
accept_url: import_zod13.z.url().max(500),
|
|
2252
3869
|
/** Replaces `mail_sent: boolean` — see `mailStatus` for why one bit was not enough. */
|
|
2253
3870
|
mail: mailStatus
|
|
2254
3871
|
});
|
|
2255
|
-
var
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
});
|
|
2261
|
-
var acceptInvitationRequest = import_zod12.z.object({
|
|
2262
|
-
token: import_zod12.z.string().min(1),
|
|
2263
|
-
password
|
|
2264
|
-
});
|
|
2265
|
-
var createDeveloperInvitationRequest = import_zod12.z.object({
|
|
2266
|
-
email: import_zod12.z.email(),
|
|
2267
|
-
role: orgMemberRole,
|
|
2268
|
-
send_mail: import_zod12.z.boolean()
|
|
2269
|
-
});
|
|
2270
|
-
var developerInvitation = import_zod12.z.object({
|
|
2271
|
-
id: import_zod12.z.uuid(),
|
|
2272
|
-
email: import_zod12.z.email(),
|
|
2273
|
-
role: orgMemberRole,
|
|
2274
|
-
expires_at: import_zod12.z.iso.datetime(),
|
|
2275
|
-
accept_url: import_zod12.z.url(),
|
|
2276
|
-
mail: mailStatus
|
|
2277
|
-
});
|
|
2278
|
-
var pendingDeveloperInvitation = import_zod12.z.object({
|
|
2279
|
-
id: import_zod12.z.uuid(),
|
|
2280
|
-
email: import_zod12.z.email(),
|
|
2281
|
-
role: orgMemberRole,
|
|
2282
|
-
expires_at: import_zod12.z.iso.datetime()
|
|
3872
|
+
var pendingUserInvite = import_zod13.z.object({
|
|
3873
|
+
id: import_zod13.z.uuid(),
|
|
3874
|
+
email: import_zod13.z.email(),
|
|
3875
|
+
group_id: import_zod13.z.uuid(),
|
|
3876
|
+
expires_at: import_zod13.z.iso.datetime()
|
|
2283
3877
|
});
|
|
2284
|
-
var
|
|
3878
|
+
var userInviteListResponse = import_zod13.z.object({
|
|
2285
3879
|
/** Pending only. An accepted invitation is history, not something to revoke. */
|
|
2286
|
-
invitations:
|
|
3880
|
+
invitations: import_zod13.z.array(pendingUserInvite)
|
|
2287
3881
|
});
|
|
2288
|
-
var
|
|
2289
|
-
token:
|
|
3882
|
+
var acceptUserInviteRequest = import_zod13.z.object({
|
|
3883
|
+
token: import_zod13.z.string().min(1),
|
|
2290
3884
|
password
|
|
2291
3885
|
});
|
|
2292
|
-
var
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
3886
|
+
var patchUserRequest = import_zod13.z.object({
|
|
3887
|
+
display_name: import_zod13.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable().optional(),
|
|
3888
|
+
mcp_access: mcpAccess.optional()
|
|
3889
|
+
}).strict();
|
|
3890
|
+
var moveUserGroupRequest = import_zod13.z.object({
|
|
3891
|
+
group_id: import_zod13.z.uuid(),
|
|
3892
|
+
acknowledge_assignment_loss: import_zod13.z.literal(true)
|
|
3893
|
+
}).strict();
|
|
3894
|
+
var putAppGroupRequest = import_zod13.z.object({
|
|
3895
|
+
group_id: import_zod13.z.uuid(),
|
|
3896
|
+
acknowledge_assignment_loss: import_zod13.z.literal(true)
|
|
3897
|
+
}).strict();
|
|
3898
|
+
var putAssignmentRequest = import_zod13.z.object({ role_id: import_zod13.z.uuid() }).strict();
|
|
3899
|
+
var tierChangeRequest = import_zod13.z.object({ tier: orgAdminTier }).strict();
|
|
3900
|
+
var groupUsageResponse = import_zod13.z.object({
|
|
3901
|
+
/** The group the change would move the subject **into**. */
|
|
3902
|
+
target_group_id: import_zod13.z.uuid(),
|
|
3903
|
+
/** Assignments the change would delete. `0` means the change is not destructive. */
|
|
3904
|
+
assignments_removed: import_zod13.z.number().int().nonnegative(),
|
|
2299
3905
|
/**
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
3906
|
+
* Distinct users who would lose access. For a *user* move this is 0 or 1 and
|
|
3907
|
+
* adds nothing to `assignments_removed`; it exists for the *app re-link*,
|
|
3908
|
+
* where one edit can cut many people off and a count of assignments alone
|
|
3909
|
+
* reads far smaller than the thing actually being decided.
|
|
2303
3910
|
*/
|
|
2304
|
-
|
|
2305
|
-
/**
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
app_identifier: appIdentifier,
|
|
2312
|
-
email: import_zod12.z.email(),
|
|
2313
|
-
password
|
|
2314
|
-
});
|
|
2315
|
-
var clientRegisterResponse = import_zod12.z.object({
|
|
2316
|
-
mail: mailStatus
|
|
3911
|
+
users_affected: import_zod13.z.number().int().nonnegative(),
|
|
3912
|
+
/**
|
|
3913
|
+
* The apps whose assignments would die, by identifier — a bare count cannot
|
|
3914
|
+
* be read by whoever has to approve it. For an app re-link this is the one
|
|
3915
|
+
* app being moved.
|
|
3916
|
+
*/
|
|
3917
|
+
app_identifiers: import_zod13.z.array(import_zod13.z.string())
|
|
2317
3918
|
});
|
|
2318
|
-
var
|
|
2319
|
-
|
|
3919
|
+
var createGroupRequest = import_zod13.z.object({
|
|
3920
|
+
name: import_zod13.z.string().min(1).max(GROUP_NAME_MAX),
|
|
3921
|
+
mcp_enabled: import_zod13.z.boolean().default(false)
|
|
3922
|
+
}).strict();
|
|
3923
|
+
var patchGroupRequest = import_zod13.z.object({
|
|
3924
|
+
name: import_zod13.z.string().min(1).max(GROUP_NAME_MAX).optional(),
|
|
3925
|
+
mcp_enabled: import_zod13.z.boolean().optional()
|
|
3926
|
+
}).strict();
|
|
3927
|
+
var tierRequiredDetails = import_zod13.z.object({
|
|
3928
|
+
required: orgAdminTier,
|
|
3929
|
+
/** The caller's own tier — theirs to know, and it is what makes the message actionable. */
|
|
3930
|
+
actual: orgAdminTier
|
|
2320
3931
|
});
|
|
2321
|
-
var passwordChangeRequest =
|
|
2322
|
-
current_password:
|
|
3932
|
+
var passwordChangeRequest = import_zod13.z.object({
|
|
3933
|
+
current_password: import_zod13.z.string().min(1),
|
|
2323
3934
|
new_password: password
|
|
2324
3935
|
});
|
|
2325
|
-
var passwordResetRequest =
|
|
2326
|
-
email:
|
|
3936
|
+
var passwordResetRequest = import_zod13.z.object({
|
|
3937
|
+
email: import_zod13.z.email()
|
|
2327
3938
|
});
|
|
2328
|
-
var clientPasswordResetRequest =
|
|
3939
|
+
var clientPasswordResetRequest = import_zod13.z.object({
|
|
2329
3940
|
app_identifier: appIdentifier,
|
|
2330
|
-
email:
|
|
3941
|
+
email: import_zod13.z.email()
|
|
2331
3942
|
});
|
|
2332
|
-
var passwordResetConfirm =
|
|
2333
|
-
token:
|
|
3943
|
+
var passwordResetConfirm = import_zod13.z.object({
|
|
3944
|
+
token: import_zod13.z.string().min(1),
|
|
2334
3945
|
new_password: password
|
|
2335
3946
|
});
|
|
2336
|
-
var idpClaimMapping =
|
|
3947
|
+
var idpClaimMapping = import_zod13.z.object({
|
|
2337
3948
|
/** Which claim is the stable identity. `sub` unless the developer knows better. */
|
|
2338
|
-
subject_claim:
|
|
2339
|
-
email_claim:
|
|
3949
|
+
subject_claim: import_zod13.z.string().min(1).max(100).default("sub"),
|
|
3950
|
+
email_claim: import_zod13.z.string().min(1).max(100).default("email")
|
|
2340
3951
|
});
|
|
2341
|
-
var idpIssuer =
|
|
3952
|
+
var idpIssuer = import_zod13.z.url().max(500).refine((v) => {
|
|
2342
3953
|
let url;
|
|
2343
3954
|
try {
|
|
2344
3955
|
url = new URL(v);
|
|
@@ -2353,37 +3964,109 @@ var idpIssuer = import_zod12.z.url().max(500).refine((v) => {
|
|
|
2353
3964
|
return false;
|
|
2354
3965
|
return url.hostname.length > 0;
|
|
2355
3966
|
}, { message: "issuer must be an http(s) URL with no credentials, query or fragment" });
|
|
2356
|
-
var idpConfig =
|
|
2357
|
-
app_id:
|
|
3967
|
+
var idpConfig = import_zod13.z.object({
|
|
3968
|
+
app_id: import_zod13.z.uuid(),
|
|
2358
3969
|
issuer: idpIssuer,
|
|
2359
|
-
client_id:
|
|
2360
|
-
scopes:
|
|
3970
|
+
client_id: import_zod13.z.string().min(1).max(200),
|
|
3971
|
+
scopes: import_zod13.z.array(import_zod13.z.string().min(1).max(60)).min(1).max(20),
|
|
2361
3972
|
claims: idpClaimMapping,
|
|
2362
|
-
link_verified_emails: import_zod12.z.boolean(),
|
|
2363
3973
|
/** Never the secret itself — see `idpConfigRequest`. */
|
|
2364
|
-
has_client_secret:
|
|
2365
|
-
updated_at:
|
|
3974
|
+
has_client_secret: import_zod13.z.boolean(),
|
|
3975
|
+
updated_at: import_zod13.z.iso.datetime()
|
|
2366
3976
|
});
|
|
2367
|
-
var idpConfigRequest =
|
|
3977
|
+
var idpConfigRequest = import_zod13.z.object({
|
|
2368
3978
|
issuer: idpIssuer,
|
|
2369
|
-
client_id:
|
|
2370
|
-
client_secret:
|
|
2371
|
-
scopes:
|
|
2372
|
-
claims: idpClaimMapping.optional()
|
|
2373
|
-
link_verified_emails: import_zod12.z.boolean()
|
|
3979
|
+
client_id: import_zod13.z.string().min(1).max(200),
|
|
3980
|
+
client_secret: import_zod13.z.string().min(1).max(500).optional(),
|
|
3981
|
+
scopes: import_zod13.z.array(import_zod13.z.string().min(1).max(60)).min(1).max(20),
|
|
3982
|
+
claims: idpClaimMapping.optional()
|
|
2374
3983
|
}).strict();
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
3984
|
+
var orgFederationPolicy = import_zod13.z.object({
|
|
3985
|
+
/**
|
|
3986
|
+
* Joins a federated login to an existing integrated account **only if** the
|
|
3987
|
+
* IdP also asserts `email_verified`. Either condition alone is account
|
|
3988
|
+
* takeover: without `email_verified`, an IdP that lets anyone type any
|
|
3989
|
+
* address into a profile hands over every matching account; without this
|
|
3990
|
+
* flag, a developer who connects an IdP for a *subset* of their users
|
|
3991
|
+
* silently merges strangers.
|
|
3992
|
+
*/
|
|
3993
|
+
link_verified_emails: import_zod13.z.boolean(),
|
|
3994
|
+
updated_at: import_zod13.z.iso.datetime()
|
|
2382
3995
|
});
|
|
2383
|
-
var
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
3996
|
+
var orgFederationPolicyRequest = import_zod13.z.object({
|
|
3997
|
+
link_verified_emails: import_zod13.z.boolean()
|
|
3998
|
+
}).strict();
|
|
3999
|
+
var jitGrant = import_zod13.z.object({
|
|
4000
|
+
app_id: import_zod13.z.uuid(),
|
|
4001
|
+
role_id: import_zod13.z.uuid()
|
|
4002
|
+
}).strict();
|
|
4003
|
+
var groupOidcProvider = import_zod13.z.object({
|
|
4004
|
+
group_id: import_zod13.z.uuid(),
|
|
4005
|
+
issuer: idpIssuer,
|
|
4006
|
+
client_id: import_zod13.z.string().min(1).max(200),
|
|
4007
|
+
scopes: import_zod13.z.array(import_zod13.z.string().min(1).max(60)).min(1).max(20),
|
|
4008
|
+
/** Whether an unknown `(issuer, sub)` is provisioned rather than refused (D3). */
|
|
4009
|
+
jit_enabled: import_zod13.z.boolean(),
|
|
4010
|
+
/**
|
|
4011
|
+
* The grants an unknown user receives on their first federated login.
|
|
4012
|
+
* Bounded for the reason `scopes` and `accept_url` are: an unbounded array
|
|
4013
|
+
* on a shape that is stored, logged and rendered is a size nobody chose.
|
|
4014
|
+
*/
|
|
4015
|
+
jit_grants: import_zod13.z.array(jitGrant).max(50),
|
|
4016
|
+
created_at: import_zod13.z.iso.datetime()
|
|
4017
|
+
}).strict();
|
|
4018
|
+
var putGroupOidcProviderRequest = import_zod13.z.object({
|
|
4019
|
+
issuer: idpIssuer,
|
|
4020
|
+
client_id: import_zod13.z.string().min(1).max(200),
|
|
4021
|
+
/** Write-only; absent means keep the stored secret. Never echoed by `groupOidcProvider`. */
|
|
4022
|
+
client_secret: import_zod13.z.string().min(16).max(500).optional(),
|
|
4023
|
+
scopes: import_zod13.z.array(import_zod13.z.string().min(1).max(60)).min(1).max(20),
|
|
4024
|
+
jit_enabled: import_zod13.z.boolean().default(false),
|
|
4025
|
+
jit_grants: import_zod13.z.array(jitGrant).max(50).default([])
|
|
4026
|
+
}).strict();
|
|
4027
|
+
var oidcCallbackErrorCode = import_zod13.z.enum([
|
|
4028
|
+
/** The IdP could not be reached, or its discovery document could not be read. */
|
|
4029
|
+
"idp_unreachable",
|
|
4030
|
+
/** The authorization code could not be exchanged for tokens (`invalid_grant` and friends). */
|
|
4031
|
+
"exchange_failed",
|
|
4032
|
+
/** The IdP answered, but the token is missing a `sub` or `email` the flow needs. */
|
|
4033
|
+
"claims_incomplete",
|
|
4034
|
+
/** An unknown identity on a group with JIT off — no route in, by design (D3). */
|
|
4035
|
+
"jit_disabled",
|
|
4036
|
+
/**
|
|
4037
|
+
* JIT would provision, but the asserted email already belongs to a user —
|
|
4038
|
+
* of this org or any other, since email is globally unique (Andre,
|
|
4039
|
+
* 2026-08-29). **Refusal, never auto-link** (account-takeover guard, D3);
|
|
4040
|
+
* resolution is manual, by an org admin.
|
|
4041
|
+
*/
|
|
4042
|
+
"email_collision",
|
|
4043
|
+
/** The provider is set up wrong (bad client, secret rejected) — the developer's to fix. */
|
|
4044
|
+
"provider_misconfigured"
|
|
4045
|
+
]);
|
|
4046
|
+
var oidcCallbackError = import_zod13.z.object({
|
|
4047
|
+
code: oidcCallbackErrorCode,
|
|
4048
|
+
/** Safe user-facing text — no issuer, no token internals; bounded because it is rendered. */
|
|
4049
|
+
message: import_zod13.z.string().min(1).max(300)
|
|
4050
|
+
}).strict();
|
|
4051
|
+
var impersonationChoice = import_zod13.z.discriminatedUnion("mode", [
|
|
4052
|
+
import_zod13.z.object({ mode: import_zod13.z.literal("role"), role_id: import_zod13.z.uuid() }).strict(),
|
|
4053
|
+
import_zod13.z.object({ mode: import_zod13.z.literal("user"), user_id: import_zod13.z.uuid() }).strict()
|
|
4054
|
+
]);
|
|
4055
|
+
var authMeResponse = import_zod13.z.object({ org, user: orgUser });
|
|
4056
|
+
var patchOrgRequest = import_zod13.z.object({ name: import_zod13.z.string().min(1).max(120) }).strict();
|
|
4057
|
+
var patchAuthMeRequest = import_zod13.z.object({ display_name: import_zod13.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable() }).strict();
|
|
4058
|
+
|
|
4059
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/audit.js
|
|
4060
|
+
var import_zod14 = require("zod");
|
|
4061
|
+
var auditActor = import_zod14.z.object({
|
|
4062
|
+
kind: import_zod14.z.enum(["developer", "end_user", "server_key", "bridge"]),
|
|
4063
|
+
id: import_zod14.z.uuid(),
|
|
4064
|
+
label: import_zod14.z.string().min(1).max(200)
|
|
4065
|
+
});
|
|
4066
|
+
var auditEvent = import_zod14.z.object({
|
|
4067
|
+
id: import_zod14.z.uuid(),
|
|
4068
|
+
org_id: import_zod14.z.uuid(),
|
|
4069
|
+
at: import_zod14.z.iso.datetime(),
|
|
2387
4070
|
/**
|
|
2388
4071
|
* A monotonic counter, ascending in write order, unique across the log
|
|
2389
4072
|
* (W6b).
|
|
@@ -2407,46 +4090,139 @@ var auditEvent = import_zod13.z.object({
|
|
|
2407
4090
|
* Required, not optional: an event without a sequence cannot be ordered
|
|
2408
4091
|
* against one that has it, and a log with two orderings has none.
|
|
2409
4092
|
*/
|
|
2410
|
-
seq:
|
|
4093
|
+
seq: import_zod14.z.number().int().positive(),
|
|
2411
4094
|
actor: auditActor,
|
|
2412
4095
|
/** Stable dotted name, e.g. `end_user.invited`, `config.published`. */
|
|
2413
|
-
action:
|
|
4096
|
+
action: import_zod14.z.string().min(1).max(80),
|
|
2414
4097
|
/**
|
|
2415
4098
|
* What the action was about, if anything — a robot, an app, a user. Free
|
|
2416
4099
|
* of ids the console cannot resolve: carry the label with it.
|
|
2417
4100
|
*/
|
|
2418
|
-
target:
|
|
2419
|
-
kind:
|
|
2420
|
-
id:
|
|
2421
|
-
label:
|
|
4101
|
+
target: import_zod14.z.object({
|
|
4102
|
+
kind: import_zod14.z.string().min(1).max(40),
|
|
4103
|
+
id: import_zod14.z.string().min(1),
|
|
4104
|
+
label: import_zod14.z.string().min(1).max(200)
|
|
2422
4105
|
}).nullable(),
|
|
2423
|
-
/**
|
|
2424
|
-
|
|
4106
|
+
/**
|
|
4107
|
+
* Action-specific extras.
|
|
4108
|
+
*
|
|
4109
|
+
* **Nothing redacts this.** There is no denylist, no allowlist and no pass
|
|
4110
|
+
* over what a call site puts here — the call site is responsible, and this
|
|
4111
|
+
* comment is where that responsibility is written down. Since the org event
|
|
4112
|
+
* stream, the same object also reaches every developer with the console
|
|
4113
|
+
* overview open, not only whoever later reads the audit log.
|
|
4114
|
+
*
|
|
4115
|
+
* So: never credentials, never tokens. That is a rule, not a guarantee the
|
|
4116
|
+
* schema enforces.
|
|
4117
|
+
*/
|
|
4118
|
+
details: import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()).nullable()
|
|
2425
4119
|
});
|
|
2426
|
-
var
|
|
2427
|
-
|
|
4120
|
+
var auditTimestampMs = wireTimestampMs;
|
|
4121
|
+
var auditQuery = import_zod14.z.object({
|
|
4122
|
+
/** Only events with a smaller `seq` — the next, older page. */
|
|
4123
|
+
before_seq: wireSeqCursor.optional(),
|
|
4124
|
+
/**
|
|
4125
|
+
* Same shape as DEF-059's `historyQuery.limit`: a union whose input branch
|
|
4126
|
+
* **is the wire**. A `z.coerce` cannot be published — zod renders the
|
|
4127
|
+
* coercion's result in either `io` direction, so the artifact would describe
|
|
4128
|
+
* a shape a query string can never carry.
|
|
4129
|
+
*/
|
|
4130
|
+
limit: import_zod14.z.union([import_zod14.z.string().regex(/^\d{1,4}$/), import_zod14.z.number().int()]).transform((v) => Number(v)).pipe(import_zod14.z.number().int().positive().max(500)).optional(),
|
|
4131
|
+
/** Exact action name, e.g. `config.published`. No prefix matching: a filter that matches more than it says is not one. */
|
|
4132
|
+
action: import_zod14.z.string().min(1).max(80).optional(),
|
|
4133
|
+
/**
|
|
4134
|
+
* Everything under a dotted prefix, e.g. `server_key.` for all three
|
|
4135
|
+
* server-key actions.
|
|
4136
|
+
*
|
|
4137
|
+
* **A separate parameter, not a widening of `action`.** The sentence on
|
|
4138
|
+
* `action` above — a filter that matches more than it says is not one —
|
|
4139
|
+
* still stands; this is a different question with a name that says which
|
|
4140
|
+
* one it is. Setting both is refused rather than resolved, because a query
|
|
4141
|
+
* naming an exact action *and* a prefix is a caller mistake, not a
|
|
4142
|
+
* combination anyone should have to guess the meaning of.
|
|
4143
|
+
*
|
|
4144
|
+
* **The published artifact cannot express that refusal**: a cross-field
|
|
4145
|
+
* `.refine()` has no JSON Schema rendering, so `audit-query.schema.json`
|
|
4146
|
+
* describes two independent optional strings and validates both-at-once
|
|
4147
|
+
* happily. The cloud is the only enforcement point — the same residual
|
|
4148
|
+
* `orgLatencyQuery` and `orgUsageQuery` already name.
|
|
4149
|
+
*/
|
|
4150
|
+
action_prefix: import_zod14.z.string().min(1).max(80).optional(),
|
|
4151
|
+
/**
|
|
4152
|
+
* Only events by this actor.
|
|
4153
|
+
*
|
|
4154
|
+
* **`z.uuid()`, because the column is one (Argus-W9, W9 review).** This was
|
|
4155
|
+
* `z.string().min(1).max(200)`, so any non-uuid value reached Postgres as a
|
|
4156
|
+
* uuid parameter and threw: `?actor_id=not-a-uuid` answered **500
|
|
4157
|
+
* `internal_error`**, on the list route and the export alike.
|
|
4158
|
+
*
|
|
4159
|
+
* Not a SQL-injection finding — Drizzle parameterises, and `' or 1=1--`
|
|
4160
|
+
* failed at the same cast. It is a **500 where a 400 belongs**, and a 500 is
|
|
4161
|
+
* the answer that explains nothing.
|
|
4162
|
+
*
|
|
4163
|
+
* The place is the part worth keeping: **this same wave pulled
|
|
4164
|
+
* `refuseIfNotUuid` through ~15 call sites** so a typo could be told from a
|
|
4165
|
+
* deletion — and the brand-new filter, whose field has exactly that shape,
|
|
4166
|
+
* is the one that did not get it. A rule applied to the sites in front of
|
|
4167
|
+
* you is not a rule applied to the class.
|
|
4168
|
+
*/
|
|
4169
|
+
actor_id: import_zod14.z.uuid().optional(),
|
|
4170
|
+
/** Only events about this kind of target, e.g. `robot`. */
|
|
4171
|
+
target_kind: import_zod14.z.string().min(1).max(40).optional(),
|
|
4172
|
+
/**
|
|
4173
|
+
* Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
|
|
4174
|
+
* same rule the history shapes follow (DEF-062).
|
|
4175
|
+
*
|
|
4176
|
+
* **Bounded to years 1..9999, and the bound is borrowed rather than
|
|
4177
|
+
* invented.** `nonnegative()` alone let `253402300800000` (year 10000)
|
|
4178
|
+
* through, where the Postgres bind path has no representation and the route
|
|
4179
|
+
* answered 500 — measured either side of the edge: `253402300799000` → 200,
|
|
4180
|
+
* `253402300800000` → 500 (Argus-W9). `history-query.ts`'s `parseTimeExprMs`
|
|
4181
|
+
* already carries exactly this range, with M3's reasoning for why
|
|
4182
|
+
* `Number.isSafeInteger` is wider than what a timestamp can be; this is that
|
|
4183
|
+
* same number, not a second one that happens to agree.
|
|
4184
|
+
*/
|
|
4185
|
+
from_ms: auditTimestampMs.optional(),
|
|
4186
|
+
to_ms: auditTimestampMs.optional()
|
|
4187
|
+
}).strict().refine((query) => !(query.action !== void 0 && query.action_prefix !== void 0), {
|
|
4188
|
+
message: "action and action_prefix cannot be combined",
|
|
4189
|
+
path: ["action_prefix"]
|
|
4190
|
+
});
|
|
4191
|
+
var auditListResponse = import_zod14.z.object({
|
|
4192
|
+
events: import_zod14.z.array(auditEvent),
|
|
4193
|
+
/**
|
|
4194
|
+
* The `seq` a caller sends as `before_seq` to keep reading — or `null` when
|
|
4195
|
+
* there is nothing further.
|
|
4196
|
+
*
|
|
4197
|
+
* **`null` means the end, and that is a promise rather than an
|
|
4198
|
+
* observation.** A caller who instead compares `events.length` against
|
|
4199
|
+
* `limit` is wrong the moment a filter makes a page thin: a short page does
|
|
4200
|
+
* not mean *no more* here. The same distinction `historySamples` was given
|
|
4201
|
+
* `truncated` for.
|
|
4202
|
+
*/
|
|
4203
|
+
next_cursor: import_zod14.z.number().int().positive().nullable()
|
|
2428
4204
|
});
|
|
2429
4205
|
|
|
2430
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
2431
|
-
var
|
|
2432
|
-
var apiError =
|
|
2433
|
-
code:
|
|
2434
|
-
message:
|
|
2435
|
-
details:
|
|
4206
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/errors.js
|
|
4207
|
+
var import_zod15 = require("zod");
|
|
4208
|
+
var apiError = import_zod15.z.object({
|
|
4209
|
+
code: import_zod15.z.string().min(1),
|
|
4210
|
+
message: import_zod15.z.string().min(1),
|
|
4211
|
+
details: import_zod15.z.unknown().optional()
|
|
2436
4212
|
});
|
|
2437
|
-
var parameterViolation =
|
|
2438
|
-
field:
|
|
4213
|
+
var parameterViolation = import_zod15.z.object({
|
|
4214
|
+
field: import_zod15.z.string().min(1),
|
|
2439
4215
|
/** Which rule failed — `min`, `max`, `enum`, `pattern`, `required`, `undeclared`. */
|
|
2440
|
-
rule:
|
|
2441
|
-
message:
|
|
4216
|
+
rule: import_zod15.z.string().min(1),
|
|
4217
|
+
message: import_zod15.z.string().min(1)
|
|
2442
4218
|
});
|
|
2443
|
-
var parameterInvalidDetails =
|
|
2444
|
-
violations:
|
|
4219
|
+
var parameterInvalidDetails = import_zod15.z.object({
|
|
4220
|
+
violations: import_zod15.z.array(parameterViolation).min(1)
|
|
2445
4221
|
});
|
|
2446
4222
|
|
|
2447
|
-
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-
|
|
2448
|
-
var
|
|
2449
|
-
var oauthErrorCode =
|
|
4223
|
+
// node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_1727a3f05719ef3ec49509ea01c84aa4/node_modules/@fleetless/contracts/dist/oauth.js
|
|
4224
|
+
var import_zod16 = require("zod");
|
|
4225
|
+
var oauthErrorCode = import_zod16.z.enum([
|
|
2450
4226
|
"invalid_request",
|
|
2451
4227
|
"invalid_client",
|
|
2452
4228
|
"invalid_grant",
|
|
@@ -2459,11 +4235,11 @@ var oauthErrorCode = import_zod15.z.enum([
|
|
|
2459
4235
|
/** RFC 8707: the `resource` named is not one this server issues tokens for. */
|
|
2460
4236
|
"invalid_target"
|
|
2461
4237
|
]);
|
|
2462
|
-
var oauthError =
|
|
4238
|
+
var oauthError = import_zod16.z.object({
|
|
2463
4239
|
error: oauthErrorCode,
|
|
2464
|
-
error_description:
|
|
4240
|
+
error_description: import_zod16.z.string().min(1).max(500).optional(),
|
|
2465
4241
|
/** Echoed back per RFC 6749 §4.1.2.1 so a client can match the response. */
|
|
2466
|
-
state:
|
|
4242
|
+
state: import_zod16.z.string().min(1).max(500).optional(),
|
|
2467
4243
|
/**
|
|
2468
4244
|
* **A Fleetless reason carried inside a standard envelope, and it exists
|
|
2469
4245
|
* because the alternative lost a distinction.**
|
|
@@ -2481,10 +4257,10 @@ var oauthError = import_zod15.z.object({
|
|
|
2481
4257
|
* our own tooling switches on. RFC 6749 §5.2 permits additional members, and
|
|
2482
4258
|
* a client that ignores this one still behaves correctly.
|
|
2483
4259
|
*/
|
|
2484
|
-
fleetless_code:
|
|
4260
|
+
fleetless_code: import_zod16.z.string().min(1).max(60).optional()
|
|
2485
4261
|
});
|
|
2486
|
-
var oauthClientRegistration =
|
|
2487
|
-
var redirectUri =
|
|
4262
|
+
var oauthClientRegistration = import_zod16.z.enum(["developer", "dynamic"]);
|
|
4263
|
+
var redirectUri = import_zod16.z.string().min(1).max(2e3).refine((v) => {
|
|
2488
4264
|
let url;
|
|
2489
4265
|
try {
|
|
2490
4266
|
url = new URL(v);
|
|
@@ -2499,16 +4275,16 @@ var redirectUri = import_zod15.z.string().min(1).max(2e3).refine((v) => {
|
|
|
2499
4275
|
return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
2500
4276
|
return false;
|
|
2501
4277
|
}, { message: "redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment" });
|
|
2502
|
-
var codeChallengeMethod =
|
|
2503
|
-
var oauthClient =
|
|
2504
|
-
id:
|
|
2505
|
-
app_id:
|
|
4278
|
+
var codeChallengeMethod = import_zod16.z.enum(["S256"]);
|
|
4279
|
+
var oauthClient = import_zod16.z.object({
|
|
4280
|
+
id: import_zod16.z.uuid(),
|
|
4281
|
+
app_id: import_zod16.z.uuid(),
|
|
2506
4282
|
/** The `client_id` on the wire — opaque, and not the app identifier. */
|
|
2507
|
-
client_id:
|
|
2508
|
-
client_name:
|
|
4283
|
+
client_id: import_zod16.z.string().min(1).max(200),
|
|
4284
|
+
client_name: import_zod16.z.string().min(1).max(200),
|
|
2509
4285
|
registration: oauthClientRegistration,
|
|
2510
|
-
redirect_uris:
|
|
2511
|
-
created_at:
|
|
4286
|
+
redirect_uris: import_zod16.z.array(redirectUri).min(1).max(20),
|
|
4287
|
+
created_at: import_zod16.z.iso.datetime(),
|
|
2512
4288
|
/**
|
|
2513
4289
|
* **Only `dynamic` clients expire, and the field is nullable rather than
|
|
2514
4290
|
* absent so a consumer must decide what it means.** A self-registered client
|
|
@@ -2516,106 +4292,152 @@ var oauthClient = import_zod15.z.object({
|
|
|
2516
4292
|
* behind; a developer's own client is a configured thing that should not
|
|
2517
4293
|
* vanish under them.
|
|
2518
4294
|
*/
|
|
2519
|
-
expires_at:
|
|
2520
|
-
last_used_at:
|
|
4295
|
+
expires_at: import_zod16.z.iso.datetime().nullable(),
|
|
4296
|
+
last_used_at: import_zod16.z.iso.datetime().nullable()
|
|
2521
4297
|
});
|
|
2522
|
-
var dynamicClientRegistrationRequest =
|
|
2523
|
-
client_name:
|
|
2524
|
-
redirect_uris:
|
|
4298
|
+
var dynamicClientRegistrationRequest = import_zod16.z.object({
|
|
4299
|
+
client_name: import_zod16.z.string().min(1).max(200),
|
|
4300
|
+
redirect_uris: import_zod16.z.array(redirectUri).min(1).max(20),
|
|
2525
4301
|
/** Accepted and echoed for conformance; this server issues only this pair. */
|
|
2526
|
-
grant_types:
|
|
2527
|
-
response_types:
|
|
4302
|
+
grant_types: import_zod16.z.array(import_zod16.z.enum(["authorization_code", "refresh_token"])).optional(),
|
|
4303
|
+
response_types: import_zod16.z.array(import_zod16.z.enum(["code"])).optional(),
|
|
2528
4304
|
/** RFC 7591 allows `none` for public clients; OAuth 2.1 + PKCE is the defence. */
|
|
2529
|
-
token_endpoint_auth_method:
|
|
2530
|
-
scope:
|
|
4305
|
+
token_endpoint_auth_method: import_zod16.z.enum(["none"]).optional(),
|
|
4306
|
+
scope: import_zod16.z.string().max(500).optional()
|
|
2531
4307
|
}).strict();
|
|
2532
|
-
var dynamicClientRegistrationResponse =
|
|
2533
|
-
client_id:
|
|
2534
|
-
client_name:
|
|
2535
|
-
redirect_uris:
|
|
2536
|
-
grant_types:
|
|
2537
|
-
response_types:
|
|
2538
|
-
token_endpoint_auth_method:
|
|
2539
|
-
client_id_issued_at:
|
|
4308
|
+
var dynamicClientRegistrationResponse = import_zod16.z.object({
|
|
4309
|
+
client_id: import_zod16.z.string().min(1).max(200),
|
|
4310
|
+
client_name: import_zod16.z.string().min(1).max(200),
|
|
4311
|
+
redirect_uris: import_zod16.z.array(redirectUri),
|
|
4312
|
+
grant_types: import_zod16.z.array(import_zod16.z.string()),
|
|
4313
|
+
response_types: import_zod16.z.array(import_zod16.z.string()),
|
|
4314
|
+
token_endpoint_auth_method: import_zod16.z.literal("none"),
|
|
4315
|
+
client_id_issued_at: import_zod16.z.number().int().nonnegative(),
|
|
2540
4316
|
/** Seconds since the epoch, per RFC 7591. `0` would mean "never expires". */
|
|
2541
|
-
client_secret_expires_at:
|
|
4317
|
+
client_secret_expires_at: import_zod16.z.literal(0)
|
|
2542
4318
|
});
|
|
2543
|
-
var oauthTokenRequest =
|
|
2544
|
-
|
|
2545
|
-
grant_type:
|
|
2546
|
-
code:
|
|
4319
|
+
var oauthTokenRequest = import_zod16.z.discriminatedUnion("grant_type", [
|
|
4320
|
+
import_zod16.z.object({
|
|
4321
|
+
grant_type: import_zod16.z.literal("authorization_code"),
|
|
4322
|
+
code: import_zod16.z.string().min(1).max(500),
|
|
2547
4323
|
redirect_uri: redirectUri,
|
|
2548
|
-
client_id:
|
|
2549
|
-
code_verifier:
|
|
2550
|
-
resource:
|
|
4324
|
+
client_id: import_zod16.z.string().min(1).max(200),
|
|
4325
|
+
code_verifier: import_zod16.z.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)"),
|
|
4326
|
+
resource: import_zod16.z.url().optional()
|
|
2551
4327
|
}),
|
|
2552
|
-
|
|
2553
|
-
grant_type:
|
|
2554
|
-
refresh_token:
|
|
2555
|
-
client_id:
|
|
2556
|
-
resource:
|
|
4328
|
+
import_zod16.z.object({
|
|
4329
|
+
grant_type: import_zod16.z.literal("refresh_token"),
|
|
4330
|
+
refresh_token: import_zod16.z.string().min(1).max(500),
|
|
4331
|
+
client_id: import_zod16.z.string().min(1).max(200),
|
|
4332
|
+
resource: import_zod16.z.url().optional(),
|
|
2557
4333
|
/** RFC 6749 §6 — a refresh may narrow scope, never widen it. */
|
|
2558
|
-
scope:
|
|
4334
|
+
scope: import_zod16.z.string().max(500).optional()
|
|
2559
4335
|
})
|
|
2560
4336
|
]);
|
|
2561
|
-
var oauthTokenResponse =
|
|
2562
|
-
access_token:
|
|
2563
|
-
token_type:
|
|
4337
|
+
var oauthTokenResponse = import_zod16.z.object({
|
|
4338
|
+
access_token: import_zod16.z.string().min(1),
|
|
4339
|
+
token_type: import_zod16.z.literal("Bearer"),
|
|
2564
4340
|
/** Seconds, per RFC 6749 §5.1 — not a timestamp, and not milliseconds. */
|
|
2565
|
-
expires_in:
|
|
2566
|
-
refresh_token:
|
|
2567
|
-
scope:
|
|
2568
|
-
});
|
|
2569
|
-
var authorizationServerMetadata =
|
|
2570
|
-
issuer:
|
|
2571
|
-
authorization_endpoint:
|
|
2572
|
-
token_endpoint:
|
|
2573
|
-
registration_endpoint:
|
|
2574
|
-
response_types_supported:
|
|
2575
|
-
grant_types_supported:
|
|
2576
|
-
code_challenge_methods_supported:
|
|
2577
|
-
token_endpoint_auth_methods_supported:
|
|
2578
|
-
scopes_supported:
|
|
2579
|
-
});
|
|
2580
|
-
var protectedResourceMetadata =
|
|
2581
|
-
resource:
|
|
2582
|
-
authorization_servers:
|
|
2583
|
-
bearer_methods_supported:
|
|
2584
|
-
scopes_supported:
|
|
2585
|
-
});
|
|
2586
|
-
var consentGrant =
|
|
2587
|
-
client_id:
|
|
2588
|
-
app_id:
|
|
2589
|
-
end_user_id:
|
|
2590
|
-
role_id:
|
|
2591
|
-
scope:
|
|
2592
|
-
granted_at:
|
|
2593
|
-
});
|
|
2594
|
-
var
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
client_name:
|
|
4341
|
+
expires_in: import_zod16.z.number().int().positive(),
|
|
4342
|
+
refresh_token: import_zod16.z.string().min(1).optional(),
|
|
4343
|
+
scope: import_zod16.z.string().max(500).optional()
|
|
4344
|
+
});
|
|
4345
|
+
var authorizationServerMetadata = import_zod16.z.object({
|
|
4346
|
+
issuer: import_zod16.z.url(),
|
|
4347
|
+
authorization_endpoint: import_zod16.z.url(),
|
|
4348
|
+
token_endpoint: import_zod16.z.url(),
|
|
4349
|
+
registration_endpoint: import_zod16.z.url().optional(),
|
|
4350
|
+
response_types_supported: import_zod16.z.array(import_zod16.z.literal("code")),
|
|
4351
|
+
grant_types_supported: import_zod16.z.array(import_zod16.z.enum(["authorization_code", "refresh_token"])),
|
|
4352
|
+
code_challenge_methods_supported: import_zod16.z.array(codeChallengeMethod),
|
|
4353
|
+
token_endpoint_auth_methods_supported: import_zod16.z.array(import_zod16.z.literal("none")),
|
|
4354
|
+
scopes_supported: import_zod16.z.array(import_zod16.z.string()).optional()
|
|
4355
|
+
});
|
|
4356
|
+
var protectedResourceMetadata = import_zod16.z.object({
|
|
4357
|
+
resource: import_zod16.z.url(),
|
|
4358
|
+
authorization_servers: import_zod16.z.array(import_zod16.z.url()).min(1),
|
|
4359
|
+
bearer_methods_supported: import_zod16.z.array(import_zod16.z.literal("header")),
|
|
4360
|
+
scopes_supported: import_zod16.z.array(import_zod16.z.string()).optional()
|
|
4361
|
+
});
|
|
4362
|
+
var consentGrant = import_zod16.z.object({
|
|
4363
|
+
client_id: import_zod16.z.string().min(1).max(200),
|
|
4364
|
+
app_id: import_zod16.z.uuid(),
|
|
4365
|
+
end_user_id: import_zod16.z.uuid(),
|
|
4366
|
+
role_id: import_zod16.z.uuid(),
|
|
4367
|
+
scope: import_zod16.z.string().max(500),
|
|
4368
|
+
granted_at: import_zod16.z.iso.datetime()
|
|
4369
|
+
});
|
|
4370
|
+
var consentGrantSummary = import_zod16.z.object({
|
|
4371
|
+
client_id: import_zod16.z.string().min(1).max(200),
|
|
4372
|
+
/**
|
|
4373
|
+
* The client's own declared name. **Not trusted, and the console/page must
|
|
4374
|
+
* not render it as if Fleetless vouched for it** — a self-registered client
|
|
4375
|
+
* chooses this string, and W7c already measured what that buys: one called
|
|
4376
|
+
* itself *"Fleetless Official Helper"*.
|
|
4377
|
+
*/
|
|
4378
|
+
client_name: import_zod16.z.string().min(1).max(200),
|
|
4379
|
+
app_id: import_zod16.z.uuid(),
|
|
4380
|
+
app_name: import_zod16.z.string().min(1).max(120),
|
|
4381
|
+
role_id: import_zod16.z.uuid(),
|
|
4382
|
+
role_name: import_zod16.z.string().min(1).max(120),
|
|
4383
|
+
scope: import_zod16.z.string().max(500),
|
|
4384
|
+
granted_at: import_zod16.z.iso.datetime()
|
|
4385
|
+
});
|
|
4386
|
+
var consentGrantListResponse = import_zod16.z.object({
|
|
4387
|
+
grants: import_zod16.z.array(consentGrantSummary).max(200),
|
|
4388
|
+
/**
|
|
4389
|
+
* **`true` heisst: es gibt mehr, und diese Antwort zeigt sie nicht** (W9
|
|
4390
|
+
* review, Argus-W9; DEF-151).
|
|
4391
|
+
*
|
|
4392
|
+
* Das `.max(200)` oben war eine Grenze auf dem Draht und im Store keine —
|
|
4393
|
+
* ein Endnutzer ueber 200 bekam eine Antwort, die dieser Vertrag verbietet.
|
|
4394
|
+
* Das Limit nachzuziehen machte die Antwort **vertragskonform statt
|
|
4395
|
+
* vertragswidrig — und stumm statt vollstaendig**: wer 250 Grants hat, sah
|
|
4396
|
+
* 200 und erreichte die uebrigen 50 nie.
|
|
4397
|
+
*
|
|
4398
|
+
* Dieselbe Unterscheidung, die `historySamplesResponse` mit `truncated_by`
|
|
4399
|
+
* traegt und `auditListResponse` mit `next_cursor`: **eine kurze Seite
|
|
4400
|
+
* heisst nicht, dass nichts mehr da ist.** Sie fehlte ausgerechnet auf der
|
|
4401
|
+
* einen Liste, auf der ein Nutzer etwas **abstellen** will — dort ist
|
|
4402
|
+
* *"ich sehe es nicht"* und *"es gibt es nicht"* der teuerste aller
|
|
4403
|
+
* Unterschiede.
|
|
4404
|
+
*
|
|
4405
|
+
* Bewusst ein Boolean und kein Cursor: Blaettern waere eine Zusage ueber
|
|
4406
|
+
* Reihenfolge und Stabilitaet, die diese Liste heute nicht macht. `true`
|
|
4407
|
+
* sagt, was der Nutzer wissen muss — **hier fehlt etwas, frag jemanden** —
|
|
4408
|
+
* ohne einen Mechanismus zu versprechen, den es nicht gibt.
|
|
4409
|
+
*/
|
|
4410
|
+
truncated: import_zod16.z.boolean()
|
|
4411
|
+
});
|
|
4412
|
+
var consentRevokeResponse = import_zod16.z.object({
|
|
4413
|
+
revoked: import_zod16.z.boolean(),
|
|
4414
|
+
tokens_revoked: import_zod16.z.number().int().nonnegative()
|
|
4415
|
+
});
|
|
4416
|
+
var oauthInteraction = import_zod16.z.object({
|
|
4417
|
+
interaction_id: import_zod16.z.string().min(1).max(200),
|
|
4418
|
+
app_name: import_zod16.z.string().min(1).max(120),
|
|
4419
|
+
idp: import_zod16.z.object({ button_label: import_zod16.z.string().min(1).max(60) }).nullable()
|
|
4420
|
+
});
|
|
4421
|
+
var oauthConsentInteraction = import_zod16.z.object({
|
|
4422
|
+
interaction_id: import_zod16.z.string().min(1).max(200),
|
|
4423
|
+
app_name: import_zod16.z.string().min(1).max(120),
|
|
4424
|
+
client_name: import_zod16.z.string().min(1).max(200),
|
|
2603
4425
|
registration: oauthClientRegistration,
|
|
2604
|
-
role_name:
|
|
2605
|
-
scope:
|
|
4426
|
+
role_name: import_zod16.z.string().min(1).max(120),
|
|
4427
|
+
scope: import_zod16.z.string().max(500)
|
|
2606
4428
|
});
|
|
2607
|
-
var oauthLoginRequest =
|
|
2608
|
-
interaction_id:
|
|
2609
|
-
email:
|
|
2610
|
-
password:
|
|
4429
|
+
var oauthLoginRequest = import_zod16.z.object({
|
|
4430
|
+
interaction_id: import_zod16.z.string().min(1).max(200),
|
|
4431
|
+
email: import_zod16.z.email(),
|
|
4432
|
+
password: import_zod16.z.string().min(1)
|
|
2611
4433
|
}).strict();
|
|
2612
|
-
var oauthRedirectResponse =
|
|
2613
|
-
redirect_to:
|
|
4434
|
+
var oauthRedirectResponse = import_zod16.z.object({
|
|
4435
|
+
redirect_to: import_zod16.z.string().min(1).max(2e3)
|
|
2614
4436
|
});
|
|
2615
|
-
var consentDecision =
|
|
4437
|
+
var consentDecision = import_zod16.z.object({
|
|
2616
4438
|
/** The opaque handle the authorize step handed the consent screen. */
|
|
2617
|
-
interaction_id:
|
|
2618
|
-
approved:
|
|
4439
|
+
interaction_id: import_zod16.z.string().min(1).max(200),
|
|
4440
|
+
approved: import_zod16.z.boolean()
|
|
2619
4441
|
});
|
|
2620
4442
|
var OAUTH_PATHS = {
|
|
2621
4443
|
authorizationServerMetadata: "/.well-known/oauth-authorization-server",
|
|
@@ -2673,7 +4495,24 @@ var OAUTH_PATHS = {
|
|
|
2673
4495
|
* A failed `POST` answers `apiError`, not `oauthError`: the caller is our own
|
|
2674
4496
|
* page, not a standard client. See the dialect note at the top of this file.
|
|
2675
4497
|
*/
|
|
2676
|
-
login: "/login"
|
|
4498
|
+
login: "/login",
|
|
4499
|
+
/**
|
|
4500
|
+
* **The org-admin impersonation interstitial** (spec `org-identity-redesign`
|
|
4501
|
+
* D4) — and, like `consent` and `login`, **one path with both verbs**: `GET`
|
|
4502
|
+
* serves the "sign in as" page, `POST` accepts an `impersonationChoice` and
|
|
4503
|
+
* answers an `oauthLoginResponse`.
|
|
4504
|
+
*
|
|
4505
|
+
* Reached only by an org admin whose `POST /login` authenticated against the
|
|
4506
|
+
* Org Admins group: instead of a session, they are redirected here to pick a
|
|
4507
|
+
* role to preview or a user to sign in as. The page is **server-rendered by
|
|
4508
|
+
* the cloud from its own origin** — the console never builds it and never
|
|
4509
|
+
* hardcodes this path; the login page merely follows the `redirect_to` it is
|
|
4510
|
+
* handed. It belongs in this list for the reason `idpStart`/`idpCallback` do:
|
|
4511
|
+
* a server-owned target a client is sent to, which must have exactly one
|
|
4512
|
+
* definition rather than a literal in `cloud/src/routes/oauth.ts` that a
|
|
4513
|
+
* second reader could drift from.
|
|
4514
|
+
*/
|
|
4515
|
+
impersonate: "/oauth/impersonate"
|
|
2677
4516
|
};
|
|
2678
4517
|
|
|
2679
4518
|
// src/pkce.ts
|
|
@@ -2758,18 +4597,6 @@ var ServerKeyCredentials = class {
|
|
|
2758
4597
|
return false;
|
|
2759
4598
|
}
|
|
2760
4599
|
};
|
|
2761
|
-
function createPasswordResetMethods(http, appIdentifier2) {
|
|
2762
|
-
return {
|
|
2763
|
-
async requestPasswordReset(email) {
|
|
2764
|
-
const body = { app_identifier: appIdentifier2, email };
|
|
2765
|
-
await http.request("/api/client/password/reset", { method: "POST", skipAuth: true, body, expectEmptyBody: true });
|
|
2766
|
-
},
|
|
2767
|
-
async confirmPasswordReset(token, newPassword) {
|
|
2768
|
-
const body = { token, new_password: newPassword };
|
|
2769
|
-
await http.request("/api/client/password/reset/confirm", { method: "POST", skipAuth: true, body, expectEmptyBody: true });
|
|
2770
|
-
}
|
|
2771
|
-
};
|
|
2772
|
-
}
|
|
2773
4600
|
function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
2774
4601
|
return {
|
|
2775
4602
|
async login(email, password2) {
|
|
@@ -2825,34 +4652,39 @@ function createSessionAuth(http, tokenStore, appIdentifier2) {
|
|
|
2825
4652
|
async logout() {
|
|
2826
4653
|
const session = await tokenStore.load();
|
|
2827
4654
|
let revoked = true;
|
|
4655
|
+
let idpLogout = null;
|
|
2828
4656
|
if (session) {
|
|
2829
4657
|
const body = { refresh_token: session.refresh_token };
|
|
2830
|
-
|
|
4658
|
+
try {
|
|
4659
|
+
const response = await http.request("/api/client/logout", {
|
|
4660
|
+
method: "POST",
|
|
4661
|
+
skipAuth: true,
|
|
4662
|
+
body
|
|
4663
|
+
});
|
|
4664
|
+
revoked = true;
|
|
4665
|
+
idpLogout = response.idp_logout;
|
|
4666
|
+
} catch {
|
|
4667
|
+
revoked = false;
|
|
4668
|
+
idpLogout = null;
|
|
4669
|
+
}
|
|
2831
4670
|
}
|
|
2832
4671
|
await tokenStore.save(null);
|
|
2833
|
-
return { revoked };
|
|
4672
|
+
return { revoked, idp_logout: idpLogout };
|
|
2834
4673
|
},
|
|
2835
4674
|
async me() {
|
|
2836
4675
|
return http.request("/api/client/me", {});
|
|
2837
4676
|
},
|
|
2838
|
-
async register(email, password2) {
|
|
2839
|
-
const body = { app_identifier: appIdentifier2, email, password: password2 };
|
|
2840
|
-
return http.request("/api/client/register", { method: "POST", skipAuth: true, body });
|
|
2841
|
-
},
|
|
2842
|
-
async confirmRegistration(token) {
|
|
2843
|
-
const body = { token };
|
|
2844
|
-
const tokens = await http.request("/api/client/register/confirm", { method: "POST", skipAuth: true, body });
|
|
2845
|
-
await tokenStore.save(tokens);
|
|
2846
|
-
},
|
|
2847
4677
|
async changePassword(currentPassword, newPassword) {
|
|
2848
4678
|
const body = { current_password: currentPassword, new_password: newPassword };
|
|
2849
4679
|
const tokens = await http.request("/api/client/password/change", { method: "POST", body });
|
|
2850
4680
|
await tokenStore.save(tokens);
|
|
2851
4681
|
},
|
|
2852
|
-
|
|
4682
|
+
passwordResetUrl() {
|
|
4683
|
+
return `${http.baseUrl.replace(/\/+$/, "")}/reset-password`;
|
|
4684
|
+
}
|
|
2853
4685
|
};
|
|
2854
4686
|
}
|
|
2855
|
-
function createServerKeyAuth(http
|
|
4687
|
+
function createServerKeyAuth(http) {
|
|
2856
4688
|
return {
|
|
2857
4689
|
async login() {
|
|
2858
4690
|
throw new Error("auth.login is not available on a client constructed with a serverKey.");
|
|
@@ -2869,16 +4701,12 @@ function createServerKeyAuth(http, appIdentifier2) {
|
|
|
2869
4701
|
async me() {
|
|
2870
4702
|
return http.request("/api/client/me", {});
|
|
2871
4703
|
},
|
|
2872
|
-
async register() {
|
|
2873
|
-
throw new Error("auth.register is not available on a client constructed with a serverKey.");
|
|
2874
|
-
},
|
|
2875
|
-
async confirmRegistration() {
|
|
2876
|
-
throw new Error("auth.confirmRegistration is not available on a client constructed with a serverKey.");
|
|
2877
|
-
},
|
|
2878
4704
|
async changePassword() {
|
|
2879
4705
|
throw new Error("auth.changePassword is not available on a client constructed with a serverKey.");
|
|
2880
4706
|
},
|
|
2881
|
-
|
|
4707
|
+
passwordResetUrl() {
|
|
4708
|
+
throw new Error("auth.passwordResetUrl is not available on a client constructed with a serverKey.");
|
|
4709
|
+
}
|
|
2882
4710
|
};
|
|
2883
4711
|
}
|
|
2884
4712
|
|
|
@@ -3168,6 +4996,29 @@ function createDatapointsApi(http, channel, slugSubscriptions) {
|
|
|
3168
4996
|
};
|
|
3169
4997
|
}
|
|
3170
4998
|
|
|
4999
|
+
// src/grants.ts
|
|
5000
|
+
function createSessionGrants(http) {
|
|
5001
|
+
return {
|
|
5002
|
+
async list() {
|
|
5003
|
+
const response = await http.request("/api/client/grants", {});
|
|
5004
|
+
return response.grants;
|
|
5005
|
+
},
|
|
5006
|
+
async revoke(clientId) {
|
|
5007
|
+
return http.request(`/api/client/grants/${pathSegment(clientId)}`, { method: "DELETE" });
|
|
5008
|
+
}
|
|
5009
|
+
};
|
|
5010
|
+
}
|
|
5011
|
+
function createServerKeyGrants() {
|
|
5012
|
+
return {
|
|
5013
|
+
async list() {
|
|
5014
|
+
throw new Error("grants.list is not available on a client constructed with a serverKey.");
|
|
5015
|
+
},
|
|
5016
|
+
async revoke() {
|
|
5017
|
+
throw new Error("grants.revoke is not available on a client constructed with a serverKey.");
|
|
5018
|
+
}
|
|
5019
|
+
};
|
|
5020
|
+
}
|
|
5021
|
+
|
|
3171
5022
|
// src/job-subscriptions.ts
|
|
3172
5023
|
function keyOf2(robotId, slug2) {
|
|
3173
5024
|
return `${robotId} ${slug2}`;
|
|
@@ -3596,18 +5447,21 @@ function createClient(options) {
|
|
|
3596
5447
|
}
|
|
3597
5448
|
const webSocketImpl = options.WebSocket ?? globalThis.WebSocket;
|
|
3598
5449
|
let auth;
|
|
5450
|
+
let grants;
|
|
3599
5451
|
let http;
|
|
3600
5452
|
let credentials;
|
|
3601
5453
|
if (options.serverKey !== void 0) {
|
|
3602
5454
|
credentials = new ServerKeyCredentials(options.serverKey);
|
|
3603
5455
|
http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials });
|
|
3604
|
-
auth = createServerKeyAuth(http
|
|
5456
|
+
auth = createServerKeyAuth(http);
|
|
5457
|
+
grants = createServerKeyGrants();
|
|
3605
5458
|
} else {
|
|
3606
5459
|
const tokenStore = options.tokenStore ?? new InMemoryTokenStore();
|
|
3607
5460
|
http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials: noCredentials });
|
|
3608
5461
|
credentials = new SessionCredentials(http, tokenStore);
|
|
3609
5462
|
http.setCredentials(credentials);
|
|
3610
5463
|
auth = createSessionAuth(http, tokenStore, config.appIdentifier);
|
|
5464
|
+
grants = createSessionGrants(http);
|
|
3611
5465
|
}
|
|
3612
5466
|
const channel = new RealtimeChannel({ url: config.realtimeUrl, WebSocket: webSocketImpl, credentials });
|
|
3613
5467
|
const slugSubscriptions = createSlugSubscriptions(channel);
|
|
@@ -3634,6 +5488,7 @@ function createClient(options) {
|
|
|
3634
5488
|
return {
|
|
3635
5489
|
config,
|
|
3636
5490
|
auth,
|
|
5491
|
+
grants,
|
|
3637
5492
|
datapoints,
|
|
3638
5493
|
actions,
|
|
3639
5494
|
services,
|