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