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