@fleetless/sdk 1.0.0 → 2.0.1

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